TComboBox 'Control has no parent window' in destructor - delphi

I'm using Delphi XE2. I build a custom TComboBox so that I can easily add key/string pairs and handle the cleanup in the component's destructor.
All if not (csDesigning in ComponentState) code is omitted for brevity.
interface
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
TMyComboBox = class(TComboBox)
public
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
end;
implementation
destructor TMyComboBox.Destroy;
var i: Integer;
begin
for i := 0 to Self.Items.Count - 1 do
Self.Items.Objects[i].Free;
Self.Clear;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
Self.Items.AddObject(Value, rec);
end;
When the application closes, the destructor is called, but the Items.Count property is inaccessible because the TComboBox must have a parent control to access this property. By the time the destructor is called, it no longer has a parent control.
I saw this problem once before and had to store the objects in a separate TList and free them separately. But that only worked because the order that I added them to the TList was always the same as the strings added to the combo box. When the user selected a string, I could use the combo box index to find the correlating object in the TList. If the combo box is sorted, then the indexes won't match, so I can't always use that solution.
Has anyone else seen this? How did you workaround the issue? It would be really nice to be able to free the objects in the component's destructor!

You can override function GetItemsClass:
function GetItemsClass: TCustomComboBoxStringsClass; override;
It is called by Combo to create Items (by default it is TComboBoxStrings probably).
Then you can create your own TComboBoxStrings descendant, for example TComboBoxStringObjects, where
you can free object linked with item (when item deleted).

After reading the link from Sertac (David Heffernan's comment and NGLN's answer), I believe a solution that stores the objects in a managed list and not in a GUI control is the best. To that end, I have create a combo box that descends from TCustomComboBox. This lets me promote all the properties except for Sorted to published. This keeps the internal FList in sync with the strings in the combo boxes Items property. I just make sure they are sorted the way I want before adding them...
The following shows what I did. I only included the essential code (less range checking) for brevity, but included some conditional logic that allows the combo box to be used without objects as well.
FList is properly destroyed in the destructor, freeing all objects without any run-time exceptions and the object list is managed within the component itself instead of having to manage it elsewhere -- making it very portable. It works when the control is added to a form at design-time, or when it is created at run-time. I hope this is useful to someone else!
interface
type
TMyComboBox = class(TCustomComboBox)
private
FList: TList;
FUsesObjects: Boolean;
function GetKey: string;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
procedure ClearAllItems;
procedure DeleteItem(const Index: Integer);
property Key: string read GetKey;
published
// all published properties (except Sorted) from TComboBox
end;
implementation
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
function TMyComboBox.GetKey: string;
begin
if not FUsesObjects then
raise Exception.Create('Objects are not used.');
Result := TKeyRec(FList.Items[ItemIndex]).Key;
end;
constructor TMyComboBox.Create(AOwner: TComponent);
begin
inherited;
if not (csDesigning in ComponentState) then
begin
FUsesObjects := False;
FList := TList.Create;
end;
end;
destructor TMyComboBox.Destroy;
begin
if not (csDesigning in ComponentState) then
begin
ClearAllItems;
FreeAndNil(FList);
end;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
FUsesObjects := True;
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
FList.Add(rec);
Items.Add(Value);
end;
procedure TMyComboBox.ClearAllItems;
var i: Integer;
begin
if not (csDesigning in ComponentState) then
begin
if FUsesObjects then
begin
for i := 0 to FList.Count - 1 do
TKeyRec(FList.Items[i]).Free;
FList.Clear;
end;
if not (csDestroying in ComponentState) then
Clear; // can't clear if the component is being destroyed or there is an exception, 'no parent window'
end;
end;
procedure TMyComboBox.DeleteItem(const Index: Integer);
begin
if FUsesObjects then
begin
TKeyRec(FList.Items[Index]).Free;
FList.Delete(Index);
end;
Items.Delete(Index);
end;
end.

There's a way that avoid the need of rewrite the component to use another list to save the objects. The solution is to use the WM_DESTROY message along with the ComponentState property.
When the component is about to be destroyed, its state change to csDestroying, so the next time it receives a WM_DESTROY message it will be not part of the window recreation process.
We use this method in our component library sucessfully.
TMyCombo = class(TCombobox)
...
procedure WMDestroy(var message: TMessage); message WM_DESTROY;
...
procedure TMyCombo.WMDestroy(var message: TMessage);
var
i: integer;
begin
if (csDestroying in ComponentState) then
for i:=0 to Items.Count - 1 do
Items.Objects[i].Free;
inherited;
end;

Related

Create component to update global properties of controls

I have a set of components, that share some global variables to control common properties, e.g. style features.
These are currently accessed at run-time via a global class, e.g. MyCompsSettings().SomeProperty.
I thought it might be useful to allow users to configure some of these properties at design-time, so I converted the global class to a component, and because these properties need to be shared between MyCompsSettings() and instances of my TMyCompsSettings component(s), I used global vars to store the state, e.g.
type
TMyCompsSettings = class(TComponent)
private
function GetBackgroundColor(): TColor;
procedure SetBackgroundColor(const v: TColor);
function GetTitleText(): string;
procedure SetTitleText(const v: string);
published
property BackgroundColor: TColor read GetBackgroundColor write SetBackgroundColor;
property TitleText: string read GetTitleText write SetTitleText;
end;
implementation
var
gBackgroundColor: TColor;
gTitleText: string;
function TIEGlobalSettings.GetBackgroundColor(): TColor;
begin
Result := gBackgroundColor;
end;
procedure TIEGlobalSettings.SetBackgroundColor(const v: TColor);
begin
gBackgroundColor := v;
end;
function TIEGlobalSettings.GetTitleText(): string;
begin
Result := gTitleText;
end;
procedure TIEGlobalSettings.SetTitleText(const v: string);
begin
gTitleText := v;
end;
However, I overlooked that the IDE will also maintain the var states, so when I:
Add a TMyCompsSettings component to a form
Set MyCompsSettings1.TitleText to 'ABC' in the object inspector
Open a different project
Add a TMyCompsSettings component to a form
-> MyCompsSettings1.TitleText is already 'ABC'!
Obvious of course, but I didn't consider that, and it breaks my whole model.
Is there a correct way to do this? e.g. Fields at design-time, vars at run-time, e.g.
type
TMyCompsSettings = class(TComponent)
private
FAuthoritative: Boolean; // Set to true for first instance, which will be MyCompsSettings()
FBackgroundColor: TColor;
FTitleText: string;
function GetBackgroundColor(): TColor;
procedure SetBackgroundColor(const v: TColor);
function GetTitleText(): string;
procedure SetTitleText(const v: string);
published
property BackgroundColor: TColor read GetBackgroundColor write SetBackgroundColor;
property TitleText: string read GetTitleText write SetTitleText;
end;
implementation
function TIEGlobalSettings.GetBackgroundColor(): TColor;
begin
if FAuthoritative or ( csDesigning in ComponentState ) then
Result := FBackgroundColor
else
Result := MyCompsSettings().BackgroundColor;
end;
procedure TIEGlobalSettings.SetBackgroundColor(const v: TColor);
begin
if FAuthoritative or ( csDesigning in ComponentState ) then
FBackgroundColor := v
else
MyCompsSettings().BackgroundColor := v;
end;
function TIEGlobalSettings.GetTitleText(): string;
begin
if FAuthoritative or ( csDesigning in ComponentState ) then
Result := FTitleText
else
Result := MyCompsSettings().TitleText;
end;
procedure TIEGlobalSettings.SetTitleText(const v: string);
begin
if FAuthoritative or ( csDesigning in ComponentState ) then
FTitleText := v
else
MyCompsSettings().TitleText := v;
end;
As the IDE is a process, global variables in the process will remain in the process.
If you want to be able to track the settings between different projects in the IDE (which, if they're in a project group, could both have forms open at the same time) then you will need to find a way of tracking them.
Probably the simplest way is to have the settings held in an object - there can be a global object loaded in an initialization section and freed in a finalization section. Your form based TComponents can check if they are in design mode or not and if they are in design mode then they create a new separate copy of the object, if not they connect to the global instance of the object.
Other components that then access those settings will all use the global object - to ensure that the contents of the object match the design time version you would need to overwrite the global object with any form loaded version. You can do this in the TComponent's Loaded routine.
This code is unchecked, but should give you an outline of how it might work.
implementation
type
TMySettings = class(TPersistent) // so you can .Assign
protected
FOwner: TPersistent;
function GetOwner(): TPersistent; override;
public
constructor Create(AOwner: TPersistent); reintroduce;
property
Owner: TPersistent read GetOwner();
end;
TMySettingsComponent = class(TComponent)
protected
procedure Loaded(); override;
public
destructor Destroy(); override;
procedure AfterConstruction(); override;
end;
implementation
var
gpMySettings: TMySettings;
constructor TMySettings.Create(AOwner: TPersistent);
begin
Self.FOwner:=AOwner;
inherited Create();
end;
function TMySettins.GetOwner(): TPersistent;
begin
Result:=Self.FOwner;
end;
destructor TMySettingsComponent.Destroy;
begin
if(Self.FSettings.Owner = Self) then
FreeAndNIl(Self.FSettings);
inherited;
end;
procedure TMySettingsComponent.AfterConstruction();
begin
// our ComponentState will not yet be set
if( (Self.Owner <> nil) And
(csDesigning in Self.Owner.ComponentState) ) then
Self.FSettings:=TMySettings.Create(Self)
else
Self.FSettings:=gpMySettings;
inherited;
end;
procedure TMySettingsComponent.Loaded;
begin
if( (Self.FMySettings.Owner=Self) And
(gpMySettings<>nil) ) then
gpMySettings.Assign(Self.FMySettings);
end;
initialization
gpMySettings:=TMySettings.Create(nil);
finalization
FreeAndNIl(gpMySettings);
You would also want to ensure that in your TMySettingsComponent you update the global object when the user is changing the properties. This could be as simple as:
procedure TMyComponentSettings.SetBackgroundColour(FNewValue: TColor);
begin
if(Self.FSettings.FBkColour<>FNewValue) then
begin
Self.FSettings.FBkColour:=FNewValue;
if( (Self.FSettings.Owner=Self) And
(gpMySettings<>nil) ) then
gpMySettings.Assign(Self.FSettings);
// -- or use gpMySettings.FBkColour:=FNewValue;
end;
end;

How to properly invoke a custom component editor form for a component in delphi

I'm getting access violation when calling the Edit method of TComponentEditor class:
type
TLBIWXDataGridEditor = class(TComponentEditor)
public
function GetVerbCount: Integer; override;
function GetVerb(Index: Integer): string; override;
procedure ExecuteVerb(Index: Integer); override;
procedure Edit; override;
end;
Here is the overridden Edit Method:
procedure TLBIWXDataGridEditor.Edit;
var
_DsgForm: TLBIWXDataGridDesigner;
begin
_DsgForm := TLBIWXDataGridDesigner(Application);
try
_DsgForm.DataGrid := TLBIWXDataGrid(Self.Component);
_DsgForm.ShowModal;
finally
FreeAndNil(_DsgForm);
end;
end;
All TLBIWXDataGrid properties will be changeable only inside the design form, because it doesn't have any published properties.
When calling the Edit method by double clicking the component at design time I either get AV or the IDE Crashes abruptly.
I don't think the problem is related to the other overridden methods, but here are their implementations:
procedure TLBIWXDataGridEditor.ExecuteVerb(Index: Integer);
begin
case Index of
0: MessageDlg ('add info here', mtInformation, [mbOK], 0);
1: Self.Edit;
end;
end;
function TLBIWXDataGridEditor.GetVerb(Index: Integer): string;
begin
case Index of
0: Result := '&About...';
1: Result := '&Edit...';
end;
end;
function TLBIWXDataGridEditor.GetVerbCount: Integer;
begin
result := 2;
end;
What am I missing?
This line is wrong:
_DsgForm := TLBIWXDataGridDesigner(Application);
It is typecasting the Application object into a TLBIWXDataGridDesigner, which will not work.
Use this instead:
_DsgForm := TLBIWXDataGridDesigner.Create(Application);
Or this, since you are freeing the dialog manually, so it does not need an Owner assigned:
_DsgForm := TLBIWXDataGridDesigner.Create(nil);

Property setter never seems to fire in Lazarus?

Overview
I have a TCustomControl I am working on in Lazarus and outside of this class I have a separate TPersistent class which will be used for some properties.
The TPersistent class when published from the TCustomControl should show in the Object Inspector as sub-properties as I don't want certain properties to be shown from the top level, basically this is putting some properties into its own group within the TCustomControl.
The structure of this code is as follows:
type
TMyControlHeaderOptions = class(TPersistent)
private
FOnChange: TNotifyEvent;
FHeight: Integer;
FVisible: Boolean;
procedure SetHeight(const Value: Integer);
procedure SetVisible(const Value: Boolean);
protected
procedure Changed;
public
constructor Create(AOwner: TComponent); virtual;
destructor Destroy; override;
procedure Assign(Source: TPersistent); override;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
published
property Height: Integer read FHeight write SetHeight default 20;
property Visible: Boolean read FVisible write SetVisible default True;
end;
TMyControl = class(TCustomControl)
private
FHeaderOptions: TMyControlHeaderOptions;
procedure SetHeaderOptions(const Value: TMyControlHeaderOptions);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Align;
property BorderStyle default bsSingle;
property HeaderOptions: TMyControlHeaderOptions read FHeaderOptions write SetHeaderOptions;
end;
Here is the code for TMyControlHeaderOptions:
constructor TMyControlHeaderOptions.Create(AOwner: TComponent);
begin
FHeight := 20;
FVisible := True;
end;
destructor TMyControlHeaderOptions.Destroy;
begin
inherited Destroy;
end;
// this method never fires (see TMyControl.SetHeaderOptions)
procedure TMyControlHeaderOptions.Assign(Source: TPersistent);
begin
if (Source is TMyControlHeaderOptions) then
begin
FHeight := (Source as TMyControlHeaderOptions).Height;
FVisible := (Source as TMyControlHeaderOptions).Visible;
end
else
inherited Assign(Source);
end;
procedure TMyControlHeaderOptions.Changed;
begin
if Assigned(FOnChange) then
begin
FOnChange(Self);
end;
end;
procedure TMyControlHeaderOptions.SetHeight(const Value: Integer);
begin
if Value <> FHeight then
begin
FHeight := Value;
Changed;
end;
end;
procedure TMyControlHeaderOptions.SetVisible(const Value: Boolean);
begin
if Value <> FVisible then
begin
FVisible := Value;
Changed;
end;
end;
And the TCustomControl code:
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FHeaderOptions := TMyControlHeaderOptions.Create(Self);
Self.ControlStyle := Self.ControlStyle + [csAcceptsControls];
Self.BorderStyle := bsSingle;
Self.Height := 200;
Self.Width := 250;
end;
destructor TMyControl.Destroy;
begin
FHeaderOptions.Free;
inherited Destroy;
end;
// this method never fires which is why TMyControlHeaderOptions.Assign
// never fires either. So the task is understanding and solving why this
// procedure never gets fired?
procedure TMyControl.SetHeaderOptions(const Value: TMyControlHeaderOptions);
begin
FHeaderOptions.Assign(Value);
end;
Problem
The property HeaderOptions never triggers or gets fired at designtime or runtime and I just can't understand or see why not? As you can see from the comments included in the code above SetHeaderOptions doesn't appear to be doing anything at all, it never responds to changes made at designtime or runtime.
I don't have Delphi installed to compare or test with but the code has been taken from custom controls I had been previously working on and I am pretty much certain it should work, I don't seem to have missed anything out that I can see. My only assumption at this point is the differences in Lazarus and Delphi and so the problem possibly lies within Lazarus?
Question
So my question is why does the property setter HeaderOptions never get fired and what can be done to make sure it does?
I sense something simple or obvious but I just cannot figure out what it is.
When you change a property inside this TPersistent, it fires the property setter of that particular property. It's not supposed to call the setter of the TPersistent itself. That only occurs in two scenarios: a) When the DFM is streamed in on creation, or b) when you manually assign a new value to the actual TPersistent. If you want to capture when any property is changed, you need to capture on each property individually, perhaps triggering an OnChange notify event which feeds back to its owner. That's actually how things such as the TFont or TStrings work.
Take a look at some of the built-in classes, such as TFont and TStrings - they use a TNotifyEvent named OnChange to handle such changes.
I am still perplexed as to why this was not working in Lazarus as I am almost certain it did work in Delphi.
I managed to come up with a workaround in the meantime:
TMyControl = class(TCustomControl)
private
FHeaderOptions: TMyControlHeaderOptions;
procedure HeaderOptionsChanged(Sender: TObject); // added this line
procedure SetHeaderOptions(const Value: TMyControlHeaderOptions); // removed this procedure
published
property Align;
property BorderStyle default bsSingle;
property HeaderOptions: TMyControlHeaderOptions read FHeaderOptions write FHeaderOptions; // changed this
end;
Then added this in the constructor:
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FHeaderOptions := TMyControlHeaderOptions.Create(Self);
FHeaderOptions.OnChange := #HeaderOptionsChanged; // added this line
Self.ControlStyle := Self.ControlStyle + [csAcceptsControls];
Self.BorderStyle := bsSingle;
Self.Height := 200;
Self.Width := 250;
end;
Code for the new HeaderOptionsChanged procedure:
procedure TMyControl.HeaderOptionsChanged(Sender: TObject);
begin
// header options changed
Invalidate;
end;

Logging MenuItem OnClick Event

I have a project (Delphi 10 Seattle, win32) with a many menus and many items in those menus. Some of the menu items are created at design time, some of them at run time.
What I'm looking to do is log some information about the TMenuItem, such as the name/caption, timestamp, etc. when the OnClick event is triggered.
I could simply add a procedure call to the start of every function which is assigned to the TMenuItem OnClick event but I was wondering if there was a more elegant solution.
Also to note, I have tried Embarcadero's AppAnalytics but I found it didn't give me the information or flexibility I wanted and was rather pricey.
Edit: I'll add some more information detailing what options I have considered (which I probably should've done to start with).
The simple adding a function to every menuitem click I want to log, which would mean doing this for a lot of functions and would have to add it to every new menu item added.
procedure TSomeForm.SomeMenuItem1Click(Sender: TObject);
var
item : TMenuItem;
begin
item := Sender as TMenuItem;
LogMenuItem(item); // Simple log function added to the start of each menuitem click
end;
By 'more elegant solution' I mean would it be possible to add a 'hook' so that all TMenuItem OnClick events triggered another procedure (which would do the logging) before calling the procedure assigned to the OnClick Event.
Or another option I considered was creating a class which inherited from TMenuItem which would override TMenuItem.Click and do the logging before generating the OnClick event. But then I didn't know how that would work for the design time menu items without a lot of work remaking the menus.
This is much easier to achieve using actions. That has the benefit that you'll pick up actions invoked by UI elements other than menus, for instance toolbars, buttons etc.
Use an action list, or an action manager, as you prefer. For example, with an action list the action list object has an OnExecute event that fires when any action is executed. You can listen for that event and there log the details of the action being executed.
I absolutely agree that actions is the way to go, but for completeness sake and those cases where you quickly want to debug an application using old-style menus, here's a unit that you can use with menu items. It will even work if the menu items have actions linked to them, but it won't work for any other controls with actions like a TActionMainMenuBar. All the debugging code is in this unit to keep your normal code clutter-free. Just add the unit to the uses clause and call StartMenuLogging with any applicable component, e.g. a menu component, form component or even Application! Any menu items in the tree under it will be hooked. So you can potentially debug all menu-clicks in all forms with just those two lines in your production code. You can use StopMenuLogging to stop, but it's optional. Warning: This unit was not tested properly - I took an old debug unit I wrote and cleaned it up for this purpose and just tested it superficially.
unit LogMenuClicks;
interface
uses
Classes;
procedure StartMenuLogging(AComponent: TComponent);
procedure StopMenuLogging(AComponent: TComponent);
procedure StopAllMenuLogging;
implementation
uses
SysUtils,
Menus;
type
PLoggedItem = ^TLoggedItem;
TLoggedItem = record
Item: TMenuItem;
OldClickEvent: TNotifyEvent;
end;
TLogManager = class(TComponent)
private
FList: TList;
FLog: TFileStream;
procedure Delete(Index: Integer);
function FindControl(AItem: TMenuItem): Integer;
procedure LogClick(Sender: TObject);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddControl(AItem: TMenuItem);
procedure RemoveControl(AItem: TMenuItem);
end;
var
LogMan: TLogManager = nil;
{ TLogManager }
constructor TLogManager.Create(AOwner: TComponent);
begin
inherited;
FLog := TFileStream.Create(ChangeFileExt(ParamStr(0), '.log'), fmCreate or fmShareDenyWrite);
FList := TList.Create;
end;
destructor TLogManager.Destroy;
var
i: Integer;
begin
i := FList.Count - 1;
while i >= 0 do
Delete(i);
FList.Free;
FLog.Free;
inherited;
end;
procedure TLogManager.Notification(AComponent: TComponent; Operation: TOperation);
begin
if Operation = opRemove then
RemoveControl(TMenuItem(AComponent));
inherited;
end;
procedure TLogManager.Delete(Index: Integer);
var
li: PLoggedItem;
begin
li := FList[Index];
with li^ do
begin
Item.RemoveFreeNotification(Self);
Item.OnClick := OldClickEvent;
end;
Dispose(li);
FList.Delete(Index);
end;
function TLogManager.FindControl(AItem: TMenuItem): Integer;
begin
Result := FList.Count - 1;
while (Result >= 0) and (PLoggedItem(FList[Result]).Item <> AItem) do
Dec(Result);
end;
procedure TLogManager.AddControl(AItem: TMenuItem);
var
li: PLoggedItem;
begin
if not Assigned(AItem) then
Exit;
if FindControl(AItem) >= 0 then
Exit;
New(li);
li.Item := AItem;
li.OldClickEvent := AItem.OnClick;
AItem.OnClick := LogClick;
FList.Add(li);
AItem.FreeNotification(Self);
end;
procedure TLogManager.RemoveControl(AItem: TMenuItem);
var
i: Integer;
begin
if Assigned(AItem) then
begin
i := FindControl(AItem);
if i >= 0 then
Delete(i);
end;
end;
procedure TLogManager.LogClick(Sender: TObject);
var
s: string;
begin
s := Format('%s: %s' + sLineBreak, [TComponent(Sender).Name, FormatDateTime('', Now)]);
FLog.WriteBuffer(s[1], Length(s));
PLoggedItem(FList[FindControl(TMenuItem(Sender))]).OldClickEvent(Sender);
end;
procedure StartMenuLogging(AComponent: TComponent);
procedure CheckControls(Comp: TComponent);
var
i: Integer;
begin
if Comp is TMenuItem then
LogMan.AddControl(TMenuItem(Comp))
else
for i := 0 to Comp.ComponentCount - 1 do
CheckControls(Comp.Components[i]);
end;
begin
if not Assigned(LogMan) then
LogMan := TLogManager.Create(nil);
CheckControls(AComponent);
end;
procedure StopMenuLogging(AComponent: TComponent);
procedure CheckControls(Comp: TComponent);
var
i: Integer;
begin
if Comp is TMenuItem then
LogMan.RemoveControl(TMenuItem(Comp))
else
for i := 0 to Comp.ComponentCount - 1 do
CheckControls(Comp.Components[i]);
end;
begin
if Assigned(LogMan) then
CheckControls(AComponent);
end;
procedure StopAllMenuLogging;
begin
LogMan.Free;
end;
initialization
finalization
if Assigned(LogMan) then
LogMan.Free;
end.

How to make subcomponent TAction-s available at design time?

In my custom component I created some TAction-s as subcomponents. They're all published, but I could not assign them at design time since they were not available through object inspector.
How do you make them "iterable" by the object inspector? I have tried to set the Owner of the actions to the Owner of the custom component (which is the hosting Form) to no success.
EDIT: It looks like Embarcadero changed Delphi IDE behaviour related with this problem. If you are using Delphi versions prior XE, you should use solution from my own answer. For XE and above, you should use solution from Craig Peterson.
EDIT: I've added my own answer that solves the problem, i.e. by creating a TCustomActionList instance in my custom component and setting its Owner to the hosting form (owner of the custom component). However I am not too happy with this solution, since I think the instance of TCustomActionList is kind of redundant. So I am still hoping to get better solution.
EDIT: Add code sample
uses
.., ActnList, ..;
type
TVrlFormCore = class(TComponent)
private
FCancelAction: TBasicAction;
FDefaultAction: TBasicAction;
FEditAction: TBasicAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure CancelActionExecute(ASender: TObject); virtual;
procedure EditActionExecute(ASender: TObject); virtual;
public
constructor Create(AOwner: TComponent); override;
published
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
end;
implementation
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TAction.Create(Self);
with FDefaultAction as TAction do
begin
SetSubComponent(True);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FCancelAction := TAction.Create(Self);
with FCancelAction as TAction do
begin
SetSubComponent(True);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FEditAction := TAction.Create(Self);
with FEditAction as TAction do
begin
SetSubComponent(True);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
end;
As far as I can tell you're not supposed to do it that way.
The easy way to do what you want is to create new standalone actions that can work with any TVrlFormCore component and set the target object in the HandlesTarget callback. Take a look in StdActns.pas for examples. The actions won't be available automatically when sommeone drops your component on the form, but they can add them to their action list manually using the New Standard Actions... command. There's a good article on registering standard actions here.
If you really want to auto-create the actions you need to set the action Owner property to the form and you need to set the Name property. That's all that's necessary, but it does introduce a bunch of issues you need to work around:
The form owns the actions so it will add them its declaration's published section and will auto-create them as part of the streaming process. To work around that you can just disable streaming by overwriting the action's WriteState method and skip the inherited behavior.
Since you aren't writing the state, none of the properties will be persisted. To avoid confusing your users you should switch make the actions descend from TCustomAction instead of TAction, so it doesn't expose anything. There may be way to make the action stream properly, but you didn't say whether it was necessary.
You need to register for free notifications in case the form frees the action before you can.
If someone drops more than one of your component on the action names will conflict. There's multiple ways to handle that, but the cleanest would probably be to override the component's SetName method and use its name as a prefix for the actions' names. If you do that you need to use RegisterNoIcon with the new class so they don't show up on the form.
In the IDE's Structure pane the actions will show up directly under the form, rather than nested like ActionList shows. I haven't found a way around that; none of SetSubComponent, GetParentComponent/HasParent, or GetChildren have any effect, so this may be hard-coded behavior. You can delete the action from the structure pane, separate from the component, too.
I'm sure it can be improved, but this works without any custom property editors:
type
TVrlAction = class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TComponent)
private
FDefaultAction: TVrlAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
property DefaultAction: TVrlAction read FDefaultAction;
end;
procedure Register;
implementation
// TVrlAction
procedure TVrlAction.WriteState(Writer: TWriter);
begin
// No-op
end;
// TVrlFormCore
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction do
begin
FreeNotification(Self);
Name := 'DefaultAction';
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
end;
destructor TVrlFormCore.Destroy;
begin
FDefaultAction.Free;
inherited;
end;
procedure TVrlFormCore.DefaultActionExecute(ASender: TObject);
begin
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation = opRemove then
if AComponent = FDefaultAction then
FDefaultAction := nil;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
end;
procedure Register;
begin
RegisterComponents('Samples', [TVrlFormCore]);
RegisterNoIcon([TVrlAction]);
end;
EDIT: Use this solution for Delphi versions prior to Delphi XE. For XE and later, use Craig Peterson answer (which does not require redundant TCustomActionList instance).
After meddling around and using information from Craig Peterson's answer, I've decided to instantiate a TCustomActionList in my custom component. So far it is the only way to get list of actions in Object Inspector.
Here is the code:
uses
..., ActnList, ...;
type
TVrlAction=class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
published
property Caption;
end;
TVrlActionList=class(TCustomActionList)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TVrlItemSource)
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{ TVrlAction }
procedure TVrlAction.WriteState(Writer: TWriter);
begin
end;
{ TVrlActionList }
procedure TVrlActionList.WriteState(Writer: TWriter);
begin
end;
{ TVrlFormCore }
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FActions := TVrlActionList.Create(AOwner);
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FActions.AddAction(TContainedAction(FDefaultAction));
FCancelAction := TVrlAction.Create(AOwner);
with FCancelAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FActions.AddAction(TContainedAction(FCancelAction));
FEditAction := TVrlAction.Create(AOwner);
with FEditAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
FActions.AddAction(TContainedAction(FEditAction));
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation=opRemove then
begin
if AComponent = FMaster then
FMaster := nil
else if (AComponent is TVrlFormCore) then
FDetails.Remove(TVrlFormCore(AComponent))
else if AComponent=FDefaultAction then
FDefaultAction := nil
else if AComponent=FCancelAction then
FCancelAction := nil
else if AComponent=FEditAction then
FEditAction := nil;
end;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FActions<>nil then
FActions.Name := NewName + '_Actions';
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
if FCancelAction <> nil then
FCancelAction.Name := NewName + '_CancelAction';
if FEditAction <> nil then
FEditAction.Name := NewName + '_EditAction';
end;
You cannot assign them because they are read only by design:
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
You should change your class' interface to:
property DefaultAction: TBasicAction read FDefaultAction write FDefaultAction;
property CancelAction : TBasicAction read FCancelAction write FCancelAction;
property EditAction : TBasicAction read FEditAction write FEditAction;
or write appropriate setter for each action.
Edit:
What you need is then
to implement your 3 custom actions as Predefined Actions (See StdActns.pas for samples).
to register them by calling ActnList.RegisterActions. (See RAD Studio documentation)
to add to the form a TActionList and/or TActionManager to allow your Predefined Actions appear in the list of predefined actions in the action list editor of every TControl's descendent.
You may do extensive search on google for the topic and find some concrete example.

Resources