Introduction
When you have done some .NET (WinForms) development you might have noticed that controls
don't have a hint property. However when you drop a ToolTip component on a form,
suddenly all controls get a new ToolTip property! The first time I saw this I found this pretty cool and thought: why doesn't Delphi (win32) have this?
About a week ago I read the article Flowing with Delphi by Ed Vander Hoek.
In it I read that by putting a control on a TFlowPanel, the control suddenly gets a new property: ControlIndex.
I thought two things: "Wow!" and "How did they do that!".
It took some figuring out (using "Lutz Roeder's Reflector"), but succeeded and this article will explain you how it works.
The source code for this article can be found on codecentral.
A solution looking for a problem
So I did find out how the TFlowPanel developers did that and I wanted to test that by creating my own fake property.
At first I couldn't find anything that was both simple and useful, until I thought about the parent property.
Each control already has a parent property, and by exposing that I wouldn't have to create some way to store
the new property's value: the parent property is already implicitly stored in the dfm.
The most important thing introduced in BDS 2006 to support adding a property is a new interface: ISelectionPropertyFilter defined in DesigntInf.pas.
In previous versions of Delphi you could already register a SelectionEditor (using DesignIntf.RegisterSelectionEditor), but I won't go into that in this article and refer you to Delphi's DesingIntf.pas for more information.
BDS 2006 extends this SelectionEditor by allowing it to implement this new interface to add (or delete!) properties!
Let's see how to do that.
In the interface of a unit we define our new TAddPropertyFilter:
type
TAddPropertyFilter = class(TSelectionEditor, ISelectionPropertyFilter)
procedure FilterProperties(const ASelection: IDesignerSelections; const
ASelectionProperties: IInterfaceList);
end;
As you can see TAddPropertyFilter inherits from TSelectionEditor, a class containing a default implementation for the ISelectionEditor interface.
It also implements the new ISelectionPropertyFilter. In the implementation we register our selection editor and implement the FilterProperties method,
the only method of ISelectionPropertyFilter:
procedure Register;
begin
// register our selectioneditor for TControl
DesignIntf.RegisterSelectionEditor(TControl, TAddPropertyFilter);
end;
procedure TAddPropertyFilter.FilterProperties(const ASelection:
IDesignerSelections; const ASelectionProperties: IInterfaceList);
var
ParentProperty: TControlParentProperty;
begin
// for convenience we only support 1 selected item
if aSelection.Count <> 1 then
Exit;
// it should be a TControl (we have only registered for TControl)
if (aSelection[0] is TControl) then
begin
ParentProperty := TControlParentProperty.Create(inherited Designer, 1);
ParentProperty.Control := TControl(ASelection[0]);
ASelectionProperties.Add(ParentProperty as IProperty);
end;
end;
So what we have here is the Register procedure, registering our SelectionEditor: TAddPropertyFilter.
And the FilterProperties method, checking if we should add a property and if so we
add the new property by adding a class that implements the IProperty interface
to the ASelectionProperties list.
For IProperty, defined in DesignIntf.pas, we need to implement a whole bunch of methods
but most of them are rather straight forward.
For convenience I have first created a TBaseComponentPropertyEditor which implements most
of the IProperty and also adds some convenient properties like Designer and Component,
which can be used by it's descendants.
TBaseComponentPropertyEditor = class(TBasePropertyEditor)
private
FComponent: TComponent;
FDesigner: IDesigner;
protected
// we skip all the IProperty methods here
...
public
constructor Create(const ADesigner: IDesigner; APropCount: Integer); override;
property Component: TComponent read FComponent write FComponent;
property Designer: IDesigner read FDesigner;
end;
From this base class we descend our TControlParentProperty implementing
the methods of IProperty that are specific for our fake property.
We also add a property Control for the TControl we are editing, which is set by our TAddPropertyFilter.
TControlParentProperty = class(TBaseComponentPropertyEditor, IProperty,
IPropertyKind)
private
function GetControl: TControl;
procedure SetControl(const Value: TControl);
protected
function GetEditValue(out Value: string): Boolean;
function GetKind: TTypeKind;
function GetName: string; reintroduce;
function GetValue: string; reintroduce;
procedure SetValue(const Value: string); reintroduce;
function ControlIsChildOf(aControl, aParent: TControl): Boolean;
function GetAttributes: TPropertyAttributes;
procedure GetValues(Proc: TGetStrProc);
public
property Control: TControl read GetControl write SetControl;
end;
Implementation
The implementation of these methods is pretty straight forward.
A few may need some explanation, I will explain them below but first some source code:
{ TControlParentProperty }
function TControlParentProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paValueList, paSortList, paRevertable];
end;
function TControlParentProperty.GetControl: TControl;
begin
Result := TControl(Component);
end;
procedure TControlParentProperty.SetControl(const Value: TControl);
begin
Component := Value;
end;
function TControlParentProperty.GetEditValue(out Value: string):
Boolean;
begin
Value := GetValue();
Result := True;
end;
function TControlParentProperty.GetKind: TTypeKind;
begin
Result := tkClass;
end;
function TControlParentProperty.GetName: string;
begin
Result := 'Parent';
end;
So this is what they do:
-
GetAttributes tells the Object inspector which attributes our property has. We include
paValueList because we want to be able to pick from a list of parents.
-
GetControl and SetControl use the Component property of our base class and fill it with the Control we are currently editing.
-
GetEditValue is implemented by calling GetValue method, since they should both return the same value.
-
GetKind is a method needed because we also implement the IPropertyKind interface.
It tells the object inspector the type of the new property.
-
GetName just returns the name of our fake property.
Now on to the more difficult part of the code:
function TControlParentProperty.GetValue: string;
begin
if Assigned(Control) and Assigned(Control.Parent) then
begin
Result := Control.Parent.Name;
end
else
Result := '';
end;
procedure TControlParentProperty.GetValues(Proc: TGetStrProc);
begin
Designer.GetComponentNames(GetTypeData(TypeInfo(TWinControl)), Proc);
if Assigned(Control) and Assigned(Control.Owner) then
Proc(Control.Owner.Name);
end;
procedure TControlParentProperty.SetValue(const Value: string);
var
P: TWinControl;
begin
if Assigned(Control) and Assigned(Control.Owner) then
begin
if SameText(Control.Owner.Name, Value) then
P := Control.Owner as TWinControl
else
P := Control.Owner.FindComponent(Value) as TWinControl;
if Assigned(P) then
begin
if ControlIsChildOf(P, Control) then
raise EInvalidOperation.CreateRes(@SControlParentSetToSelf);
Control.Parent := P;
end;
end;
end;
As you see only a little more difficult.
- GetValue should return the value of our property, so
it returns the name of the parent of our Control.
- GetValues should return the possible values to show in the object inspector for our property.
We let the Designer do the most of the work, and only add the name of our owner which is the Form (or Frame).
-
SetValue needs to set the value of our property, and return its value as a string.
To do this we need to locate the TWinControl with this name (a Parent is of type TWinControl)
and check for circular references: a Control cannot be its own parent directly or indirectly.
(Trust me on this one: or else your BDS goes into an endless loop.)
I created a new method ControlIsChildOf to take care of this:
function TControlParentProperty.ControlIsChildOf(aControl, aParent: TControl):
Boolean;
var
P: TWinControl;
begin
// find if aControl is a child of aParent
Result := False;
P := aControl.Parent;
while Assigned(P) do
begin
if P=aParent then
begin
Result := True;
Break;
end;
P := P.Parent;
end;
end;
This method walks the chain of parents for aControl to check if aParent is a parent for aControl.
Well that was all! We install this into BDS 2006 by adding it to a package and installing that package.
The result can be seen below in this screenshot, the new 'fake' Parent property is
just below the 'fake' ControlIndex property:
Storing properties
The value of the Parent property in this example is saved in the dfm implicitly:
it is derived from the way the controls are nested in the dfm.
Similar, the value of the ControlIndex property of controls on a TFlowPanel is saved in the dfm implicitly:
it is derived from the order in which the controls are stored in the dfm.
When you really want to add a 'fake' property to a component and store that value, you need to something extra.
Say, for example, you want to add a GroupID to every control, then you could create a TGroupIDs component that maintains
a collection of TGroupIDItems, each consisting of a reference to a TControl and an associated GroupID.
This component then takes care of storing these GroupIDs.
But keep in mind that the fake GroupID property must only be shown when a TGroupIDs component is on the form, just like the
tooltip property is only shown when there is a ToolTip component in WinForms. Otherwise there is no place to save the GroupID!
Conclusion
So now you know how to add a fake property to a component. I leave it to you
to create great features for it. Oh and remember this only works in Delphi 2006: so go and get it!
About the Author:
Tjipke A. van der Plaats works as a software engineer for Agrovision B.V.where he works on various projects.
He also has his own company Tiriss, that sells
two products for developers (CB4 Tables and ChangeRes).
He also creates software or components on request. If you have questions feel free to contact him.
Copyright ) MMVI by Tjipke A. van der Plaats