Property lists and how to make them using the plist class
From REALbasicWiki
| Overall article skill | ✭ |
plist files are XML files used to store application preferences (OS X apps may also store a plist file in a binary format, though). The plist class is written in pure REALbasic by Maccrafters and is cross-platform. The advantage of the plist class is that it is easy to use and can store even "complex" objects like listboxes or arrays. So one can simply save and load listboxes with one line of code. Or have windows which remember their last position.
Note that you could also use the plist class to save your data on disk in an open format (xml) that others will be able to open and read even when your app has long gone (or in other words: how many files do you have that you can no longer open because the program creating them no longer exists / no longer runs on the latest system?)
The plist class is available at http://maccrafters.com/plist/ - as of 9th of January 2010 the plist class contains five serious bugs (see BUGS & bug fixes further down). These have been fixed in the version you can download from here http://dl.dropbox.com/u/992591/REALbasic/Classes/plist%202010-01-09.zip
Contents |
[edit] How to use the plist class in your own project
Add the plist class and the pListDict to your project (best option-command drag it in so that just a reference to the original file is added - that way when you open it and fix bugs all projects using the plist class will benefit from it)
add a module called Preferences to your project
add the following properties to the module Preferences :and set their scope to global
PreferenceFile as folderitem
prefs as plist
add the methods LoadPreferences and SavePreferences to the module Preferences and set their scope to global
In the app.open event simply call LoadPreferences
LoadPreferences // LoadPreferences will instantiate a new PreferenceFile if it does not exist
In the app.close event simply call SavePreferences
SavePreferences // Saves the preferences and closes the file
In the LoadPreferences method first establish where the pref file will be and what it's name is (here it will be in the user's preferences folder)
PreferenceFile = SpecialFolder.PreferencesFolder.Child("AppName.plist") // select your own AppName
in newer versions of REALbasic SpecialFolder.PreferencesFolderhas been replaced by SpecialFolder.Preferences so this should be
PreferenceFile = SpecialFolder.Preferences.Child("AppName.plist") // select your own AppName
If you want to put your plist in its own folder then that line becomes
dim f as folderItem
f = PreferencesFolder.child("Foldername") // tell f where it should be and what its name is
f.createAsFolder // create f as folder
PreferenceFile = SpecialFolder.PreferencesFolder.Child("FolderName").Child("AppName.plist") // select your own AppName
Now instantiate the plist
// Create plist pointing to the PreferenceFile established in the app.open event
prefs = new plist(PreferenceFile) // this also loads the Preferences into the plist
if you want to share the plist between users then use SpecialFolder.SharedDocuments (though you might have to consider what to do if more than one user is logged in, for example in a game where a user loads the high score plist, then a FastUserSwitch without logging out, the second user loads and modifies the high score plist, then fastUserSwitch back to the first user; I think it is better to give each user his own plist and have a shared highscore plist which is freshly loaded, compared with, modified when necessary, and saved again immediately to reduce the chance of a conflict)
PreferenceFile = SpecialFolder.SharedDocuments.Child("AppName.plist")
and accordingly if you want to put it in its own folder then that becomes
dim f as folderItem
f = SpecialFolder.SharedDocuments.child("FolderName") // tell f where it should be and what its name is
f.createAsFolder // create f as folder
PreferenceFile = SpecialFolder.SharedDocuments.Child("FolderName").Child("AppName.plist")
Note: I had previously used SpecialFolders.SharedPreferences instead of SpecialFolder.SharedDocuments, However only users with admin privileges are allowed to write to SharedPreferences, wheras all users can write to SharedDocuments.
In the SavePreferences method you save the changed prefs like this
// save the plist
prefs.Save
// Set MacCreator and MacType of your PreferenceFile
PreferenceFile.MacCreator = "ttxt"
PreferenceFile.MacType = "TEXT"
From where and when you call the SavePreferences is up to you - here we put it into the app.close event but you can call it at any time and from more than one place. For example you could have a preferences window in your program and in the close event of the window call SavePreferences as well - that way any change the user makes is immediately saved (so even if your computer suddenly crashes the changes have been saved)
Now we can save and load the plist containing our property values - but where do we actually get and set the property values? The best place to read and write these property values is in the respective object itself of course (that is what object orientation is about).
So for example to read the value for an EditField put this into the EditField's open event (note that you can specify default values here):
prefs.root.GetEditField(NameOfEditfield) // get the value from the plist
and to put the changed value back into the plist put this into the editfield's close event:
prefs.root.SetEditField(NameOfEditfield)
To read the value for a ListBox put this into the ListBox's open event:
prefs.root.GetListbox(NameOfListbox, false)
and to put the changed value back into the plist put this into the editfield's close event:
prefs.root.SetListbox(NameOfListbox)
For a window to remember it's window position from one program start to the next just add
in the Window.open event
prefs.root.GetWindow("Window Main", wMain)
and in the Window.close event
prefs.root.SetWindow("Window Main", wMain)
Or to set a checkbox put in its open event
prefs.root.GetCheckBox(CheckBox1)
and in its close event
prefs.root.SetCheckBox(CheckBox1)
That is one thing to note - getting and setting values in the prefs plist does not have to be done in the LoadPreferences and SavePreferences methods (they only load and save the plist) - it is best done in the objects which have the properties to be set (like in the window or the checkbox). Very nifty, very object oriented - if you delete a Window or any object from your project you also delete the setting and getting, so you don't have to modify code elsewhere.
[edit] When to save changed preferences
Be aware that Mac OS follows a different paradigm than Windows and Linux when it comes to updating preferences the user can set via controls:
On the Mac, changes to a preference should be immediate if possible, and a preference settings window should be a non-modal window, not a dialog with OK and Cancel buttons! For example, if your app lets the user choose whether to show certain information by setting a checkbox, then if the user checks or unchecks the checkbox, the certain information should be shown or hidden immediately, and the app's preferences file should be updated accordingly right away as well.
On Windows (and Linux usually, too), the user rather expects that the settings window has buttons such as OK, Cancel and optionally Apply. Changes to settings will only be applied if the user clicks on OK or Apply, and saved only if the user closes the settings window by clicking OK.
[edit] CHANGES to the original plist class
All these changes are implemented in the modified version.
1. REALsoftware changed the old style constructor to new style at some point, so if you use a newer version of REALbasic you have to change the old style constructors in plist (this has been implemented in MacCrafter's release 2.2 from July 2008, though the bug fixes have not been implemented)
Change
Sub plist(f As FolderItem)
plist f,f
End Sub
to
Sub Constructor(f As FolderItem)
LoadWithOptionalTemplate f,f
End Sub
and
Sub plist(f As folderItem,template As FolderItem)
Load(f,template)
End Sub
to
Sub LoadWithOptionalTemplate (f As folderItem,template As FolderItem)
Load(f,template)
End Sub
2. Updated the pList class to include elChupete's modifications to save the new TextFields.
Sub GetTextField(field as TextField)
field.Text=GetString(field.Name,field.text)
End Sub
Sub SetTextField(field as TextField)
SetString(field.name,field.Text)
End Sub
[edit] BUGS & bug fixes:
Bug in GetDate
It's rather platform/region specific, it exists in the GetDate method of plistDict.
Where the ParseDate function is used in this method, if the host computer is set to use British date format, this sets dt to an incorrect date due to the reversal of the date and month. I think it may be better to assign the month and days individually to ensure accurate cross-border results. i.e. Code:
if ParseDate(datePart,dt) then
If the submitted date is "2009-08-17" (17th Aug 2009), this will return a date of 8th May 2010 with a British setup.
Bug fix: replace the code in plistDict Function GetDate(key As string,default As date) As date with:
dim dt As new Date
if not Exists(key) then
SetDate(key,default)
end
if TypeOK(key) then
dt.SQLDateTime=GetValue(key)
end
return dt
and in function SetDate(key As string, dt As date) with
dim result As integer
result=CheckType(key,"date")
if result<>0 then
values.value(key)=dt.SQLDateTime
types.value(key)="date"
searched.Value(key)=false
end
Bug in SetColor()
the following line is buggy as hex does not pad single digits into a double-digit format (it does not return 0x but only x, and therefore a color like &c00FF01 becomes &c0F1)
value=hex(v.red)+hex(v.green)+hex(v.blue)
Bug fix: replace the buggy line with
dim var as Variant = v // store the color value in a variant and let the variant class do the conversion to string
value = mid(var.StringValue,3)
Double Trouble affecting SetDouble() and SetReal() (included in version 3, probably out Feb 2009)
Str uses scientific notation, so
dim x as double = 1234567890.1234567890
StaticText.text = Str(x)
results in 1.234568e+9, so reading it back results in 124568000
You can ameliorate the bug somewhat by replacing
values.value(key)=Str(v)
with
values.value(key) = Format(v, "-#.#########")
which results in 1234567890.1234567165
Be aware of two potential problems when using doubles. One is that a computer works in the binary system, so fractions can only be shown with a certain degree of precision (see the wrong last three numbers above).
The other is that while double can take on a value between 2.2250738585072013 e-308 and 1.7976931348623157 e+308, it can only do so with a precision of 17 digits.
dim x as double = 12345678901234567890.0 // NOTE THE .0 THE END
ST_Double.text = Str(x)
ST_Double2.text = Format(x, "-#.##########")
produces 1.234568e+19 and 12345678901234567168. respectively (not the wrong digits from position 18 till 20)
Be aware of the .0 at the end of the number in the code. If you try the same without it then
dim x as double = 12345678901234567890 // <- THIS IS AN INTEGER BEING STUFFED INTO A DOUBLE
ST_Double.text = Str(x)
ST_Double2.text = Format(x, "-#.##########")
produces -6,101065e+18 and -6101065172474983424. respectively as you now in your code exceed the limits of what an integer can be (-2,147,483,648 to 2,147,483,647).
If you need to work with large numbers then Bob Delaney's precision, extended or decimal plugins might be useful. These data types are not supported by plist yet but I hope to include them in version 3.
Bug in StripAll()
If the plist becomes corrupted then this bug will result in your app hanging on startup when it reads in the plist.
The method StripAll in pListDict removes all non-printing characters from the left side of the supplied string: [rbcode]Protected Function StripAll(s As string) As string
dim temp As string
if s<>"" then
temp=s
while asc(left(temp,1))<32
temp=right(temp,len(temp)-1)
wend
end
return temp
End Function[/rbcode] The problem is: what happens if the preference file is corrupted and the supplied string ONLY CONTAINS NON-PRINTING CHARS?
In that case "while asc(left(temp,1))<32" remains true and the code hangs itself in an endless loop.
I renamed the function to LStripNonPrintingChars to better reflect what it does, and the corrected code is
[rbcode]Protected Function LStripNonPrintingChars(s As string) As string
// this strips non-printing chars of asc values < 32 from the left side of the supplied string
dim temp As string
if s<>"" and len(s)>0 then
temp=s
while asc(left(temp,1))<32 and len(temp)>0 // remove the non-chars, but allow space = asc(32)
temp=right(temp,len(temp)-1)
wend
end
return temp
End Function[/rbcode]
[edit] To be implemented
At the moment the plist only saves the string of an EditField or TextArea - what remains to be done is to save the styledText info.
See Property list on Wikipedia.
Categories: OS | Beginner | HowTo | Tutorials
