2008. december 2., kedd

Application Settings (article 1)


Problem/Question/Abstract:

Managing Application Settings

Answer:

Introduction

Almost every program we write these days has a set of application settings, commonly called Program Options, that needs to be managed. Typically, the application needs to be able to save and restore these options to the registry and display them to the user for modification. Most developers simply create a unit to hold these option settings as a set of a global variables, or as a properties of a global application settings object.

The problem with this approach is that a lot of tedious code is required in order to save and load these settings. Additional code is then required to display these settings to the end user and allow the user to modify them as required. For example, you've probably written code like this many times before:

procedure TForm1.LoadSettings;
begin
  cbWordWrap.Checked := Settings.WordWrap;
  edFontName.Text := Settings.FontName;
end;

procedure TForm1.SaveSettings;
begin
  Settings.WordWrap := cbWordWrap.Checked;
  Settings.FontName := edFontName.Text;
end;

The purpose of this article is to demonstrate an alternative way to manage application settings by taking advantage of RTTI, Run Time Type Information. In part 1 of this article, we talk about creating a basic application settings object that will automatically save and load itself to and from the registry. In part 2, we will create an object dataset that enables you to connect this application settings object to data aware controls, thereby eliminating the tedious code above.

RTTI 101

The goal of part 1 is to create an application settings object that can save and load itself to and from the registry automatically. For those of you familiar with RTTI, this part may seem to be quite trivial, however for those of you new to RTTI, the very concept of RTTI can seem somewhat magical. Thus the first thing we should do is briefly cover RTTI, what it is and how it works. This will not be an in depth discussion of RTTI, but will hopefully be sufficient for the purpose of this article. Note that the best discussion of RTTI is in Ray Lischners book, Secrets of Delphi 2.

RTTI is a mechanism provided by Delphi that describes the published properties of an object. It provides a means for third party code to be able to interact with objects even though this code has no intimate knowledge of the objects. The object inspector in Delphi is a great example of RTTI in action. Have you ever wondered how the object inspector is able to display all published properties of any component even though it obviously has no intimate knowledge of the component it is displaying. The answer is RTTI. By using RTTI the object inspector is able to list all of the properties of a component and what the current values of those properties are. By again using RTTI, the object inspector is able to allow an end user, the Delphi developer in this case, to change the values of those properties as desired.

RTTI functionality is encapsulated in the VCL unit TypInfo.pas. This unit is not documented, however you can find it in your VCL/Source directory. Starting with Delphi 5, Borland added a significant number of easy access RTTI methods to TypInfo.pas in an effort to make using RTTI easier.

I have include a small RTTI utility unit called GXRTTI.pas with the code of this article. Let's take a look at one of those routines to get an idea of how we can use RTTI.

function GetPropName(Instance: TPersistent; Index: Integer): string;
var
  PropList: PPropList;
  PropInfo: PPropInfo;
  Data: PTypeData;
begin
  Result := '';
  Data := GetTypeData(Instance.Classinfo);
  GetMem(PropList, Data^.PropCount * Sizeof(PPropInfo));
  try
    GetPropInfos(Instance.ClassInfo, PropList);
    PropInfo := PropList^[Index];
    Result := PropInfo^.Name;
  finally
    FreeMem(PropList, Data^.PropCount * Sizeof(PPropInfo));
  end;
end;

The function above returns a property name for a given object at a given index in the list of published properties for that object. For example, Name might be the first property of TListBox. Thus calling GetPropName(ListBox1,0) would return the string "Name".

This function works by first getting the type data for the given instance using the GetTypeData function in TypInfo.pas. Once we have the type data, we can then retrieve a list of properties for this class. Note that you must allocate memory to hold this property list as above. Once we have the list of properties in the PropList pointer, it's easy to retrieve the property name of a given property.

Creating a Base Application Settings Object

Now that we understand a bit more about RTTI, let's take a look at creating our base application settings object. The intent is that we should be able to derive a project specific settings object from the base settings object. For example we might have a base object called TGXAppSettings and for a word processor app we might create a TWordAppSettings class to hold the specific options for this project. The TWordAppSettings object descends from TAppSettings. The point of creating a base class TGXAppSettings is that the base class will contain all of the logic needed to load and save itself to and from the registry, regardless of the properties we add to descendant classes. Thus if we added a published WordWrap property to the TWordAppSettings class, the code in the base TAppSettings class will automatically save and load the new WordWrap property forcing the developer to add any new code.

Thus in a nutshell the purpose of the base object is to provide a mechanism to automatically save and load itself to the registry, regardless of what properties are added in descendant classes. So let's take a look at the type declaration of our TGXAppSettings object.

type
  TGXAppSettings = class(TComponent)
  private
    FRegistryKey: string;
    FIgnoreProperty: TStrings;
    FAutoLoad: Boolean;
    procedure SetIgnoreProperty(Value: TStrings);
  protected
    procedure Loaded; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure SaveToRegistry; virtual;
    procedure LoadFromRegistry; virtual;
    procedure Assign(Source: TPersistent); override;
    property IgnoreProperty: TStrings read FIgnoreProperty write SetIgnoreProperty;
  published
    property AutoLoad: Boolean read FAutoLoad write FAutoLoad;
    property RegistryKey: string read FRegistryKey write FRegistryKey;
  end;

Note that the object descends from TComponent rather then TPersistent as might be expected. I do this because I want to be able to place the project specific setting objects that will descend from TGXAppSettings on the component palette in order to drop them on a form. This feature will be used when I present the object dataset in Part 2.

As we can see above there are actually very few methods in the base TGXAppSettings object. There is a SaveToRegistry method to save the object to the registry and a LoadFromRegistry method to load it from the registry. We have also overriden the assign method in order to write code to enable us to easily copy one settings object to another.

Next two properties have been added, AutoLoad and RegistryKey. AutoLoad specifies where or not the component automatically loads itself from the registry when Delphi has loaded a form the component is sitting on. It also tells the component to save itself back to the registry automatically when it is being destroyed. The property called RegistryKey has been added to enable the developer to specify where to save and load the object in the registry.

The Constructor and Destructor have been overriden to allow us to create the IgnoreProperty stringlist. The IgnoreProperty stringlist is used to instruct the class of which properties to ignore when writing properties out to the registry. Descendant setting classes should not have to utilize this feature as it is primarily intented to prevent the AutoLoad, RegistryKey, Name and Tag properties from being written to the Registry.

Finally, the Loaded method has been overriden to give the component the chance to load itself from the registry if the AutoLoad property is set to true.

SaveToRegistry and LoadToRegistry Method

The SaveToRegistry method contains the code needed to save the object to the registry. It uses RTTI to automatically save all published properties of the object to the registry. Now you might be think what's the point of saving the published properties since this object doesn't have any published properties. While it is true that this object has no published properties, application setting objects that descend from this base class will have published properties and this method will save those properties auto-magically (to use my favourite term).

Here is the code for SaveToRegistry:

procedure TGXAppSettings.SaveToRegistry;
var
  Registry: TRegistry;
  Index: Integer;
  PropName: string;
  MStream: TMemoryStream;
begin
  if RegistryKey = '' then
    exit;
  Registry := TRegistry.Create;
  try
    Registry.RootKey := HKEY_CURRENT_USER;
    Registry.OpenKey(RegistryKey, True);
    for Index := 0 to GetPropCount(Self) - 1 do
    begin
      PropName := GetPropName(Self, Index);
      if (FIgnoreProperty.Indexof(Propname) >= 0) then
        Continue;
      case PropType(Self, GetPropName(Self, Index)) of
        tkLString, tkWString, tkString: Registry.WriteString(PropName,
          GetStrProp(Self, PropName));
        tkChar, tkEnumeration, tkInteger: Registry.WriteInteger(PropName,
          GetOrdProp(Self, PropName));
        tkInt64: Registry.WriteString(PropName, IntToStr(GetInt64Prop(Self,
          PropName)));
        tkFloat: Registry.WriteString(PropName, FloatToStr(GetFloatProp(Self,
          PropName)));
        tkClass:
          begin
            if (TPersistent(GetOrdProp(Self, PropName)) is TStrings) then
            begin
              MStream := TMemoryStream.Create;
              try
                TStrings(GetOrdProp(Self, PropName)).SaveToStream(MStream);
                Registry.WriteBinaryData(PropName, MStream.Memory^, MStream.Size);
              finally
                MStream.Free;
              end;
            end;
          end;
      end;
    end;
  finally
    Registry.Free;
  end;
end;

In the code above, we first open the registry at the desired key. We then iterate through each property of the settings object and write it out to the registry. The function GetPropCount is a utility function in GXRTTI.pas that returns the number of published properties in a given object. As we go through each property, we first get the property name using the GetPropName function in GXRTTI.pas. Finally, dependant on the type of property, we write the property out to the registry using the appropriate registry function. Functions like GetStrProp and GetOrdProp retrieve the value of the given property and are contained in the unit TypeInfo.pas.

For properties based on TStrings, we retrieve a pointer to the TStrings object using GetOrdProp and write it to the registry using WriteBinaryData. A similar technique could be used for TPicture based properties if you wished to add this feature to the class.

The LoadToRegistry method is almost identical, except the reverse functionality is performed. I won't show it here, however you can see it in the downloable code at the end of this article.

Assign method

We override the assign method in order to enable us to copy one application settings object to another. This will let us create a temporary application settings object the user can edit. We need this capability so that if the user hits the cancel button, the changes the user made are thrown away with the temporary application settings object.

The assign method appears as such:

procedure TGXAppSettings.Assign(Source: TPersistent);
begin
  if Source is Self.ClassType then
    CloneClass(Source, Self)
  else
    inherited Assign(Source);
end;

This method is deceptively simple, however note the call to CloneClass. This routine is in GXRTTI.pas and it copies all published properties from one class to another by using RTTI.

An Example Project

Now that we have done all of that work, let's create an example to see how this all fits together. I've copied the code from Borland's Richedit demo and added an options dialog to the project in order to see how this works. Our rich edit settings class appears as follows:

type
  TRichEditSettings = class(TGXAppSettings)
  private
    FWordWrap: Boolean;
    FFontName: string;
    FFontSize: Integer;
  public
    procedure UpdateSettings(Editor: TRichEdit);
  published
    property FontName: string read FFontName write FFontName;
    property FontSize: Integer read FFontSize write FFontSize;
    property WordWrap: Boolean read FWordWrap write FWordWrap;
  end;

As we see above, three properties have been added. These properties are the options for the Richedit application.

Next, I added one method called UpdateSettings. I use this method to apply the options to the actual application. In this example, the application passes the richedit control to the method that it desires to have the application settings applied to. How you apply option settings to the project will vary considerably from project to project and it is entirely up to you to decide on the best way to do this. The UpdateSettings method appears as follows:

procedure TRichEditSettings.UpdateSettings(Editor: TRichedit);
begin
  Editor.WordWrap := WordWrap;
  Editor.DefAttributes.Name := FontName;
  Editor.DefAttributes.Size := FontSize;
end;

Now that we have created our TRichEditSettings component, we need to integrate it into the application which as we will see, could not be any easier. The first thing we do is create a project specific package, GXRichEdit.dpk. We add the unit GXProjSt.pas which contains our TRichEditSettings class. We then compile and install the package, thereby adding the TRichEditSettings component to the component palette.

Once we have the TRichEditSettings component on the palette, we simply drop it on the main form. We set the RegistryKey property to where we want to save the settings in the registry. Next we set the FontName, FontSize and WordWrap properties to the desired default values. Here are the property values as set in the example code.



The beauty of this approach is that it leverages RAD development techniques to minimize the hassle of dealing with application settings. If at some point in the future, you need to add a new setting, simply define a new property in TRichEditSettings and recompile the package. You can then use the object inspector to set the default value of the new setting.

This concludes Part 1 of how to manage application settings, in Part 2 we will see how we can connect the RichEditSettings component to an object dataset to enable the user to quickly and easily change application settings.

Limitations

The code I have presented above has a few limitations that you should be aware of before applying it in your own projects. The major limitation is that class properties other then TStrings is not currently supported. Adding support for TPicture is relatively easy but TFont is more difficult primarly due to the limitations of the object dataset presented in Part 2.

The code presented in this article has not been tested in a production environment, buyer beware.

Code

Download the code from this article here. Please be sure to read Install.txt included in the zip file before opening the project in Delphi.

Nincsenek megjegyzések:

Megjegyzés küldése