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

时间:2017-04-13 15:26:00

标签: delphi access-violation vcl

问题

当运行用C编写的应用程序,它使用在Delphi XE7中编写的一些dll时,我在以下代码中遇到访问冲突,该代码位于vcl库的vcl.forms.pas中。 / p>

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源

以前,这个相同的bug出现在forms.pas中,并通过将该文件的副本添加到项目中然后检查该LParam&lt;&gt;来修复。此功能为0。 我尝试使用现在使用的vcl.forms.pas执行相同的操作,但这会导致编译错误。即使答案为here,我也无法构建它。然而,许多谷歌点击率也表明改变vcl中的内容通常是一个坏主意,所以我尽量避免这种选择。

StackOverFlow上的其他问题

This article给了我关于底层系统的好信息以及Message.LParam为0可能发生的情况。但是,我不知道如何找到消息的来源或我应该是什么类寻找那就产生了它。

解决方案

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

1 个答案:

答案 0 :(得分:4)

在正常情况下,您所描述的内容是不可能的。

整个VCL中只有两个位置发送CM_APPSYSCOMMAND

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

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

    with PWMSysCommand(Message.lParam)^ do
    begin
      ...
      if SendAppMessage(CM_APPSYSCOMMAND, CmdType, Key) <> 0 then
        Message.Result := 1;
    end;
    
  3. 你提到的other question解释了VCL如何使用CM_APPSYSCOMMAND,但没有说明LParamTCustomForm.CMAppSysCommand()TApplication.WndProc()如何为0的任何内容,因为在正常情况下它不能为0。它可以在CM_APPSYSCOMMAND中为0,但这完全没问题。

    我能想到的唯一可能性是,如果有人手动将假CM_BASE + 23 = $B017条消息(WM_APP + $3017,即TForm)直接发送到您的TWinControl窗口。只有TWinControl应该这样做。由于Perform()使用SendMessage()代替TWinControl.WMSysCommand()进行该发送,因此您应该在TCustomForm.CMAppSysCommand()的调用堆栈上看到SendMessage()。如果你不这样做,那么这个消息是假的。如果使用Perform()而不是TForm发送,则无法知道邮件的来源。

    然而,在任何情况下,这都很容易防范,而不会改变任何VCL源代码。只需让您的DLL的CM_APPSYSCOMMAND类为message提供自己的消息处理程序,可以使用WndProc()指令,也可以覆盖虚拟LParam方法。无论哪种方式,如果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; 为0,您可以丢弃该消息,例如:

    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;
    

    col1   col2   col3
    12     34     35
    56     34     35
    13     56     35
    56     34     35
    12     56     34