在 Delphi VCL 表单中使用 LParam 0 确定 WM_SYSCOMMAND 的发送者

Determining the sender of WM_SYSCOMMAND with LParam 0 in Delphi VCL forms

问题

当 运行使用 C 编写的应用程序时,它使用了一些用 Delphi XE7 编写的 dll,我 运行 进入以下代码中的访问冲突,它位于vcl.forms.pas vcl 库。

procedure TCustomForm.CMAppSysCommand(var Message: TMessage);
{$IF NOT DEFINED(CLR)}
type
  PWMSysCommand = ^TWMSysCommand;
{$ENDIF}
begin
  Message.Result := 0;
  if (csDesigning in ComponentState) or (FormStyle = fsMDIChild) or
   (Menu = nil) or Menu.AutoMerge then
{$IF DEFINED(CLR)}
    with TWMSysCommand.Create(Message) do
{$ELSE}
    with PWMSysCommand(Message.lParam)^ do
{$ENDIF}
    begin
      SendCancelMode(nil);
      if SendAppMessage(CM_APPSYSCOMMAND, CmdType, Key) <> 0 then   //Here the debugger shows the access violation
        Message.Result := 1;
    end;
end;

访问冲突发生在与SendAppMessage 的行上,似乎是由于Message.LParam 为0 造成的。消息是WM_SYSCOMMAND 消息。有没有办法跟踪此消息的来源?在调用堆栈中,所有函数都是VCL或系统文件的一部分。

This answer 建议通常 很难 追踪 windows 消息的发件人。但是,由于在我的情况下所有内容都在同一个应用程序中,我希望这样可以更容易。

我尝试了什么?

否决 vcl 源

以前,这个相同的错误出现在 forms.pas 中,并通过将该文件的副本添加到项目然后检查此函数中的 LParam <> 0 来修复。 我尝试用现在使用的 vcl.forms.pas 做同样的事情,但这会导致编译错误。即使答案为 here 我也无法构建它。然而,许多 google 的点击率也表明在 vcl 中更改内容通常不是一个好主意,因此我尽量避免该选项。

关于 Whosebug 的其他问题

给了我关于底层系统的很好的信息,以及 Message.LParam 是 0 是如何发生的。但是,我不知道如何找到消息的来源或什么class我应该找那个生成它的。

解决方案

正如 Remy 在下面接受的答案中所述,可以通过让 class 提供 CMAppSysCommand 函数来防止 LParam = 0 来解决眼前的问题。

您描述的情况在正常情况下应该是不可能的。

整个VCL中只有两个地方是CM_APPSYSCOMMAND发自:

  1. TWinControl.WMSysCommand(),当 UI 控件收到 WM_SYSCOMMAND 消息时调用。 CM_APPSYSCOMMAND 消息的 LParam 永远不会设置为 0,它被设置为指向原始 WM_SYSCOMMAND 消息的 TMessage 记录的指针:

    Form := GetParentForm(Self);
    if (Form <> nil) and
      (Form.Perform(CM_APPSYSCOMMAND, 0, Winapi.Windows.LPARAM(@Message)) <> 0) then
      Exit;
    
  2. TCustomForm.CMAppSysCommand(),Form收到CM_APPSYSCOMMAND消息时调用。它将消息转发到 TApplication window(使用 SendAppMessage(),它仅使用提供的参数调用 SendMessage(Application.Handle, ...)):

    with PWMSysCommand(Message.lParam)^ do
    begin
      ...
      if SendAppMessage(CM_APPSYSCOMMAND, CmdType, Key) <> 0 then
        Message.Result := 1;
    end;
    

你提到的 解释了 VCL 如何使用 CM_APPSYSCOMMAND,但没有说明它的 LParam 如何在 [=21= 中为 0 ],因为它在正常情况下永远不可能为 0。在 TApplication.WndProc() 中可以为 0,但完全可以。

我能想到的唯一可能性是,如果有人手动向您的 TForm window。只有 TWinControl 应该这样做。由于 TWinControl 使用 Perform() 而不是 SendMessage() 发送,您应该在 TCustomForm.CMAppSysCommand() 的调用堆栈上看到 TWinControl.WMSysCommand()。如果您不这样做,则该消息是假的。如果它是使用 SendMessage() 而不是 Perform() 发送的,则无法知道消息来自何处。

然而,无论如何,这很容易防范,无需更改任何 VCL 源代码。只需让您的 DLL 的 TForm class 为 CM_APPSYSCOMMAND 提供自己的消息处理程序,使用 message 指令,或覆盖虚拟 WndProc() 方法。无论哪种方式,如果 LParam 为 0,您都可以丢弃该消息,例如:

type
  TMyForm = class(TForm)
  ...
  private
    procedure CMAppSysCommand(var Message: TMessage); message CM_APPSYSCOMMAND;
  ...
  end;

procedure TMyForm.CMAppSysCommand(var Message: TMessage);
begin
  if Message.LParam = 0 then
    Message.Result := 0
  else
    inherited;
end;

type
  TMyForm = class(TForm)
  ...
  protected
    procedure WndProc(var Message: TMessage); override;
  ...
  end;

procedure TMyForm.WndProc(var Message: TMessage);
begin
  if (Message.Msg = CM_APPSYSCOMMAND) and (Message.LParam = 0) then
    Message.Result := 0
  else
    inherited;
end;