Marco's Web Center

Delphi Developers' Handbook

Chapter 15: Other Delphi Extensions

Copyright Marco Cantu' 1997

Using Custom Design Modules

In Delphi 1, you could only design forms based on classes derived directly from TForm. In Delphi 2, Borland added form inheritance and data modules as editable component containers. With Delphi 3, Borland has introduced different kinds of design- time forms, including Active Forms, Web data modules, remote data modules, and many others. To make the design-time architecture flexible, Delphi 3 introduced the ability to specify a base class for a form designer, and also to customize a form designer to display specific menu items for that base class.

By creating a custom form designer you’ll be able to extend the idea of a component editor to manipulating entire forms or modules. Unfortunately, we haven’t found a way to customize the designer that Delphi uses for standard forms. Along the way to our custom form designer, we’ll see many interesting examples, including how to publish and view your own form properties in the Object Inspector, which is a significant breakthrough. However, let’s proceed step-by-step, and introduce the new VCL functions and some key ideas before we begin building the examples.

The TCustomModule Class and the RegisterCustomModule Procedure

The key step in creating your own form designer in Delphi is the RegisterCustomModule registration procedure:

procedure RegisterCustomModule(
  ComponentBaseClass: TComponentClass;
  CustomModuleClass: TCustomModuleClass);
The first parameter is the base class of the class you want to edit, such as TForm; the second parameter is the form editor’s class (the class of the editor that the designer activates).

The first thing to notice is that the object you pass as the first parameter can be of a class derived from TCustomForm (including the TForm class), from TDataModule, from TWinControl or from another component class. As documented in the source code of the DsgnIntf unit, the only compulsory element is that the base class must call the InitInheritedComponent in its constructor to load the proper form description from the associated DFM file. As you’d expect, the behavior of the designer depends on the ComponentBaseClass type:

  • If your form is a TCustomForm descendant, you’ll be able to edit it as usual, but you’ll have access to all the published properties of the base class (which is now your class instead of TForm) in the Object Inspector.
  • If the class derives from TWinControl instead of TCustomForm, Delphi places an instance of that control in a temporary design-time form. This is a good technique for creating compound components, like aggregate components based on a panel, as we’ll see in the section "Creating a Compound Component".
  • If the class derives from TComponent, Delphi will create a nonvisual container like that of a data module. You’ll notice that the comments in the source code file imply that you must derive the class from TDataModule, which isn’t true. Even using TComponent as the ComponentBaseClass type generates a valid designer.

The other important element of the module’s registration is the custom designer. Delphi provides (in the DsgnIntf unit) a TCustomModule class.You can use this as a base class from which to derive new classes; but you can also use it directly, since it isn’t an abstract class:

type
  TCustomModule = class
  private
    FRoot: TComponent;
  public
    constructor Create(ARoot: TComponent); virtual;
    procedure ExecuteVerb(Index: Integer); virtual;
    function GetAttributes: TCustomModuleAttributes; virtual;
    function GetVerb(Index: Integer): string; virtual;
    function GetVerbCount: Integer; virtual;
    procedure Saving; virtual;
    procedure ValidateComponent(Component: TComponent); virtual;
    property Root: TComponent read FRoot;
  end;
The constructor saves the form or data module we are working on as the Root property, so we’ll have access to it. The GetVerbCount, GetVerb, and ExecuteVerb methods create the local menu and run the associated commands, using the approach first implemented by the VCS interface and the component editors.

The GetAttributes method is used only by editors of a TWinControl to determine the alignment of the control you are designing, relative to the temporary form containing it. Currently, there’s only one attribute you can retrieve using this method, which suggests that it’s there to support future extensions. Delphi calls the other two methods at specific times: it calls the Saving method every time you save a file (before saving the file) to allow you to perform specific processing; and it calls the ValidateComponent method every time you add a new component (passed as a parameter) to the designer, which allows you to interrupt the operation if the component is not a valid type. In fact, this is exactly what happens when you add a TWinControl-derived component to a data module.

Before we look at specific examples, let’s reconsider the difference between the TForm class and its base class, TCustomForm. Borland added this new class to allow you to create a new designer based on a form that doesn’t have the standard properties and methods of a TForm form. The relationship between these classes is the same as that between TEdit and TCustomEdit, (as well as all the other TCustom... classes of the VCL). To clarify this further, here’s the definition of the TForm class (for Delphi 3, since the TCustomForm class wasn’t available before):

type
  TForm = class(TCustomForm)
  public
    procedure ArrangeIcons;
    procedure Cascade;
    procedure Next;
    procedure Previous;
    procedure Tile;
    property ActiveMDIChild;
    property ClientHandle;
    property MDIChildCount;
    property MDIChildren;
    property TileMode;
  published
    property ActiveControl;
    property BorderIcons;
    property BorderStyle;
    property AutoScroll;
    property Caption;
    property ClientHeight;
    property ClientWidth;
    // and so on, including events...
  end;

Publishing the Custom Properties of a Form

Our first example of a custom designer will show you something users of earlier versions of Delphi have longed for. If you adhere to good OOP programming conventions, you should be frequently adding properties to your form classes. However, the properties you add to a form (even if they’re published) won’t appear in the Object Inspector. Although there’s no "magic" way to add a property to a form and see it immediately in the Object Inspector, you can compile the form class, add it to a package, and then see the new properties in a further inherited class.

In the FormWVal unit (part of the Chapter 15 package and therefore included in the Comps directory for this chapter), we’ve declared the following form:

type
  TFormWithValue = class (TForm)
  private
    fValue: Integer;
    fOnChangeValue: TNotifyEvent;
    procedure SetValue (Value: Integer);
  published
    property Value: Integer
      read fValue write SetValue;
    property OnChangeValue: TNotifyEvent
      read fOnChangeValue write fOnChangeValue;
  end;
This form class defines a new property that stores a value, and an event that is triggered when the value changes:
procedure TFormWithValue.SetValue (Value: Integer);
begin
  if Value <> fValue then
  begin
    fValue := Value;
    if Assigned (fOnChangeValue) then
      fOnChangeValue (self);
  end;
end; 
In the Register procedure, we install this form as a new designer, but we associate it with the standard custom module:
RegisterCustomModule (TFormWithValue, TCustomModule);

How can we create a new module of this class at design-time? Simply registering a new form as a custom module (using the call above) adds it to the system, but Delphi doesn’t automatically make it available in the File » New dialog box. So we’re on our own again, and we must create the new form by (as you might guess) writing a new wizard. In this case, we’ve written a new bare-bones standard wizard, which displays a Form With Value Wizard menu item that executes the following Execute method:

procedure TFormWithValueExpert.Execute;
var
  ModuleName, FormName, FileName: string;
  ModIntf: TIModuleInterface;
begin
  ToolServices.GetNewModuleAndClassName (
    'FormWithValue', ModuleName, FormName, FileName);
  ModIntf := ToolServices.CreateModuleEx (FileName, FormName,
    'FormWithValue', '', nil, nil,
    [cmNewForm, cmAddToProject, cmUnNamed]);
  ModIntf.ShowSource;
  ModIntf.ShowForm;
  ModIntf.Release;
end;

The first call, GetNewModuleAndClassName, retrieves the unit and class name. You’ll notice the use of the first parameter, which indicates a prefix for the class name: By default, Delphi adds a T in front of the class name, and a form number after it, such as TFormWithValue1. The second and more important call, CreateModuleEx, generates the new module and adds it to the current project. Again we have to specify the base class, but this time without the initial T, which is quite odd! This method returns a module interface we can use to open the source code file in the editor and display the form. The result is a new design-time form, derived from the given type and with the given properties, as you can see in Figure 15.7.

Figure 15.7: You can install in Delphi a new designer, and add custom properties to a form within the environment, so that they’ll appear in the Object Inspector.
Figure 15.7

Creating a Compound Component

Another interesting technique is to use a panel component as a designer that hosts other components. Using this approach, we can use Delphi to design compound components. Actually this is just a suggestion: the technique is flexible enough that you can use it to edit almost any component as if it were a form!

Here’s the registration code of the new PanelEd unit (the source code of this new wizard is in the Comps directory, as usual):

procedure Register;
begin
  RegisterCustomModule (TPanel, TPanelModule);
  RegisterLibraryExpert(TPanelEditExpert.Create);
end;

Simple, isn’t it? As in the last example, we’ve built a Panel Edit Wizard to create new modules based on this custom module. Here’s the updated version of the Execute method:

procedure TPanelEditExpert.Execute;
var
  ModuleName, FormName, FileName: string;
  ModIntf: TIModuleInterface;
begin
  ToolServices.GetNewModuleAndClassName (
    'Panel', ModuleName, FormName, FileName);
  ModIntf := ToolServices.CreateModuleEx (FileName, FormName,
    'Panel', '', nil, nil,
    [cmNewForm, cmAddToProject, cmUnNamed]);
  ModIntf.ShowSource;
  ModIntf.ShowForm;
  ModIntf.Release;
end;

This time, however, we’ve also defined a new custom module class for the editor. This custom module allows users to place only Button controls in the panel at design-time, and it provides a popup menu as a shortcut to change the name of the component. Here’s the declaration of the custom module class:

type
  TPanelModule = class (TCustomModule)
  public
    procedure ExecuteVerb(Index: Integer); override;
    function GetVerb(Index: Integer): string; override;
    function GetVerbCount: Integer; override;
    procedure ValidateComponent(Component: TComponent); override;
  end;
The code of its methods is actually quite simple, but we’ve included it here to show how easily you can define a custom module designer:
function TPanelModule.GetVerbCount: Integer;
begin
  Result := 1;
end;
 
function TPanelModule.GetVerb(Index: Integer): string;
begin
  if Index = 0 then
    Result:= 'Rename...';
end;
 
procedure TPanelModule.ExecuteVerb(Index: Integer);
var
  NewName: string;
begin
  if Index = 0 then
  begin
    NewName := Root.Name;
    if InputQuery ('Panel Module Editor',
        'New panel name:', NewName) then
      Root.Name := NewName;
  end;
end;
 
procedure TPanelModule.ValidateComponent(Component: TComponent);
begin
  if not (Component is TButton) and
      not (Component is TSpeedButton) then
    raise Exception.Create ('The panel can host only buttons');
end;

As soon as you add this unit to a package (it’s part of the Chapter 15 package), and activate the Panel Edit Wizard, Delphi displays an editor for the panel, as you can see in Figure 15.8. Although there is a form surrounding the panel, it isn’t really used, and it’s not saved in the DFM or PAS files for this module. Here’s the complete DFM file for a very simple example (notice that there is no form!):

object Panel2: TPanel2
  Left = 0
  Top = 0
  Width = 290
  Height = 173
  TabOrder = 0
  object Button1: TButton
    Left = 96
    Top = 72
    Width = 75
    Height = 25
    Caption = 'Button1'
    TabOrder = 0
  end
end
Figure 15.8: In Delphi you can install a designer for a Panel, and edit it in the environment as usual.
Figure 15.8

And here is the complete PAS file generated by Delphi:

unit Unit2;
 
interface
 
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, 
  Forms, Dialogs, StdCtrls;
 
type
  TPanel2 = class(TPanel)
    Button1: TButton;
  private
    { Private declarations }
  public
    { Public declarations }
  end;
 
var
  Panel2: TPanel2;
 
implementation
 
{$R *.DFM}
 
end.
If you try to add a non-Button component to the panel, you’ll see an error message (the exception raised by the designer); and if you right-click on the form you’ll see a new entry in the designer’s local menu, as shown in Figure 15.9. The extra menu items allow you to change the name of the derived panel class and the name of the object.
Figure 15.9: The extra menu of our panel’s custom designer. We’ll install this panel as a component.
Figure 15.9

Now you might ask, "we can edit the panel, but what’s this for?" We’ve used the panel of Figure 15.9 in two different ways: directly inside a program and as the basis of a new component. In both cases we have a problem: Delphi saves the status of the panel in a DFM file, but it can’t reload the panel from the file. In fact, instead of using TPanel as a designer, we probably should have created a custom subclass to manage the initialization. However, for simplicity we’ve decided to add this code to the panel (something you might want to avoid if you need many such panels). Here’s the custom constructor we add to the derived panel:

constructor TPanel3Button.Create (AOwner: TComponent);
begin
  inherited Create (AOwner);
  InitInheritedComponent (self, TPanel);
end;
In the CustPane directory on the companion CD you’ll find this code, simple event handlers for the three buttons, and the PanelPrj project. This project has a main form plus the panel. You now have to remove the panel from the list of automatically created forms in the Project Options (after all, it isn’t a form), so you can use the panel by writing code like this:
procedure TForm1.FormCreate(Sender: TObject);
begin
  Panel3Button := TPanel3Button.Create (Application);
  Panel3Button.Parent := self;
end;
This code will display the panel at run-time inside the main form, as you can see in Figure 15.10.
Figure 15.10: You can use the custom panel directly inside a program, as demonstrated by the PanelPrj example (located in the CustPane directory).
Figure 15.10

At the same time, you can add a Register procedure to the panel source code created by Delphi, and install the panel as a component:

procedure Register;
begin
  RegisterComponents ('DDHB', [TPanel3Button]);
end;
Now, simply adding this unit to a package will turn the panel we’ve built with the custom designer into a fully working component! As usual, we’ve installed this component in the PanelPack package (stored in the CustPane directory, as well). We’ve also build a very simple project based on this component. You can find it in the PaneDemo directory, and see it at design time in Figure 15.11. As you can see in the Object Inspector, there is only one component in the form, which means that the project considers the compound component to be a single component.
Figure 15.11: The PaneDemo project uses the panel component build with the panel designer we’ve just installed in Delphi.
Figure 15.11

Besides designing special classes, such as the panel we just built, you can apply the same technique to many other uses. You can even create nonvisual compound components based on the TComponent class. Simply write:

RegisterCustomModule (TComponent, TCustomModule);
and generate the module in a wizard exactly as before:
  ToolServices.GetNewModuleAndClassName (
    'Component', ModuleName, FormName, FileName);
  ModIntf := ToolServices.CreateModuleEx (FileName, FormName,
    'Component', '', nil, nil,
    [cmNewForm, cmAddToProject, cmUnNamed]);
  ModIntf.ShowSource;
  ModIntf.ShowForm;
  ModIntf.Release;
Executing this code produces something similar to a data module, but the base class will be TComponent. Again you’ll need to add the code for the constructor to this component, and call the InitInheritedComponent method.

Since the practical use of a generic designer seems somewhat limited, we’ve decided not to build more examples. To pursue this further, you may want to consider creating a wizard that allows you to choose the base class for the designer, and, if necessary, generates the proper constructor code automatically (you’ll need to do this only if the base class is not TDataModule or TCustomForm, because we’ve already written similar code).

[Chapter 15 Index]