Base Forms and Interfaces
You have seen that when you need two similar forms in an application, you can use visual form inheritance to inherit one from the other or both of them from a common ancestor. The advantage of visual form inheritance is that you can use it to inherit the visual definition: the DFM. However, this is not always required.At times, you might want several forms to exhibit a common behavior or respond to the same commands without having any shared component or user interface elements. Using visual form inheritance with a base form that has no extra components makes little sense to me. I rather prefer to define my own custom form class, inherited from TForm, and then manually edit the form class declarations to inherit from this custom base form class instead of the standard one. If you only need to define shared methods or override TForm virtual methods in a consistent way, defining custom form classes can be a good idea.
Using a Base Form Class
A simple demonstration of this technique is available in the FormIntf demo; it also showcases the use of interfaces for forms. In a new unit called SaveStatusForm, I've defined the following form class (with no related DFM file—instead of using the New Form command, create a new unit and type the code in it):
type
TSaveStatusForm = class (TForm)
protected
procedure DoCreate; override;
procedure DoDestroy; override;
end;
The two overridden methods are called at the same time as the event handler so you can attach extra code (allowing the event handler to be defined as usual). Inside the two methods, you load or save the form position in an INI file of the application, in a section marked with the form caption. Here is the code for the two methods:
procedure TSaveStatusForm.DoCreate;
var
Ini: TIniFile;
begin
inherited;
Ini := TIniFile.Create (ExtractFileName (Application.ExeName));
Left := Ini.ReadInteger(Caption, 'Left', Left);
Top := Ini.ReadInteger(Caption, 'Top', Top);
Width := Ini.ReadInteger(Caption, 'Width', Width);
Height := Ini.ReadInteger(Caption, 'Height', Height);
Ini.Free;
end;
procedure TSaveStatusForm.DoDestroy;
var
Ini: TIniFile;
begin
Ini := TIniFile.Create (ExtractFileName (Application.ExeName));
Ini.WriteInteger(Caption, 'Left', Left);
Ini.WriteInteger(Caption, 'Top', Top);
Ini.WriteInteger(Caption, 'Width', Width);
Ini.WriteInteger(Caption, 'Height', Height);
Ini.Free;
inherited;
end;
Again, this is a simple common behavior for your forms, but you can define a complex class here. To use this as a base class for the forms you create, let Delphi create the forms as usual (with no inheritance) and then update the form declaration to something like the following:
type
TFormBitmap = class(TSaveStatusForm)
Image1: TImage;
OpenPictureDialog1: TOpenPictureDialog;
...
Simple as it seems, this technique is very powerful, because all you need to do is change the definition of your application's forms to refer to this base class. If even this step is too tedious, because you might want to change this base class in your program at some point, you can use an extra trick: "interposer" classes.
To save information about the status of an application in order to restore it the next time the program is executed, you can use the explicit support Windows provides for storing this kind of information. INI files, the old Windows standard, are once again the preferred way to save application data. The alternative is the Registry, which is still quite popular. Delphi provides ready-to-use classes to manipulate both.The TIniFile Class
For INI files, Delphi has a TIniFile class. Once you have created an object of this class and connected it to a file, you can read and write information to it. To create the object, you need to call the constructor, passing a filename to it, as in the following code:
var
IniFile: TIniFile;
begin
IniFile := TIniFile.Create ('myprogram.ini');
There are two choices for the location of the INI file. The code just listed will store the file in the Windows directory or a user folder for settings in Windows 2000. To store data locally to the application (as opposed to local to the current user), you should provide a full path to the constructor.INI files are divided into sections, each indicated by a name enclosed in square brackets. Each section can contain multiple items of three possible kinds: strings, integers, or Booleans. The TIniFile class has three Read methods, one for each kind of data: ReadBool, ReadInteger, and ReadString. There are also three corresponding methods to write the data: WriteBool, WriteInteger, and WriteString. Other methods allow you to read or erase a whole section. In the Read methods, you can also specify a default value to be used if the corresponding entry doesn't exist in the INI file.Notice that Delphi uses INI files quite often, but they are disguised with different names. For example, the desktop (.dsk) and options (.dof) files are structured as INI files.The TRegistry and TRegIniFile classes
The Registry is a hierarchical database of information about the computer, software configuration, and user preferences. Windows has a set of API functions to interact with the Registry; you basically open a key (or folder) and then work with subkeys (or subfolders) and values (or items), but you must be aware of the structure and the details of the Registry.Delphi provides two approaches to using the Registry. The TRegistry class provides a generic encapsulation of the Registry API, whereas the TRegIniFile class provides the interface of the TIniFile class but saves the data in the Registry. This class is the natural choice for portability between INI-based and Registry-based versions of the same program. When you create a TRegIniFile object, your data ends up in the current user information, so you'll generally use a constructor like this:
IniFile := TRegIniFile.Create ('Software\MyCompany\MyProgram');
By using the TIniFile and the TRegIniFile classes offered by the VCL, you can move from one model of local and per-user storage to the other. Not that I think you should use the Registry much, because having a centralized repository for the settings of each application was an architectural error—even Microsoft acknowledges this fact (without really admitting the error) by suggesting, in the Windows 2000 Compatibility Requirements, that you no longer use the Registry for applications settings, but instead go back to using INI files within the Documents and Settings folder of the current user (something not many programmers know of ).
An Extra Trick: Interposer Classes
In contrast with Delphi VCL components, which must have unique names, Delphi classes in general must be unique only within their unit. Thus you can have two different units defining a class with the same name. This technique looks weird at first sight, but can be useful. For example, Borland uses this approach to provide compatibility between VCL and VisualCLX classes. Both have a TForm class, one defined in the Forms unit and the other in the QForms unit.
Note | This technique is much older than CLX/VCL. For example, the service and control panel applet units define their own TApplication object, which is not related to the TApplication used by VCL visual GUI applications and defined in the Forms unit. |
I've seen a technique called "interposer classes" mentioned in an old issue of The Delphi Magazine. It suggested replacing standard Delphi class names with your own versions that have the same class name. This way, you can use Delphi's form designer and refer to Delphi standard components at design time, but use your own classes at run time.The idea is simple. In the SaveStatusForm unit, you can define the new form class as follows:
type
TForm = class (Forms.TForm)
protected
procedure DoCreate; override;
procedure DoDestroy; override;
end;
This class is called TForm, and it inherits from TForm of the Forms unit (this last reference is compulsory to avoid a kind of recursive definition). In the rest of the program, you don't need to change the class definition for your form, but simply add the unit defining the interposer class (the SaveStatusForm unit in this case) in the uses statement after the unit defining the Delphi class. The order of the unit in the uses statement is important, and is the reason some people criticize this technique, because it is difficult to know what is going on. I have to agree: I find interposer classes handy at times (more for components than for forms), but their use makes programs less readable and at times harder to debug.
Using Interfaces
Another technique, which is slightly more complex but even more powerful than the definition of a common base form class, is to create forms that implement specific interfaces. You can have forms that implement one or more of these interfaces, query each form for the interfaces it implements, and call the supported methods.As an example (available in the same FormIntf program I began discussing in the last section), I've defined a simple interface for loading and storing:
type
IFormOperations = interface
['{DACFDB76-0703-4A40-A951-10D140B4A2A0}']
procedure Load;
procedure Save;
end;
Each form can optionally implement this interface, as in the following TFormBitmap class:
type
TFormBitmap = class(TForm, IFormOperations)
Image1: TImage;
OpenPictureDialog1: TOpenPictureDialog;
SavePictureDialog1: TSavePictureDialog;
public
procedure Load;
procedure Save;
end;
The example code includes the Load and Save methods, which use the standard dialog boxes to load or save the image. (In the example's code, the form also inherits from the TSaveStatusForm class.)When an application has one or more forms implementing interfaces, you can apply a given interface method to all the forms supporting it, with code like this (extracted from the main form of the FormIntf example):
procedure TFormMain.btnLoadClick(Sender: TObject);
var
i: Integer;
iFormOp: IFormOperations;
begin
for i := 0 to Screen.FormCount - 1 do
if Supports (Screen.Forms [i], IFormOperations, iFormOp) then
iFormOp.Load;
end;
Consider a business application in which you can synchronize all the forms to the data of a specific company or a specific business event. Also consider that, unlike inheritance, you can have several forms that implement multiple interfaces, with unlimited combinations. This is why using such an architecture can improve a complex Delphi application a great deal, making it much more flexible and easier to adapt to implementation changes.