查找哪个进程注册了全局热键?(Windows API)

145
据我所知,Windows没有提供API函数来告诉我们是哪个应用程序通过RegisterHotkey注册了全局热键。我们只能在RegisterHotkey返回false时发现一个热键已经被注册了,但无法确定是哪个应用程序“拥有”该热键。
在没有直接API的情况下,是否有迂回的方法呢?Windows维护着与每个注册热键相关联的句柄 - 无法获取这些信息真的令人感到恼火。
以下是不太可能奏效的示例:发送(模拟)一个已注册的热键,然后拦截Windows将发送给注册它的进程的热键消息。首先,我认为拦截消息也不会显示目标窗口句柄。其次,即使可能,发送热键将触发各种潜在的不需要的活动。
虽然这不是什么关键问题,但我经常看到对此类功能的请求,并且自己曾经成为注册热键却未在UI或文档中披露的应用程序的受害者。
(在Delphi中工作,并且WinAPI只是初学者,请多多指教。)
7个回答

134
一种可能的方法是使用Visual Studio工具Spy++
试试这个:
  1. 如果你还没有安装,先安装Visual Studio社区版。Spy++会随之安装。
  2. 运行工具(对我来说,在C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\目录下)。注意:有spyxx.exe(32位版本)和spyxx_amd64.exe(64位版本)- 如果在64位中看不到任何内容,请使用32位版本(它只捕获相同架构的消息)
  3. 在菜单栏中选择Spy -> Log messages...(或按下Ctrl + M
  4. Additional Windows框中勾选All Windows in System
  5. 切换到Messages选项卡
  6. 点击Clear All按钮
  7. 在列表框中选择WM_HOTKEY,或者在Message Groups中勾选Keyboard(如果你可以接受更多的潜在噪音)
  8. 点击OK按钮
  9. 按下所需的热键(例如Win + R
  10. Messages (All Windows)窗口中选择WM_HOTKEY行,右键点击,并选择Properties...
  11. Message Properties对话框中,点击Window Handle链接(这将是接收到消息的窗口的句柄)
  12. Window Properties对话框中,点击Synchronize按钮。这将在主Spy++窗口的树视图中显示该窗口(如果它是Windows本身或某个弹出应用程序,则不会显示任何内容)。
  13. Window Properties对话框中,选择Process选项卡
  14. 点击Process ID链接。这将显示进程(在我的Win + R案例中: EXPLORER

17
好的回答!只是需要注意,Spy++ 的 64 位版本仅捕获 64 位应用程序的消息,因此如果您在按下热键后在“消息日志”中看不到 WM_HOTKEY 消息,则可能需要运行 Spy++ 的 32 位版本。 - David Ferenczy Rogožan
10
第8步并没有要求输入热键,按下任何热键后消息窗口仍然保持为空。我在Windows 10中使用了https://github.com/westoncampbell/SpyPlusPlus的32位和64位版本。 - CoolMind
谢谢。另外,你是怎么知道这个复杂的过程的?哈哈。 - undefined
我知道这就是全局热键的工作原理,而且Spy++可以窥探窗口消息。我只是很高兴有这样的工具存在。 - undefined
非常感谢,不然我根本找不到罪魁祸首。联想显示控制中心一直捕捉到了右Alt+E(尽管显示为CTRL+ALT+E),导致我无法使用美国国际版、Alt Gr死键键盘布局输入“é”。禁用联想应用程序的快捷键解决了这个问题。 - undefined
显示剩余6条评论

21

你的问题引起了我的兴趣,所以我进行了一些调查。虽然不幸的是,我没有一个正确的答案给你,但我想分享一下我所掌握的信息。

我找到了这个创建键盘钩子的示例(使用Delphi编写),它是在1998年编写的,但是经过一些修改后可以在Delphi 2007中编译。

这是一个DLL文件,其中调用了SetWindowsHookEx并传递了回调函数,从而可以拦截按键:在这种情况下,它会改变按键的值,比如将左箭头变为右箭头等等。一个简单的应用程序调用该DLL文件,并根据TTimer事件返回其结果。如果您有兴趣,我可以发布基于Delphi 2007的代码。

该示例有很好的文档和注释,您可以将其作为确定按键输入的基础。如果您能够获取发送按键的应用程序的句柄,那么您可以通过这种方式追踪按键。有了该句柄,您就可以轻松地获取所需的信息。

其他应用程序尝试通过查找快捷方式来确定热键,因为它们可能包含一个快捷键,这也是热键的另一个术语。然而,大多数应用程序不倾向于设置此属性,因此可能不会返回太多信息。如果您对该路线感兴趣,Delphi可以访问IShellLink COM接口,您可以使用它从中加载快捷方式并获取其热键:

uses ShlObj, ComObj, ShellAPI, ActiveX, CommCtrl;

procedure GetShellLinkHotKey;
var
  LinkFile : WideString;
  SL: IShellLink;
  PF: IPersistFile;

  HotKey : Word;
  HotKeyMod: Byte;
  HotKeyText : string;
begin
  LinkFile := 'C:\Temp\Temp.lnk';

  OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, SL));

  // The IShellLink implementer must also support the IPersistFile
  // interface. Get an interface pointer to it.
  PF := SL as IPersistFile;

  // Load file into IPersistFile object
  OleCheck(PF.Load(PWideChar(LinkFile), STGM_READ));

  // Resolve the link by calling the Resolve interface function.
  OleCheck(SL.Resolve(0, SLR_ANY_MATCH or SLR_NO_UI));

  // Get hotkey info
  OleCheck(SL.GetHotKey(HotKey));

  // Extract the HotKey and Modifier properties.
  HotKeyText := '';
  HotKeyMod := Hi(HotKey);

  if (HotKeyMod and HOTKEYF_ALT) = HOTKEYF_ALT then
    HotKeyText := 'ALT+';
  if (HotKeyMod and HOTKEYF_CONTROL) = HOTKEYF_CONTROL then
    HotKeyText := HotKeyText + 'CTRL+';
  if (HotKeyMod and HOTKEYF_SHIFT) = HOTKEYF_SHIFT then
    HotKeyText := HotKeyText + 'SHIFT+';
  if (HotKeyMod and HOTKEYF_EXT) = HOTKEYF_EXT then
    HotKeyText := HotKeyText + 'Extended+';

  HotKeyText := HotKeyText + Char(Lo(HotKey));

  if (HotKeyText = '') or (HotKeyText = #0) then
    HotKeyText := 'None';

  ShowMessage('Shortcut Key - ' + HotKeyText);
end;

如果你可以访问Safari Books Online,在Steve Teixeira和Xavier Pacheco所著的Borland Delphi 6 Developer's Guide中有一个关于快捷方式/ shell链接的好章节。我上面的示例是从那里的一个被修改过的版本以及这个网站上得来。

希望能对你有所帮助!


11

经过一些研究,似乎您需要访问MS用于存储热键的内部结构。ReactOS具有一个干净的实现,通过迭代内部列表并提取与调用参数匹配的热键来实现GetHotKey调用。

根据ReactOS的实现与MS实现的接近程度,您可能可以在内存中查找该结构,但这超出了我的能力范围...

BOOL FASTCALL
GetHotKey (UINT fsModifiers,
           UINT vk,
           struct _ETHREAD **Thread,
           HWND *hWnd,
           int *id)
{
   PHOT_KEY_ITEM HotKeyItem;

   LIST_FOR_EACH(HotKeyItem, &gHotkeyList, HOT_KEY_ITEM, ListEntry)
   {
      if (HotKeyItem->fsModifiers == fsModifiers &&
            HotKeyItem->vk == vk)
      {
         if (Thread != NULL)
            *Thread = HotKeyItem->Thread;

         if (hWnd != NULL)
            *hWnd = HotKeyItem->hWnd;

         if (id != NULL)
            *id = HotKeyItem->id;

         return TRUE;
      }
   }

   return FALSE;
}

我认为这个sysinternals上的帖子是由与这个问题相关的人提出的,但我还是想把它链接在一起,以保持两者的联系。 这个帖子看起来非常有趣,但我怀疑如果没有访问MS内部,可能需要进行一些深入的挖掘才能弄清楚。


3

从我脑海中想到的,你可以尝试使用EnumWindows枚举所有窗口,然后在回调函数中向每个窗口发送WM_GETHOTKEY。

编辑:显然我错了。MSDN有更多信息:

WM_HOTKEY与WM_GETHOTKEY和WM_SETHOTKEY热键无关。 WM_HOTKEY消息用于通用热键,而WM_SETHOTKEY和WM_GETHOTKEY消息与窗口激活热键有关。

注意:这里有一个声称具有您要查找的功能的程序。您可以尝试反编译它。


4
程序的链接现在完全失效了。那是哪个程序?有很多次我想弄清楚是哪个程序注册了我的热键,因为突然间它们不再起作用或者做出烦人的新动作。 - James Oltmans
通常情况下,Wayback Machine可以帮助我们找回已经消失的网页,但在这种情况下,它只是镜像了一个404 Not Found页面:http://web.archive.org/web/*/tds.diamondcs.com.au/dse/detection/hotkeys.php。 - Aaron Thoma

1
另一个线程提到了全局NT级键盘钩子: 重新分配/覆盖热键(Win + L)以锁定Windows 也许你可以通过这种方式获取调用钩子的进程句柄,然后将其解析为进程名称。
(免责声明:我把它放在我的书签中,但没有真正尝试/测试过)

1

我知道你可以截取任何窗口内流的消息,就像VB6中的子类化一样(虽然我不记得函数名,也许是SetWindowLong?)。我不确定是否可以对自己进程外的窗口这样做。但在本帖中,假设你找到了这么做的方法。然后,你可以简单地拦截所有顶层窗口的消息,监视WM_HOTKEY消息。一开始你可能无法知道所有的键,但按下它们时,你可以轻松地弄清楚哪个应用程序正在使用它们。如果将结果持久化到磁盘并在每次运行监视程序时重新加载,你可以随着时间的推移提高应用程序的性能。


0

这并不完全回答了关于Windows API的问题,但它回答了关于全局热键列表及其所属应用程序的部分问题。

免费的Hotkey Explorer(http://hkcmdr.anymania.com/)显示了所有全局热键及其所属应用程序的列表。这帮助我找出为什么应用程序特定的快捷键停止工作以及如何修复它(通过重新配置在注册了该快捷键的应用程序中注册的全局热键),只需几秒钟即可解决。


5
我安装了它,但它的表现就像是一个病毒,或至少带有恶意的程序,就像一个恶作剧一样。打开各种窗口,激活语音朗读等。 - Flion
3
@Flion: 参考 http://superuser.com/a/191090/3617 的讨论。基本上,某些热键工具的功能是通过探测每个可能的按键组合实现的。在Windows 8+(以及在Windows 7上较小程度上),这种探测技术会触发每个按键组合,而不仅仅是查询它。 - Brian
@Flion FWIW,我在Win7上使用它成功了。但如果我正确理解Brian的话,这可能在后来的版本中效果不佳,并且它的效果也取决于定义的自定义热键。 - Gerhard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接