| |||||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||||
Print This Article REALbasic University: Column 035
RBU Pyramid VIIIThis 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 HistoryEarly 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:
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.
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:
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?
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.
So now that we understand how it works, let's see how we go about implementing this system in REALbasic!
Adding a Preference SystemNormally, 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.
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.
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:
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:
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:
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 WeekEvery good program needs a cool About Box -- next week we'll create one for RBU Pyramid.
LettersHere'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:
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:
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 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:
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 See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
| |||||||||||||||||||||||||||||