In this example, we’ll be creating a simple application in which the main form displays a list of possible secondary forms to create:
Keeping things simple, these secondary windows will just be forms with different colours rather than forms with functional differences:
Nonetheless, their classes will be added to a central registry so that the main form will be completely ignorant of where they are actually defined: on startup, it will just poll for what classes are available, and thereafter not distinguish between those that are returned.
To begin, create a new VCL forms application, add a TListView and TPanel to the main form, followed by a TButton to the
panel. Call the controls lsvForms, panRight and btnCreateForm respectively, before arranging them in the designer to look
like the following:
Here, the panel has its Align property set to alRight, with the list view’s to alClient; the list view’s AlignWithMargins
property has also been set to True, and its ViewStyle set to vsReport. The columns have then been defined using the
columns editor, invoked by double-clicking on the list view. Finally, the button’s Default property has been set to True.
Next, we’ll define the base class and registered form type list. To do this, add a new unit to the project (an easy way is to right click on the EXE’s node in the Project Manager, select Add New, then Unit), and save it as ClientForms.pas. The base
class will be a direct descendant of TForm, with the list of registered descendants being held as static data internal to the
base class itself. This list will then be available for read-only access by external units. To this effect, amend the new unit’s interface section — i.e., its top half — to look like this:
unit ClientForms;
interface
uses
System.Generics.Collections, Vcl.Controls, Vcl.Forms;
type
TClientForm = class;
TClientFormClass = class of TClientForm;
TClientForm = class(TForm) private class var
FRegisteredForms: TList<TClientFormClass>; class constructor Create;
class destructor Destroy; protected
class procedure RegisterForm; public
class function Title: string; virtual; abstract; class function Description: string; virtual; abstract; end;
function RegisteredClientForms: TEnumerable<TClientFormClass>;
A few things may need explaining here. Firstly, notice how the explicit metaclass type is declared before the class itself, a fact that requires a ‘forward declaration’ of the class at the top (i.e., the TClientForm = class; line). This is because the
metaclass is referred to within the class definition itself (viz., in the type of FRegisteredForms), and the Delphi compiler
(following Pascal tradition) works in a ‘top down’ fashion.
Secondly, the return type of the RegisteredClientForms function may look a bit odd. The explanation is that TList<T>
descends from TEnumerable<T>, and TEnumerable<T> defines only read-only access to the contained items. Since we want
client units to only have read-only access to our registered class list, we can therefore simply expose it typed to its parent class to get the desired outcome.
Lastly, notice that a virtual constructor isn’t defined, contrary to my earlier advice. The reason is that an ancestor class (specifically, TComponent) has already declared one. Consequently, there’s no need to declare our own, notwithstanding
the fact we could.
The implementation section of the unit (i.e., its bottom half) is trivial:
implementation
function RegisteredClientForms: TEnumerable<TClientFormClass>; begin
Result := TClientForm.FRegisteredForms; end;
class constructor TClientForm.Create; begin
FRegisteredForms := TList<TClientFormClass>.Create; end;
class destructor TClientForm.Destroy; begin
FRegisteredForms.Free; end;
class procedure TClientForm.RegisterForm; begin
if not FRegisteredForms.Contains(Self) then FRegisteredForms.Add(Self);
end;
end.
On start-up, the main form can therefore call the RegisteredClientForms function, populating the list view with the
information returned. In this, it will enumerate registered classes, retrieving the metadata added in the form of virtual class functions — the client forms’ titles and descriptions. The reference to each metaclass itself can then be stored in the respective list view item’s Data property, to be retrieved and used when the user clicks the Create Form button.
Let’s now add some code behind the main form. Firstly, create an OnCreate event handler, either via the Events tab of
Object Inspector or by double clicking on a blank area of the form in the designer. Implement the handler as thus:
procedure TfrmMain.FormCreate(Sender: TObject); var FormClass: TClientFormClass; NewItem: TListItem; begin lsvForms.Items.BeginUpdate; try
for FormClass in RegisteredClientForms do begin NewItem := lsvForms.Items.Add; NewItem.Caption := FormClass.Title; NewItem.SubItems.Add(FormClass.Description); NewItem.Data := FormClass; end; finally lsvForms.Items.EndUpdate; end; end;
To compile, you’ll also need to add ClientForms to a uses clause — either do it manually or via File|Use Unit... (it can just
go into the implementation section’s uses clause).
Next we will define the button’s OnClick handler. Create the stub by double clicking on the button, then amend the
generated code to look like the following:
procedure TfrmMain.btnCreateFormClick(Sender: TObject); var
NewForm: TForm; begin if lsvForms.SelCount = 0 then begin Beep; Exit; end; NewForm := TClientFormClass(lsvForms.Selected.Data).Create(Self); NewForm.Show; end;
If you so wish, you can also assign this method (btnCreateFormClick) as the list view’s OnDblClick handle. Either way,
compile and run the application. All should be well and good, but for a lack of registered form classes!
Let’s then define a few. Add a new form to the project as normal (e.g. by right-clicking on the EXE’s node in the Project Manager, selecting Add New, then Form), before adding ClientForms to its interface section uses clause and manually editing
its base class to TClientForm. The Title and Description class methods can (and should) then be overridden. Since the
forms are being created on request, you should also ensure they don’t appear in the application’s auto-create list (Project|Options, Forms).
For the demo, just add a button called btnClose on each client form to close it: unit RedClientForm;
interface
uses
System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls, ClientForms;
type
TfrmRedClient = class(TClientForm) btnClose: TButton;
procedure btnCloseClick(Sender: TObject); public
class function Title: string; override; class function Description: string; override; end;
implementation
{$R *.dfm}
class function TfrmRedClient.Title: string; begin
Result := 'Red form'; end;
class function TfrmRedClient.Description: string; begin
Result := 'A form that''s Labour and proud of it'; end;
procedure TfrmRedClient.btnCloseClick(Sender: TObject); begin Close; end; initialization TfrmRedClient.RegisterForm; end.
Notice the form is registered not in a class constructor but the unit’s initialization section. The reason is simple: given
the class is not explicitly referenced elsewhere, the class constructor would never be called! You can find the full code for this demo in the book’s source code repository (http://delphi- foundations.googlecode.com/svn/trunk/).