1. Tech

Your suggestion is on its way!

An email with a link to:

http://delphi.about.com/library/bluc/text/uc092501d.htm

was emailed to:

Thanks for sharing About.com with others!

RTL reference|Glossary|Tips/Tricks|FREE App/VCL|Best'O'Net|Books|Link To
 
Creating Custom Delphi Components, Part III
Page 4: Dialog property editors; Advanced property editors
 More of this Feature
• Page 1: Custom Editors
• Page 2: Component Editors
• Page 3: Property Editors

• Download Demo Projects

Printer friendly versionPrinter friendly version
 Join the Discussion
"Post your questions, concerns, views and comments to this article..."
Discuss!
 Related Resources
• Custom VCL development
• VCL with source
• Third party VCL
• VCL using

   Dialog property editors
Most of the time, when creating a custom property editor, the purpose is to provide a graphical means of interacting with the property.

This first example is a very simple way of allowing the user to enter multiple lines in the "Caption" property of a TLabel. Although this example is not very complicated, it demonstrates how to include a form within your editor.

Step 1:
Select File, New Application from the main menu. This will create a form, name the form "fmLabelEdit", add a TMemo to the form named memCaption. Add two buttons, "OK" and "Cancel" with the ModalResult properties set to mrOK and mrCancel respectively.

Step 2:
Add DsgnIntf and TypInfo to your uses clause.

Caption property editor

Step 3:
Add the following property editor code to your unit.

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

And register the property editor like so

procedure Register;

implementation
{$R *.DFM}

procedure Register;
begin
  RegisterPropertyEditor(TypeInfo(TCaption), TLabel,
    'Caption', TCaptionProperty); 
end;

Step 4:
Add the following code in order for the object inspector to display the […] edit button after the property name.

function TCaptionProperty.GetAttributes: TPropertyAttributes;
begin
  Result := inherited GetAttributes + [paDialog];
end;

Step 5:
Finally, we create an instance of our editor form, set the contents of the memo to the current caption, and then show the form modally.

procedure TCaptionProperty.Edit;
var
  I: Integer;
begin
  with TfmLabelEdit.Create(Application) do
  try
    memCaption.Lines.Text := GetStrValue;
    ShowModal;

{If the modal result of the form is mrOK, we
need to set the "Caption" property of each TLabel.}

    if ModalResult = mrOK then
      for I:=0 to PropCount-1 do
        TLabel(GetComponent(I)).Caption := memCaption.Lines.Text;
  finally
    Free;
  end;
end;

Step 6:
Install the unit into the package, and then try out the new editor !

   Advanced property editors
Anyone who has ever used TActionList or TDataSet (TTable / TQuery) will have experience of the following example, possibly without even realising.

The ActionList editor is obviously a custom editor as it allows grouping of actions, whereas the FieldsEditor of TDataSet may at first seem like a standard editor, but upon closer inspection has a popup menu with items such as "Add fields". However, the most remarkable feature of both of these editors is not that they are custom dialog editors (similar to the one we covered earlier), but the fact that the items they create are included in the main class declaration of the current unit.

type
  TForm1 = class(TForm)
    ActionList1: TActionList;
    Action1: TAction;
    Action2: TAction;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

The benefit of this is that the IDE is made aware of these items, therefore allowing them to be selected from a list of objects whenever the property of a component requires them.

Action property

In the above illustration, two actions are added to a TActionList, clicking the "Action" property of Button1 shows a list consisting of the actions added. The two actions are also added to the Form's class declaration, and can therefore be referred to by name (Action1, Action2).

The trick here lies entirely in the property editor and not within the component. When a property editor is triggered (ie the Edit method is called) the Designer property contains a valid reference to an IFormDesigner (TFormDesigner in Delphi 4). Many of the functions of this interface are not within the scope of this article, if you wish to learn more about the capabilities of this interface I would recommend a book called Delphi Developer's Handbook by Marco Cantu.

Some of the methods include

function MethodExists(const Name: string): Boolean;
procedure RenameMethod(const CurName, NewName: string);
procedure SelectComponent(Instance: TPersistent);
procedure ShowMethod(const Name: string);
function GetComponent(const Name: string): TComponent;
function CreateComponent(ComponentClass: TComponentClass; 
  Parent: TComponent;
  Left, Top, Width, Height: Integer): TComponent;

Some of the above calls are fairly elementary, MethodExists for example will return True or False depending on whether or not a method name already exists within the form of the current unit (FormCreate, Button1Click etc). ShowMethod will move the cursor to the named method, and RenameMethod will change the name of a method.

The two methods that are of interest to use at this point are:

CreateComponent
Given a component class, a parent to hold the component, and position / dimensions, the designer will create an instance of the class as if the developer had selected it from the component palette and added it to the form themself.

Modified
Informs the designer that something has been altered (a property etc). This alters the state of the unit so that the IDE knows it should be saved before closing (it also enables the save button in the IDE).

When adding items to our array all we need to do is to get TMyProperty.Designer to create a component on our behalf. This component will then be added to the form and any property that refers to a class of this type will automatically be aware of it. In the case of TActionList and TDataSet the components that are added to the form are not visible at design-time, the owner component acts as a kind of "manager" for the components.

During design-time you wont see a TAction or a TField component on the component palette which would possibly make you suspect they are not registered, yet the IDE is still able to create instances of these components (and they are also not visible). The answer is not that they aren't registered, this behaviour is a result of "how" the component is registered.

Whereas RegisterComponents will add your components to the component palette, the RegisterNoIcon method will register your component without adding it to the component palette, registering in this way also tells the IDE that the component should not be displayed during design-time.

In the following example we will create a component called a TWavSound (a additional component called TWavButton is included in the source code that accompanies this article as an example). TWavSound will simply hold data from a WAV file, and play the sound on demand. Although it would be simple for us to drop one TWavSound onto our form for each WAV sound we require, our form could soon start to become unmanageable, therefore we will also create a manager class called TWavList.

Every technique used in the source code to these components was covered in part two of this series of articles so the source code will not be covered in any great level of detail. However, I will show the class declarations of these components just to give you an idea of how they are structured.

Note: At the bottom of the unit, within the initialization section of the unit you may notice the following code:

initialization RegisterClass(TWavSound);

The reason is that RegisterNoIcon doesn't seem to do a complete job. Although it allows us to create instances of the registered component from our property editor something seems to go wrong when a project is re-loaded containing these components. A "Class not registered" message box is displayed and the project is corrupted. Additionally registering the class in this way seems to fix the problem

   TWavSound

type
  PWavData = ^TWavData;
  TWavData = packed record
    Size: Longint;
    Data: array[0..0] of byte;
  end;

  TWavSound = class(TComponent)
  private
    FWavData: PWavData;
    FWav: TWav;
    procedure ReadWavData(Stream: TStream);
    procedure WriteWavData(Stream: TStream);
  protected
    procedure DefineProperties(Filer: TFiler); override;
  public
    destructor Destroy; override;
    procedure Clear;
    procedure LoadFromFile(const Filename: TFilename);
    procedure LoadFromStream(Stream: TStream);
    procedure Play;
  published
  end;

FWavData
Will be used to store the contents of the WAV file once loaded from a stream or a file.

Clear
Will free the memory holding FWavData.

Play
Will use the sndPlaySound API call in MMSystem.pas to play the data in FWavData.Data.

ReadWavData and WriteWavData
Will be used internally by the IDE when it needs to read / write the data stored within FWavData.

DefineProperties
Will specify a "hidden" property called WavData, and tell the IDE that ReadWavData and WriteWavData should be used for streaming the data.

FWav
Is set internally by the TWav class when TWav.WavSound is set to our component. The reason is that this collection item will need to be freed when our TWavSound component is freed, in order to stop it from pointing to an invalid object.

   TWavSound

type
  TWav = class(TCollectionItem)
  private
    FWavSound: TWavSound;
    procedure SetWavSound(const Value: TWavSound);
  protected
  public
    procedure Play;
  published
    property WavSound: TWavSound read FWavSound write SetWavSound;
  end;

SetWavSound
Will ensure that the WavSound to which it points will have its FWav set correctly.

   TWavSound

TWavs
Is a standard implementation of TCollection so will not be covered in this article.

   TWavList
TWavList is simply a component that publishes a TWavs property to allow us to edit the list of wavs at design-time.

   TWavsProperty
TWavsProperty is the property editor that has been designed to handle this class. Although a standard TCollection editor would be sufficed (to a point) I decided to create a new editor in order to allow the playing / clearing of WAVs at design-time.

First I created a new unit with a form in. I added a few TSpeedButtons and a TListBox to list the items in.

TWaws
Additionally, I added the following items to the Form's class declaration

 FWavs: TWavs;
 FComponent: TComponent;
 TheDesigner: IFormDesigner;

FWavs
Will hold a reference of the TCollection that we are editing.

FComponent
Will hold a reference to the component that owns the collection. As our form will not be shown modally we will need to close our form if this component is destroyed (using the Notification method of our form).

TheDesigner
Will hold a reference to the current Designer object passed to our property editor. This will be used to call CreateComponent, and to select our hidden TWavSound into the object inspector whenever an item is selected in our listbox.

The actually property editor is a very simple one.

type
  TWavsProperty = class(TClassProperty)
  public
    function GetAttributes: TPropertyAttributes; override;
    function GetDisplayName: string;
    procedure Edit; override;
  end;

The only real method worth mentioning here is the Edit method. The implementation of which is

procedure TWavsProperty.Edit;
begin
  if fmWavsEditor = nil then
    fmWavsEditor := TfmWavsEditor.Create(Application);

  with fmWavsEditor do begin
    TheDesigner := Self.Designer;  //Don't forget SELF !!
    Caption := Self.GetName;

    //Setup the display, and then show the form
    Edit(TComponent(GetComponent(0)), TWavs(GetOrdValue));
  end;
end; 

First the editor form is created (if not already created).

"TheDesigner" of the Form is set to Self.Designer. Do not forget the "Self" here as TForm also has a Designer property which at this point will be nil.

GetComponent(0) is used to retrieve the component that owns the property. FreeNotification is called for this component to ensure that our form is notified if the component is destroyed (so that we can close our form).

GetOrdValue is used to retrieve the class object (the "Wavs" property") that is to be edited, the result is typecast as TWavs.

The Edit method that is called is part of TfmWavsEditor, it is a method I added which simply clears the listbox and populates the items with the names of the FWavs entries. It then shows the form.

Note: Later versions of Delphi return TPersistent from the GetComponent function, therefore the result must be typecast to TComponent.

   Talking to IFormDesigner
The main two parts of this editor (except for clearing the WAV and playing the WAV) are the parts where "TheDesigner" is interacted with.

The first part to mention should be the part where the "New" button is clicked, a new item is added to the collection, a new TWavSound is added to our form's class declaration, and finally the TWavSound is selected into the object inspector.

procedure TfmWavsEditor.sbNewClick(Sender: TObject);
var
  Wav: TWav;
  WavSound: TWavSound;
begin
  //Add an item to the collection
  Wav := FWavs.Add;

  //Ask TheDesigner to create a new TWavSound component for us
  WavSound := TWavSound(TheDesigner.CreateComponent(TWavSound, 
    nil, 0, 0, 0, 0));

  //Set the Wav (CollectionItem) to point to our new TWavSound component
  Wav.WavSound := WavSound;

  //Select our new TSoundComponent into the object inspector
  //so that it may be renamed if so desired
  TheDesigner.SelectComponent(WavSound);

  //Internally refresh the items in the listbox
  RefreshList;
  lbItems.ItemIndex := FWavs.Count-1;

  //Tell the IDE that something has changed
  TheDesigner.Modified;
end;

The second part to mention is where the correct TWavSound is selected into the object inspector when an item is clicked in the listbox.

procedure TfmWavsEditor.lbItemsClick(Sender: TObject);
begin
  with lbItems do
    if ItemIndex >=0 then
      TheDesigner.SelectComponent(FWavs[ItemIndex].WavSound);
end;

   Avoiding access violations
Finally we need to ensure that we are not left referencing an object that is no longer valid. This is quite simply achieved by following the following two steps

1. Make sure our form is notified when the component that owns our class property is destroyed.
2. Override the Notification method of our form and close the form if the relevant component is destroyed.

To ensure we are notified when the component is destroyed:

procedure TfmWavsEditor.Edit(AComponent: TComponent; AWavs: TWavs);
begin
  //First we need to remove notification for the current component
  if FComponent <> nil then
    FComponent.RemoveFreeNotification(Self);

  //Now we need to add notification for the current component
  AComponent.FreeNotification(Self);
  FComponent := AComponent;

  FWavs := AWavs;
  lbItems.ItemIndex := -1;
  RefreshList;

  Show;
end;

What to do when a component is destroyed:

procedure TfmWavsEditor.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if Operation = opRemove then begin
    //If the owner component is destroyed
    //we should close our form
    if (AComponent = FComponent) then
      Close
    else
    //If the component that is destroyed
    //we refresh our list just incase it affects our component
    if (AComponent is TWavSound) then
      RefreshList;
  end;
end;

   Summary
In this article we covered how to write a component editor, we then moved on to creating simple property editors, finally we covered more advanced property editors (including minimal use of the IFormDesigner interface). All of the demonstrated techniques in this article (and more) have been used in my DIB (device independent bitmap) components. These components are available for free download from http://www.howtodothings.com and are open-source (so any development contributions would be greatly appreciated).

First page > Custom Editors > Page 1, 2, 3, 4

All graphics (if any) in this feature created by Peter Morris.

 More Delphi
· Learn another routine every day - RTL Quick Reference.
· Download free source code applications and components.
· Talk about Delphi Programming, real time.
· Link to the Delphi Programming site from your Web pages.
· Tutorials, articles, tech. tips by date: 2001|2000|1999|1998 or by TOPIC.
· NEXT ARTICLE: Database reports with Delphi and ADO - DB/17.
Chapter seventeen of the free Delphi Database Course for beginners. How to use QuickReport set of components to create database reports with Delphi. See how to produce database output with text, images, charts and memos - quickly and easily.
 Stay informed with all new and interesting things about Delphi (for free).
Subscribe to the Newsletter
Name
Email

 Got some code to share? Got a question? Need some help?

©2014 About.com. All rights reserved.