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:
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;
procedure TSaw.Cut;
begin
if not(FBusy) then
begin
FBusy := True;
try
Do_Cut;
finally
FBusy := False;
end;
end
else
begin
end;
end;
procedure TSaw.Initialise;
begin
FBusy := False;
end;
procedure TSaw.Finalise;
begin
end;
procedure TSaw.MyOnClickEvent(ASender : TObject);
begin
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:
TRipSaw = class(TSaw)
private
FWood : TWood;
protected
procedure Do_Cut; override;
procedure Initialise; override;
procedure Finalise; override;
end;
procedure TRipSaw.Do_Cut;
begin
end;
procedure TRipSaw.Initialise;
begin
inherited Initialise;
FWood := TWood.Create;
end;
procedure TRipSaw.Finalise;
begin
FreeAndNil(FWood);
end;
THackSaw = class(TSaw)
private
FMetal : TMetal;
protected
procedure Do_Cut; override;
procedure Initialise; override;
procedure Finalise; override;
end;
procedure THackSaw.Do_Cut;
begin
end;
procedure THackSaw.Initialise;
begin
inherited Initialise;
FMetal := TMetal.Create;
end;
procedure THackSaw.Finalise;
begin
FreeAndNil(FMEtal);
end;
I chose the example above because of the following facts:
- A ripsaw and a hacksaw are both saws
- A ripsaw cuts wood but cannot cut metal
- 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:
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:
var
Saw_Ref : TMyRefSaw;
MySaw : TSaw;
begin
SawRef := THackSaw;
MySaw := Saw_Ref.Create;
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:
<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:
[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:
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...
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:
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:
constructor TConfigurator.Create(
const AConfigurationFileExtension : string;
const AParentSectionsDelimiter : Char);
var
LApplicationFileName : array[0..MAX_PATH] of Char;
begin
inherited Create;
FConfigurationFileExtenstion := AConfigurationFileExtension;
FParentSectionsDelimiter := AParentSectionsDelimiter;
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:
your implementation...
procedure Initialise; virtual;
procedure Finalise; virtual;
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;
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;
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