REALbasic Cross-Platform Application Development
Prior to compiling our new application, I want to implement a way to handle preferences in a cross-platform way. For the Windows version of the application, I'll use the Registry, and for the Macintosh version I'll use the preferences folder (for brevity's sake, I'll leave out Linux for the moment). Exporting and Importing Items
In the following sections, I will be subclassing the Properties class that I shared with you in Chapter 2, "The REALbasic Programming Language." To be able to subclass it, I will need to import the Properties class into my current project. To accomplish this, you need to open the original project and select the item you would like to export. In the File menu, select Export, and the class will be exported to the file system. You can do this with any class in the Project Editor. To import the Properties class, you can use the Import item in the File menu, or you can just drag the exported item from the file system and drop it into the Project Editor. If you do this, you have made a copy of the original class and any changes you make to it in this application will not be reflected in the original application, which may be exactly what you want to happen. Often, developers do not want that to be the case. They want to be able to share the same class among applications, but have only one copy of that class available at any given time, meaning that if they make a change in one application, the change is reflected in all applications. In REALbasic 2005, you can make a class external, which accomplishes this task for you, but in a somewhat awkward way. If you want to have a class be an external class that is shared among different applications, you need to export the class from the original application, delete the item from the IDE in the original application, and then drag the exported class back into the IDE while holding down the Command+Option keys (Control+Alt for Windows). You will be able to tell that it is an external class because it will be italicized in the Project Editor and will have a little arrow icon near the name. You should be careful when doing this because it is easy to inadvertently break your other applications if you make a change in one of them. Be sure to test the other applications regularly to make sure they work as planned. Creating the Preferences Class
For the Macintosh version, I'll subclass the Properties class and use that as the format for my preferences file. To make my future programming easier, I would like to use the same interface regardless of whether I am using a Macintosh or a Windows machine. This is a perfect situation for interfaces, so the first thing I will do is create a class interface called iPreferences, which will be the interface I use to manage preferences regardless of which platform I am on. The interface defines four methods, as follows: Interface iPreferences Function get(aKey as String) As String End Function Sub set(aKey as String, assigns aValue as String) End Sub Sub setAppIdentifier(aName as String) End Sub Function getAppIdentifier() As String End Function End Interface The get and set functions are modeled after the get and set functions of the Properties class. I've also added to other methods, getAppIdentifier and setAppIdentifier. Regardless of the platform, I have to use a unique name to identify the application, so these methods allow me to set and get this value. After the Interface is defined, I need to subclass the Properties class so that it implements the interface; then I need to implement a class for Windows that interacts with the Registry but that also implements this interface. The Properties subclass is called Preferences, and the first thing I do is add a new constructor that takes the application name as the parameter: Sub Preferences.Constructor(anApplicationRef as String) // expects application name like info.choate.metawrite dim pf as folderItem // Use the PreferencesFolder function pf = PreferencesFolder me.appID = anApplicationRef if pf<> nil then if pf.child(me.appID).exists then parsePropertyFile(pf.childme.appID)) else createPreferenceFile(pf, me.appID) end if end if End Sub
In this example, I use the PreferencesFolder() function to get a reference to the Preferences folder. The preferences folder is the root folder, not the folder I'm looking for. The next thing I do is check to see if a file exists with the same name as the application name that I passed in the constructor. If it does exist, I open and parse the file. If it doesn't exist, I have to create the preferences file for the first time. Here is the createPreferenceFile method: Sub Preferences.createPreferenceFile(aPrefFolder as FolderItem, anAppID as String) Dim tos as TextOutputStream tos = aPrefFolder.child(anAppID).createTextFile tos.writeLine("#Metawrite Preference File v.05") // write default data tos.close me.file = aPrefFolder.child(anAppID) End Sub
Whenever you are going to write to a file, you need an instance of a TextOutputStream. In this case, you get a reference to that instance by calling the CreateTextFile method on the FolderItem you are creating. TextOutputStreams work similarly to TextInputStreams, but in the opposite direction. In this case, I write just one line to the file by calling tos.WriteLine(). After that, I close the TextOutputStream and the string has been written to the file. I also assign a reference to the preference file to the file property so that I can reference it later. In addition to creating the file, I also need to be able to save data to the file. Because it makes sense to be able to save Properties files, too, it makes the most sense to implement the save method in the Properties class rather than the Preferences class: Sub Properties.save Dim textOutput as TextOutputStream textOutput = me.file.CreateTextFile TextOutput.Write(me.getString) TextOutput.close End Sub When saving the file, I start with the file property and call CreateTextFile again. This creates a new file and erases whatever was there before. Also, like before, it returns a TextOutputStream. Recall that the Properties class is a Dictionary subclass and when the properties file gets parsed, the values are parsed into key/value pairs and assigned to the dictionary. When it's time to save the file, the values in the dictionary have to be converted to a string to be saved. I do this by called the getString() method, which I write to the TextOutputStream. Note that in this example, I used Write instead of WriteLine. The primary difference is that WriteLine ends the line with an appropriate EndOfLine character and will append additional lines when it is called again. Write doesn't append any EndOfLine characters and writes the data to the file as is. After the string is written to the file, the TextOutputStream is closed and the file is saved. When I have written the Preferences class, I need to create a WindowsPreferences class that implements the iPreferences interface. To get a RegistryItem to implement the iPreferences interface, I have to subclass RegistryItem and implement the appropriate methods. The methods that I develop in turn call the underlying methods of the RegistryItem class. The WindowsPreferences class looks like this: Class WindowsPreferences Inherits RegistryItem Implements iPreferences Function getAppIdentifier() As String return me.appID End Function Sub setAppIdentifier(aName as String) me.appID = aName End Sub Function get(aKey as String) As String return me.Value(aKey).StringValue End Function Sub set(aKey as String, assigns aValue as String) me.Value(aKey)= aValue End Sub Sub Constructor(anAppID as String) If anAppID <> "" Then RegistryItem.RegistryItem("HKEY_CURRENT_USER\SOFTWARE\" + anAppID) me.setAppIdentifier(anAppID) Else //raise an error End If End Sub appID as String End Class
The first two methods get and set the appID property (the appID property is added to the WindowsPreferences class as well) and this works just as you would expect it to. The get and set methods are implemented as defined in the Interface, and you can see that all they do is take the key/value pair that is passed to the method and use it to call the RegistryItem.Value method. This is an example of creating methods purely to implement an interface. After the interface methods are implemented, I create a new Constructor that takes anAppID argument in the parameter. What I want to do is take the string that represents the application and use it to build a path to create a reference to a RegistryItem. This is complicated, because I'm doing this from a Constructor of a subclass of the RegistryItem. In this situation, I get the string, build the path, and then call the Constructor of the super class. Although you usually use the word "Constructor" to name the constructor method, you can also use the name of the class itself, which is why when I call RegistryItem.RegistryItem(), I am calling the constructor of the super class. When I was writing this code, I was using REALbasic 2005 r2. When I first wrote it, I called RegistryItem.Constructor("the path"), but it created an error, so I changed the reference from Constructor to RegistryItem and it worked fine. At first I thought this was a bug, but I'm not so sure it would qualify as such; it is an example of some of the trickiness that you can encounter when overriding a method. In the WindowsPreferences class, the signature for the constructor is the same as the constructor for the parent RegistryItem class. Both constructors take a string in the parameter. As far as the compiler is concerned, it's the same signature, even though the meaning of the string is different in both cases. In WindowsPreferences, the string is the name of the application, whereas in RegistryItem the string is the path to the particular RegistryItem. Calling RegistryItem.Constructor("the path") from within the WindowsPreferences.Constructor("the app id") method confuses the compilerit's like a Constructor calling itself. By calling the RegistryItem constructor method, the compiler was able to recognize the method as the constructor of the parent class and execute it accordingly. The rest of the constructor assigns the name of the application to the appID property. Now we have two classes that implement the iPreferences interface, each intended for a different platform. The next question is what do you do with them? In this example, I create the following property of the App object: App.Prefs as iPreferences
Next, I turn to the Open() event. The next chapter discusses events in detail, but it is sufficient right now to know that the Open() event is triggered when the application first opens. Any code you place in the Open() event gets executed right when the application starts. This is where I'll put the code that configures our cross-platform preference files: App.Open() Event #if TargetWin32 then Prefs = iPreferences(new _ WindowsPreferences("info.choate.metawrite")) #elseif TargetMacOS then Prefs = iPreferences(new Preferences("info.choate.metawrite")) #endif If Prefs <> Nil Then Prefs.set("Name") = "Mark Choate" Prefs.set("Location") = "Raleigh, NC" #if TargetMacOS Then Preferences(Prefs).save #endif End If End Event
I use compiler directives to determine the steps I take. If the application is running on a Windows computer, I instantiate a WindowsPreferences object and assign the value of that object to the App.Prefs property. If I were to instantiate the Windows.Preferences class and assign it to Prefs without first casting it to iPreferences, the compiler would generate an error message and tell me that it was expecting an iPreferences, but got a WindowsPreferences instead. Therefore, I have to explicitly cast it as an iPreference before assigning it to Pref. If I am running on a Macintosh, I instantiate a Preferences object instead. Just like the WindowsPreferences object, I have to first cast it as an iPreference object before assigning it to Prefs. Here's the beauty of interfaces. From here on out, my application just calls Prefs as is, without any concern which platform the application is running from. Both classes implement the same interface, and the other parts of your program need to know which methods to call and that is that. In the next part of the event, I assign some values to the Prefs object so that when this application is run on Windows, those values are stored in the Windows Registry and on a Macintosh they are stored in a file in the users Preferences folder. After adding the values, I had to do one additional step and test to see if I was on the Macintosh platform to know if I needed to save the file. The Registry automatically saves the values, but my preferences class doesn't. A better place for this step would be in the App.Close() event, so that it would save the values only on the application closing, but I placed it here so that the values would be written to the file immediately (you can check to see the contents of the file while the application is still running). |
Категории