3

I am having a problem getting an Action assigned to a custom component's inherited Action property to work when the code is entirely created at run time (i.e. no form designer components). If I use an ActionList in the form designer and then use the same code things work fine.

Here is my constructor of a 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 the custom control with this code, add it to the toolbar, place it on a form in a new VCL Forms application and then run the application, when I press the shortcut key nothing happens. If I create the control without this code, place it on a form and assign an Actionlist to the form, and then put the code lines just involving creating an action and assigning it to the component's Action property into an onclick event handler for the button, it then responds to the shortcut keypress correctly. For the life of me I can't see what is different, but hopefully you Actions Delphi gurus can...

The purpose of this Action is to allow the developer to assign a custom shortcut to the button in the Object Inspector via a property. I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property. (Obviously I could do this via the other HotKey delphi functionality and will if I have to but I also want to understand Actions and this seems a good place to start...)

NGLN
  • 43,011
  • 8
  • 105
  • 200
  • 1
    What I would like to know is why you would want to assign to `Action` from inside the component's code. It would seem to me that is your fundamental problem. I would expect components not to do that. Once you stop doing that, there's no problem any more. – David Heffernan Nov 21 '14 at 12:25

3 Answers3

3

You don't need to create ActionList at design time. Use following code in your 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;

During run-time creation of control, you can have situation where aOwner passed to your control will not be form itself, but another control. In that case instead of creating action list with aOwner you would have to call function that will give you the 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));
Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
  • aha. i was suspecting that I should use owner instead of parent (as it has to be set prior to the call to create) but this gem of info is a treasure! Will implement in the morning and post the results. Thanks for the help! – Al The Developer Nov 21 '14 at 09:11
  • If aOwner isn't the MainForm, then the ShortCut won't be handled. +1 – NGLN Nov 21 '14 at 11:21
  • @NGLN ShortCut will be handled even if aOwner is not MainForm, as long as form in question has focus. – Dalija Prasnikar Nov 21 '14 at 12:38
  • Thanks Dalija for your help! One small question: why do we need to call `SetSubComponent(TRUE)` ? – Al The Developer Nov 21 '14 at 14:05
  • @AlTheDeveloper actually you don't have to call SetSubComponent(true), but if you for any reason decide to add FBuffonSCAction as published property, that would allow it to be edited during design time. – Dalija Prasnikar Nov 21 '14 at 14:17
  • oh. okay. thanks. will try everything out later today and let everyone know how it worked. I think this is going to do just what i need it to! – Al The Developer Nov 21 '14 at 14:40
  • Ok, I did a little digging. For a ShortCut to be processed, the Action's ActionList needs to be own by the MainForm or by the currently focussed form. This means that an ActionList owned by a secundary form, and a focussed MainForm, the ShortCut of the Action attached to an ActionControl on the MainForm will not be processed. First `Application.IsKeyMsg` will be called. If false, then `Application.IsShortCut` will be called. – NGLN Nov 21 '14 at 16:42
  • Furthermore: in D7 the ActionList had to be registered in TCustomForm.FActionLists. In XE2 that field is gone and TCustomForm just traverses all indirectly owned ActionLists. So in recent Dellphi versions, the owner of the ActionList (your `aOwner`) can be `Self` too. – NGLN Nov 21 '14 at 18:13
2

Summary

There is no built-in Action component in TControl. It is an Action property that is unassigned by default. The user of the control can assign the property with whatever Action is desired. The designer of the control (you) does not have to provide an Action nor ActionList.

The actual problem

I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property.

That built-in Action is by default just an unassigned TAction property. And if the property is not assigned, i.e. the property does not point to an Action component, then its ShortCut property does not exist.

The purpose of this Action is to allow the developer (red. the user of your component/control) to assign a custom shortcut to the button in the Object Inspector via a property.

If that is your sole goal, then simply publish the Action property and do nothing further:

type
  TMyControl = class(TCustomControl)
  published
    property Action;
  end;

This will result in the appearance of the property in the developer's Object Inspector. The developer simply has to assign one of his own actions to it, and to set the ShortCut property of thát action. Thus the actual solution is to get rid of all your current code.

Why your current code doesn't work

self.FButtonSCActionList := TActionList.Create( self.Parent );

Self.Parent is nil during the constructor. Two things about that:

  • Unless you destroy the ActionList yourself in de destructor, you have a memory leak.
  • For default ShortCut processing, the application traverses all ActionLists which are (indirectly) owned by the currently focussed form or by the MainForm. Your ActionList has no owner, thus its ShortCuts are never evaluated.

Solution for the current code

First, some well-intentioned comments on your code:

  • Self is implicit and is not needed, nor customary.
  • Runtime made components do not need a Name property set.
  • The Visible and Enabled properties of an action are True by default.

Secondly, as Dalija Prasnikar already said, the ActionList is not needed at design time. And the ActionList has to be indirectly owned by the form that the control owns. So the control can own the ActionList too (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;

Somehere before XE2, at least still in D7, the ActionList had to be registered by the form that the control owns. (There is more to it, but since it is unlikely that the control is parented by another form nor that the action is invoked when another form is focussed, this simplification can be made). Registration could be done by making the form the owner of the ActionList. Since you give ownership of the ActionList beyond the control, let the ActionList notify its possibly destruction to the control with FreeNotification. (Ok, this is far-fetched, since typically the control then will be destroyed as well, but this is how it strictly 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 GetOwningForm returns False (when the developer creates the control without owner), the ActionList is not created because it cannot resolve the owning form. Overriding SetParent could fix that.

Because transfering ownership to another component feels unnecessary (and could give problems with the IDE's streaming system when the code is run if csDesigning in ComponentState), there is another way to register the ActionList to 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 not desirable. You should not create action components within your custom control. If you have to, offer them seperately so that the user of your control can decide to which ActionList the custom Action will be added. See also: How do I add support for actions in my component?
  • TControl.Action is a public property, and TControl.SetAction is not virtual. This means that the user of the control can assign a different Action, rendering this Action useless, and you cannot do anything about nor against it. (Not publishing is not enough). Instead, declare another Action property, or - again - offer a separate Action component.
Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • But if I use self in the ActionList create call, the ActionList will have an owner of type TCustomComponent which isn't a Form, Frame or Datamodule. should I perhaps do all this code in an overload to the Loaded() method instead where I can assign the Parent property instead? – Al The Developer Nov 21 '14 at 04:35
  • I just tried using "Self" and It crashes delphi (IDE and app) when the shortcut key is pressed. Also, when i start a new forms application with this code in place i get a strange error message about "unable to open bds.default" when the ide is starting the new app. Any thoughts? – Al The Developer Nov 21 '14 at 05:05
  • I edited the answer, because my initial statement was false. Regarding your errors: I suspect you didn't remove the previous instance of your control from the form designer. Anyway, the ActionList isn't created designtime anymore, so that should be solved too. – NGLN Nov 21 '14 at 11:20
  • hrm. I understand your point, and thank you for all this effort! I will test this code just to be sure I understand it, but will implement shortcuts using the other non-action functionality for such things. – Al The Developer Nov 21 '14 at 13:00
  • Yeah, I completely misinterpreted your question. I thought you wánted to add a ShortCut to your control's action, but now I understand you want the user of your control to be able to add it. In that case, [David's comment](http://stackoverflow.com/questions/27053769/runtime-assigned-actions-shortcut-does-not-fire-in-custom-component#comment42636952_27053769) hits the right nail on the head. – NGLN Nov 21 '14 at 15:07
  • +one this is a very nice answer – David Heffernan Nov 22 '14 at 06:41
0

Thanks so much for all the help! For those who will use this question for later google-fu (I live in google these days when not in the Delphi IDE...) here is the final fully functional code for a 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 kudos to NGLN, David and Dalija!