Delphi Clinic C++Builder Gate Delphi Notes Weblog Delphi for .NET Prism for .NET
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics
 Delphi Property and Component Editors
See Also: Delphi Papers and Columns

Delphi offers an Open Tools API, to allow programmers to extent the functionality of the Delphi IDE itself. There are four kinds of Open Tools APIs: property editors, component editors, experts/wizards and version control systems. This paper will focus on Property and Component Editors, giving several examples of how to write our own Delphi Property and Component Editors.

Property Editors
Property editors are extensions of the Delphi IDE. That may sound very difficult or complex, but is in fact very easy. We can even construct a property editor without even knowing it - for enumerated types, for example. Remember the colour property of a TForm? When we want to enter a value, we get a dropdown-list where all possible choices are given for us. That's an enumerated type property editor, a very easy one, and we can make one with only a few lines of code (nothing special). Note that in this case the programmer, who may be programming for the best blog hosting site or a company site, is not actually writing a property editor, he is merely instructing Delphi to use the enumerated property editor, customized for his custom enumeration.

Existing Property Editors
Before we actually take a look what a property editor looks like from the inside, let's first examine what kinds of property editors already exist in Delphi. To do that, start a new project, add "uses DsgnIntf;" to the implementation section, compile, open the browser and search for TPropertyEditor (just type TPrope):

Object Browser

If we count correctly, then there are at least 21 custom property editors registered by the DSGNINTF unit. Note, however, that there are actually far more property editors available in other units, like the TPictureEditor for images and bitmaps in C:\DELPHI\LIB\PICEDIT.DCU.

TPropertyEditor
The Object Inspector will offer default editing for all kinds of properties. Using a special property editor, we can override this behaviour in several different ways (the 21 pre-defined property editors all extend the Object Inspector in how to work with properties). What does a property editor actually look like from the inside? Well, for starters, it is derived from a base class, from which we need to override some methods in order to make things work our way. The TPropertyEditor base class is defined as follows (I've left out the private parts, as we can't touch them anyway). The five new Delphi 2.0 methods - three of which are "variant" related - have been entered between compiler {$IFDEF WIN32} statements to make the following code definition valid for all versions of Delphi:


Type
  TPropertyEditor = class
  protected
    function GetPropInfo: PPropInfo;
    function GetFloatValue: Extended;
    function GetFloatValueAt(Index: Integer): Extended;
    function GetMethodValue: TMethod;
    function GetMethodValueAt(Index: Integer): TMethod;
    function GetOrdValue: Longint;
    function GetOrdValueAt(Index: Integer): Longint;
    function GetStrValue: string;
    function GetStrValueAt(Index: Integer): string;
  {$IFDEF WIN32}
    function GetVarValue: variant;
    function GetVarValueAt(Index: Integer): variant;
  {$ENDIF}
    procedure Modified;
    procedure SetFloatValue(Value: Extended);
    procedure SetMethodValue(const Value: TMethod);
    procedure SetOrdValue(Value: Longint);
    procedure SetStrValue(const Value: string);
  {$IFDEF WIN32}
    procedure SetVarValue(const Value: variant);
  {$ENDIF}

  public
    destructor Destroy; override;

    procedure Activate; virtual;
    function AllEqual: Boolean; virtual;
    procedure Edit; virtual;
    function GetAttributes: TPropertyAttributes; virtual;
    function GetComponent(Index: Integer): TComponent;
    function GetEditLimit: Integer; virtual;
    function GetName: string; virtual;
    procedure GetProperties(Proc: TGetPropEditProc); virtual;
    function GetPropType: PTypeInfo;
    function GetValue: string; virtual;
    procedure GetValues(Proc: TGetStrProc); virtual;
    procedure Initialize; virtual;
  {$IFDEF WIN32}
    procedure Revert;
  {$ENDIF}
    procedure SetValue(const Value: string); virtual;
  {$IFDEF WIN32}
    procedure ValueAvailable: Boolean;
  {$ENDIF}

    property Designer: TFormDesigner read FDesigner;
    property PrivateDirectory: string read GetPrivateDirectory;
    property PropCount: Integer read FPropCount;
    property Value: string read GetValue write SetValue;
  end;

A TPropertyEditor edits a property of a component, or list of components, selected into the Object Inspector. The property editor is created based on the type of the property being edited as determined by the types registered by RegisterPropertyEditor. An example that indicates how the programmer is to use this procedure will follow shortly. Each published property of a component is shown by the Object Inspector, and a property editor (for the property type) is used whenever the designer wants to read or write the value of the property.

For this session, we will only focus on an important subset of the methods that can be overridden to change the behaviour of the property editor, to give us a good idea what the most important possibilities are.

TFileNameProperty
With these few basic (but important) methods we already have enough power at our disposal to write our first non-trivial property editor; an open file name dialog property editor for filename properties! We must remember that writing components is essentially a non-visual task, and writing property editors is no different. We have to write a new unit by hand in the editor (see the listing for unit FileName below). We need to specify that we want a 'Dialog' type of property editor, so we return [paDialog] in the GetAttributes function. Then, we can do as we like in the Edit procedure, which in this case involves a TOpenDialog to let us find any existing file.


unit FileName;
interface
uses
  SysUtils, DsgnIntf;

Type
  TFileNameProperty = class(TStringProperty)
  public
    function GetAttributes: TPropertyAttributes; override;
    procedure Edit; override;
  end;

  procedure Register;

implementation
uses
  Dialogs, Forms;

  function TFileNameProperty.GetAttributes: TPropertyAttributes;
  begin
    Result := [paDialog]
  end {GetAttributes};

  procedure TFileNameProperty.Edit;
  begin
    with TOpenDialog.Create(Application) do
    try
      Title := GetName; { name of property as OpenDialog caption }
      Filename := GetValue;
      Filter := 'All Files (*.*)|*.*';
      HelpContext := 0;
      Options := Options + [ofShowHelp, ofPathMustExist, ofFileMustExist];
      if Execute then SetValue(Filename);
    finally
      Free
    end
  end {Edit};

  procedure Register;
  begin
    RegisterPropertyEditor(TypeInfo(TFileName),nil, '', TFileNameProperty)
  end;
end.

Note that we call the GetName function of the property editor to get the name of the actual property for which we want to fire up the TOpenDialog.

As stated before, a Property Editor needs a register procedure to register itself with Delphi (for actual usage in Delphi applications). We can register a property editor that only applies to the properties of a certain type in one component, or we can register a property editor to apply to all properties of a given type. For example, the TFileNameProperty in the example above will work for all properties of type TFileName on all components in our component library. Of course, the property editor must be installed and registered first.

To register a property inside the Register procedure, we need to call RegisterPropertyEditor with four parameters. The first one is the type-information pointer for the type of the property. In our case, we want to work only on properties of type TFileName, so we can use the built-in function TypeInfo on this type which results in TypeInfo(TFileName). The second one is the type of the component to which this editor applies. If this parameter is nil, then the editor applies to all properties of the given type for all components. In our case, we want the property editor to work for any property of type TFileName for all components, so we just leave the second parameter nil. The third parameter is the name of the property. This parameter only has meaning if the previous parameter specified a particular type of component (there is no real use in defining a property editor for only a property name without specifying the component as well). Again, we are interested in properties of type TFileName for any kind of component, and any name of the property itself, so we use an empty string here. The fourth parameter to RegisterPropertyEditor is the type of the property editor itself, (TFileNameProperty in our case).

Installing a property editor is just like installing a component. In this case, the property editor has its own register procedure (it's not a property editor for a specific component, but for a specific property). Generally, if a property editor is for a specific property of a specific component, then it's a good idea to let the property editor register itself together with that component. For now, let's just add the unit FILENAME width he TFileNameProperty to the Component Palette (using Options | Install Components in Delphi 1, or Component | Install in Delphi 2).

After installation, for a property of type TFileName in any component (like the TOpenDialog component), we see an elipsis to indicate that there is a dialog property editor installed for this property.

Object Inspector on property of type TFileName

If we click on the elipsis, this would result in the following open-file dialog for Delphi 2 running on Windows 95:

Win95 FileOpen Dialog

In only a few lines of code, we've written a TFileName property editor that will provide support at design time for all our components that use a property of type TFileName. This only illustrates that property editors have an enormous potential especially for designers of Delphi components and applications.

Before we continue with another property editor example, let's examine the other methods from the class TPropertyEditor that we can override:

Other properties and methods that are quite useful when creating a new TPropertyEditor class are the following:

TPicture Property Editor
So far, we've seen how to make property editors that behave like dialogs. And this reminds me of the most irritating property editor of Delphi today: the picture editor for glyphs, icons, bitmaps etc. It's not the fact that it doesn't work, it's just the fact that it isn't very user friendly. If we click on the Load button, then we get a TOpenDialog that gives us the option to select a .BMP, .ICO or whatever file we wish. However, we don't get to see what's actually inside this file, until we close the TOpenDialog. And then we're back in the Picture Editor, and decide it's not yet what we want, so we have to click on the Load button again and start all over again. Especially annoying if we have to browse through a directory with a lot of little bitmap files.

We'd want a preview option, to see what an image in a file looks like while we're browsing through a directory! This sounds exactly like a new property editor to me (note: since Borland didn't provide us with the source of PICEDIT.DCU, we have no use of PICEDIT.DFM either and just have to write our own picture editor instead of enhancing the already existing one)...

TImageForm
First of all, we have to design the actual dialog or form that will be used by our new property editor. I've designed something like the one below, where the image of Dr.Bob in the lower-right corner is in fact used to display the image of any file that is currently selected in the file listbox. Depending on our needs, we can even stretch this image (not recommended for little TBitBtn bitmap images, but useful if we have large bitmaps and want only a preview):

Win31:

Win95:

This form was one that was designed by hand, so it looks almost the same in the Windows 95 version for Delphi 2.0 (unlike the TOpenDialog component which is able to change its appearance on the different operating systems when recompiled).

Note that I've used the TDirectoryOutliner in this form. The optimised version, of course, as can be read in the Delphi Efficiency session of the BDC'96.

TPictureEditor
Now that we have a form to ask for the image to use, let's see how we can get this to work as a property editor. We need to take a look at GRAPHIC.PAS to see what kinds of pictures, glyphs and images exist in the first place. We are limited to two descendants of TPersistent: TPicture and TGraphic, with descendant TBitmap of TGraphic. For this column, let's just focus on .BMP files, and hence on TPicture and TBitmap classes only. This means we want to offer the new Image Property Editor for properties of type TPicture and TBitmap.


unit PictEdit;
interface
uses
  DsgnIntf;

Type
  TPictureEditor = class(TClassProperty)
  public
    function GetAttributes: TPropertyAttributes; override;
    procedure Edit; override;
  end;

  procedure Register;

implementation
uses
  SysUtils, Controls, Graphics, TypInfo, ImageFrm;

  function TPictureEditor.GetAttributes: TPropertyAttributes;
  begin
    Result := [paDialog]
  end {GetAttributes};

  procedure TPictureEditor.Edit;
  begin
    with TImageForm.Create(nil) do
    try
      ImageDrBob.Picture := TPicture(GetOrdValue);
      if ShowModal = mrOk then
      begin
        if (GetPropType^.Name = 'TPicture') then
          SetOrdValue(LongInt(ImageDrBob.Picture))
        else { Bitmap }
          SetOrdValue(LongInt(ImageDrBob.Picture.Bitmap))
      end
    finally
      Free
    end
  end {Edit};

  procedure Register;
  begin
    RegisterPropertyEditor(TypeInfo(TPicture), nil, '', TPictureEditor);
    RegisterPropertyEditor(TypeInfo(TBitmap), nil, '', TPictureEditor)
  end;
end.

Note that since we don't explicitly want the TPictureEditor to belong to one specific component, we have to register it ourselves here, and install it just like any other custom component or expert with the Options | Install Components dialog. After rebuilding our Component Library (remember to make that backup first!), we will get the new picture editor for each TPicture (in a TImage) or TBitmap (in a TSpeedButton or TBitBtn for example).

One last important thing: there already was a property editor for TPictures and TBitmaps; namely the picture editor Borland provided. Won't we get into trouble if we want to use the one of our own? No we won't, since the last property editor to register for a particular component or property type will actually override a previous one. So, if we ever install another property editor for TBitmaps, for example, we will have overridden the one we've just built. And we, on our turn, will now have overridden the default Borland picture editor with our new and enhanced TPictureEditor.

So far, we've only seen a few of the possible kinds of property editors we can write. We've especially focused on paDialog property editors; in my view the easiest way to customise the (developer)-entry of property values at design time. There are far more kinds and ways to write property editors, but I'll just have to leave you here, at a point where you can experiment with the TPropertyEditor class for yourself. For now, let's move on to the related topic of Component Editors. I hope we've seen you enough to chew on to write new property editors and to enhance your own Delphi Component Integrated Developers Environment!

Component Editors
Component editors are like property editors, in that they are used to enhance the integrated development environment of Delphi. And, like property editors, they are basically derived from a single base class where some abstract methods need to be overriden and re-defined in order to give the component editor the desired behaviour. Other than property editors, however, component editors are component, and not property, specific. They are bound to a particular component type, and are generally executed by a click with the right mouse button on the component (when dropped on a form). This way of activation is a bit different from property editors, but other than that, the process of writing our own component editor is essentially the same.

A component editor is created for each component that is selected in the form designer based on the component's type (see also GetComponentEditor and RegisterComponentEditor in DSGNINTF.PAS). When the component is double-clicked the Edit method is called. When the context menu for the component is invoked the GetVerbCount and GetVerb methods are called to build the menu. If one of the verbs is selected, then ExecuteVerb is called. Paste is called whenever the component is pasted to the clipboard. We only need to create a component editor if we wish to add verbs to the context menu, change the default double-click behaviour, or paste an additional clipboard format.

The class definition for the base class TComponentEditor can be found in DSGNINTF.PAS and is as follows (equal for both versions of Delphi).


Type
  TComponentEditor = class
  private
    FComponent: TComponent;
    FDesigner: TFormDesigner;
  public
    constructor Create(AComponent: TComponent;
                       ADesigner: TFormDesigner); virtual;
    procedure Edit; virtual;
    procedure ExecuteVerb(Index: Integer); virtual;
    function GetVerb(Index: Integer): string; virtual;
    function GetVerbCount: Integer; virtual;
    procedure Copy; virtual;

    property Component: TComponent read FComponent;
    property Designer: TFormDesigner read FDesigner;
  end;

There are six virtual methods that can be overriden by the component editor programmer (this leads to a somewhat easier editor building process compared to property editors):

Default Component Editor
Apart from the general type TComponentEditor, there is also a default component editor that is used by most components (unless another component editor is installed to override the default one). The TDefaultEditor default component editor implements Edit to search the properties of the component and to generate (or navigate to an already existing) the OnCreate, OnChange or OnClick event, whichever it finds first, or just the first alphabetic event handler which is available.


Type
  TDefaultEditor = class(TComponentEditor)
  private
    FFirst: TPropertyEditor;
    FBest: TPropertyEditor;
    FContinue: Boolean;
    procedure CheckEdit(PropertyEditor: TPropertyEditor);
  protected
    procedure EditProperty(PropertyEditor: TPropertyEditor;
                           var Continue, FreeEditor: Boolean); virtual;
  public
    procedure Edit; override;
  end;

Whenever the component editor modifies the component, it must call the Designer.Modified method to inform the designer that (the component on) the form has been modified. The Designer is the Designer property of type TFormDesigner that can be obtained from the TComponentEditor base class. Any component editor is able to talk to the designer (or at least tell the designer that the component has been modified). If we only use the component editor to display some information (like a general about box, for example) there is no need to inform the designer.

Custom Component Editors
As we saw in the Components chapter, when building custom components, there are generally a few possible base classes that can be considered. Every class from VCL is derived from the TObject root. The class TObject contains the Create and Destroy methods that are needed to create and destroy instances of classes. The class TPersistent, derived from TObject, contains methods for reading and writing properties to and from a form file. TComponent is the class to derive all components from, as it contains the methods and properties that allow Delphi to use TComponents as design elements, view their properties with the Object Inspector and place these components in the Component Palette.


  TObject 
  +- TPersistent 
     +- TComponent 
        +- TControl 
           +- TGraphicControl 
           +- TWinControl 
              +- TCustomControl 
If we want to create a new non-visual component from scratch, then TComponent is the class we need to derive from. Visual component classes are derived from the TControl class, that already contains the basic functionality for visual design components, like position, visibility, font and caption. Derived from TControl are TGraphicControl and TWinControl. The difference between a TGraphicControl and a TWinControl is the fact that a TWinControl contains an actual Windows Handle, while a TGraphicControl does not. Therefore, derived from a TWinControl we will find classes like the standard Windows controls, while controls like TBevel, TImage, TSpeedButton and TShape are derived from TGraphicControl. Finally, the class TCustomControl is much alike both TWinControl and TGraphicControl together.

Why would we need to rehash this information? Well, basically because we need to understand when the default component editor is useful in order to be able to determine when to write a custom component editor of our own. It would seem that since the default component editor is capable of generating event handler skeleton code for the OnCreate, OnChange or OnClick event, we would at first look for those components that don't have or need these events. Like a component that does not have a Windows handle (i.e. does not need the input focus at any given time), and certainly non-visual components derived from TComponent.

Another type of component where a Component Editor would be handy, is a common dialog component (such as the TOpenDialog, TSaveDialog, ColorDialog, PrintDialog and PrinterSetupDialog). The FontDialog, FindDialog and ReplaceDialog already do have some events, so these don't apply (the default component editor will put us in the code editor to write one of their event handlers). The five interesting dialogs have no events, and hence no default behaviour for the default component editor.

What could a component editor mean to these dialogs? Well, a preview of what they'd look like in the real-world would be nice. Have you never had the need to know if you've set all needed properties for the TOpenDialog? The only way we can check this is by running our application, which includes a way to actually execute our dialog. Wouldn't it be much simpler to just double-click on the dialog to get it to preview itself in its current state? Yes, I think so too, and that's what we'll be doing in the rest of this session.

For this, we only need to override the edit method of the TComponentEditor class, see what kind of class our "TComponent" actually is (using run-time type information, or RTTI) and then Execute it:


  procedure TCommonDialogComponentEditor.Edit;
  begin
    if (Component IS TOpenDialog) then { also TSaveDialog }
      (Component AS TOpenDialog).Execute
    else
      if (Component IS TPrintDialog) then
        (Component AS TPrintDialog).Execute
      else
        if (Component IS TPrinterSetupDialog) then
          (Component AS TPrinterSetupDialog).Execute
        else
          if (Component IS TColorDialog) then
            (Component AS TColorDialog).Execute
          else
            inherited Edit { default behaviour }
  end {Edit};

Note that the "inherited Edit" is necessary to make sure the default component editor behaviour is still triggered when we are not one of the five derived classed from TCommonDialog. Since we're installing this component editor for all derived classes of TCommonDialog, we need to ensure that all other classes (except the five we want) still get their default behaviour back!

Registering a component editor is needed, just as for plain components or property editors. However, it's much simpler compared to registering property editors, since we only need two parameters to a procedure called RegisterComponentEditor. The first one is the name (type) of the component for which this component editor is meant to be (TDialog in our case), and the second parameter is the type of the component editor itself (TDialogEditor in our case).

Installing a component editor is again much like installing a component or property editor. Just add it to our Component Library. Adding this component editor to the Component Library where the corresponding component TXXXDialog is already installed, leads to the Execute method being executed at design-time! A component editor can be created for a single class, or for a set of classes (all derived classes are included). In our case, the component editor is valid and meant for a specific set of derived classes from TCommonDialog:


unit CompEdit;
{ TCommonDialogComponentEditor version 0.1 }
interface
uses
  DsgnIntf;

Type
  TCommonDialogComponentEditor = class(TComponentEditor)
  public
    procedure Edit; override;
  end;

  procedure Register;

implementation
uses
  Dialogs;

  procedure TCommonDialogComponentEditor.Edit;
  begin
    if (Component IS TOpenDialog) then { also TSaveDialog }
      (Component AS TOpenDialog).Execute
    else
      if (Component IS TPrintDialog) then
        (Component AS TPrintDialog).Execute
      else
        if (Component IS TPrinterSetupDialog) then
          (Component AS TPrinterSetupDialog).Execute
        else
          if (Component IS TColorDialog) then
            (Component AS TColorDialog).Execute
          else
            inherited Edit { default behaviour }
  end {Edit};

  procedure Register;
  begin
    { register TCommonDialogComponentEditor for
      TCommonDialog and all its derived classes }
    RegisterComponentEditor(TCommonDialog, TDialogEditor)
  end;
end.

After we've installed this TCommonDialogComponentEditor in the Component Library, we can drop a TOpenDialog on a form and double-click on it. An instant preview will appear with all properties exactly like they've been defined in the Object Inspector at run-time. So, has Delphi suddenly become an interpreter, or what??

TCommonDialogComponentEditor

Unfortunately, if we try to test our component editor on the TFontDialog, for example, the default behaviour (editing the OnApply event) does not happen. We do call the inherited Edit method, but it's the TComponentEditor.Edit method that we're calling, not the default component editor (TDefaultEditor)'s Edit method. There are two ways to fix this. We could either make sure we install the TDialogEditor only for the five specific derived classes of TCommonDialog that we need, or we should derive our TDialogEditor from the TDefaultEditor class instead of the TComponentEditor class. Both solutions will work. We will implement either one of them as we continue to write other kinds of component editors later in this article.

Menu Component Editors
Component Editors can do much more than only one thing (which is done in the Edit method). In fact, we can create our own pop-up menus with several different options and actions to choose from. Let's try to do the same "Execute/Preview" thing for the common dialogs again, but this time add a little information message as second choice. To avoid that we "break" the default behaviour of the derived classes of TCommonDialog, we will only register our TCommonDialogComponentEditor version 0.2 for the five classes that have no event properties. We need to override two functions and one procedure this time; function GetVerbCount, function GetVerb and procedure ExecuteVerb.


  TCommonDialogComponentEditor = class(TComponentEditor)
  public
    function GetVerbCount: Integer; override;
    function GetVerb(Index: Integer): string; override;
    procedure ExecuteVerb(Index: Integer); override;
  end;
Since we only want two menu options, one for the Dialog.Excute and one for the "About" message, the function GetVerbCount should return a value of 2:

  function TCommonDialogComponentEditor.GetVerbCount: Integer;
  begin
    GetVerbCount := 2
  end {GetVerbCount};
Now, remember that Delphi most often counts from 0 up, including in this case. So, the next function, GetVerb takes an index parameter which can get the values 0 or 1 for the two possible values of the menu entries. For 0 we should return 'Execute', while for 1 (or any other value) we should return 'About':

  function TCommonDialogComponentEditor.GetVerb(Index: Integer): string;
  begin
    if Index >= 1 then GetVerb := '&About...'
                  else GetVerb := '&Execute...'
  end {GetVerb};
In order to show which common dialog we want to execute, we could change the GetVerb for index 0 to the following:

    GetVerb := '&Execute ' + Component.ClassName + '...'
Finally, we can execute the verbs. For the index 0 we do the exact same thing we previously did in the TDialogEditor (namely, call the Execute method of the correct class), and for index equal to 1 or higher we just show a nice (About) MessageDlg.

  procedure TCommonDialogComponentEditor.ExecuteVerb(Index: Integer);
  begin
    if index >= 1 then
      MessageDlg('TCommonDialogComponentEditor (c) 1996 by Dr.Bob',
                  mtInformation, [mbOk], 0)
    else
      if (Component IS TOpenDialog) then { also TSaveDialog }
        (Component AS TOpenDialog).Execute
      else
        if (Component IS TPrintDialog) then
          (Component AS TPrintDialog).Execute
        else
          if (Component IS TPrinterSetupDialog) then
            (Component AS TPrinterSetupDialog).Execute
          else
            if (Component IS TColorDialog) then
              (Component AS TColorDialog).Execute;
  end {ExecuteVerb};
Notice that we can use the Component property to get to the actual component for which we are an editor. Also notice that we need to use the run-time type information of our associated component to get to its actual contents.

If we try this code, we get a nice pop-up component editor menu, as follows:

Pop-up Menu

If we execute the TColorDialog, and select another colour, white for example, and close the dialog, we will notice that the Color property of the TColorDialog in the Object Inspector still has the old value. Why didn't it get updated to the new one we selected? Is this process one-way-only, or what??

Actually, we just forgot to call the Designer.Modified method to inform the designer (and hence the Object Inspector) that the component has changed; i.e. that one or more of its properties has just gotten a new value. If we include this line (as the last line of the ExecuteVerb method), then everything works fine as expected.

The complete source code (including the new ExecuteVerb method) for the new pop-up menu component editor TCommonDialogComponentEditor can be seen in the following listing:


unit CompMenu;
{ TCommonDialogComponentEditor version 0.5 }
interface
uses
  DsgnIntf;

Type
  TCommonDialogComponentEditor = class(TComponentEditor)
    function GetVerbCount: Integer; override;
    function GetVerb(index: Integer): String; override;
    procedure Executeverb(index: Integer); override;
  end;

  procedure Register;

implementation
uses
  Dialogs;

  function TCommonDialogComponentEditor.GetVerbCount: Integer;
  begin
    GetVerbCount := 2
  end {GetVerbCount};

  function TCommonDialogComponentEditor.GetVerb(index : Integer): String;
  begin
    if Index >= 1 then GetVerb := '&About...'
                  else GetVerb := '&Execute...'
  end {GetVerb};

  procedure TCommonDialogComponentEditor.ExecuteVerb(index: Integer);
  begin
    if index >= 1 then
      MessageDlg('TCommonDialogComponentEditor (c) 1996 by Dr.Bob',
                  mtInformation, [mbOk], 0)
    else
      if (Component IS TOpenDialog) then { also TSaveDialog }
        (Component AS TOpenDialog).Execute
      else
        if (Component IS TPrintDialog) then
          (Component AS TPrintDialog).Execute
        else
          if (Component IS TPrinterSetupDialog) then
            (Component AS TPrinterSetupDialog).Execute
          else
            if (Component IS TColorDialog) then
              (Component AS TColorDialog).Execute;
    Designer.Modified { inform the Object Inspector of the change }
  end {Edit};

  procedure Register;
  begin
    RegisterComponentEditor(TCommonDialog, TCommonDialogComponentEditor)
  end;
end.

Menu Default Component Editors
Now that we've seen how we can write a new menu component editor for the TCommonDialogs that do not have a default behaviour, why not extend it to cover the ones that already do have a default behaviour, but without losing this behaviour, i.e. offer a pop-up menu with as first (default) choice the OnEvent handling code editor, as second choice the "preview" execute and as third choice the "About" MessageDlg. For this we need another class TCommonDialogDefaultEditor derived from TDefaultEditor. Again, we need to override two functions and one procedure this time; function GetVerbCount, function GetVerb and procedure ExecuteVerb.


  TCommonDialogDefaultEditor = class(TDefaultEditor) { not TComponentEditor }
  public
    function GetVerbCount: Integer; override;
    function GetVerb(Index: Integer): string; override;
    procedure ExecuteVerb(Index: Integer); override;
  end;
We now want three instead of two menu options, one for the default action (the TDefaultEditor.Edit) and one for the Dialog.Excute and the "About" message, hence the function GetVerbCount should return a value of 3:

  function TCommonDialogDefaultEditor.GetVerbCount: Integer;
  begin
    GetVerbCount := 3
  end {GetVerbCount};
GetVerb takes an index parameter which can get the values 0, 1 or 2 for the three possible values of the menu entries. For 0 we should return 'OnEvent handler code', for 1 'Execute TXxxDialog', while for 2 (or any other value) we should return 'About':

  function TCommonDialogComponentEditor.GetVerb(Index: Integer): string;
  begin
    case Index of
        0: GetVerb := '&OnEvent handler code';
        1: GetVerb := '&Execute ' + Component.ClassName + '...';
      else GetVerb := '&About...'
    end
  end {GetVerb};
Finally, we can execute the verbs. For the index 1 we do the exact same thing we previously did in the TCommonDialogComponentEditor for index 0 (namely, call the Execute method of the correct class), and for index equal to 2 or higher we just show a nice (About) MessageDlg.

  procedure TCommonDialogComponentEditor.ExecuteVerb(Index: Integer);
  begin
    if index >= 2 then
      MessageDlg('TCommonDialogDefaultEditor (c) 1996 by Dr.Bob',
                  mtInformation, [mbOk], 0)
    else
    if index = 1 then
    begin
      if (Component IS TFindDialog) then { also TReplaceDialog }
        (Component AS TFindDialog).Execute
      else
        if (Component IS TFontDialog) then
          (Component AS TFontDialog).Execute;
      Designer.Modified
    end
    else inherited Edit { TDefaultEditor.Edit for index = 0 }
  end {ExecuteVerb};
I think you've got the idea by now, but just to be sure, the complete source code will be on the CD-ROM as well. If we install and activate this new TCommonDialogDefaultEditor, on a TFindDialog for example, then we get the following pop-up menu:

TFindDialog

If we select the first option (or double-click on the component, which will by default execute the first menu-verb), then we automatically jump to the code editor in the OnEvent handler code (the OnFind in this case). The second pop-up menu option will give the preview, and the third will show the about message. Just as we've expected and quite handy, if I may say so myself. The final source code for the unit CompMenu with the component editors TCommonDialogComponentEditor and TCommonDialogDefaultEditor is as follows:


unit CompMenu;
interface
uses
  DsgnIntf;

Type
  TCommonDialogComponentEditor = class(TComponentEditor)
    function GetVerbCount: Integer; override;
    function GetVerb(index: Integer): String; override;
    procedure Executeverb(index: Integer); override;
  end;

  TCommonDialogDefaultEditor = class(TDefaultEditor)
    function GetVerbCount: Integer; override;
    function GetVerb(index: Integer): String; override;
    procedure Executeverb(index: Integer); override;
  end;

  procedure Register;

implementation
uses
  Dialogs;

  { TCommonDialogComponentEditor }

  function TCommonDialogComponentEditor.GetVerbCount: Integer;
  begin
    GetVerbCount := 2
  end {GetVerbCount};

  function TCommonDialogComponentEditor.GetVerb(index : Integer): String;
  begin
    if Index >= 1 then GetVerb := '&About...'
                  else GetVerb := '&Execute ' + Component.ClassName + '...'
  end {GetVerb};

  procedure TCommonDialogComponentEditor.ExecuteVerb(index: Integer);
  begin
    if index >= 1 then
      MessageDlg('TCommonDialogComponentEditor (c) 1996 by Dr.Bob',
                  mtInformation, [mbOk], 0)
    else
    begin
      if (Component IS TOpenDialog) then { also TSaveDialog }
        (Component AS TOpenDialog).Execute
      else
        if (Component IS TPrintDialog) then
          (Component AS TPrintDialog).Execute
        else
          if (Component IS TPrinterSetupDialog) then
            (Component AS TPrinterSetupDialog).Execute
          else
            if (Component IS TColorDialog) then
              (Component AS TColorDialog).Execute;
      Designer.Modified
    end
  end {ExecuteVerb};

  { TCommonDialogDefaultEditor }

  function TCommonDialogDefaultEditor.GetVerbCount: Integer;
  begin
    GetVerbCount := 3
  end {GetVerbCount};

  function TCommonDialogDefaultEditor.GetVerb(index : Integer): String;
  begin
    case Index of
      0: GetVerb := '&OnEvent handler code';
      1: GetVerb := '&Execute ' + Component.ClassName + '...';
      else GetVerb := '&About...'
    end
  end {GetVerb};

  procedure TCommonDialogDefaultEditor.ExecuteVerb(index: Integer);
  begin
    if index >= 2 then
      MessageDlg('TCommonDialogDefaultEditor (c) 1996 by Dr.Bob',
                  mtInformation, [mbOk], 0)
    else
    begin
      if (Component IS TFindDialog) then { also TReplaceDialog }
        (Component AS TFindDialog).Execute
      else
        if (Component IS TFontDialog) then
          (Component AS TFontDialog).Execute;
      Designer.Modified
    end
    else inherited Edit { TDefaultEditor.Edit }
  end {ExecuteVerb};

  procedure Register;
  begin
    { empty default component editors }
    RegisterComponentEditor(TOpenDialog, TCommonDialogComponentEditor);
    RegisterComponentEditor(TPrintDialog, TCommonDialogComponentEditor);
    RegisterComponentEditor(TPrinterSetupDialog, TCommonDialogComponentEditor);
    RegisterComponentEditor(TColorDialog, TCommonDialogComponentEditor);
    { event default component editors }
    RegisterComponentEditor(TFontDialog, TCommonDialogDefaultEditor);
    RegisterComponentEditor(TFindDialog, TCommonDialogDefaultEditor)
  end;
end.

Conclusion
In this session, we have seen how we can use property editors and component editors to extend and enhance the Delphi environment at design time. We've seen how to use existing editors and - more importantly - how to write our own. Component editors and property editors are only the first two of the so-called Open Tools API, and not even the most powerful, but they sure were the most fun to start with.

Bibliography
If you want more interesting and technical information on the Open Tools API of Delphi, then you should check out my articles in The Delphi Magazine or the book The Revolutionary Guide to Delphi 2, ISBN 1-874416-67-2 published by WROX Press.


This webpage © 2000-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.