The Study

home |  about

Programming

Introduction

Delphi

Java

Projects

Graphics

Introduction

POV-Ray

Terragen

Music

Introduction

Work in Progress

Oddments

Library

Miscellaneous

Delphi Property Editors - An Introduction

Contents

Before we begin

This introduction to property editors assumes that you are already familiar with properties and with creating Delphi components -- if you are not, you shouldn't start from here -- both these subjects are covered adequately by Delphi's own documentation, and there are plenty of books which go into them in as much depth as you would want.

Property Editors, however, are barely mentioned in Delphi's documentation, and I have yet to find a book which gives them adequate coverage either. This document attempts to fill the gap, and is based on what I have discovered from various different sources, and from my own experimentation. If there are any errors, I apologise -- please let me know about them, and I will update this document. If you have additional information about property editors which you think could be included, again let me know.

-- Back to contents

Default property editors

As soon as you create a published property, it automatically acquires a default property editor, based on its type. For most purposes, this is actually quite adequate, as it gives you all the functionality that Delphi's own properties have. For example, if you have a property which is a TColor, the standard drop-down list of colours will appear in the Object Inspector - you don't need to do anything at all, it is handled by Delphi. Very nice.

A few points about this, however, are not immediately apparent. The first is that if you have written a Get function or a Set procedure for the property, these methods will be invoked by Delphi at design time. In other words, your component is effectively 'live' as soon as it is dropped on to a form. This is obvious from Delphi's own components (when you change the Caption property of a button, the button display is immediately updated to match), but you may not realise that you don't have to do anything else to achieve this. It also means that sometimes you have to be very careful about what you do in the Get/Set methods -- they must not attempt to do things which are not appropriate or not possible at design time. One way of avoiding this problem is to use the ComponentState property (which all components have) -- check whether or not csDesigning is included in the ComponentState value, and use this to determine whether or not to carry out the action.

if not (csDesigning in ComponentState) then
  { Do run-time only stuff here... }

Another, closely related point, is that if your property is itself referencing an object, you must ensure that the object is created as soon as the component is created. If you do not, as soon as the Object Inspector attempts to access the property...bang! The dreaded 'access violation' error.

For example, suppose your property is actually a string-list. Naturally, Delphi will supply its own string-list editor, and will invoke it as soon as you click on the '...' button for the property in the Object Inspector. If your string-list does not exist, you will get an immediate access violation (unless, of course, you are using a Get method to access the string-list, and your Get method creates the string-list if necessary).

-- Back to contents

A brief aside

On the subject of string-lists, and indeed any 'object' properties: if you make these properties writeable, remember that if you assign another object to them you are simply changing the property to point to a different object. This is rarely what you want to do (what if the program then goes and frees the object that has been assigned?), and besides, it results in your original object becoming 'orphaned' -- there is no longer a direct reference to it, so it will not be freed, and you will end up with a memory leak. For this reason, properties which are objects should use a Set procedure if they are to be writeable. Normally this procedure would copy the contents of the object. To use a simple, and common, example, if your property is a string-list, the Set procedure would copy the strings to your internal string-list. Happily Delphi makes this easy, as the TStringList object has an Assign method which does just that; for example, FStringListProperty.Assign(NewStringList).

The inevitable exception: some properties are intended to hold references to 'external' objects. For example, the SpinButton control has an Associate property, which holds a pointer to the TEdit control to which the spin button is to be attached. In this case, all the Set procedure needs to do is to re-assign the pointer -- the object which is being referenced is not actually part of the component, and therefore is not created or destroyed by the component.

-- Back to contents

Creating property editors

Sometimes, however, Delphi's default editors are not sufficient, and you have a control that needs its own, special property editor. Usually these special editors call up a dialog to allow the user to set a number of different values, which is the avenue that I am going to pursue here.

-- Back to contents

Another brief aside

If you are going to create your own property editors, and you have a copy of Delphi's source code (supplied with some versions of Delphi), you should look in the TOOLSAPI subdirectory and read the DSGNINTF.PAS file, which details the property editors, and is extensively commented. Much of my own information about property editors is derived from this file, which is so well commented that it almost makes up for Delphi not covering property editors in the documentation.

-- Back to contents

Overview

First, we need to understand how the Object Inspector deals with properties, and what it expects a property editor to do.

The Object Inspector displays (where possible and appropriate) the current value for the property, as a string. In order to know what string to display, it calls the GetValue method of the property editor. This method simply returns a string representing the current state of the property -- Delphi's default property editor contains a GetComponents method which returns a pointer to the currently selected component(s) so that GetValue knows where to get the value from. In order to know what kind of display is required (in other words, is an 'edit' button required, or is a drop-down list to be used, etc.) it calls the GetAttributes method of the property editor.

When the user attempts to change the value of a property, or clicks on the '...' button or the drop-down button (for enumerated properties), the Object Inspector calls the Edit method of the property editor. This method will carry out the actual editing, and will write the results back to the component, generally using the SetValue method of the property editor.

Basically, then, to create a new property, you create a new class based on Delphi's existing TPropertyEditor class, and you override GetValue, SetValue, GetAttributes, and Edit to provide your own functionality. GetValue should return a string representing the current state of the property, SetValue should update the state of the property based on a supplied string, GetAttributes should return the set of attributes to tell Delphi's Object Inspector how to handle the property, and Edit should allow the user to edit the current state of the property.

-- Back to contents

Details

All property editors should descend from either TPropertyEditor, or from one its existing descendants. It will need to override one or more of the GetValue, SetValue, GetAttributes, and Edit methods.

-- Back to contents

GetAttributes

The GetAttributes method returns a value indicating the attributes that should be used by the Object Inspector for displaying the property.

function TTestPropertyEditor.GetAttributes: TPropertyAttributes;
begin
  Result := [paDialog, paReadOnly];
end;

The possible values which can be included in the set returned by GetAttributes are:

paValueList

The property editor can return an enumerated list of values for the property. This will cause the drop-down button to appear to the right of the property in the Object Inspector.

paSortList

The Object Inspector will sort the list returned by GetValues.

paSubProperties

The property editor has sub-properties that will be displayed indented and below the current property in standard outline format. If GetProperties will generate property objects then this attribute should be set.

paDialog

Indicates that the Edit method will bring up a dialog. This will cause the '...' button to be displayed to the right of the property in the Object Inspector.

paMultiSelect

Allows the property to be displayed when more than one component is selected. Some properties are not appropriate for multi-selection (e.g. the Name property).

paAutoUpdate

Causes the SetValue method to be called on each change made to the editor instead of after the change has been approved (e.g. the Caption property).

paReadOnly

Value is not allowed to change. (Actually, the property editor itself can still change the value -- paReadOnly simply means that the user cannot change the displayed contents of the property.)

paRevertable

Allows the property to be reverted to the original value. Things that shouldn't be reverted are nested properties (e.g. Fonts) and elements of a composite property such as set element values.

-- Back to contents

GetValue

The GetValue method returns the string that should be displayed by the Object Inspector. The following example returns the string for an integer value:

function TTestPropertyEditor.GetValue: string;
begin
  Result := IntToStr(TTestComponent(GetComponent(0)).IntValue);
end;

The above example also introduces a new subject -- the GetComponent method. This returns a reference to one of the currently selected components. If GetAttributes did not include paMultiSelect in its set, there will never be more than one selected component, and this can be returned by GetComponent(0). If more than one component is selected, GetComponent can be used to cycle through all the components.

Note that because GetComponent simply returns a TComponent, you will probably need to typecast it to the correct component in order to read the property value.

-- Back to contents

SetValue

The SetValue procedure is passed the new value as a string, and should update the actual property with this value. Delphi's own property editors commonly convert the value, then pass it to one of the following methods, which will update all selected components:

SetFloatValue
SetOrdValue
SetStrValue
SetVarValue (for Variants)
SetMethodValue

-- Back to contents

Edit

If you are creating your own property editor, this is the crucial method to override. Assuming that you are creating a dialog to edit the property, you simply create and display the dialog in the normal way. As an example, here is Delphi's own code for the Font property editor:

procedure TFontProperty.Edit;
var
  FontDialog: TFontDialog;
begin
  FontDialog := TFontDialog.Create(Application);
  try
    FontDialog.Font := TFont(GetOrdValue);
    FontDialog.HelpContext := hcDFontEditor;
    FontDialog.Options := FontDialog.Options +
      [fdShowHelp, fdForceFontExist];
    if FontDialog.Execute then
      SetOrdValue(Longint(FontDialog.Font));
  finally
    FontDialog.Free;
  end;
end;

One point to note about this piece of code is the use of the SetOrdValue and the GetOrdValue methods -- these are being used to store a pointer to the TFont object. This approach can be used for any property which holds an object (unless the property references an external object) -- just remember to make sure that the object exists first!

-- Back to contents

And finally...

An entirely trivial example. The following ZIP file contains the units for a simple example of a component with a property editor for one of its properties. The property is IntValue and simply contains an integer value which can be edited using a very simple dialog. This is completely pointless, but hopefully provides a suitable starting-point for creating your own property-editors.

-- Back to contents