In my last blog post, I spoke of how the QuantumGrid was able to implement selectable subcomponents to add a bunch of properties, without contributing to a noisy experience in the Object Inspector.
I presented in my CodeRage session a way to do that for their own components, let me show how that works (sample code attached).
I'm going to build an example using a trivial component as the parent container and two trivial persistent classes as child delegates. Firstly make a Project Group and add two packages, a run-time only one called cxSpecializedComponent and a design-time only one called dclSpecializedComponent. When building components I usually add a test project (TestSpecializedComponent.dpr) with a simple form that uses the control I am building.
You may recognize the naming convention as the one used by Developer Express in our cross platform components. This trick will work for Kylix as well as Delphi. At CodeRage I was using Delphi 2009 (and the screen shots from my last blog post was using Delphi 7) but the same trick will work in Delphi 4+ and CBuilder 4+ all the way up to RAD studio 2010.
The run-time package
In the run-time package we are going to put our parent component TParent, a base class for all children called TCustomChild, and two descendants from it called TChildOne and TChildTwo.
The TParent must have a property for storing an instance of a TCustomChild, an instance of its class type and its class name. We will need a class registration mechanism to register all the child classes to and return the type give a class name.
Let's look at the interface for the run-time package;
type
TCustomChild = class; // Base class for all Child classes
TCustomChildClass = class of TCustomChild; // type of the child Base class
TParent = class(TComponent)
private
FChild: TCustomChild;
FChildClass: TCustomChildClass;
procedure SetChildClassName(const Value: String);
function GetChildClassName: String;
procedure SetChildClass(const Value: TCustomChildClass);
public
property ChildClass : TCustomChildClass
read FChildClass write SetChildClass;
procedure CreateChild; virtual;
procedure DestroyChild; virtual;
published
property ChildClassName : String
read GetChildClassName write SetChildClassName;
property Child : TCustomChild
read FChild write FChild;
end;
TCustomChild = class(tPersistent)
private
FBaseText: String;
published
property BaseText : String
read FBaseText write FBaseText;
end;
TChildOne = class(TCustomChild)
private
FOneText: String;
published
property OneText : String
read FOneText write FOneText;
end;
TChildTwo = class(TCustomChild)
private
FTwoText: String;
published
property TwoText : String
read FTwoText write FTwoText;
end;
const
sChildOne = 'Child One';
sChildTwo = 'Child Two';
function GetRegisteredChilds : TcxRegisteredClasses;
To do our magic we will expose the TParent.Child property (which contains an instance of the child class), but give it a drop down affect that sets the class type by it's name and creates an instance of the child class. It's all pretty simple to implement, so let's have a look at some code.
First the registration machinery, I am using TcxRegisteredClasses a utility class from a Devex unit called cxClasses, if you don't have the Express Quantum Grid you will have to build one but the exercise is trivial so I won't bore you with the details;
var
FRegisteredChilds: TcxRegisteredClasses;
function GetRegisteredChilds : TcxRegisteredClasses;
begin
if FRegisteredChilds = nil then
FRegisteredChilds := TcxRegisteredClasses.Create;
Result := FRegisteredChilds;
end;
initialization
GetRegisteredChilds.Register(TChildOne, sChildOne);
GetRegisteredChilds.Register(TChildTwo, sChildTwo);
finalization
GetRegisteredChilds.Unregister(TChildOne);
GetRegisteredChilds.Unregister(TChildTwo);
end.
Note we are registering our child classes in the singleton class registration object, and later on we can retrieve the type of any class from its name, that will be used mostly by the design-time editors to complete the magic.
The Children don't need any implementation, so let's finish off the run-time unit by implementing TParent;
procedure TParent.CreateChild;
begin
if FChildClass <> nil then
FChild := FChildClass.Create;
end;
procedure TParent.DestroyChild;
begin
FreeAndNil(FChild);
end;
function TParent.GetChildClassName: String;
begin
if FChild = nil then
Result := ''
else
Result := FChild.ClassName;
end;
procedure TParent.SetChildClass(const Value: TCustomChildClass);
begin
if FChildClass <> Value then
begin
DestroyChild; // remove the preexisting Child Instance
FChildClass := Value;
CreateChild; // create a new Child Instance
end;
end;
procedure TParent.SetChildClassName(const Value: String);
begin
ChildClass := TCustomChildClass(GetRegisteredChilds.FindByClassName(Value));
end;
The important thing to note in the implementation of the parent is that by setting the ChildClassName property the ChildClass property holding the class type of the Child to be used is created and an instance of the selected class constructed in the Child property (And if one was there previously it was destructed first).
The design-time package
The design-time package will be "installed" in the palette and will include a Register procedure that registers our component, and 2 property editors that will do our magic.
All you need in the interface is a register procedure.
procedure Register;
In the implementation you will need to access CodeGears design-time units, in versions of Delphi from 6 onwards they were not available in source or in run-time packages so you will have to require designide.dcp in your design-time package. Here is the uses clause for the implementation of the Design-time unit.
uses
Types, DesignIntf, DesignEditors, VCLEditors,
DsgnIntf, TypInfo, Classes,
SpecializedComponent, // our run-time component
cxClasses; // for TcxRegisteredClasses generic class registration class
Now we need to make a Class Property editor descendant that tells the TParent component to construct a new child class by giving it a class name chosen from a drop down. The prototype for that class will look like this;
// Property editor to provide a dropdown selection of registered classes and expose nested properties
type
TParentChildProperty = class(TClassProperty)
protected
function HasSubProperties: Boolean;
public
function GetAttributes: TPropertyAttributes; override;
function GetValue: String; override;
procedure GetValues(Proc: TGetStrProc); override;
procedure SetValue(const Value: String); override;
end;
Now our property is going to have a Jekyll and Hyde personality change depending on whether we have an instance in the Child property. If we don't have a Child instance then it will be a drop down property editor (a regular Class Editor where you select a class from a given list), if we do it will be a subcomponent editor.
The HasSubProperties method will be used to gate that personality change. If we already have a Child class instance then HasSubProperties will return true, but if we have selected multiple components we need to check them all.
function TParentChildProperty.HasSubProperties: Boolean;
var
I: Integer;
begin // do all components being edited have the ChildClass set
for I := 0 to PropCount - 1 do
begin
Result := TParent(GetComponent( I)).Child <> nil;
if not Result then Exit;
end;
Result := True;
end;
The GetAttributes method actually does the work of setting up the different persionalities. Note: paVolatileSubProperties is available in Delphi6 and up only, but the property editor will function just fine in earlier IDEs (where all sub properties where assumed to be volatile).
function TParentChildProperty.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes;
if not HasSubProperties then
Exclude(Result, paSubProperties);
Result := Result - [paReadOnly] + [paValueList, paSortList, paRevertable, paVolatileSubProperties];
end;
Now we need to populate the drop down list from our Component registration object.
procedure TParentChildProperty.GetValues(Proc: TGetStrProc);
var
I: Integer;
begin
for I := 0 to GetRegisteredChilds.Count - 1 do
Proc(GetRegisteredChilds.Descriptions[ I ]);
end;
When a value is selected from the drop down it will be a class name. We then have to tell the TParent in the Object inspector to go ahead and make one of those and put it in its Child property. Then we call Modified to tell the object inspector to redraw this editor, and in doing that getAttributes will be called and the new personality established.
procedure TParentChildProperty.SetValue(const Value: String);
var
ChildClass: TCustomChildClass;
I: Integer;
begin
ChildClass := TCustomChildClass(GetRegisteredChilds.FindByClassName(Value));
if ChildClass = nil then
ChildClass := TCustomChildClass(GetRegisteredChilds.FindByDescription(Value));
for I := 0 to PropCount - 1 do
TParent(GetComponent(I)).ChildClass := ChildClass;
Modified;
end;
We flesh out the editor by giving it the text we want to display when it has a valid class.
function TParentChildProperty.GetValue: string;
begin
if HasSubProperties then
Result := GetRegisteredChilds.GetDescriptionByClass(TCustomChild(GetOrdValue).ClassType)
else
Result := '';
end;
Finally, you need to expose the components and property editors to the IDE in the register procedure. Note we are actually removing the property ChildClassName from the Object Inspector by giving it a nil editor. We do this because with the TParentChildProperty editor we are serving a dual purpose of selecting the class type and fixing it's sub-properties in the object inspector.
procedure Register;
begin
RegisterComponents('RAM', [TParent]);
RegisterPropertyEditor(TypeInfo(String), TParent, 'ChildClassName', nil);
RegisterPropertyEditor(TypeInfo(TCustomChild), TParent,'Child', TParentChildProperty);
end;
Can I add child events in the same way?
At Developer Express we have built utility classes to enable events to be cascaded in the same way properties are. This is of course a proprietary technology and can not be redistributed; however for completeness I will show how to use these classes.
If you are building components for developers who already have the Express Quantum Grid 4, for example custom editors that use the Express Editors 4 architecture, you can require the package dclcxLibraryVCLXX where XX is the compiler initial (D,C,K) and the version (ie; D4, C5, K3) and you will be able to descend from our nested event editors.
You will have to use the unit cxPropEditors in the design-time unit.
{$IFDEF DELPHI6}
Types, DesignIntf, DesignEditors,
{$IFDEF VCL}
VCLEditors,
{$ENDIF}
{$ELSE}
DsgnIntf,
{$ENDIF}
TypInfo,
Classes,
SpecializedComponent,
cxPropEditors, // for NestedEvent property editor classes
cxClasses; // for TcxRegisteredClasses generic class registration class
In your TParent you will have to add a dummy editor to use as a placeholder for the location that the events of the child will be hosted.
property ChildEvents: TNotifyEvent read FSubClassEvents write FSubClassEvents;
The design-time editor is quite simple with the Developer Express , simply descend from the TcxNestedEventProperty editor and override the GetInstance method to return a reference to the Child component being edited.
// Property Editor to expose nested events from persistent sub components
type
TParentSpecilizationEventsProperty = class(TcxNestedEventProperty)
protected
function GetInstance: TPersistent; override;
end;
function TParentSpecilizationEventsProperty.GetInstance: TPersistent;
begin
Result := TParent(GetComponent(0)).Child;
end;
And finally register it for the dummy event made earlier.
RegisterPropertyEditor(TypeInfo(TNotifyEvent),
TParent,
'ChildEvents',
TParentSpecilizationEventsProperty);
Next time you rebuild your design-time package, you will see any events in the Child component classes exposed in the TParent object inspector.
Can I freely use this technique?
You are welcome to use this technique in your components as long as you do not redistribute any Developer Express units.
To build one set of source for all platforms I suggest building a file similar to the cxGridVer.inc file from ExpressQuantumGrid 4 which will map versions into conditional definitions that will allow you to build uses statements that work correctly for all platforms. You only need this for the Design-time unit.
unit RegSpecializedComponent;
{$I cxGridVer.inc}
interface
procedure Register;
implementation
uses
{$IFDEF DELPHI6}
Types, DesignIntf, DesignEditors,
{$IFDEF VCL}
VCLEditors,
{$ENDIF}
{$ELSE}
DsgnIntf,
{$ENDIF}
TypInfo,
Classes,
SpecializedComponent,
cxClasses; // for TcxRegisteredClasses generic class registration class
Conclusion
This gives components a very powerful in-place delegation mechanism that will allow you to partition your components into container classes and behavior classes that the containers delegate to, so palette components can have multiple behaviors allowing you to reduce the palette clutter without reducing functionality.
I've attached some source code, including code to implement events if you have the Quantm Grid.