覆盖应用程序的低级键盘挂钩问题

Posted

技术标签:

【中文标题】覆盖应用程序的低级键盘挂钩问题【英文标题】:Low level keyboard hook issue with an overlay application 【发布时间】:2020-10-05 09:44:06 【问题描述】:

第一次在这里发帖,多年来我一直在为这个问题的正确解决方案而苦恼。

我有自己的 UI 引擎和自己的键盘处理,并使用它来显示游戏覆盖。游戏覆盖层本身对键盘和窗口事件都是透明的,为了尽量减少对游戏的干扰,但为了使覆盖层本身具有交互性,我需要借助键盘和鼠标挂钩来阻止某些事件到达游戏。对于鼠标输入,这非常简单,而且效果很好。这是我遇到问题的低级键盘挂钩。

在这一点上,我有一些在大多数情况下都可用的东西。我设法解决了几个涉及死键和错误输入的问题,但从未设法创建一个可以主动阻止游戏键盘输入的钩子 - 总会出错。

例如,当用户试图在叠加层上的文本框中写入一些文本并且不希望游戏处理相同的击键时,主动阻止键盘输入将是最有用的。

我当前的问题是,如果我通过在挂钩过程中返回一个非零值来阻止键盘输入,则叠加层的 UI 引擎将停止感知 Ctrl 键的状态,从而导致无法能够复制/粘贴到覆盖的文本框中。有趣的是,在 Alt-Tab'ing 之前,一切正常,但在那之后,Ctrl 按键将钩子从VK_CONTROL 变为VK_LCONTROL。更有趣的是,在 UI 端,GetKeyState(VK_CONTROL)GetAsyncKeyState(VK_CONTROL)GetAsyncKeyState(VK_LCONTROL) 都没有将 Ctrl 键注册为按下状态。

由于多年的实验和变通方法,下面的键盘挂钩代码有点混乱。我会尽可能地评论它。

LRESULT __stdcall KeyboardHook( int code, WPARAM wParam, LPARAM lParam )

  // this is an early exit if the game tells me that it actively has focus
  if ( disableHooks || mumbleLink.textBoxHasFocus )
    return CallNextHookEx( 0, code, wParam, lParam );

  // the following two early exits are remnants from earlier experimentation
  if ( code < 0 )
    return CallNextHookEx( 0, code, wParam, lParam );

  if ( wParam != WM_KEYDOWN && wParam != WM_KEYUP && wParam != WM_CHAR && wParam != WM_DEADCHAR && wParam != WM_UNICHAR )
    return CallNextHookEx( 0, code, wParam, lParam );

  // this checks if either the game or the overlay are in focus and otherwise ignores keyboard input
  auto wnd = GetForegroundWindow();
  if ( code != HC_ACTION || !lParam || ( wnd != gw2Window && App && wnd != (HWND)App->GetHandle() ) )
    return CallNextHookEx( 0, code, wParam, lParam );

  // this ignores the overlay itself if it's in focus for some odd reason
  if ( App && wnd == (HWND)App->GetHandle() )
    return CallNextHookEx( 0, code, wParam, lParam );

  KBDLLHOOKSTRUCT *kbdat = (KBDLLHOOKSTRUCT*)lParam;
  UINT mapped = MapVirtualKey( kbdat->vkCode, MAPVK_VK_TO_CHAR );

  // this bool tests if the overlay has a textbox in focus and the keyboard input should be blocked from propagating further
  bool inFocus = App->GetFocusItem() && App->GetFocusItem()->InstanceOf( "textbox" );

  // forcefully inject a WM_CHAR message to the overlay's UI engine - never figured out how to trigger a message that would be translated into a WM_CHAR properly
  if ( !( mapped & ( 1 << 31 ) ) && !inFocus && wParam == WM_KEYDOWN )
    App->InjectMessage( WM_CHAR, mapped, 0 );

  if ( inFocus )
  
    PostMessage( (HWND)App->GetHandle(), wParam, kbdat->vkCode, 1 | ( kbdat->scanCode << 16 ) + ( kbdat->flags << 24 ) );

    /////////////////////////////////////////////////
    return 1; // this is where the key input should be blocked, but it causes the mentioned issues with the ctrl key (and probably others too)
    /////////////////////////////////////////////////
  

  return CallNextHookEx( 0, code, wParam, lParam );

UI 引擎本身会通过 GetKeyState() 检查 CtrlShiftAlt 状态,因为通过 WM_SYSKEYDOWN 消息跟踪这些状态会,例如,导致 Alt-TabAlt 键卡住,因为窗口永远不会收到 WM_SYSKEYUP 消息。检查 Ctrl/Shift/Alt 键状态的函数在必要时在多个不同的WM_... 消息上调用。但是,一旦VK_LCONTROL 消息开始被键盘钩子而不是VK_CONTROL 截获,该函数总是报告所有键未按下。

【问题讨论】:

你为什么不让你的文本框像往常一样拥有输入焦点?那么你就不需要这些东西了。 有问题的 UI 引擎是一个 directx 绘制的自定义应用程序框架,而不是标准的 windows 窗体。系统中的所有小部件都是自定义的,包括文本框。因此,没有“正常输入焦点”之类的东西。另外,由于覆盖层本身对键盘和鼠标输入是透明的(因此需要挂钩),即使在您提到问题的情况下,问题仍然是相同的:我需要使用键盘挂钩来为覆盖层提供窗口消息并阻止一些输入进入游戏。 没有正常焦点,如何区分键盘输入与文本框和其他小部件? 您所说的要求窗口是非输入透明的以直接捕获窗口消息,但这违背了显示信息的全屏覆盖(有时是交互式的)的整个想法) 而不会干扰其背后发生的事情。是的,UI 确实有焦点处理代码,您可以在发布的文章中看到如何使用它来过滤窗口挂钩的消息。 【参考方案1】:

您可以尝试不同的方法。如果在此期间你的叠加层是活动窗口,那么你可以在没有钩子的情况下处理键盘和鼠标事件,如果你想将事件转发给游戏,你可以为游戏窗口合成事件。

【讨论】:

没有支持“为[特定]窗口合成[一个]事件”的方法SendInput 转到前台窗口,PostMessage won't work。 是的,这种方法行不通 - 枚举的游戏输入破坏了太多东西,这就是为什么我选择了只阻止一些输入的侵入性较小的方法。这样也更有利于未来。

以上是关于覆盖应用程序的低级键盘挂钩问题的主要内容,如果未能解决你的问题,请参考以下文章

用于 C# 和 WPF 的高级全局键盘钩子,用于读取键盘楔形卡扫描仪

区分鼠标设备和低级鼠标挂钩

更改键盘挂钩回调中的键盘布局

为啥我的低级 Windows 键挂钩停止工作?

Swing 应用程序线程被 JNA 锁定

全局键盘挂钩