Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Pascal

The Configurator

5.00/5 (6 votes)
22 Nov 2011CPOL11 min read 30.2K   472  
This will allow you to create a configuration file in either INI or XML format minus all the hard work in Delphi.

Introduction

This little library will allow you to create/update/save/load configuration files in Delphi. The examples provided will be able to create INI and XML configuration files. As an extra, I have also included some extra bits of coding titbits that will help you understand certain aspects of coding principles and Delphi design/coding principles. Since some of you may not have Delphi installed, I provided two executables of the compiled source, one for the INI configuration and one for the XML configuration implementations.

Background

I, like most of you, have written a lot of software. And if you've been in the game long enough like me, then you have found the need to write/read configuration settings that your application uses in its functionality. You would also have used several different configuration file format options at your disposal:

  • INI files
  • XML files
  • The Windows registry
  • Some or other format of persistance that works in a way that you like in your own evil way (taking over the world one programmable Blu-Ray player at a time)

Considering all these methods, I have found two things that are important:

  • Microsoft doesn't exactly condone you using the Windows registry. Not because they don't allow you to do it (obviously), but because of the fact that the user using your application may not actually have rights to write or even read the information you need from the registry entry. How many times have you found that (especially on the newer platforms like Vista and Windows 7) that you cannot access certain things and that was only because of the fact that the evil network administrator doesn't allow you to access that specific bit of data.
  • You don't want to waste your time figuring out and writing out all the things that you need to write/read your application's configuration settings over and over again.

Based on those observations, I have created a set (or little library) of files that I hope will help with this situation with regards to creating, saving, loading and updating your Delphi application's configuration settings. I have created a base configuration object and two descendant classes which will create either an INI configuration file or an XML configuration file (which coincidentally, is very similar to the .config file which is created in Visual Studio).
IMPORTANT: When using the INI configuration object, the "AParentSections" and "ASectionName" parameter in the exposed methods are merged together to form the actual section name. This was intentional as the limitation of INI files is that there cannot be nested settings like you can have in XML files. Personally, I prefer using the XML configuration object.

The configuration object allows you to create configuration entries for the following types (If anyone would like me to add any other kind of settings to save, let me know and I will update the article accordingly):

  • string
  • integer
  • boolean
  • real (NOTE: you can also save TDateTime values since a TDateTime is actually a special type of real type.)

What This Code Will Show You

This code will introduce a lot of different coding scenarios when it comes to Pascal/Delphi, including how to work with abstract classes. Some of you people will know it. For the benefit of everyone, I will explain a few things about what I have done:

  • Inheritance and Polymorphism: This is the design principle in which you define one class, and several other descendant classes from that one class. This is where you can define one or more descendant classes and those classes do different things when you call the same common function(s).
  • Class References: Where you can instantiate a concrete class, using only a variable as the instantiation object.
  • Method overloading: Where you can define several methods in your code with the same method name and the only thing that is different for these overloaded methods, is the parameter list.
  • When to use virtual and abstract methods.
  • When to use the protected keyword.
  • The Template Design Pattern: Which is basically one of many object-orientated design principles to simplify the process of creating base and descendant classes.
  • Custom events: You will learn how to create your own events (like your very own OnClick or OnCreate events), but even more cooler.
  • Some cool little tricks that you might not have known about hidden methods in the VCL of Delphi.

Okay, that was a mouthful, so let me get started by explaining...

The inheritance, polymorphism, virtual, abstract, Template Design, protected, class references and Delphi event concepts...

Most, if not all of you understand the core concept of what inheritance and polymorphism is, but because this is my article and there are probably some new people to coding in Delphi, here goes. The concept of polymorphism is described as two (or more) different classes derived from the same ancestor where one or more common methods will produce different results when called by the descendant classes". In English, that means if I define the following ancestor base class:

Delphi
// in the interface section...

TSaw = class(TObject)
private
  FBusy : Boolean;
  procedure MyOnClickEvent(ASender : TObject);
protected
  procedure Do_Cut; virtual; abstract;
  procedure Initialise; virtual;
  procedure Finalise; virtual;
public
  procedure Cut;
public
  property Busy : Boolean read FBusy;
end;

TMyRefSaw = class of TSaw;

// in the implementation section...

procedure TSaw.Cut;
begin
  if not(FBusy) then
  begin
    FBusy := True;
    try
      Do_Cut;
    finally
      FBusy := False;
    end;
end
else
  begin
    // Show some or other message to indicate that the TSaw is still busy cutting...
  end;
end;

procedure TSaw.Initialise;
begin
  FBusy := False;
end;

procedure TSaw.Finalise;
begin
  // This, for the time being is still blank,
  // because we don't need to clear up anything yet...
end;

procedure TSaw.MyOnClickEvent(ASender : TObject);
begin
  // Perform some or other custom type of coding here...
end;

Now, here we are faced with what we have above us. This kind of class provides you with your (first?) glimpse as to what a Template Design Pattern ideally would like to achieve. This class can do certain things. In this case, the object knows, and can set, when it is busy in the Cut method. However, it does not know how to cut. So, we have a template, but we need to refine it in our descendant classes to tell it how to Do_Cut. Which brings me to why the Initialise and Finalise only has the virtual; keyword next to the definition, and the Do_Cut has the virtual; abstract; next to it. The first two mean that there will be a 'base' implementation of them that will be available in the base class. We want to do this, because, as in the case of Initialise we need to ensure that FBusy is set to False. When it comes to Do_Cut, this base class knows absolutely nothing about how to perform that method, so it should always be implemented within the base classes. Here are two samples of what two descendant classes would look like:

Delphi
// in the interface section...

TRipSaw = class(TSaw)
private
  FWood : TWood;
protected
  procedure Do_Cut; override;
  procedure Initialise; override;
  procedure Finalise; override;
end;

// in the implementation section...

procedure TRipSaw.Do_Cut;
begin
  // Here, We will perform some or other actions that work with the FWood object...
end;

procedure TRipSaw.Initialise;
begin
  inherited Initialise;
  FWood := TWood.Create; // instantiate the TWood class...
end;

procedure TRipSaw.Finalise;
begin
  FreeAndNil(FWood); // Free the TWood class...
end;

// in the interface section...

THackSaw = class(TSaw)
private
  FMetal : TMetal;
protected
  procedure Do_Cut; override;
  procedure Initialise; override;
  procedure Finalise; override;
end;

// in the implementation section...

procedure THackSaw.Do_Cut;
begin
  // Here, We will perform some or other actions that world with the FMetal object...
end;

procedure THackSaw.Initialise;
begin
  inherited Initialise;
  FMetal := TMetal.Create; // instantiate the TWood class...
end;

procedure THackSaw.Finalise;
begin
  FreeAndNil(FMEtal); // Free the TMeta=l class...
end;    

I chose the example above because of the following facts:

  1. A ripsaw and a hacksaw are both saws
  2. A ripsaw cuts wood but cannot cut metal
  3. A hacksaw cuts metal but not wood

Now, I put those methods in the protected keyword context, because I wanted to be able to override the descendant class' methods while keeping the context of the methods within the class, but still leaving them exposed for access to the descendant classes. It's like a level higher than private, but is cooler because it is accessible within only the descendant classes, and NOT when an instance of the class which is created.

We've all used the OnClick or OnCreate events in Delphi. We usually just double click on the event viewer and BOOM! we have the code for this created for us and we can do what we need to do. Now, Let's assume that we have a TButton on a form where we have created the TRipSaw or THackSaw objects. We don't want to double click on the OnClick event of the TButton (in this example, we will call it MyButton), because we already defined our own event MyOnClickEvent for when the button is clicked. In this case, we only need to do this:

Delphi
MyButton.OnClick := MySawInstance.MyOnClickEvent;

For the final part of the concepts, I want to outlay: Class references. This is little trick in Delphi that allows for you to create a class using a variable of the class type. So, when I am creating a class descended from TSaw, I will do the following:

Delphi
var
  Saw_Ref : TMyRefSaw;
  MySaw : TSaw;
begin
  SawRef := THackSaw;     // or Saw := TRipSaw;
  MySaw := Saw_Ref.Create;     // which will create either of the THackSaw 
            // or the TRipSaw classes :)
end;

Cool trick, hey? As always, if anyone needs a deeper explanation of how this works, just post a message and I will explain where I can. Which brings me on to...

Using the Code

Using the code is actually very easy. To create an instance of one of the descendants, the constructor requires that you provide it with a file extension name and a delimiter type (for the parent sections). For the file extension parameter, you just need to use something like "ini" or "xml" or in my case "config". You must not put in the "." value as the configurator does that for you already. For the delimiter parameter, this will be for the AParentSections delimiter. By default, it is the normal "," value so you should not need to change that. All you need to do is include the base file (uBaseConfigurator.pas) and the file that contains the actual configuration descendant class that you would like to have in the uses clause of the unit you want to use it in

For the purposes of this article, I created a base form which has basic loading and reading of the configurator (the position, width and height of the form) and two derived forms where the one does a bit more loading and saving (the main form) and the other nothing else (the one which displays when the events are fired). In the main form which contains the string grid, I used the AParentSections parameter to tell the configuration object that the configuration that I want to save must be nested. The AParentSections parameter is basically just a delimited string value (or a TStrings object). The configuration object also performs a check on the parameters that you pass into it, so if you send in empty string values for the ASectionName and AValueName parameters, it will raise an exception and since you cannot have spaces in your section and/or value names, it will replace the spaces with underscores. This will ensure that the configuration object will be able to persist your configuration settings correctly. Here is an example of the configuration file created when using the XML configuration object:

XML
<Configurator>
    <LogEventActionsForm>
        <Left>873</Left>
        <Top>259</Top>
        <Width>551</Width>
        <Height>346</Height>
    </LogEventActionsForm>
    <MainForm>
        <Left>294</Left>
        <Top>364</Top>
        <Width>490</Width>
        <Height>335</Height>
        <Edit1>Edit1</Edit1>
        <Edit2>0</Edit2>
        <DateTimePicker1>40866.6819596412</DateTimePicker1>
        <CheckBox1>True</CheckBox1>
        <Edit3>0</Edit3>
        <StringGrid1>
            <Columns>
                <Col0>64</Col0>
                <Col1>64</Col1>
                <Col2>64</Col2>
                <Col3>64</Col3>
                <Col4>64</Col4>
            </Columns>
        </StringGrid1>
    </MainForm>
</Configurator>

Here is an example of the configuration file created when using the INI configuration object:

Delphi
[LogEventActionsForm]
Left=655
Top=501
Width=687
Height=346
[MainForm]
Left=118
Top=415
Width=490
Height=335
Edit1=Edit1
Edit2=0
DateTimePicker1=40866.6819596412
CheckBox1=1
Edit3=0
[MainForm_StringGrid1_Columns]
Col0=64
Col1=64
Col2=64
Col3=64
Col4=64

Points of Interest

I included my own custom events, and if you are interested, you can try creating your own custom events based on how I designed mine. Please look through the code to see how I implemented these and feel free to ask any questions pertaining to them. I will go one of the custom events just to give you a base idea of what happens:

C#
TOnBeforeManipulateSection = procedure(
    ASender : TConfigurator;
    AParentSection : TStrings;
    var AValueName : string) of object;

Which I defined as a means of being able to fire (as the name indicates and the optimal use of where to implement this event) just before the object adds, removes or checks on a section in the configuration file. Hence in my (base) class I define...

Delphi
// Please check the code for the full implementation...
property OnBeforeAddSection : TOnBeforeManipulateSection read FOnBeforeAddSection 
    write FOnBeforeAddSection;
property OnBeforeRemoveSection : TOnBeforeManipulateSection read FOnBeforeRemoveSection 
    write FOnBeforeRemoveSection;
property OnBeforeHasSection : TOnBeforeManipulateSection read FOnBeforeHasSection 
    write FOnBeforeHasSection;

and to implement (one of them), I do this:

Delphi
if Assigned(OnBeforeAddSection) then
  OnBeforeAddSection(Self, LParentSections, LSectionName);
Do_AddSection(LParentSections, LSectionName);

And that, folks, is how you implement your own custom events. In the example that I provide, I only implement a handful of the custom events built into the configurator.

As an added bonus, the configuration class will correctly create the configuration file for service applications as well. Why is this an added bonus, you ask? The answer is simple: A service application is executed in a different way to normal applications and as such, when you use ExtractFilePath(Application.ExeName), it does NOT give you the result you expect (the actual path of the application), instead it will be the %Windows%\System32 folder. Which is why in the constructor of the base configuration class, you will find the code which mitigates this particular problem:

Delphi
constructor TConfigurator.Create(
  const AConfigurationFileExtension : string;
  const AParentSectionsDelimiter : Char);
var
  LApplicationFileName : array[0..MAX_PATH] of Char;
begin
  inherited Create;

  FConfigurationFileExtenstion := AConfigurationFileExtension;
  FParentSectionsDelimiter := AParentSectionsDelimiter;

  //  This allows to get the executing module's full name whether
it's a normal or service application...
  FillChar(LApplicationFileName, SizeOf(LApplicationFileName), #0);
  GetModuleFileName(HInstance, LApplicationFileName, MAX_PATH);

  FModuleName := LApplicationFileName;
  FModulePath := ExtractFilePath(FModuleName);

  FRootName := ChangeFileExt(ExtractFileName(FModuleName), '');
  FSettingsFileName := ChangeFileExt(FModuleName,
'.'+ConfigurationFileExtenstion);

  Initialise;
end;

You can create your own derived class from the base class (TConfigurator) if you need to persist your configuration options to another kind of format that meets your particular needs. All you need to do is override the following methods:

Delphi
    // Initialization and finalization of your specific objects for
your implementation...
    procedure Initialise; virtual;
    procedure Finalise; virtual;

    //For creating/adding/removing sections of the configuration
file...
    procedure Do_AddSection(
      const ASectionName : string;
      const AParentSections : TStrings); overload; virtual;
abstract;
    procedure Do_RemoveSection(
      const ASectionName : string;
      const AParentSections : TStrings); overload; virtual;
abstract;
    function Do_HasSection(
      const ASectionName : string;
      const AParentSections : TStrings) : Boolean; overload;
virtual; abstract;

    // For writing the associated values to the configuration
file...
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : string); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Integer); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Boolean); overload; virtual; abstract;
    procedure Do_WriteConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const AValue : Real); overload; virtual; abstract;

    // For reading the associated values from the configuration
file...
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : string) : string; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Integer) : Integer; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Boolean) : Boolean; overload; virtual;
abstract;
    function Do_ReadConfigurationValue(
      const AParentSections : TStrings;
      const ASectionName : string;
      const AValueName : string;
      const ADefaultValue : Real) : Real; overload; virtual;
abstract;

NOTE: The only methods which do NOT need to get overridden are the Initialise and Finalise methods. If you do not override the other methods, you will get an EAbstractError exception when you try to call the method(s) which you did not override. If you are a little confused as to how to create your own implementation, just look at the descendant classes which I derived and you should be able to figure it out from there :).

Requirements

This example requires that you have Delphi 7 or higher (you could probably wing it with Delphi 6, but since I don't have that installed, you may have to tweak one or two things to get this demo to work. If you have any issues regarding this, let me know and I will assist where possible), although you might need to change part of it to work with AnsiString/UnicodeString depending on what your default options are for Delphi 2009 and higher, but I don't really think that will be a problem. If anyone out there can confirm this, please let me know and I will update this article appropriately.

History

  • 20th November, 2011: Initial version
  • 22nd November, 2011: Overhaul of the base code and reshuffle/rewrite of the initial article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)