The property editor
Property editors are used by the IDE to allow special editing of individual properties within a component. Some editors are very simple, some are much more complicated. Delphi already has a number of standard property editors, some of these are:
.TIntegerProperty. Used for inputting integers.
.TCharProperty. Used for inputting a single character.
.TEnumProperty. Used for selected an individual element in an enumerated type (alTop, alClient etc).
.TBoolProperty. Used for selecting "True" or "False" for Boolean properties.
.TFloatProperty. Used for inputting floating point numbers (Variable type Float / Extended etc. The "Real" type should not be used for component properties)
.TStringProperty. Used for inputting strings up to a maximum of 255 characters.
.TSetProperty. Used for including / excluding individual elements of a Set property. Each element is displayed as a Boolean sub-property. Setting the value to "True" includes the element, setting it to "False" excludes it.
.TClassProperty. This is the base class to descend from when you want to create a custom editor to be invoked for properties of a certain class (when you have a class as a property, such as TImage.Picture).
All of these property editors descend directly or indirectly from TPropertyEditor. TPropertyEditor has many properties and methods, the most significant are.
function AllEqual: Boolean; virtual;
function GetAttributes: TPropertyAttributes; virtual;
procedure Edit; virtual;
function GetValue: string; virtual;
procedure GetValues(Proc: TGetStrProc); virtual;
|
AllEqual
When multiple components are selected the object inspector filters its list of properties to only the ones that all the selected components have in common. If the value in each component for any given property (eg Width) is the same, the value will be displayed, otherwise no value will be shown. AllEqual is the routine that determines if each value is identical.
function TStringProperty.AllEqual: Boolean;
var
I: Integer;
V: string;
begin
Result := False;
if PropCount > 1 then
begin
V := GetStrValue;
for I := 1 to PropCount - 1 do
if GetStrValueAt(I) <> V then Exit;
end;
Result := True;
end;
|
In the above example TStringProperty compares each value (using GetStrValueAt) with the value of the first component in the list (using GetStrValue, GetStrValueAt(0) would have done the same). The size of the list is determined by using PropCount, this returns the total amount of components selected.
GetAttributes
GetAttributes is called by the IDE when it needs to gather information about the property editor. The object inspector displays an appropriate editor based on the information supplied. The result of GetAttributes (TPropertyAttributes) is a set, so it may contain a combination of the following values (this is not a complete list)
paDialog
Tells the object inspector to show a […] button after the property name, when the user clicks this button the Edit method is triggered.
paSubProperties
Tells the object inspector to show a [+] expand button before the property name, clicking this button will show an expanded list of sub properties (usually the published properties of a class property).
paValueList
The object inspector will show a combobox with a list of values, this list is determined by the IDE by calling the GetValues method.
NOTE: The GetValues method, not the GetValue method which is completely different
paSortList
If combined with paValueList, the values displayed will be sorted alphabetically.
paMultiSelect
This specifies to the IDE that the property is allowed to be displayed when multiple components are selected. This item is not present for editors such a TClassProperty.
paAutoUpdate
Causes the SetValue method to be called each time the value is altered within the object inspector, rather than waiting for the user to press or edit another property. This is used for "Caption" and "Text" properties, to give a live representation of the value the user is entering.
paReadOnly
If this element is included the value in the object inspector is read-only. This is typically used in conjunction with paDialog. GetValue would be overridden to return a descriptive representation of the property.
Edit
This method is called when the […] button for the property is clicked. This button appears if the paDialog element is included within the result of GetAttributes.
GetValue
This method is called when the object inspector needs to know how to display the property as a string. This is typically used when [paDialog, paReadOnly] are specified within the result of GetAttributes.
GetValues
This method is called when the object inspector needs to retrieve a list of values to display when paValueList is specified within the result of GetAttributes.
GetValues passes a parameter called "Proc" which is of type TGetStrProc. GetStrProc is declared as TGetStrProc = procedure(const S: string) of object;
The IDE expects "Proc" to be called once for every value that should be displayed in the object inspector for this property.
procedure THintProperty.GetValues(Proc: TGetStrProc);
begin
Proc('First item to display');
Proc('Second item to display');
end;
|
The following example shows how to provide a list of default values for the "Hint" property of all components, whilst still allowing the user to enter a value not in the list.
type
THintProperty = class(TStringProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(String), nil, 'Hint', THintProperty);
end;
{ THintProperty }
function THintProperty.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList, paSortList];
end;
procedure THintProperty.GetValues(Proc: TGetStrProc);
begin
Proc('This is a required entry');
Proc('Press F1 for more information');
Proc('This value is read-only');
end;
|
First GetAttributes is overridden, and [paValueList, paSortList] are included in the result. Next GetValues is overridden and three values are added to the drop down list by calling the "Proc" procedure.
Registering property editors
Finally the property editor is registered using RegisterPropertyEditor. RegisterPropertyEditor takes four parameters:
PropertyType: PTypeInfo
Requires a pointer to a TTypeInfo record. This sounds much more complicated than it really is, all we need to do is add TypInfo to our uses clause, and use the TypeInfo function to retrieve the pointer for us. TypeInfo(SomeVariableType)
ComponentClass: TClass
This is the base class that this editor should apply to. The editor will apply to this class and any classes that descend from it. If nil is specified, this editor will apply to any class.
const PropertyName: String
If this editor should only apply to a specific property then the name of the property should be specified here. If the editor should apply to any property of the type specified in PropertyType this value should be ''.
EditorClass: TPropertyEditorClass
This is the class that has been created to deal with the property. In the above example the class is THintProperty.
Using RegisterPropertyEditor incorrectly
It is important when using RegisterPropertyEditor that you supply the correct information. Supplying the incorrect information could mean either that your editor affects incorrect properties (eg All string properties) or incorrect components.
At the other extreme, setting the parameters incorrectly could mean that only a specific property in a specific component (and descendants) is associated with your editor. This does not seem like much of a problem at first, but descendant components may wish to implement additional properties of the same type. As these properties will obviously have a different name they will not have the correct property editor assigned to them.
An example of badly registered editor already exists within the VCL. The standard editor for TCollection was registered for all classes descended from TComponent. The problem is that the lowest class capable of being displayed in the object inspector is TPersistent (the class that TComponent descends from).
If a component has a property of type TPersistent (which by default exposes its sub-properties in an expandable list), and one of its properties is of type TCollection, the result is a […] button in the object inspector that does nothing when clicked (as we saw in part two of this article series).
The solution to this problem seems quite simple. Rather than our sub-property being descended from TPersistent we could descend it from TComponent instead. However, the default behaviour for a property of type TComponent (As determined by the property editor TComponentProperty editor) is to show a list of other components, rather than the sub-properties of an embedded component.
The actual solution really is simple, but only if you know how to write a property editor.
Step 1:
type
TExpandingRecord = class(TPersistent)
|
Should be changed to read
type
TExpandingRecord = class(TComponent)
|
Step 2:
Create a property editor like so
type
TExpandingRecordProperty = class(TClassProperty)
public
function GetAttributes : TPropertyAttributes; override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Article', [TExpandingComponent]);
RegisterPropertyEditor(TypeInfo(TExpandingRecord),
nil, '', TExpandingRecordProperty);
end;
{ TExpandingRecordProperty }
function TExpandingRecordProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paReadOnly, paSubProperties];
end;
|
Step 3:
Remove the RegisterComponents call from the component unit, and register it within the editor unit instead. This way we can ensure the component will not be registered without the component.
Now our property of type TExpandingRecord will show as an expanding property (due to us returning paSubProperties from GetAttributes), and the default editor for TCollection will work as the owner of the TCollection property is a TComponent.
Next page > Dialog Editors > Page 1, 2, 3, 4