I have a custom control. It has a published property of type TOwnedCollection
, which of course contains several instances of TCollectionItem
(inherited to my own implementation). Let's call them TMyComponent
, TMyCollection
, and TMyCollectionItem
.
Within TMyCollectionItem
, I have a TPersistent
property (let's call it MyProp: TMyProp
), which also has a corresponding design-time editor installed into the IDE. All this works just fine, so when you're editing the collection, select a collection item, you can click the little button in the object inspector to invoke the property editor. All of this works fine.
What I would like to add is the ability to double-click the item within the collection editor to also invoke that very same property editor. However, I cannot find decent information how to do this. ChatGPT gave me code that simply didn't compile, probably because it was for much older versions of Delphi.
How do I implement the ability to double-click a TMyCollectionItem
to invoke the property editor for TMyCollectionItem.MyProp
?
NOTE: Looking into the source of ColnEdit.pas
(for this collection editor), I see the TListView
implements OnClick
, but not OnDblClick
, and so I'm questioning whether it's even possible to do what I want without rolling out my own collection editor.
The IDE's default Collection Editor does not natively support the feature you are looking for. As you noted:
NOTE: Looking into the source of
ColnEdit.pas
(for this collection editor), I see theTListView
implementsOnClick
, but notOnDblClick
, and so I'm questioning whether it's even possible to do what I want without rolling out my own collection editor.
The default Collection Editor simply does not implement any logic when a TListView
item is double-clicked on.
(BTW, it is not the TListVew.OnClick
event that updates the Object Inspector, it is the TListVew.OnChange
event that does. That is the event that fires when ListView items are selected.)
However, all is not lost! There IS a way you can access the TListView
of the default Collection Editor and assign your own OnDblClick
event handler to it:
Derive a new class from ColnEdit.TCollectionEditor
, and have it assign an OnDblClick
handler to the inherited TListView
.
Derive a new class from ColnEdit.TCollectionProperty
and override its virtual GetEditorClass()
method to return your TCollectionEditor
-derived class type.
Register your TCollectionProperty
-derived class with DesignIntf.RegisterPropertyEditor()
to override the default collection editor for your TMyCollection
class.
Your derived Collection Editor can then invoke your TMyProp
property editor when needed by using the DesignEditors.GetComponentProperties()
function:
Use DesignIntf.CreateSelectionList()
to create a list and Add()
the appropriate TMyCollectionItem
object to it.
Pass that list to GetComponentProperties()
, along with a callback that will receive an IPropertyEditor
for each property of the TMyCollectionItem
object.
When the callback sees your TMyProp
property editor, it can call the editor's Edit()
method.
For example:
uses
..., Vcl.ComCtrls, DesignIntf, DesignEditors, System.TypInfo, ColnEdit;
type
...
TMyCollectionEditor = class(TCollectionEditor)
private
procedure ListViewDblClick(Sender: TObject);
procedure InvokeMyPropEditor(const Prop: IProperty);
public
constructor Create(AOwner: TComponent); override;
end;
TMyCollectionProperty = class(TCollectionProperty)
public
function GetEditorClass: TCollectionEditorClass; override;
end;
procedure Register;
...
constructor TMyCollectionEditor.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ListView1.OnDblClick := ListViewDblClick;
end;
procedure TMyCollectionEditor.ListViewDblClick(Sender: TObject);
var
Item: TListItem;
Components: IDesignerSelections;
begin
Item := ListView1.Selected;
if Item = nil then Exit;
Components := CreateSelectionList();
Components.Add(Collection.Items[Item.Index]);
GetComponentProperties(Components, [tkClass], Designer, InvokeMyPropEditor);
end;
procedure TMyCollectionEditor.InvokeMyPropEditor(const Prop: IProperty);
begin
if Prop.GetPropType = typeinfo(TMyProp) then
Prop.Edit;
end;
function TMyCollectionProperty.GetEditorClass: TCollectionEditorClass;
begin
Result := TMyCollectionEditor;
end;
procedure Register;
begin
RegisterComponents('...', [TMyComponent]);
RegisterPropertyEditor(typeinfo(TMyCollection), TMyComponent, '', TMyCollectionProperty);
...
end;
That being said, I did test this approach and while it DOES work functionally, there is a slight visual side effect - in 10.2.1 and later, the IDE's theming feature will no longer apply to your Collection Editor window when it is shown. So, for instance, if you use a Dark theme in the IDE, the Collection Editor will not appear Dark anymore.
To fix this, you need to use the IOTAIDEThemingServices
interface in the OpenTools API to register your TCollectionEditor
-derived class so the IDE will theme it:
Using IDE Styles in Third-Party Plugins
David Hoyle has also written an article on this topic:
For example:
uses
..., ToolsAPI;
type
...
TMyCollectionEditor = class(TCollectionEditor)
private
FThemingService: IOTAIDEThemingServices;
FThemingNotifier: Integer;
...
procedure ThemeChanged;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TThemeChangedProc = procedure of object;
TMyThemingNotifier = class(TInterfacedObject, IOTANotifier, INTAIDEThemingServicesNotifier)
public
OnThemeChanged: TThemeChangedProc;
constructor Create(AProc: TThemeChangedProc);
procedure AfterSave;
procedure BeforeSave;
procedure Destroyed;
procedure Modified;
procedure ChangingTheme;
procedure ChangedTheme;
end;
...
constructor TMyCollectionEditor.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
...
if Supports(BorlandIDEServices, IOTAIDEThemingServices, FThemingService) then
begin
ThemeChanged;
FThemingNotifier := FThemingService.AddNotifier(TMyThemingNotifier.Create(ThemeChanged) as INTAIDEThemingServicesNotifier);
end;
end;
destructor TMyCollectionEditor.Destroy;
begin
if Assigned(FThemingService) then
FThemingService.RemoveNotifier(FThemingNotifier);
inherited Destroy;
end;
procedure TMyCollectionEditor.ThemeChanged;
begin
if FThemingService.IDEThemingEnabled then
begin
FThemingService.RegisterFormClass(TMyCollectionEditor);
FThemingService.ApplyTheme(Self);
end;
end;
constructor TMyThemingNotifier.Create(AProc: TThemeChangedProc);
begin
inherited Create;
OnThemeChanged := AProc;
end;
procedure TMyThemingNotifier.AfterSave;
begin
end;
procedure TMyThemingNotifier.BeforeSave;
begin
end;
procedure TMyThemingNotifier.Destroyed;
begin
end;
procedure TMyThemingNotifier.Modified;
begin
end;
procedure TMyThemingNotifier.ChangingTheme;
begin
end;
procedure TMyThemingNotifier.ChangedTheme;
begin
if Assigned(OnThemeChanged) then
OnThemeChanged();
end;
Despite Embarcadero's documentation saying the following (emphasis is mine):
In your plugin initialization or your form’s constructor, call
IOTAIDEThemingServices.RegisterFormClass
with the class of your form, that is, the form type. You only need to do this once to apply the style hooks to your form. You do not need to unregister it.
I find that if RegisterFormClass()
is not called again before ApplyStyle()
after a Theme change, then ApplyStyle()
DOES NOT apply the new theme correctly! As-if the IDE lost the previous Form class registration during the Theme change (IDE bug? I have reported it: RSP-43972). Calling RegisterFormClass()
before each call to ApplyStyle()
works.
I also noticed that the default TCollectionEditor
does not react to IDE Theme changes at all (definitely an IDE bug, which I have reported: RSP-43971).