Runtime assigned action ShortCut does not start in custom component
I'm having a problem getting the action assigned to a property of an inherited custom component to work when the code is completely generated at runtime (i.e. no form designer components). If I use ActionList in the form constructor and then use the same code everything works fine.
Here is my constructor for the component derived from TCustomControl
:
self.FButtonSCActionList := TActionList.Create( self.Parent ); self.FButtonSCActionList.Name := 'ButtonSCActionList'; self.FButtonSCAction := TAction.Create( self.FButtonSCActionList ); self.FButtonSCAction.Name := 'ClickShortcutAction'; self.FButtonSCAction.OnExecute := self.ExecuteButtonShortcut; self.FButtonSCAction.ShortCut := TextToShortCut('CTRL+K'); self.FButtonSCAction.Enabled := TRUE; self.FButtonSCAction.Visible := TRUE; self.FButtonSCAction.ActionList := self.FButtonSCActionList; self.Action := FButtonSCAction;
If I create a custom control using this code, add it to the toolbar, put it on a form in a new VCL Forms application, and then start the application, when I press the shortcut key, nothing happens. If I create a control without this code, put it in the form and assign the list of actions to the form, and then put the lines of code just related to creating the action and assign it the Action component property in the onclick event handler for the button, it then responds with the correct keystroke. For the life of me, I do not see what is different, but I hope you, the Actions of Guru Delphi, can ...
The purpose of this action is to allow a developer to assign a custom shortcut to a button in the Object inspector via a property. I would like to assign the "inline" action directly, but cannot find out how to access the property of the shortcut. (Obviously I could do this with other Delphi functions in HotKey, and if needed, but I also want to understand the steps, and that seems like a good place to start ...)
source to share
You don't need to create an ActionList at design time. Use the following code in the Create method:
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.SetSubComponent(true);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
FButtonSCAction.Enabled := TRUE;
FButtonSCAction.Visible := TRUE;
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(aOwner);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
When creating a control at runtime, you might have a situation where the aOwner passed to your control is not the form itself, but another control. In this case, instead of creating a list of actions with aOwner, you will have to call a function that will give you a form from the aOwner parameter.
function GetOwnerForm(Component: TComponent): TComponent;
begin
Result := Component;
while (Result <> nil) and (not (Result is TCustomForm)) do
begin
Result := Result.Owner;
end;
end;
FButtonSCActionList := TActionList.Create(GetOwnerForm(aOwner));
source to share
Summary
There is TControl
no built-in Action component. By default, the Action property is not assigned. The user of the control can assign the property to any desired action. The control developer (you) does not have to provide an Action or ActionList.
Actual problem
I would like to assign the "inline" action directly, but cannot figure out how to access its shortcut.
This built-in default action is just an unassigned property TAction
. And if the property is not assigned, i.e. The property does not point to the Action component, then its ShortCut property does not exist.
The purpose of this action is to allow the developer (the red user of your component / control) to assign a custom shortcut to a button in the object inspector via a property.
If that's your only goal, just post the Action property and do nothing:
type
TMyControl = class(TCustomControl)
published
property Action;
end;
This will cause the property to appear in the developer's Object Object Inspector. The developer simply has to assign it one of their actions and set the ShortCut property of that action. So the actual solution is to get rid of all your current code.
Why is your current code not working
self.FButtonSCActionList := TActionList.Create( self.Parent );
Self.Parent
nil
during constructor. Two things about this:
- If you don't destroy the ActionList yourself in the destructor, you have a memory leak.
- To handle ShortCut, by default, the application traverses all action lists that (indirectly) belong to the current focused form or MainForm. Your ActionList doesn't have an owner, so its ShortCuts are never evaluated.
Solution for the current code
First, some well intentioned about your code:
-
Self
is implicit and unnecessary, and is not normal. - Runtime components do not need a set of properties
Name
. - By default,
Visible
and propertiesEnabled
are True.
Second, as Dalija Prasnikar said , ActionList is not needed during development. And the ActionList must be indirectly owned by the form the control owns. Thus, the control can also have an ActionList (XE2).
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
Somewhere before XE2, at least back in D7, the ActionList should have been registered with the form that the control owns. (There is more, but since it is unlikely that a control is the parent of another form, or that an action is called when another form is focused, this simplification can be done.) Registration can be done by making the form the owner of the action list. Since you are giving ownership of the ActionList out of control, let the ActionList notify it of possible destruction for the control with FreeNotification
. (Okay, this is contrived, since usually the control will also be destroyed, but this is exactly how it should be done).
type
TMyControl = class(TCustomControl)
private
FButtonSCActionList: TActionList;
FButtonSCAction: TAction;
protected
procedure ExecuteButtonShortcut(Sender: TObject);
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Form);
FButtonSCActionList.FreeNotification(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
end;
procedure TMyControl.ExecuteButtonShortcut(Sender: TObject);
begin
//
end;
procedure TMyControl.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FButtonSCActionList) and (Operation = opRemove) then
FButtonSCActionList := nil;
end;
Note that when it GetOwningForm
returns False
(when the developer creates the unowned control) the ActionList is not created because it cannot resolve the owner form. Overriding SetParent can fix this.
Since transferring ownership to another component seems unnecessary (and can lead to problems with the threading IDE system when running the code, if csDesigning in ComponentState
), there is another way to register the ActionList on the form by adding it to the protected FActionLists
field:
type
TCustomFormAccess = class(TCustomForm);
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
if TCustomFormAccess(Form).FActionLists = nil then
TCustomFormAccess(Form).FActionLists := TList.Create;
TCustomFormAccess(Form).FActionLists.Add(FButtonSCActionList)
end;
end;
end;
Reflection on this solution:
- This approach is undesirable. You don't have to create activity components in your custom control. Offer them separately if you need to, so that the user of your control can decide which ActionList the custom action will be added to. See also: How do I add action support to my component?
-
TControl.Action
is public,TControl.SetAction
not virtual. This means that the user of the control can assign a different action, making that action useless, and you can do nothing but it. (Not enough publication). Instead, declare a different Action property, or - again - provide a separate Action component.
source to share
Many thanks for the help! For those who will use this question to further google-fu (I live on Google these days when not in the Delphi IDE ...) here is the final fully functional code for the custom component:
unit ActionTester; interface uses Winapi.windows, Vcl.ExtCtrls, System.Types, System.SysUtils , System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Graphics, Messages, Vcl.Buttons, System.Variants, System.UITypes, Dialogs, Vcl.ExtDlgs, Generics.Collections, System.Actions, Vcl.ActnList, Clipbrd, TypInfo, Rtti, Menus; type TActionTester = class(TCustomControl) private { Private declarations } protected { Protected declarations } FButtonSCActionList: TActionList; FButtonSCAction: TAction; procedure ExecuteButtonShortcut(Sender: TObject); procedure Notification(AComponent: TComponent; Operation: TOperation); override; public { Public declarations } constructor Create(AOwner: TComponent); override; Procedure Paint; override; Destructor Destroy; Override; published { Published declarations } Property OnClick; end; procedure Register; implementation procedure Register; begin RegisterComponents('Samples', [TActionTester]); end; { TActionTester } constructor TActionTester.Create(AOwner: TComponent); var Form: TCustomForm; function GetOwningForm(Component: TComponent): TCustomForm; begin result := NIL; repeat if Component is TCustomForm then Result := TCustomForm(Component); Component := Component.Owner; until Component = nil; end; begin inherited Create(AOwner); FButtonSCAction := TAction.Create(Self); FButtonSCAction.OnExecute := ExecuteButtonShortcut; FButtonSCAction.ShortCut := TextToShortCut('CTRL+K'); FButtonSCAction.SetSubComponent(true); if not (csDesigning in ComponentState) then begin Form := GetOwningForm(Self); if Form <> nil then begin FButtonSCActionList := TActionList.Create(Form); FButtonSCActionList.FreeNotification(Self); FButtonSCAction.ActionList := FButtonSCActionList; end; end; end; destructor TActionTester.Destroy; begin FreeAndNil( self.FButtonSCAction ); inherited; end; procedure TActionTester.ExecuteButtonShortcut(Sender: TObject); begin if assigned( self.OnClick ) then self.OnClick( self ); end; procedure TActionTester.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if (AComponent = FButtonSCActionList) and (Operation = opRemove) then FButtonSCActionList := nil; end; procedure TActionTester.Paint; begin inherited; self.Canvas.Brush.Color := clGreen; self.Canvas.Brush.Style := bsSolid; self.Canvas.FillRect( self.GetClientRect ); end; end.
works like a charm! Major glories for NGLN, David and Dahlia!
source to share