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 ...)

+3


source to share


3 answers


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));

      

+3


source


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 properties Enabled

    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.
+2


source


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!

0


source







All Articles