原贴链接:https://www.autohotkey.com/boards/viewtopic.php?style=19&f=96&t=127074
介绍
AutoHotkeyV2 的默认发送模式是 SendInput 。它通常可靠且快速,但也存在一些很容易遇到的缺点。
太长不看
如果您想充分利用 SendInput 的优势并确保其行为一致,请确保没有其他程序(包括其他 AutoHotkey 脚本)安装了键盘钩子:其他脚本不得使用热键字符串;1:1 重映射;与 #HotIf 组合使用的热键;带有修饰符 ~、* 或 $ 的热键;任何带有向上选项(按键释放)的热键;InstallKeybdHook(1);#InputLevel 大于 0;通过 InputHook 函数激活的输入钩子;使用 SetCapsLockState/SetNumLockState/SetScrollLockState 函数。在 AutoHotkey v2.1 中,您可以使用 A_KeybdHookInstalled > 1 来检查是否存在其他带有键盘钩子的 AHK 脚本。
在所有其他情况下,我建议使用 SendMode "Event" 并设置合适的按键延迟(例如 SetKeyDelay(-1, 0) ),无论是否使用输入缓冲,以防止按键与用户输入交错(例如,请参阅此处的 InputBuffer 类 )。
低级键盘钩子
本文将多次提及底层键盘钩子,因此我们先来了解一下它是什么。Windows 有一个名为 SetWindowsHookEx 的函数,可用于安装全局钩子来捕获各种系统事件。底层键盘钩子就是其中之一,其工作原理如下:任何程序都可以调用 SetWindowsHookEx 向 Windows 注册一个新的键盘钩子,并在函数调用中提供一个回调函数。然后,一旦发生按键事件(无论是用户实际按下按键,还是程序发送模拟按键),Windows 就会开始遍历其已注册的钩子列表,并逐个调用它们的回调函数(开始处理“钩子链”),从最近安装的钩子开始。它会将按键信息(键码、按键是否被按下/释放、是否为模拟按键,以及可能通过 SendInput/keybd_event 提供的任何其他信息,这些将在下文进一步讨论)传递给回调函数,并等待结果。根据结果,可能会发生以下三种情况之一:
1)回调函数可以阻塞按键操作。这意味着不会再调用任何钩子函数,按键操作也不会在任何窗口中发送。
2) 回调函数可以允许按键事件通过,并将其传递给钩子链中的下一个钩子。如果所有其他钩子也允许按键事件通过,或者没有其他钩子,则发送该按键事件。
2) 回调函数可以显式地允许按键事件发生。这意味着不会调用其他钩子函数,按键事件将被发送,但不会调用其他钩子函数。
Windows 会监控回调函数,以确保处理按键事件的时间不会过长。如果回调函数多次处理速度不够快,则可能会移除该钩子。超时值由注册表项 HKEY_CURRENT_USER\Control Panel\Desktop 中的 LowLevelHooksTimeout 指定。
安装和删除钩子非常快,几乎是瞬间完成的,因为它们在 Windows 中是在非常底层(内核级别?)实现的。
由于 Windows 对这些钩子的实现方式,程序无法访问有关钩子的信息:我们无法知道安装了多少个钩子,我们在钩子链中的位置,钩子的安装时间,以及我们自己的钩子是否已被移除。我们也无法移除除我们自己之外的其他钩子。
热键字符串和热键是如何实现的
首先让我们尝试了解 AutoHotkey (AHK) 是如何实现其热键和热字符串的。
1) 热键字符串始终通过底层键盘钩子实现。如果使用了任何热键字符串,AHK 会自动安装它。它的作用是捕获所有键盘输入(包括人工输入和物理输入),并将其发送到 AutoHotkey 进行“过滤”。AHK 可以阻止按键输入,也可以让按键原封不动地通过。对于热键字符串,它总是允许按键输入通过,并记录哪些按键被按下。一旦检测到热键字符串匹配,就会检查所有 #HotIf 条件,并根据结果执行触发器(自动替换字符串或执行函数)。
2) 部分热键使用 RegisterHotkey 注册。它会将热键注册到 Windows 系统中,这样当按下某个键时,系统会查找所有已注册的热键,并通知已注册该热键的应用程序。RegisterHotkey 支持具有虚拟键码的按键,也支持与 Ctrl、Shift、Alt 和 Win 等修饰键组合使用。所有通过 RegisterHotkey 注册和激活的按键都将被阻止。此外,任何通过 RegisterHotkey 注册的热键都可以通过 AHK Send 函数触发,因为按键是由系统而非 AHK 处理的。
代码:
Send "a"
a::MsgBox("ok")
触发 a 热键,因为系统只是看到 a 键并通知 AHK,而 AHK 无法知道是它自己的 Send 触发了它。
所有其他快捷键都是通过底层键盘钩子实现的:
* 任何使用 #HotIf 的热键。这是因为为了检查 #HotIf 的条件是否满足,AHK 需要捕获按键,然后处理 #HotIf,最终决定是允许按键通过还是阻止它。RegisterHotkey 无法轻松可靠地实现这一点。
* 任何使用修饰符 ~、* 或 $ 的热键
RegisterHotkey 不支持任何自定义组合,例如将 a 和 b 作为自定义修饰符。
* 任何没有虚拟键码的热键
发送模式
发送模式有三种:输入(v2 中的默认模式)、事件和播放。对我们来说,最相关的是输入和事件模式。
1) SendInput 使用 Win32 的 SendInput 函数发送按键。所有输入都以批次形式发送,没有延迟,这意味着 SetKeyDelay 函数不会生效。SendInput 的主要(或许是唯一)优势在于,由于按键是批量发送的,因此即使发送长文本,用户输入也不会被打断。但是,这仅在未安装任何键盘钩子 (无论是通过 AHK 还是其他应用程序)的情况下才成立。
2) SendEvent 使用 win32 keybd_event 。每次只发送一个按键事件(按下或释放),这意味着 AHK 可以在按键按下和释放之间(按键延迟)或两个按键之间添加延迟。这些延迟可以通过 SetKeyDelay 函数进行更改。
SendInput 和 hooks
为什么这一切如此重要?因为要发挥 SendInput 的优势,AHK 必须在使用它之前移除自身的键盘钩子,否则这些优势将无法实现。AutoHotkey 可以检测其他 AHK 脚本安装的键盘钩子,如果它确定只有自己安装了钩子,那么它会在调用 SendInput 之前卸载该钩子 ,然后使用 SendInput 发送按键,最后再重新安装该钩子。
AutoHotkey 无法检测到其他程序安装的键盘钩子,因此如果存在此类钩子,它仍然会使用 SendInput ,但按键发送速度会变慢(因为其他应用程序必须处理这些按键),并且发送的按键可能会与用户在发送过程中输入的按键交错出现。在这种情况下, SendInput 实际上等同于使用 SetKeyDelay(-1, -1) 的 SendEvent ,但缺点是键盘钩子会在 Send 期间被卸载。
通常情况下,键盘钩子的安装和卸载速度非常快,不会造成明显的影响。但是,如果用户打字速度非常快,有时本应被锁定的热键可能会“泄露”出来。例如,以下热键使用了一个键盘钩子($ 修饰符强制使用钩子),如果按住“z”键,有时“z”会泄露到“n”字符之间。
代码:
$z:: {
While GetKeyState("z", "P") {
Send "{n Down}"
Sleep 200
Send "{n Up}"
}
}
在我的电脑里,大约每 20 个字符中就会有一个是“z”!
这个问题可以通过使用 SendMode Event 而不是 Input 并将 KeyDelay 设置为一个较小的值(例如 SetKeyDelay(-1, 0) )来解决,因为 SendEvent 不会卸载钩子。
默认情况下,通过 SendEvent 发送的按键不会被通过键盘钩子注册的热键检测到,因为 AHK 会在发送按键之前用当前的 SendLevel 值“标记”按键,而当按键到达键盘钩子时,SendLevel 值会被读取并用于判断是否触发热键/热字符串。默认情况下,如果 SendLevel 为 0,则不会触发热键/热字符串,而默认的 SendLevel 值就是 0。这意味着我们可以通过在发送前将 SendLevel 值提高来使用 SendEvent 触发热键。另一方面, SendInput 无法触发热字符串或通过键盘钩子为当前运行脚本实现的热键,但它可以触发由 RegisterHotkey 实现的热键。
这也解释了为什么使用值大于 0 的 #InputLevel 指令会导致安装键盘钩子:只有通过钩子,AHK 才能获取有关 InputLevel/SendLevel 的信息,因此在这种情况下必须安装它。
SendInput 回退
由于在键盘钩子激活时使用 SendInput 存在一些缺点,如果 AHK 检测到另一个 AHK 脚本也在使用键盘钩子,它会将 SendInput 切换回 SendEvent ,并设置 SetKeyDelay(-1, 0) 。这可能会比 SendInput 慢一些,也可能慢一些,而且发送的按键可能会与用户输入混杂在一起 。如果仍然需要使用 SendInput ,则必须确保没有其他 AHK 脚本安装键盘钩子(例如使用热字符串、#HotIf 等)。在 AHK v2.0 中,无法检查其他脚本是否激活了钩子,但在 v2.1 中,我们可以使用内置变量 A_KeybdHookInstalled ,如果其他脚本安装了键盘钩子,则该变量的值将为 >1。
如果您有两个 AutoHotkey 脚本正在运行,并且都安装了底层键盘钩子,那么这两个脚本都无法使用 SendInput, 否则就会回退到 Event!
结论
希望这能让大家对 SendInput 的工作原理有所了解。由于其中有很多细微差别,我个人认为,如果不太需要使用 SendInput ,或者存在按键“泄漏”的问题,那么应该使用 SendEvent 并设置合适的按键延迟(例如 SetKeyDelay(-1, 0) )。
如果我遗漏了任何其他会导致安装键盘挂钩的情况,请在下方留言。
结语
接下来我们将分析 GetKeyState 函数,它与 SendInput 无关,但与钩子函数相关。如果您还不了解, GetKeyState 会返回按键的逻辑状态(计算机认为的按键状态)或物理状态(用户是否实际按下按键)。这两种状态可能不同,例如,如果 AHK 使用 Send "{a down}" 发送一个模拟按键,那么逻辑上“a”键会被按下,但实际上它并没有被按下。
首先我们需要明白的是, Windows 本身并不提供获取按键物理状态信息的方法 。这意味着 AHK 无法得知按键的实际物理状态(除非使用像 AutoHotInterception 这样的第三方库,它们会安装驱动程序来访问该信息)。GetKeyState函数的“物理”状态其实并不准确:它实际上是通过跟踪底层键盘钩子捕获的任何人为按键来推断按键的物理状态信息。
如果没有键盘钩子, GetKeyState 会使用 win32 的 GetKeyState 函数查询 AHK 从其输入队列读取的按键的“逻辑”状态,而“物理”状态则使用 win32 的 GetAsyncKeyState 函数查询,该函数返回 Windows 报告的按键当前逻辑状态。通常情况下,这两个状态应该相同。
如果存在键盘钩子,AHK 会监控所有传入的按键(包括物理按键和人工按键),并检查它们是否设置了 LLKHF_INJECTED 标志。该标志会自动为所有通过 Win32 SendInput 或 keybd_event 发送的人工按键设置。如果该标志未设置(即物理按键),AHK 会使用新的物理状态更新一个内部按键状态数组。然后,一旦调用带有“P”标志的 GetKeyState 函数,AHK 会检查该数组中最后记录的按键状态并返回它。如果键盘钩子间歇性地被移除,就会出现问题,这可能导致物理按键被按下,但 AHK 却没有将其记录为物理按键。例如,如果用户在按下“a”键的情况下启动脚本,则逻辑状态会被报告为已按下,但物理状态会被报告为未按下,因为底层键盘钩子没有记录到该按键的按下操作。
在某些特定情况下,AHK 键盘钩子会移除按键上的 LLKHF_INJECTED 标志,但通常情况下不会改变该标志,这意味着你无法使用 SendInput/SendEvent 来模拟“物理”按键。不过,如果你确实想这么做,可以通过编写一个设置了特定标志的自定义 SendInput 函数来欺骗钩子,使其移除该标志。这是未公开的行为,将来可能会改变(尽管可能性不大),所以请记住这一点。
代码:
InstallKeybdHook(1, 1)
KeyArray := [{sc: GetKeySC("a"), event: "Down"}]
SendInputEx(KeyArray)
MsgBox "Logical state: " GetKeyState("a") "`nPhysical state: " GetKeyState("a", "P")
SendInputEx(KeyArray) {
static INPUT_KEYBOARD := 1, KEYEVENTF_KEYUP := 2, KEYEVENTF_SCANCODE := 8, InputSize := 16 + A_PtrSize*3
INPUTS := Buffer(InputSize * KeyArray.Length, 0)
offset := 0
for k, v in KeyArray {
NumPut("int", INPUT_KEYBOARD, "int", 0, "ushort", 0, "ushort", v.sc & 0xFF, "int", (v.event = "Up" ? KEYEVENTF_KEYUP : 0) | KEYEVENTF_SCANCODE | (v.sc >> 8), "int", 0, "int", 0, "int", 0xFFC3D44E, INPUTS, offset)
offset += InputSize
}
DllCall("SendInput", "UInt", KeyArray.Length, "Ptr", INPUTS, "Int", InputSize)
}
输出:
逻辑状态:1
物理状态:1
代码:
InstallKeybdHook(1, 1)
SendInput("{a down}")
MsgBox "Logical state: " GetKeyState("a") "`nPhysical state: " GetKeyState("a", "P")
输出:
逻辑状态:1
物理状态:0

评论(0)