【问题标题】:Delphi: Is system menu opened?Delphi:系统菜单打开了吗?
【发布时间】:2010-10-10 06:14:13
【问题描述】:

我是 Delphi,我需要一个函数来确定系统菜单(分别是窗口菜​​单,单击图标时出现的菜单)是否打开。原因是我正在编写一个反键盘记录器功能,它将垃圾发送到当前活动的编辑控件(这也阻止了读取 WinAPI 消息的键盘记录器来读取内容)。但是如果打开系统菜单,editcontrol仍然有焦点,所以垃圾会调用快捷方式。

如果我在 TForm1 中使用消息 WM_INITMENUPOPUP,我可以确定系统菜单何时打开,但我希望不必更改 TForm,因为我想编写一个非可视组件,不需要对 TForm-derivate-class 本身进行任何修改。

//I do not want that solution since I have to modify TForm1 for that!
procedure TForm1.WMInitMenuPopup(var Message: TWMInitMenuPopup);  
begin  
 if message.MenuPopup=getsystemmenu(Handle, False) then  
 begin  
  SystemMenuIsOpened := true;  
 end;  
end;

TApplicaton.HookMainWindow() 不会将WM_INITMENUPOPUP 发送到我的挂钩函数。

function TForm1.MessageHook(var Msg: TMessage): Boolean;  
begin  
Result := False;  
if (Msg.Msg = WM_INITMENUPOPUP) then  
begin  
// Msg.Msg IS NEVER WM_INITMENUPOPUP!  
 if LongBool(msg.LParamHi) then  
 begin  
  SystemMenuIsOpened := true;  
 end;  
end;  
end;  

procedure TForm1.FormCreate(Sender: TObject);  
begin  
 Application.HookMainWindow(MessageHook);  
end;  

procedure TForm1.FormDestroy(Sender: TObject);  
begin  
  Application.UnhookMainWindow(MessageHook);  
end;

即使经过很长时间的研究,我也没有找到任何关于如何查询系统菜单是否打开的信息。我找不到任何方法来确定该菜​​单的打开和关闭。

请问有人可以帮我解决吗?

问候
丹尼尔·马歇尔

【问题讨论】:

    标签: windows delphi winapi system


    【解决方案1】:

    Application.HookMainWindow 并没有按照您的想法行事。它挂钩隐藏的应用程序窗口,而不是主窗体。如您所见,要拦截特定表单上的WM_INITMENUPOPUP,您需要做的就是为其编写一个处理程序。

    要对组件的任何所有者表单执行此操作,您可以分配表单的WindowProc 属性来放置挂钩:

    unit FormHook;
    
    interface
    
    uses
      Windows, Classes, SysUtils, Messages, Controls, Forms;
    
    type
      TFormMessageEvent = procedure(var Message: TMessage; var Handled: Boolean) of object;
    
      TFormHook = class(TComponent)
      private
        FForm: TCustomForm;
        FFormWindowProc: TWndMethod;
        FOnFormMessage: TFormMessageEvent;
      protected
        procedure FormWindowProc(var Message: TMessage); virtual;
      public
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
      published
        property OnFormMessage: TFormMessageEvent read FOnFormMessage write FOnFormMessage;
      end;
    
    procedure Register;
    
    implementation
    
    procedure Register;
    begin
      RegisterComponents('Test', [TFormHook]);
    end;
    
    procedure TFormHook.FormWindowProc(var Message: TMessage);
    var
      Handled: Boolean;
    begin
      if Assigned(FFormWindowProc) then
      begin
        Handled := False;
    
        if Assigned(FOnFormMessage) then
          FOnFormMessage(Message, Handled);
    
        if not Handled then
          FFormWindowProc(Message);
      end;
    end;
    
    constructor TFormHook.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      FFormWindowProc := nil;
      FForm := nil;
      while Assigned(AOwner) do
      begin
        if AOwner is TCustomForm then
        begin
          FForm := TCustomForm(AOwner);
          FFormWindowProc := FForm.WindowProc;
          FForm.WindowProc := FormWindowProc;
          Break;
        end;
        AOwner := AOwner.Owner;
      end;
    end;
    
    destructor TFormHook.Destroy;
    begin
      if Assigned(FForm) and Assigned(FFormWindowProc) then
      begin
        FForm.WindowProc := FFormWindowProc;
        FFormWindowProc := nil;
        FForm := nil;
      end;
      inherited Destroy;
    end;
    
    end.
    

    然后您可以在表单上使用此组件:

    procedure TForm1.FormHook1FormMessage(var Message: TMessage; var Handled: Boolean);
    begin
      case Message.Msg of
        WM_INITMENUPOPUP:
          ...
      end;
    end;
    

    问题可能是,如果表单有任何其他组件执行相同的操作,那么您需要确保取消挂钩以相反的顺序发生(最后挂钩,第一个取消挂钩)。上面的例子在构造函数中钩子,在析构函数中解钩;即使在同一个表单上有多个实例,这似乎也有效。

    【讨论】:

    • 感谢您提供的代码。这似乎也有效。但是覆盖“WindowProc”是一个好方法吗?我可以想象,如果多个组件覆盖 WindowProc,则只有最新定义的组件才能工作。那将是一个很大的劣势。目前我使用 SetWindowLongPtr()。当我注册组件时,我更改了 WndProc CB,并在取消注册时将 CB 设置回其先前的值。 (可能会在 :-/ 之间及时更改)
    • WindowProc 只是另一种(可以说更简单)替换窗口过程的方法。无论哪种方式,如果有多个组件执行此操作,您需要确保正确的脱钩顺序。
    • 我怎样才能保证解开的顺序是一样的?我在 SetWindowLongPtr() 解决方案中也对此感到担忧。由于我想编写一个 VCL,因此组件的卸载顺序与创建时的顺序不同是很危险的。
    • 由于您无法控制将在目标表单上安装和使用哪些其他组件,恐怕您能做的最好的事情就是记录您的组件以警告用户其中的含义。跨度>
    【解决方案2】:

    如果你不想对TForm-derivate-class进行任何修改,为什么不尝试纯Windows API的方式来实现你目前的解决方案,即使用SetWindowLongPtr()拦截WM_INITMENUPOPUP消息。 Delphi VCL 风格的消息拦截实际上只是这个 Windows API 函数的封装。

    为此,使用SetWindowLongPtr()窗口过程 设置一个新地址并获得窗口过程的原始地址,两者都一气呵成。请记住将原始地址存储在LONG_PTR 变量中。在 32 位 Delphi 中,LONG_PTRLongint;假设 64 位 Delphi 将在未来发布,LONG_PTR 应该是 Int64;您可以使用$IFDEF 指令来区分它们,如下所示:

      Type
        {$IFDEF WIN32}
        PtrInt = Longint;
        {$ELSE}
        PtrInt = Int64;
        {$ENDIF}
        LONG_PTR = PtrInt;
    

    用于此目的的nIndex 参数的值是GWLP_WNDPROC。此外,将窗口过程的新地址传递给dwNewLong 参数,例如LONG_PTR(NewWndProc)NewWndProc 是处理消息的WindowProc Callback Function,它是您放置拦截标准并覆盖您要拦截的消息的默认处理的地方。回调函数可以是任意名称,但参数必须遵循WindowProc 约定。

    请注意,您必须调用CallWindowProc() 将任何未被新窗口过程处理的消息传递给原始窗口过程。

    最后,您应该在代码中的某处再次调用SetWindowLongPtr() 以将修改/新的窗口过程 处理程序的地址设置回原始地址。原地址之前已经保存好了。

    有一个Delphi code example here。它使用了SetWindowLong(),但现在微软建议使用SetWindowLongPtr(),以使其兼容32位和64位版本的Windows。

    SetWindowLongPtr()在Delphi 2009之前的Delphi的Windows.pas中是不存在的。如果你使用的是旧版本的Delphi,你必须自己声明,或者使用JEDI API LibraryJwaWinUser单位。

    【讨论】:

    • 我从来没有听说过这个功能,我不知道它如何帮助我挂钩 WM_INITMENUPOPUP。
    • GWLP_WNDPROC 传递给nIndex 参数。以 swissdelphicenter.ch/torry/…> 为例,曾经使用过 SetWindowLong(),但现在微软推荐使用 msdn.microsoft.com/en-us/library/…> 代替,以使其兼容 32 位和 64 位版本窗户。
    • 这里的风格和VCL风格不同,你必须提供一个回调函数,在这里你可以放置你的拦截条件并做进一步的处理。 Delphi VCL 风格的消息拦截实际上只是这个 Windows API 函数的一个包装器。
    • @Vantomex:您可以编辑您的答案以添加您刚刚在 cmets 中提供的额外信息。它使答案更加有用,并且避免了强迫人们扫描 cmets。即使是声望为 1 的人也可以编辑他们自己的答案。
    • 我目前正在尝试使用该功能。我希望它不会阻止其他消息处理程序对该消息作出反应。我的组件应该在任何其他功能/表单上并行工作。有代码示例吗?
    【解决方案3】:

    我自己没有尝试过,但试一试:

    使用GetMenuItemRect 获取GetSystemMenu 返回的菜单项0 的矩形。 如果系统菜单未打开,我(假设!)GetMenuItemRect 应该返回 0(因为系统无法知道菜单项的矩形,除非它打开?)如果结果非零,请检查返回的坐标是否为对于给定的屏幕分辨率是可能的。

    如果你有时间,你可以去AutoHotKey's source code看看how to monitor when system menu is open/closed

    【讨论】:

    • 唉,GetMenuItemRect() 不起作用。 TApplication.Handle 的系统菜单位于固定位置,Form1.Handle 的系统菜单位于我期望的位置。 (当我移动表格时,坐标也会改变)但是当系统菜单不可见时坐标也在那里。我假设该函数成功,因为系统菜单总是被创建并且仅在调用时切换可见/不可见。
    猜你喜欢
    • 2011-12-13
    • 2022-06-23
    • 2015-10-06
    • 1970-01-01
    • 2014-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多