Adding a 'fake' property to a component

By: Tjipke van der Plaats

Abstract: With the introduction of Delphi 2006, you can add a 'fake' property to a component without changing the component itself!

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

Server Response from: ETNASC03