运行时分配的操作的快捷方式不会在自定义组件中触发



当代码在运行时完全创建时(即没有表单设计器组件),我无法将分配给自定义组件的继承 Action 属性的操作工作。如果我在表单设计器中使用 ActionList,然后使用相同的代码,则工作正常。

这是我从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;

如果我使用此代码创建自定义控件,请将其添加到工具栏,将其放置在新的 VCL Forms 应用程序中的窗体上,然后运行该应用程序,当我按快捷键时没有任何反应。如果我创建没有此代码的控件,请将其放在窗体上并为窗体分配一个 Actionlist,然后将仅涉及创建操作并将其分配给组件的 Action 属性的代码行放入按钮的 onclick 事件处理程序中,然后它会正确响应快捷键。对于我的生活,我看不出有什么不同,但希望你们行动德尔菲大师可以......

此操作的目的是允许开发人员通过属性为对象检查器中的按钮分配自定义快捷方式。我想直接分配给"内置"操作,但找不到如何访问其快捷方式属性。(显然,我可以通过其他 HotKey delphi 功能来做到这一点,如果有必要,我会这样做,但我也想了解操作,这似乎是一个很好的起点......

不需要在设计时创建操作列表。在 Create 方法中使用以下代码:

  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;

在运行时创建控件期间,可能会出现传递给控件的 aOwner 不是窗体本身,而是另一个控件的情况。在这种情况下,您必须调用函数,而不是使用 aOwner 创建操作列表,该函数将为您提供来自 aOwner 参数的表单。

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

总结

TControl 中没有内置的操作组件。它是默认情况下未分配的 Action 属性。控件的用户可以使用所需的任何操作为属性分配。控件的设计者(您)不必提供操作或操作列表。

实际问题

我想直接分配给"内置"操作,但找不到如何访问其快捷方式属性。

默认情况下,该内置操作只是一个未分配的TAction属性。如果未分配该属性,即该属性不指向 Action 组件,则其 ShortCut 属性不存在。

此操作的目的是允许开发人员(红色,组件/控件的用户)通过属性为对象检查器中的按钮分配自定义快捷方式。

如果这是你的唯一目标,那么只需发布 Action 属性,不做任何进一步的操作:

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

这将导致属性出现在开发人员的对象检查器中。开发人员只需为其分配自己的一个操作,并设置 thát 操作的 ShortCut 属性。因此,实际的解决方案是摆脱所有当前代码。

为什么您当前的代码不起作用

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

Self.Parent在构造函数期间nil。关于这一点的两件事:

  • 除非您在析构函数中自己销毁操作列表,否则会出现内存泄漏。
  • 对于默认的快捷方式处理,应用程序遍历当前焦点窗体或 MainForm (间接)拥有的所有操作列表。您的操作列表没有所有者,因此永远不会评估其快捷方式。

当前代码的解决方案

首先,对你的代码进行一些善意的评论:

  • Self是隐含的,不需要,也不是习惯性的。
  • 运行时创建的组件不需要设置Name属性。
  • 默认情况下,操作的VisibleEnabled属性为 True。

其次,正如Dalija Prasnikar已经说过的,在设计时不需要行动清单。操作列表必须由控件拥有的窗体间接拥有。因此,控件也可以拥有操作列表 (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;

XE2 之前的某个地方,至少仍然在 D7 中,操作列表必须由控件拥有的窗体注册。(还有更多内容,但由于控件不太可能由另一个窗体作为父级,也不太可能在聚焦另一个窗体时调用该操作,因此可以进行这种简化)。登记可以通过使表格成为行动清单的所有者来完成。由于您将操作列表的所有权授予控件之外,因此让操作列表通过 FreeNotification 通知控件可能销毁的操作列表。(好吧,这是牵强附会的,因为通常控件也会被销毁,但这是严格应该完成的方式)。

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;

请注意,当GetOwningForm返回False时(当开发人员创建没有所有者的控件时),不会创建 ActionList,因为它无法解析所属窗体。覆盖 SetParent 可以解决这个问题。

由于将所有权转移到另一个组件感觉不必要(如果csDesigning in ComponentState,则在运行代码时可能会给 IDE 的流系统带来问题),因此还有另一种方法可以通过将 ActionList 添加到受保护的 FActionLists 字段中来将 ActionList 注册到表单:

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;

对此解决方案的反思:

  • 这种方法是不可取的。不应在自定义控件中创建操作组件。如果必须,请单独提供它们,以便控件的用户可以决定将自定义操作添加到哪个操作列表。另请参阅:如何在组件中添加对操作的支持?
  • TControl.Action是公共财产,TControl.SetAction不是虚拟的。这意味着控件的用户可以分配不同的操作,使此操作无用,并且您无法对其进行执行任何操作或反对它。(不发布是不够的)。相反,声明另一个 Action 属性,或者再次提供单独的 Action 组件。

非常感谢所有的帮助!对于那些将在以后的google-fu中使用这个问题的人(我现在住在谷歌,而不是在Delphi IDE中......)以下是自定义组件的最终全功能代码:

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.

就像一个魅力!向NGLN,David和Dalija致敬!

最新更新