在 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
发自:
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;
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;
问题
当 运行使用 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 的其他问题
解决方案
正如 Remy 在下面接受的答案中所述,可以通过让 class 提供 CMAppSysCommand 函数来防止 LParam = 0 来解决眼前的问题。
您描述的情况在正常情况下应该是不可能的。
整个VCL中只有两个地方是CM_APPSYSCOMMAND
发自:
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;
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;
你提到的 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;