REALbasic University Resources:

RBU: Glossary Defines common REALbasic programming terms
  Archives Previously published columns
Translations: Dutch Courtesy of Floris van Sandwijk
  Japanese Courtesy of Kazuo Ishizuka
  Chinese Courtesy of Dong Li
  RBU Translation Guide Information on Translating RBU into other languages
Books: Matt's Book (2nd Edition!) Ideal for experienced programmers
  Erick's Book Best for beginning programmers
Websites: Mother Ship The publisher of REALbasic
  RB Webring Links to hundreds of REALbasic websites
  RESExcellence Another REALbasic programming column
  REALbasic Developer Magazine The premiere source for REALbasic instruction.

REALbasic University is Sponsored by

Make your Mac do what YOU want it to. Create games, utilities, cool Mac OS X tricks. Download REALbasic now and create your own software.


Print This Article

REALbasic University: Column 035

RBU Pyramid VIII

This week we're going to set up RBU Pyramid to save preference settings. As we continue with the program, we'll add more preferences, but for today we'll just get the system in place.

Some History

Early in my experimentation with REALbasic, I found that just about every program I created needed some way to save preferences. It's a common need. Rather than reinvent the wheel for every project, I devised a simple preference system I use for all my software.

The capabilities I wanted for my preference system were the following:

  • Based on a text file, so prefs are editable with any text editor
  • Expandable, so new prefs can easily be added
  • Flexible, so different kinds of data can be stored
  • Easy to add preference settings
  • Version control, so modifying the preference file format doesn't kill an old software program
  • Portable, so I can easily reuse the system in multiple programs

The system I created does the above, and I like it a lot, but it may not appeal to everyone. For instance, some people don't want the preference files for their programs to be easily editable with any text editor. I see that as an advantage (especially for debugging), but some want to keep their preference settings secret. If you like my system, however, you're free to use it.

How does my preference system work? The basics are simple: every preference is stored on a single line inside an ordinary text file. I use a unique keyword approach to establish the identity of each setting. The keyword is preceeded by a number sign and followed by a space. After the space is the actual setting as established by the user. For example, the following line could be a preference setting to remember if sound was on or off the last time the program was used.

#soundon true

If needed, the setting could contain more than one value. The actual format of the setting varies according to need, but as I long as I'm consistent, it shouldn't cause a problem or be difficult to use. Let's say I want to remember the size and location of a window. I could save the following in my preference file:

#windowpos 980,872,385,139

The keyword (or directive) "windowpos" tells me which preference this is. Since I make up these keywords, I can name them whatever I want. So if I had a floating palette in my program, I could name my directive "toolpalettepos" just as easily.

The actual setting info, in this case, is delimited (separated) by commas. Why commas? Why not? I could have used tabs (ASCII character 9) or some other non-typable character if I wanted, but commas work fine for separating numbers. As to the numbers, it makes sense that they correspond with REALbasic's normal system of coordinates: the first two are the horizontal and vertical location, respectively, and the final two are the width and height, respectively.

Do you see how flexible this system is? Because I use keywords (directives) to identify each setting, the order of the settings in the file is irrelevant. Some methods of saving preferences are extremely rigid: the fourth line must always be a boolean (true or false), the 613th character must always be the number of letters in the user's name, etc. My system is flexible -- each pref is independent of the others.

My preference file is also easily readable by humans. Look at the following excerpt from a Z-Write preference file: Can you tell what each line does?

#version 1.0
#rememberpath Misc:Essays: Current:Software Serials.zart
#clock false
#smartquotes false
#spellingenabled true
#buttonbar true
#defaultfont Verdana
#defaultsize 18
#printmargins 36,36,36,36
#useheader true
#usefooter true
#autoexpandglossary true
#stylepalette T,1360,851,193,158
#rtfcreator MSWD
#helpfont Geneva
#infowindowstaysopen true

Remember the version control feature I mentioned earlier? That's important in software design. For example, let's say you are creating your own file format for storing some data. If in version 1.0 of your program you make the third line be a number indicating the number of records saved in the file, but in version 1.1 of your program you move that to line four, what happens when someone runs version 1.0 of your program and tries to open a version 1.1 file? Yeah, bad stuff, since data isn't where the program expects it to be. The later version of your program can be made to support earlier file formats, but your old software can't know about the new format.

The same problems are evident with a program's preference file. In fact, they are probably even more of a problem since you may change your data file's format rarely, but any time you add a preference setting your preference file changes. For instance, since Z-Write's 1.0 release in May 2000, I've changed the preference format dozens of times as I added new settings. However, because my prefs system is flexible, it causes no catastrophic problems. An older version of Z-Write just ignores any pref directives it doesn't understand.

But what happens if I change an existing directive? Let's say I decide to enhance the "default font" preference setting to let you put in a list of fonts (separated by commas) instead of a single one (in case the first font isn't installed, it would try the second, etc.). Wouldn't an older Z-Write would misinterpret the "Verdana,Helvetica,Arial" as a single font name (since that's all it expects)?

Yes, it would. The solution is to change the keyword. Instead of "defaultfont" I could name the newer directive "defaultfont2" or "defaultfonts" -- anything that's different so the older program would ignore the setting.

As you can see, this system is simple yet powerful. There's plenty of room for expansion for just about any preference we need to save, and we're protected from problems that can be caused by multiple file format versions.

In Detail

There is one key flaw with my system, as you'll see if you look through the code: because unknown directives are stripped (ignored) when read, they aren't saved when the preference file is resaved. That means if you run an older version of the program it will cause the preference file to lose any more recent directives added by more recent version of the program.

For example, the current version of Z-Write includes a "textbackgroundcolor" directive that lets you set the background color of the main text editing window. Older versions of Z-Write don't, so if you set your background color to yellow, then (either accidentally or on purpose) run Z-Write 1.1, the updated preference file won't have the yellow background color setting and the background of your window will be the default white.

Generally, this is only a cause for annoyance, and obviously few people would keep multiple versions of a program installed, but it is something to remember when using my system. I am thinking of fixing this oversight in a future version of my prefs system, making it so it will preserve directives it ignores.

So now that we understand how it works, let's see how we go about implementing this system in REALbasic!

Adding a Preference System

Normally, when I add my preference system to a program, I drag in a class and module I already created. Since you don't have that luxury (and I want you to understand the code), we'll create our manually. But once we've done this, you should be able to easily reuse these elements in other programs you create to add your own preference system.

The first thing we'll do is create a new class called directiveClass. This is an important part of my system. After opening your RBU Pyramid project, go to the File menu and choose "New Class". Name it directiveClass. Open up directiveClass (double-click on it) and add two properties (Edit menu, "New Property"): name as string and text as string. Okay, close the class: we're done with it.

Now let's add a new module (File menu, "New Module"). Name it prefModule and open it up. First we'll add an important property (Edit menu, "New Property"): gDirectives(0) as directiveClass. That's an array that's going to contain a list of all the directives (keywords) found in our preference file.

Let's now add a constant to our module:

Next, let's add some methods to this module (Edit menu, "New Method"). For now, leave the code area empty. First, two simple ones: loadPrefs and savePrefs. And then two more complicated ones:

Now let's start putting in some code. We'll begin with the simpler routine, bStr. This one accepts a boolean (true or false) data type and returns either the word "true" or "false". We'll use this often when we save boolean preference settings.

  
if b then
return "true"
else
return "false"
end if

That's so simple I don't think I need to explain.

But the next routine, extractDirectives, is the heart of the system and is much more complicated.

  
dim theFile as textInputStream
dim theLine as string
dim i as integer

if f <> nil then
theFile = f.openAsTextFile
if theFile <> nil then
while not theFile.EOF
theLine = theFile.readLine
if mid(theLine,1,1) = "#" or mid(theLine,1,1) = "'" then

if mid(theLine,1,1) <> "'" then
// we've got a directive, so store it in global array
i = inStr(theLine," ")
if i > 0 then
// since we Append, gDirectives begin at (1)!
gDirectives.Append new directiveClass

// grabs directive name
gDirectives(ubound(gDirectives)).name = lowercase(mid(theLine, 2, i - 2))

// grabs balance of line
gDirectives(ubound(gDirectives)).text = mid(theLine, i + 1)

else
// error in directive formatting -- no space, so skip it
end if
else
// It's a comment, so ignore it
end if // mid(theLine,1,1) = "'"

else // mid(theLine,1,1) <> "#"
// we've hit the first non-directive line so skip rest of the file
theFile.close
return true // sucessfully processed all directives
end if
wend
theFile.close
return true
// either no directives in file
// or only directives in file -- either is ok
end if
end if

return false // unsuccessful for some reason

This looks more messy than it is. We start out by making sure the file passed (f) is valid. If so, we open it as a text file. We set up a while-wend loop and look through the file until we hit the end of the file (theFile.EOF equals true when we're at the end of the file).

Now we get to the good stuff. We read in a single line from the file and examine in. We're basically looking for one of two things: either the first character of the line is a number sign (#) or it's an apostrophe ('). If it's neither one of those, we assume the file is done and we exit the routine.

If the line begins with an apostrophe, it's a comment, so we just skip it. Otherwise we process the line as a directive. Remember the directive structure? It's #, directive name, space, and setting. The key there is the space: that divides the name from the settings. So we find the first space character and store that position in i.

The directives we find we're going to store in the global array we created earlier, gDirectives. Since gDirectives is an object of type directiveClass, we must create a new instance of a directiveClass using the new command. (Remember, classes are objects: you cannot use them until you've created an instance of the object. The class you see in the project window is only the definition of the object, not an instance of the object.)

Next we append that new object to gDirectives. The append command allocates new space in the array (it expands the array). Note that since we're appending, the first real element in gDirectives begins at index 1 (0 is there but not used as the append adds 1).

Once we've put a new directiveClass item into the array, we can fill it with data. We store the directive name in the .name property, and the settings text in the .text property.

To get the directive name, we use mid(theLine, 2, i - 2) which extracts the middle portion of the line up to but not including the space (i - 2) and starting at the second character. To see how this formual works, use a sample set of data and test it:

  
123456789012345
#buttonbar true
123456789

The variable i will be set to 11, since that's the first space character. Subtract two from it to get 9 (the two comes from the two characters we don't want: the initial # sign and the space). That 9 is our length. So now our command is, in effect: mid(theLine, 2, 9). That returns the nine characters starting at position 2. Basically, the word "buttonbar"!

For the setting text, we just grab everything after the space (by omitting a specific length, the mid command returns the balance of the text).

Once we'll saved those two elements to our gDirectives array, we go to the next line. When the file's done, we end up with an array of directives. All we need to do then is process them!

That's done in our loadPrefs routine. It's here we call extractDirectives. Once that's been done, we use a while loop to step through each directive and see what it is. Each directive is examined inside a select case statement: if the directive matches a case, we process it, otherwise the directive is ignored. Simple!

Here's the code for loadPrefs:

  
dim f as folderItem
dim s, theValue as string
dim i, j, k as integer

// This routine loads the program's general preferences
// from the Preferences Folder. Prefs are in "Directive"
// format: each line contains a #directive followed by a
// space and the prefs for that item. for instance:
//
// #sound on
//
// We store the parsed directives in the
// global gDirectives array and then read through that
// to set our global variable flags.

f = preferencesFolder.child(kPrefName)
if extractDirectives(f) then
i = 0
while i < ubound(gDirectives)
i = i + 1

select case gDirectives(i).name

case "nosound"
s = gDirectives(i).text
gameWindow.soundBevel.value = s = "on" or left(s, 1) = "t"
gameWindow.updateSoundIcon


end select
wend
else
// no prefs file, set defaults
gameWindow.soundBevel.value = true // no sound


end if

Note that we used the preferencesFolder function to find that folder. That's a cool command because it works transparently no matter what operating system RBU Pyramid is running under. For instance, on Mac OS it returns the Preferences Folder inside the active System Folder, but under Mac OS X it returns the user's Preferences folder inside the current user's directory.

The directive we're looking for in this case is one called "nosound". The setting is either the word "on" or "off" or "true" or "false". If "nosound" is the directive, we look inside the gDirectives(i).text property to see what's there. Since this is a boolean value, we can just check for a couple of the situations. If it's "on" or the first letter begins with a "t" we know it's true. (By checking for just the first letter instead of the entire word, "t" is a valid setting.)

In the case of RBU Pyramid, we set the results of this boolean test to our soundBevel control's value setting. Then we update the sound icon with our updateSoundIcon method.

That's it! We've just read in a preference setting and applied that value to our program.

But of course when you first run the program, there is no preference file. How does RBU Pyramid react? It does nothing, of course. There is no error: it just uses the default settings. That's that last bit of code at the bottom. When we add more preference settings, we'll have to remember to put their default values here.

Now it's time to do the converse of loadPrefs with our saveprefs method. This one saves all our settings to our preference file.

Put this in your saveprefs method:

  
dim f as folderItem
dim s as string
dim t as TextOutputStream
dim i, m as integer

f = preferencesFolder.child(kPrefName)
if f <> nil then
t = f.CreateTextFile
if t <> nil then
t.writeline "#version 1.0"

// Save sound on/off setting
t.writeline "#nosound " + bStr(gameWindow.soundBevel.value)


t.close
end if // t = nil
end if // f = nil

See how simple that is? We get the folderitem via the preferencesFolder function, create a text file, and then write our settings to the file. In this case, we first write a "version" directive (which, as you noticed, we ignored in loadPrefs -- it's just nice to do this in case we ever need to know what version preference file this is).

Next, we write our "nosound" directive. We use our bStr method to write either "true" or "false" depending on the state of the soundBevel control.

For now, that's all we're going to do. As we continue with RBU Pyramid, we'll be adding a bunch of other preference settings and you'll see how easy it is to add them.

There's two other things we need to do before what we've done will actually work, however. We need to add calls to our loadPrefs and savePrefs methods!

Go to the app class in your project window and open it. In the Open event, put "loadPrefs" as the first line (we want it to be the first thing the program does). Then, in the Close event, put "savePrefs".

That's it! When you run the program, it should now remember the state of the sound on/off control. Try it. Experiment with changing the preference file manually using a text editor. Try running the compiled beta of RBU Pyramid and see how it changes the stuff in the file.

If you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.

Next Week

Every good program needs a cool About Box -- next week we'll create one for RBU Pyramid.

Letters

Here's an unusual item. Jeff Lawrence created an interesting REALbasic program called "RBU Browser" that lets you easily access the various REALbasic University columns. You simply choose the column number you want and it's displayed in your default web browser:

He's given me permission to post the source, so if you're interested, download it here. Check it out: it's pretty cool!

Rejean has a question about using pictures within a listBox:

Hi Marc,

I would like to thank you a great deal on the work you're doing in sharing your expertise and knowledge about REALBASIC. RB is a great tool with many "hidden" features and you help me a lot in discovering them and applying them to my little project. You are the one who was able to make me understand and figure out what OOP and events driven apps are all about.

I'm an old timer programmer (BASIC, FORTRAN, APL, COBOL) and I've been looking for a development package that was easy to learn with the new concept of "events driven apps" and OOP. I tried several of them but the one I found the easiest to use and learn was RB.

I just discovered RBU a week ago and I have completed the GenderChanger project. Once it was running, I started modifying it to improve its robustness (I do silly things once in a while!).

One thing I would like to do and I cannot figure out how:

In the Edit File Types window, I 've added a fourth column entitled ICON. You see now where I'm going. I would like to be able to show the icon of the file type and creator. I'm using FileIcon plug-in package from Doug Holton and it works great in getting the picture of the icon of the file. Now, the great question is: how do I put that picture in the listbox. I'm sure it must be feasible.

Again, thank you very much.

Rejean

If you've gotten Doug's FileIcon plug-in to retrieve you a picture object, you're 90% of the way there. All that's left is to put that picture into the listBox. You simply need to store the picture into a picture variable and then use the listBox's rowPicture method to stick the picture into the row.

To get this to work for GenderChanger requires four key changes:

  1. You must change the columnCount property of listBox1 from 3 to 4, and change the columnWidths string to "10%,50%,20%,20%".
  2. You must change the Open event of listBox1 to apply the correct headers.
  3. You must change the Action event of PushButtonConvert to refer to column 1 of listBox1 instead of 0 to retrieve the file path.
  4. You must rewrite listBox1's DropObject routine to insert in the picture in the row.

Here's the new code for the DropObject routine (note that this will only work if you have Doug Holton's FileIcon plug-in installed in the plugins folder of your copy of REALbasic):

   dim p as picture 

if obj.folderItemAvailable then
do
listBox1.addRow ""

p = fileicon(obj.folderItem, 16, 16, 0)
listBox1.rowPicture(listBox1.lastIndex) = p

listBox1.cell(listBox1.lastIndex, 1) = obj.folderItem.absolutePath
listBox1.cell(listBox1.lastIndex, 2) = obj.folderItem.macCreator
listBox1.cell(listBox1.lastIndex, 3) = obj.folderItem.macType
loop until not obj.nextItem
end if

You can see that we have to shift all our columns over since we've inserted a new one at column 0. I'll let you figure out the other changes. If you just want the new source code, you can download it here.

Here's what it looks like when it works (note that I made the listBox font larger so there's room vertically for the icon):

If you're adventurous, another change you could make would be to update the icons of the list of files after they've been converted: the program currently does not do that.

I also don't know if Doug's plugin works for Carbon apps under Mac OS X. I'm sure it doesn't work for Windows apps (not a concern for GenderChanger, but possibly for other apps).

Our next letter is from JP Taylor who's have a TabPanel problem:

Hi. I've been reading the REALbasic University column for a while now. It's been a tremendous help. Here's my problem. My current project uses several TabPanel controls. How do I place controls on the different individual tabs. Many of the same type of controls appear multiple times on the different tabs. I tried to copy and past them from one tab to another. But when I returned to the fist tab, so did the controls. I thought that maybe the controls had to placed individually. That didn't work either. I went out and got REALbasic of dummies. It has helped with other problems, but sadly not this one. I have the feeling that I'm doing some thing wrong that is so basic, so simple that I'm over looking it. Sadly I have no clue what it is. Can you please help? Thanx for reading.

Attached is a screen capture of my project. The tabs "Primary" and "Secondary" Have identical controls with some heading changes. This where I'm experiencing the problem.

My Steps: I copied the controls (selecting them with shift+click), then selected the "Secondary" tab and then paste. I then clicked the "Primary" tab to visually check alignment. When I did, all the copied controls returned to the first tab.

System INFO:
Computer: Stock G4(450) Cube with 576 MB ram
OS: 10.0.4
REALbasic Version: 3.5.1 trial for OS X (non carbon)

Sadly, while TabPanels are really cool devices, they are problematic. As you've figured out, they can be difficult to work with, and there are bugs with controls on one tab "showing through" onto another. Some people are choosing to go with other interfaces (such as an icon-based pane system similar to what's used by Mac OS X's System Preferences). With a little effort on your part, however, you can make them work.

For instance, as you've found, copying and pasting control sets pastes them onto the original layer. The trick I use is that immediately after you paste, while those controls are still selected, move them off the tab panel. Once they are off the panel you can switch to a different tab and move them on. Temporarily while you are doing this, make the window much larger so there's a scratch area off the panel where you can put the group of controls.

If you let go of the pasted group of controls they will be on the original tab, and getting them reselected will be a nightmare since it's they are overlapping other controls (use the undo and start over). One of my top requests for REAL Software is a "select object below" shortcut, similar to what's found in many drawing programs (like Macromedia FreeHand's option-click).

My advice is that if TabPanels work for you, use them. If they're confusing and cause you difficulties, try some other method. The headaches of getting them to work, especially if they're not working due to a bug in REALbasic, can be overwhelming. Sometimes the simpler solution is the most practical.


Keep those letters coming! I may not answer your question immediately, but I'll hopefully get around to it in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise I'll assume it's fair game.)


About the Column
REALbasic University is a weekly instructional column on programming with REALbasic and is brought to you by REALbasic Developer, the magazine for REALbasic programmers.

Each week we answer select reader questions, and we're always open to ideas for future columns. Send your questions to . (Keep your questions simple and specific. General queries like "How do I write my own web browser?" will be neglected.) Your question won't be answered immediately, but will be answered in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise it's fair game.)

About the Author
is an author, philosopher, graphic designer, photographer, film director, soccer fanatic, and programmer (among other things). He writes for MacOpinion, runs his own software company, Stone Table Software, which sells the revolutionary Z-Write word processor, and is Publisher and Editor of REALbasic Developer. He lives in Northern California with his cats, Mischief and Mayhem, and is rapidly running out of free time.

See the REALbasic University Archives


REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.

Email This Article - Comment On This Article

.

Reader Specials

Server Racks Online:
Apple Xserve CompatibleServer Racks and Universal Network Racks
42U KVM Switch Solutions:
High-End Mac and Multi-Platform KVM Matrix switching solutions!
Digital Camera Online:
Great prices on Digital Cameras and accessories!
KVM Switches Online:
Great prices on Mac KVM Switches from the leading manufacturers!
LCD Monitors Online:
Great prices on LCD Monitors from the leading manufacturers!
LCD Projectors Online:
Shop online for LCD Projectors from the leading manufacturers!
USB 2.0 Online:
Great prices on USB 2.0 products from the leading manufacturers

Serious Business Software:
Accounting, Sales, Inventory, CRM, Shipping, Payroll & more!

KVM Switch solutions for MACs:
DAXTEN is a KVM switch, KVM extender and monitor splitter specialist for PC, SUN and MAC applications from name brand manufacturers - offices worldwide.

The "Think Different Store: The iPod Accessories Store - iPod cases, iPod mini, iPod photo, speakers, itrip, inMotion, Soundstage and all other iPod accessories

Earn Cash with the ThinkDifferent Store Affiliates Program

Need A Web Site?
Applelinks Web Hosting Starting at 19.95 a Month

iTunes_RGB_9mm

.

iTunes_RGB_9mm

Cool Mac Gear


iPod 1G-2G
iPod 3G
iPod 4G
iPod Mini
PowerBook-iBook
Keyboard Skins
Garageband