65.9K
CodeProject 正在变化。 阅读更多。
Home

结合原始输入和键盘钩子选择性阻止来自多个键盘的输入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (24投票s)

2014年1月27日

CPOL

22分钟阅读

viewsIcon

115991

downloadIcon

5122

如何结合原始输入和键盘 Hook API,并使用它们有选择地阻止来自部分键盘的输入。

目录

引言

当您将多个键盘连接到您的电脑时,您可能会有一些有趣的想法来处理它们。也许您可以用一个进行标准打字,另一个用于一些特殊任务。当您深入研究多键盘设置时,您可能会问自己一个问题:“我能否根据用于生成输入的键盘来阻止正在运行的应用程序的按键输入?”。假设您打开了记事本,当您在键盘1上按下“a”键时,您希望“a”字母被写入文档,但当您在键盘2上按下“a”键时,您希望在后台执行一些任务,而记事本甚至不会注意到这个按键。当我遇到这个问题时,我很难找到答案和解决方案。因此,我决定写这篇文章,如果您也遇到同样的问题,希望能让您更容易解决。本文假设您对 Windows 编程有一定的了解,但我会尝试为您指出相关资源,以便您能够学习所需的一切。

背景

我必须解决这个问题,当时我想要为多个键盘开发一个自定义的热键管理器。基本想法是使用一个标准键盘进行标准输入,以及一个用于热键的小型数字键盘。热键应该针对各种应用程序进行配置,这样我就可以用一个键在一个应用程序中执行一个操作,而同一个键在另一个应用程序中执行不同的操作。我首先寻找现有的解决方案,但没有找到任何能满足我所有要求的。“AutoHotkey”本身无法区分多个键盘。“HID macros”不允许应用程序特定的热键设置。可以结合使用两者,但我不喜欢这种解决方案。我遇到了一些管理器,它们似乎可以完成这项工作,但它们都是许可软件。所以我决定自己编写。

简单来说,当前的问题是在管理应用程序中根据生成输入的设备决定是否阻止按键输入,然后在具有用户焦点的运行应用程序中成功阻止它。当您想在 Windows 上处理键盘输入时,基本上有两种选择:使用设备的自定义驱动程序,或使用 Windows 的键盘输入 API 之一。需要考虑的有两个 API:原始输入和键盘 Hook。

  • 自定义设备驱动程序:到目前为止,我还没有编写自己的设备驱动程序的经验。我查阅了一些文档和代码示例,这对于我的项目来说似乎非常复杂。而且它还会带来一些负面影响。管理器需要管理员权限才能安装驱动程序,这将限制可移植性。用户可能会觉得安装此类驱动程序不舒服。驱动程序可能还需要由认证机构签名(我不完全确定这是否属实,因为我没有深入研究这个问题)。
    另一个选择是使用一个提供编程接口的自定义驱动程序,这样我就不必自己编写驱动程序了。“Interception 库”似乎提供了这样的选项。但是自定义驱动程序的一些负面方面仍然存在(需要管理员权限,用户不安),此外还会增加一些。库本身的源代码是可用的,但是驱动程序的源代码不可用,因此我无法完全控制我提供给潜在用户的产品。
  • 原始输入 (Raw Input):这个接口提供了一种比标准 Windows 键盘输入消息更复杂的方式来接收来自各种设备的输入,包括键盘。最值得注意的是,它可以识别是在哪个键盘上按下了按键,这在使用标准输入消息时是不可能的。原始输入能够监控系统范围内的键盘事件,因此问题中的决策部分可以通过它解决。不幸的是,第二部分无法解决。至少我无法找到任何方法来阻止输入通过原始输入 API 到达其目标窗口。
  • 键盘 Hook:Windows Hook 是一种机制,允许应用程序拦截系统消息,例如键盘事件。如果设置正确,它们可以在系统范围内工作。它们甚至允许应用程序修改或停止相应消息的传播。这意味着 Hook 可以用来阻止击键。问题是,没有办法通常识别是在哪个键盘上按下了键。

如您所见,除非您想使用自定义设备驱动程序,否则没有一个 API 可以解决整个问题。“我可以结合这两个 API 使其工作吗?”您可能会在此时问。事实证明,是的,您可以。不幸的是,关于交互的文档基本上不存在,而且它非常违反直觉。我希望这篇文章能为任何与此问题作斗争的人提供一些启发。

使用代码

正如我之前提到的,我将假设您对 Windows 编程有一定的了解。我不想过多地介绍每个 API 的设置细节,因为我认为这些内容已经足够多了。如果您不熟悉任何一个 API,我建议您查阅文档。对于 Windows 消息系统一般情况:“关于消息和消息队列” [1];对于原始输入:“关于原始输入” [2];对于 Hook:“Hook 概述” [3]。

我在项目中使用C++,但我相信代码经过一些努力可以适用于.NET。为了简单起见,我将省略对系统调用、分配等的任何错误检查,除非它们是问题本身的一部分。演示项目使用简单的调试输出,仅用于演示目的,旨在展示主要思想。它肯定可以结构化得更好。此外,代码示例只包含最重要的内容。不会有任何完整的函数或模块可供您直接复制粘贴(为此您可以下载附带的演示项目),但我会始终指出函数和文件,以便您可以了解特定代码的归属。这些示例应该真正帮助您理解问题。

到目前为止,我只在 Windows 7 (64 位) 上测试过代码。在其他系统上,所描述的 API 行为可能会有所不同。

主要概念

我通过 CodeProject 上 "HID macros" [4] 的作者 Petr Medek 的评论,发现了这种结合原始输入和 Hook 的概念。他引导我找到了这个解决方案的基本思想。所以我必须归功于他;非常感谢 Petr!我根据这个基本思想构建了我的项目,并经历了一些额外的陷阱。在 Petr 的允许下,我将向您描述整个概念以及我遇到的所有复杂情况。

提醒您,我们的目标是拦截按键,并根据使用的键盘来决定是阻止它们还是将它们传递给活动应用程序。我们将使用原始输入进行决策,并使用 Hook 来阻止输入。那么,让我们开始吧!

基本设置

为了使用它们,我们首先需要在我们的应用程序中设置这两个 API。对于原始输入,没有什么隐藏的陷阱,所以我们简单地注册以全局接收键盘输入(通过指定 `RIDEV_INPUTSINK` 标志)。

// --- InitInstance (HookingRawInputDemo.cpp) ---
// Register for receiving Raw Input for keyboards
RAWINPUTDEVICE rawInputDevice[1];
rawInputDevice[0].usUsagePage = 1;
rawInputDevice[0].usUsage = 6;
rawInputDevice[0].dwFlags = RIDEV_INPUTSINK;
rawInputDevice[0].hwndTarget = hWnd;
RegisterRawInputDevices (rawInputDevice, 1, sizeof (rawInputDevice[0]));

至于 `Raw Input 消息` 的处理,我们现在只需检查是否正确接收到它们。我们将跟踪按键的虚拟键码以及它是否被按下或释放。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Raw Input Message
case WM_INPUT:
{
	// ...
	// Get the virtual key code of the key and report it
	USHORT virtualKeyCode = raw->data.keyboard.VKey;
	USHORT keyPressed = raw->data.keyboard.Flags & RI_KEY_BREAK ? 0 : 1;
	WCHAR text[128];
	swprintf_s (text, 128, L"Raw Input: %X (%d)\n", virtualKeyCode, keyPressed);
	OutputDebugString (text);
	// ...
}

关于 Hook,事情已经变得有点棘手了。当我第一次尝试结合使用这些 API 时,我试图使用全局低级键盘 Hook (`WH_KEYBOARD_LL`)。问题是,当我们使用低级键盘 Hook 阻止某些输入时(我们停止了消息的传播),Windows 不会生成原始输入事件,这意味着任何应用程序都不会收到相应的 `Raw Input message` (`WM_INPUT`)。因此,我们不能使用低级键盘 Hook,而必须使用标准键盘 Hook (`WH_KEYBOARD`),这设置起来有点困难。当我们要全局使用此 Hook,即对任何正在运行的应用程序使用时,其过程必须位于单独的 DLL 模块中。如果您不知道如何在 DLL 模块中设置 Hook,以下文章之一应该能帮助您:“Hook 和 DLL” [5],“使用 VC++ 的鼠标和键盘 Hook 工具” [6]。Hook 应该这样注册:

// --- InstallHook (HookingRawInputDemoDLL.cpp) ---
// Register keyboard Hook
hookHandle = SetWindowsHookEx (WH_KEYBOARD, (HOOKPROC)KeyboardProc, instanceHandle, 0);

目前,我们让 Hook 过程不对输入做任何处理,就像原始输入一样,只是检查它,并沿着 Hook 链传递。

// --- KeyboardProc (HookingRawInputDemoDLL.cpp) ---
// Get the virtual key code of the key and report it
USHORT virtualKeyCode = (USHORT)wParam;
USHORT keyPressed = lParam & 0x80000000 ? 0 : 1;
WCHAR text[128];
swprintf_s (text, 128, L"Hook: %X (%d)\n", virtualKeyCode, keyPressed);
OutputDebugString (text);

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

现在,当您运行应用程序并在另一个窗口中输入内容时,您应该会收到原始输入和 Hook 的正确通知。

沟通

我们期望使用 Hook 来阻止输入,并使用原始输入来做出决策。但它们位于独立的模块中,因此我们将使用 Windows 消息系统来处理两者之间的通信。通信将非常简单。我们将从 Hook 过程向主窗口发送一条消息,以及原始 `Hook 消息` 参数。主窗口将决定如何处理输入。如果 Hook 应该阻止输入,则消息调用的返回值为 1;否则为 0。我们可以尝试一下,而无需做出任何合理的决定。

// --- KeyboardProc (HookingRawInputDemoDLL.cpp) ---
// Report the event to the main window. If the return value is 1, block the input; otherwise pass it
// along the Hook chain
if (SendMessage (hwndServer, WM_HOOK, wParam, lParam))
{
	return 1;
}

// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	return 0;
}

请注意,使用 `SendMessage` 而非 `PostMessage` 很重要,因为我们希望等待决策。

决策

现在我们已经建立了通信,终于可以做一些事情了。让我们看看我们正在处理什么。最简单的情况,当只有几个单独的按键时,应该像这样(第一个数字是按键的十六进制虚拟键码,括号中的数字表示按键是否被按下):

Raw Input: 67 (1)
Hook: 67 (1)
Raw Input: 67 (0)
Hook: 67 (0)
Raw Input: 63 (1)
Hook: 63 (1)
Raw Input: 63 (0)
Hook: 63 (0)

每次按键,我们都会首先收到 `Raw Input message`,然后是其 `Hook message`。我可以提前告诉您,当您打字更快或使用某些特殊键时,事情可能会变得有点混乱,但我们稍后会处理,现在我们只考虑这种干净的情况。正因为如此,每当我们收到 `Raw Input message` 时,我们就可以决定如何处理输入(基于使用的键盘),记住这个决定,当我们收到 `Hook message` 时,我们只需回复我们做出的决定。

让我们首先改进我们的原始输入处理(这是做出决策的地方),以纳入键盘识别。我们的决策将非常简单,如果按下的键是数字键盘上的“7”,我们将阻止它。所有其他按键都将通过。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Raw Input Message
case WM_INPUT:
{
	// ...
	// Prepare string buffer for the device name
	GetRawInputDeviceInfo (raw->header.hDevice, RIDI_DEVICENAME, NULL, &bufferSize);
	WCHAR* stringBuffer = new WCHAR[bufferSize];

	// Load the device name into the buffer
	GetRawInputDeviceInfo (raw->header.hDevice, RIDI_DEVICENAME, stringBuffer, &bufferSize);

	// Check whether the key struck was a "7" on a numeric keyboard
	if (virtualKeyCode == 0x67 && wcscmp (stringBuffer, numericKeyboardDeviceName) == 0)
	{
		blockNextHook = TRUE;
	}
	else
	{
		blockNextHook = FALSE;
	}
	// ...
}

唯一剩下要做的就是在 Hook 事件处理中使用该决策。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	// ...
	// If next Hook message is supposed to be blocked, return 1
	if (blockNextHook)
	{
		swprintf_s (text, 128, L"Keyboard event: %X (%d) is being blocked!\n",
		            virtualKeyCode, keyPressed);
		OutputDebugString (text);
		return 1;
	}
	// ...
}

瞧!我们的应用程序,根据使用的键盘阻止键盘输入,正在运行。原始输入和键盘 Hook 组合的主要思想应该对您来说很清楚了。

最重要的对策

不幸的是,在现实世界中,事情往往会变得更加复杂。在许多情况下,我们不会像之前假设的那样得到整齐有序的 `Raw Input` 和 `Hook 消息` 序列。我们必须处理这些情况。让我们一次一个地看看这些复杂性。当您阅读各种情况时,您可能会想,“如果这样会怎么样……?这可能吗……?”。您可能说得对,事情可能会比下面单个部分所暗示的要混乱得多。我正在努力尽可能地保持简单,尽管因此我们将不得不重做一些解决方案。我认为这是理解整个问题最简单的方法。我相信最终您会明白的。

原始输入消息缓冲

当您开始快速打字(或者像用手指猛击键盘)时,您很可能会连续收到多个 `Raw Input 消息`,之后才收到匹配的 `Hook 消息`。例如像这样:

Raw Input: 44 (1)
Hook: 44 (1)
Raw Input: 4B (1)
Raw Input: 53 (1)
Hook: 4B (1)
Hook: 53 (1)

如果我们满足于目前简单的解决方案,我们的应用程序将无法正常工作,因为它会根据对“s”键(53)输入做出的单个决定,阻止“k”键(4B)和“s”键(53)的输入,因为我们只记住我们做出的最后一个决定。

因此,我们必须记住多个决策并按顺序使用它们。为了实现这一点,我们将使用一个 FIFO(先进先出)容器。我选择使用一个简单的 `Deque`。当我们收到 `Raw Input 消息` 时,我们将决定如何处理输入,并将决策推入 `Deque`。当我们收到 `Hook 消息` 时,我们只需弹出决策。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Raw Input Message
case WM_INPUT:
{
	// ...
	// Check whether the key struck was a "7" on a numeric keyboard, and remember the decision
	// whether to block the input
	if (virtualKeyCode == 0x67 && wcscmp (stringBuffer, numericKeyboardDeviceName) == 0)
	{
		decisionBuffer.push_back (TRUE);
	}
	else
	{
		decisionBuffer.push_back (FALSE);
	}
	// ...
}
// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	// ...
	// Check the buffer if this Hook message is supposed to be blocked; return 1 if it is
	BOOL blockThisHook = FALSE;
	if (!decisionBuffer.empty ())
	{
		blockThisHook = decisionBuffer.front ();
		decisionBuffer.pop_front ();
	}
	if (blockThisHook)
	{
		// ...
		return 1;
	}
	// ...
}

等待原始输入消息

到目前为止,我们一直假设 `Raw Input message` 总是先于 `Hook message` 到达。通常情况下是这样,但也有例外,`Hook message` 先到达,例如:

Raw Input: 4A (0)
Hook: 4A (0)
Hook: 48 (0)
Raw Input: 48 (0)

这意味着,当缓冲区中没有决策时,我们却想从中弹出一个决策。当这种情况发生时,我们必须等待,直到收到延迟的 `Raw Input message`。收到消息后,我们可以立即决定是否阻止输入。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	// ...
	// Check the buffer if this Hook message is supposed to be blocked; return 1 if it is
	BOOL blockThisHook = FALSE;
	if (!decisionBuffer.empty ())
	{
		// ...
	}
	// The decision buffer is empty
	else
	{
		MSG rawMessage;

		// Waiting for the next Raw Input message
		while (!PeekMessage (&rawMessage, mainHwnd, WM_INPUT, WM_INPUT, PM_REMOVE))
		{
		}

		// The Raw Input message has arrived; decide whether to block the input
		// ...
	}
	// ...
}

Hook 消息丢失

我们已经设法处理了消息序列混乱的情况,但您可能已经想知道:“是否有可能有些消息根本没有到达?如果这样,会发生什么?”。确实有可能。让我们看看当我们丢失一些 `Hook 消息` 时会发生什么,即我们收到某个事件的 `Raw Input 消息`,但没有收到相应的 `Hook 消息`。例如,当我使用“Ctrl”+“Esc”快捷键时就会发生这种情况。此序列的消息如下:

Raw Input: 11 (1)
Hook: 11 (1)
Raw Input: 1B (0)
Raw Input: 11 (0)

如您所见,Windows 忽略了向我们发送一些消息。具体来说,是“Esc”键(1B)按下时的 `Raw Input` 和 `Hook 事件`,以及“Esc”(1B)和“Ctrl”(11)键释放时的 `Hook 事件`。如果我们只是将这些击键的决策推入缓冲区,我们稍后会错误地使用它们,这可能会给我们带来一些麻烦。这个问题可以通过增强我们的决策缓冲区来解决(某种程度上)。我们不仅会记住决策本身,还会记住它所属的虚拟键码(您甚至可以包括按键是否被按下,以实现更健壮的解决方案)。这样,当 `Hook 消息` 到达,并且我们正在寻找如何处理它的决策时,我们不会简单地弹出第一个决策,而是会检查其虚拟键码是否匹配。如果不匹配,我们将遍历整个缓冲区寻找正确的决策。我提到这只是“某种程度上”解决了问题。即使有这个虚拟键码检查,潜在的危险是可能会有某个按钮被按两次,每次都在不同的键盘上。如果第一次击键的 `Hook 消息` 没有到达,我们将错误地评估第二次击键(基于为第一次击键做出的决策)。不幸的是,我无法想到任何合理的解决方案。如果您碰巧知道一个,我将非常感谢您在评论区与我们分享,或者您可以给我发电子邮件。这绝对是使用此 API 组合时需要注意的事情之一。

// --- (HookingRawInputDemo.h) ---
struct DecisionRecord
{
	USHORT virtualKeyCode;
	BOOL decision;

	DecisionRecord (USHORT _virtualKeyCode, BOOL _decision) : virtualKeyCode (_virtualKeyCode),
	                decision (_decision) {}
};
// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	// ...
	// Check the buffer if this Hook message is supposed to be blocked; return 1 if it is
	BOOL blockThisHook = FALSE;
	BOOL recordFound = FALSE;
	UINT index = 1;
	if (!decisionBuffer.empty ())
	{
		// Search the buffer for the matching record
		std::deque::iterator iterator = decisionBuffer.begin ();
		while (iterator != decisionBuffer.end ())
		{
			if (iterator->virtualKeyCode == virtualKeyCode)
			{
				blockThisHook = iterator->decision;
				recordFound = TRUE;
				// Remove this and all preceding messages from the buffer
				for (int i = 0; i < index; ++i)
				{
					decisionBuffer.pop_front ();
				}
				// Stop looking
				break;
			}
			++iterator;
			++index;
		}
	}
	// ...
}

请注意,当我们弹出决策时,我们不仅弹出找到的匹配决策,还弹出所有比当前决策更早的决策。假设没有发生其他问题,这些决策是没有匹配 `Hook 消息` 的决策。这有助于防止缓冲区不必要地变大。但当然还有更多方法可以根据您的需要来控制它。

原始输入消息丢失

当我们处理 `Hook 消息` 先于其 `Raw Input 消息` 到达的情况时,您可能对我们引入的潜在无限 while 循环有点担心。您确实应该担心,实际上。我们已经注意到,并非所有消息都会总是被传递。我们已经探讨了 `Hook 消息` 丢失时会发生什么。现在让我们看看 `Raw Input 消息` 未正确传递时会发生什么。因为存在真实场景,这种情况确实会发生。例如,“AltGr”键会给我这些消息(您可能不会遇到这种情况,因为这种行为是与区域设置相关的):

Raw Input: 12 (1)
Hook: 11 (1)
Hook: 12 (1)
Raw Input: 12 (0)
Hook: 11 (0)
Hook: 12 (0)

Windows 在单次按下“AltGr”键时会发送“Ctrl”(11)+“Alt”(12)的 `Hook 消息`(“Ctrl”+“Alt”是“AltGr”的常见替代)。问题是,它只发送“AltGr”(12)本身的 `Raw Input 消息`(“AltGr”在此目的下与“Alt”相同;据我所知,它们甚至具有相同的扫描码)。现在,采用我们的原始输入等待循环可能会导致一些严重问题,因为它可能永远不会收到它正在等待的 `Raw Input 消息`。我们必须对循环施加一些限制。

// --- WndProc (HookingRawInputDemo.cpp) ---
// Message from Hooking DLL
case WM_HOOK:
{
	// ...
	// Wait for the matching Raw Input message if the decision buffer was empty or the matching
	// record wasn't there
	DWORD currentTime, startTime;
	startTime = GetTickCount ();
	while (!recordFound)
	{
		MSG rawMessage;
		while (!PeekMessage (&rawMessage, mainHwnd, WM_INPUT, WM_INPUT, PM_REMOVE))
		{
			// Test for the maxWaitingTime
			currentTime = GetTickCount ();
			// If current time is less than start, the time rolled over to 0
			if ((currentTime < startTime ? ULONG_MAX - startTime + currentTime :
			     currentTime - startTime) > maxWaitingTime)
			{
				// Ignore the Hook message, if it exceeded the limit
				WCHAR text[128];
				swprintf_s (text, 128, L"Hook TIMED OUT: %X (%d)\n", virtualKeyCode,
				            keyPressed);
				OutputDebugString (text);
				return 0;
			}
		}
		// ...
	}
	// ...
}

即使现在循环不会无限运行,我相信这(原始输入消息丢失或延迟)是整个概念最大的问题之一。因为任何等待原始输入消息都会引入键盘输入的延迟。因此,等待限制应该非常低,这样用户就不会注意到任何延迟。甚至值得考虑使用更精确的计时器,我使用最简单的计时器只是为了演示目的。

更多怪事

我认为,我们现在拥有的对于结合原始输入和键盘 Hook 的应用程序来说是一个合理的内核。所提供的演示项目与我们目前所经历的内容相符。现在我想告诉您更多关于您在使用这些 API 时可能遇到的一些特殊行为。这主要是一些导致我们之前讨论过的问题的事情,重点是丢失的 `Raw Input 消息`。我相信它值得关注,因为正如我所提到的,丢失的 `Raw Input 消息` 可能导致键盘输入延迟,这是非常不可取的。我将向您展示我所知道的所有有问题的情况,并提出一些方法来避免,或至少最小化它可能代表的危险。但我不会在演示项目中包含这些高级调整,因为我想在清晰度和完整性之间保持一些平衡,所以我决定以此为界。我还相信,既然您已经做到这一点,您有能力自行实现所讨论的方法,甚至设计更好的方法,我们其他人也会乐于在评论区阅读。但是,如果您对这些技术中的任何一种感兴趣,并且无法自行解决,请随时在评论区询问详细信息或示例代码,我将尽力帮助您。一旦我完成我的快捷方式管理应用程序(包含源代码),我也会提供链接,其中涵盖所有这些额外的措施。

“AltGr”问题

让我们从我们已经遇到过的一些问题开始。当 Windows 生成“Ctrl”+“Alt”的 `Hook 消息`,但只生成“Alt”的 `Raw Input 消息` 时,会带来不便。您可以回过头来看看在这种情况下我们获得的消息序列。

通常,“Alt” `Raw Input 消息` 会正确到达,因此我们可以检查 `Raw Input 消息` 并从其标志中获取一个附加细节。该细节是“Alt”键是否是正确的版本,即实际上可以是“AltGr”的版本。如果是,我们不仅将“Alt”键的决策推入决策缓冲区,还会首先推入一个伪造的“Ctrl”键决策。这样,传入的“Ctrl”键 `Hook 消息` 将能够在缓冲区中找到匹配的记录,并且不会延迟输入。正如我前面提到的,这种行为只发生在某些区域设置中,因此您可以使用 `GetKeyboardLayout` 函数仅针对特定区域设置采用此解决方法。

无意义的 Hook 消息

说到区域设置相关问题,在某些区域设置中,您可能会遇到非常奇怪的行为。例如,对于单个“Win”键击(在我的捷克 QWERTZ 键盘布局上),您可能会收到以下消息序列:

Raw Input: 5C (1)
Hook: 5C (1)
Raw Input: 5C (0)
Hook: 5C (0)
Hook TIMED OUT: 11 (0)

您可能想知道那个“Ctrl” `Hook 消息` 在那里做什么,至少我是这么想的。更重要的是,当您检查消息的详细信息时。事实证明,“Ctrl” `Hook 消息` 的标志设置为指示该键正在释放,并且在事件发生之前该键是抬起的(根据 `Previous Key-State Flag`)。换句话说,“Ctrl”键在已经释放的情况下正在被释放。这种情况可能发生在键盘上的各种键(不仅仅是“Win”键)上,但据我所知,不正确的 `Hook 消息` 总是“Ctrl”在已经抬起的情况下被释放。由于我从未遇到过这种情况,即在正确的“Ctrl”键按下时出现此标志设置(我注意到它们出现在某些特殊键的常规击键中),我相信您可以监控您的 `Hook 消息` 以查找此模式,并且当此类消息到达时,不要等待 `Raw Input 消息`。

多重 Hook 消息

在某些情况下,Windows 会为单个键盘事件生成多个键盘 `Hook 消息`,而只有一个 `Raw Input 消息`。每个重复的 `Hook 消息` 都会尝试等待它的 `Raw Input 消息`(永远不会到来),这将导致延迟。这通常发生在应用程序进入菜单(无论是菜单栏还是上下文菜单)时,或者某些应用程序可能会更不可预测地引起这种行为。例如,Firefox 在我打字过快时就会出现这种情况。消息序列可能如下所示:

Hook: 27 (1)
Raw Input WAITING: 27 (1)
Hook: 27 (1)
Raw Input WAITING: 27 (0)
Hook: 27 (0)
Hook TIMED OUT: 27 (0)
Hook: 27 (0)
Hook TIMED OUT: 27 (0)

如您所见,Windows 为每个 `Raw Input 消息` 传递了两个 `Hook 消息`,但这并非一概而论,每个 `Raw Input 消息` 可能有三个甚至更多 `Hook 消息`。我目前用以下思路处理这个问题。

基础是记住应用程序收到的最后一个 `Hook 消息` 是什么。当新的 `Hook 消息` 到达时,我们可以检查它是否与上一个消息相同(虚拟键码和按键是否匹配)。如果是,我们不会等待 `Raw Input 消息`。但我们仍然应该检查决策缓冲区中是否有匹配的 `Raw Input 消息`。因为例如当某个键被按住时,会有有意义的相同 `Hook 消息` 序列与匹配的 `Raw Input 消息`,我们不应该忽视这些。

在使用这种方法时,需要记住一件事,那就是您应该对每个重复的 `Hook 消息` 应用相同的决策(是否通过 Hook 阻止输入)。比方说,您让第一个 Hook 事件通过,然后阻止所有相同的后续事件,认为第一个事件应该足以让活动窗口处理输入(例如菜单栏中的击键)。事实证明,并非如此。如果您不传递后续事件以及第一个事件,应用程序将不会执行击键。

如果您想更具选择性地使用这种方法,可以使用 `GetGUIThreadInfo` 函数检查应用程序当前是否处于菜单模式。尽管我建议始终使用它,因为有些应用程序即使不在菜单模式下也会导致这种行为。

系统快捷键

到目前为止,我们已经使用标准键盘 Hook (`WH_KEYBOARD`) 来在需要时阻止原始输入。这种设置有一个限制。有些键是由系统处理的,无论标准 Hook 是否阻止它们。例如,“Win”+“d”、“Alt”+“Tab”等快捷键,甚至一些单独的特殊键,如“计算器”按钮(我的键盘上是 B7)。即使您阻止 `Hook 消息` 的传播,当您按下它时,Windows 仍会启动计算器。

如果您想能够阻止此类按键事件,您将不得不集成低级键盘 Hook (`WH_KEYBOARD_LL`)。当我们进行设置时,我们发现无法结合低级 Hook 和原始输入 API,因此没有办法(我相信没有,如果您知道任何办法,请在评论区或通过电子邮件告诉我)阻止这些特殊键,并且仍然能够识别所使用的键盘。但至少它们可以并排使用,因此您可以拥有带有原始输入的标准 Hook 处理大多数事件,以及处理这些特殊键的低级 Hook,这些键将无法通过键盘识别来区分。

我遇到的最后一件事是“Num Lock”键(我没有注意到其他锁定键有这种情况)。即使我用低级 Hook 阻止了这个键,它仍然会影响键盘状态,就像正常的“Num Lock”按键一样(只是它不会改变键盘上的 LED 指示灯)。这可以通过在“Num Lock”键释放时(在标准键盘 Hook 中)检查键盘状态来避免,如果需要,可以模拟一个假的“Num Lock”按键,将键盘切换到之前的状态。

结论

我希望这篇文章能帮助您了解如何结合原始输入和键盘 Hook API。

如果我总结一下我对整个方法的看法,我觉得它非常有用。它能够解决问题,尽管存在一些未解决的问题。由于 Windows API 没有提供更简单的方法来解决问题,我认为这些问题是合理可控的。但是,如果您需要实现一个 100% 健壮的解决方案,您可能需要使用自定义驱动程序的方法。

如果您有任何想法、问题或任何其他要补充的内容,请随时在评论区分享。

历史

  • 2014年1月27日:文章第一版发布。

参考文献

© . All rights reserved.