记录 MenuItem 的 OnClick 事件

Logging MenuItem OnClick Event

我有一个项目 (Delphi 10 Seattle, win32),其中包含许多菜单和这些菜单中的许多项目。有些菜单项是在设计时创建的,有些是在 运行 时创建的。

我要做的是在触发 OnClick 事件时记录一些有关 TMenuItem 的信息,例如 name/caption、时间戳等。

我可以简单地在分配给 TMenuItem OnClick 事件的每个函数的开头添加一个过程调用,但我想知道是否有更优雅的解决方案。

另请注意,我已尝试 Embarcadero's AppAnalytics,但我发现它没有提供我想要的信息或灵活性,而且价格相当昂贵。

编辑: 我将添加更多信息,详细说明我考虑过的选项(我可能应该首先考虑)。

向我要记录的每个菜单项单击添加一个功能的简单方法,这意味着为很多功能执行此操作并且必须将其添加到添加的每个新菜单项中。

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;

'more elegant solution' 我的意思是可以添加一个 'hook' 以便所有 TMenuItem OnClick 事件在调用分配给 OnClick 事件的过程之前触发另一个过程(它将进行日志记录) .

或者我考虑的另一个选择是创建一个 class,它继承自 TMenuItem,它将覆盖 TMenuItem.Click 并在生成 OnClick 事件之前进行日志记录。但是后来我不知道如果不进行大量重新制作菜单的工作,那将如何用于设计时菜单项。

使用操作更容易实现这一点。这样做的好处是您可以选择菜单以外的 UI 元素调用的操作,例如工具栏、按钮等。

使用在执行任何操作时触发的 action list, or an action manager, as you prefer. For example, with an action list the action list object has an OnExecute 事件。您可以侦听该事件并记录正在执行的操作的详细信息。

我绝对同意操作是可行的方法,但是为了完整起见以及您希望使用旧式菜单快速调试应用程序的情况,这里有一个可以与菜单项一起使用的单元。如果菜单项有链接到它们的操作,它甚至会工作,但它不适用于任何其他带有 TActionMainMenuBar 等操作的控件。所有调试代码都在这个单元中,以保持您的正常代码整洁。只需将单位添加到 uses 子句并使用任何适用的组件调用 StartMenuLogging,例如菜单组件、表单组件甚至 Application!它下面的树中的任何菜单项都将被挂钩。因此,您可以仅使用生产代码中的这两行代码来调试所有形式的所有菜单点击。您可以使用 StopMenuLogging 来停止,但这是可选的。 警告:这个单元没有经过正确测试 - 我拿了一个我写的旧调试单元并为此清理了它,只是表面测试了它。

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.