解释 SetWindowsHookEx / WH_KEYBOARD_LL 的正确逻辑

Posted

技术标签:

【中文标题】解释 SetWindowsHookEx / WH_KEYBOARD_LL 的正确逻辑【英文标题】:Correct logic for interpreting SetWindowsHookEx / WH_KEYBOARD_LL 【发布时间】:2013-07-27 22:22:11 【问题描述】:

将来自SetWindowsHookExWH_KEYBOARD_LL 的消息转换为有用的按键表示的正确方法是什么?

我知道这很可能涉及了解本地键盘的映射。 (注意:为简单起见,我只考虑按下键的时间,而不是释放键的时间)

一般来说似乎有三种情况:

按下特殊键 (Ctrl/Escape/Shift/Alt) 按下标准键(A-Z0-9 等...请注意,aA 都读作 A) 一些难以定义的情况,例如数字键盘和 F1-F12

可以根据需要处理特殊键,System.Windows.Forms.Keys中有一些有用的查找

但如果我要(在英式英语键盘上)加一个感叹号,它会被检测为 Shift-Down1Shift up

由于在代码通过键盘“转换”层后,我的钩子级别太低(我相信)无法获取代码,我很好奇我将如何正确解释它们。

至于我为什么要这样做。它一开始是为了给我编写的媒体播放器提供方便的快捷方式,它可以在任何地方工作,甚至在游戏内部(有些游戏似乎会拦截击键并阻止它们传播到操作系统)。实际上,只要是我在使用该应用程序(仅限可能的用户),我实际上已经得到了足够的东西,但我的好奇心被激起了,如果我愿意,我该如何进一步发展。

【问题讨论】:

查看ToAsciiEx函数。 RegisterHotKey 会提供您正在寻找的功能,还是不能很好地处理游戏/DirectInput? 感谢@Tim 的建议,这实际上是我开始的地方。不幸的是,从那个页面RegisterHotKey fails if the keystrokes specified for the hot key have already been registered by another hot key. 和一些游戏似乎使用了这种技术(或者至少使用了类似的方法来阻止它)。 IIRC Valve 的 Source 引擎就是这样做的。我同意这会让生活更轻松。 @JonathanPotter 谢谢,看起来很有希望,我试试看。 根据语言(及其对死键的使用),这可能非常复杂。 ToAsciiEx 适用于简单的情况,但如果涉及死键或 IME,事情很快就会变得非常复杂。 Michael Kaplan's blog 有大量关于键盘布局的文章;任何涉及需要产生最终击键的 IME(日文、中文、韩文等)几乎都是不可能的。 【参考方案1】:

根据语言(以及死键的使用),这可能非常复杂。 ToAsciiEx 适用于简单的情况,但如果涉及死键或 IME,事情很快就会变得非常复杂。

Michael Kaplan's blog 有大量关于键盘布局和Keyboard Layout Creator 的文章,这是一种用于创建键盘布局(将键映射到字符的最简单方法)的工具。当一组击键映射到单个字符时,使用键盘布局创建器。在这些情况下,可以进行映射,但是当需要多个键来确定一个字符时,就变得有些棘手了。

日语、中文或韩语等语言没有从击键到字符的单一映射。对于这些语言,需要输入法编辑器;这些通常使用Text Services Framework,这是输入法编辑器(和其他文本服务,如手写识别和语音识别)用来与应用程序通信以确定从击键到字符的映射的一组广泛的 API。因为使用 IME 从击键到字符的映射是由代码定义的,所以低级键盘挂钩通常根本无法执行这种映射。

【讨论】:

感谢 Eric,这非常有帮助,同时也满足了我的好奇心。【参考方案2】:

Eric 完全正确,没有简单的方法可以做到这一点。我在下面附上了我尽力而为的代码,它可以让我完成 95% 的工作(使用 ToUnicodeEx)。

它不能正确捕捉特殊(IME/死键)字符(AltGr-4 用于英国-英语键盘上的)。但是,preserve the state of the kernel-mode keyboard buffer 确实如此(因此它不会干扰输入高级组合键)。

请注意,此代码不是生产就绪的,需要适当的错误处理等。它也仅在少数键盘布局上进行了测试。 YMMV。

Public Declare Function UnhookWindowsHookEx Lib "user32" (
  ByVal hHook As Integer) As Integer

Public Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" (
  ByVal idHook As Integer,
  ByVal lpfn As KeyboardHookDelegate,
  ByVal hmod As Integer,
  ByVal dwThreadId As Integer) As Integer

Private Declare Function GetAsyncKeyState Lib "user32" (
  ByVal vKey As Integer) As Integer

Private Declare Function CallNextHookEx Lib "user32" (
  ByVal hHook As Integer,
  ByVal nCode As Integer,
  ByVal wParam As Integer,
  ByVal lParam As KBDLLHOOKSTRUCT) As Integer

Public Structure KBDLLHOOKSTRUCT
    Public vkCode As Integer
    Public scanCode As Integer
    Public flags As Integer
    Public time As Integer
    Public dwExtraInfo As Integer
End Structure

' Low-Level Keyboard Constants
Private Const HC_ACTION As Integer = 0
Private Const LLKHF_EXTENDED As Integer = &H1
Private Const LLKHF_INJECTED As Integer = &H10
Private Const LLKHF_ALTDOWN As Integer = &H20
Private Const LLKHF_UP As Integer = &H80

Private Const WH_KEYBOARD_LL As Integer = 13&
Public KeyboardHandle As Integer

Public Declare Function ToUnicodeEx Lib "user32" (wVirtKey As UInteger, wScanCode As UInteger, lpKeyState As Byte(), <Out()> ByVal lpChar As System.Text.StringBuilder, cchBuff As Integer, wFlags As UInteger, dwhkl As IntPtr) As Integer
Public Declare Function GetKeyboardState Lib "user32" (lpKeyState As Byte()) As Boolean
Public Declare Function GetKeyState Lib "user32" (keyCode As Integer) As Short


Private Function ConvertToUnicode(lParam As KBDLLHOOKSTRUCT) As String
    Select Case lParam.vkCode
        Case 8
            Return "Backspace"
        Case 9
            Return "Tab"
        Case Else

            Dim SB As New System.Text.StringBuilder()
            Dim KeyState(255) As Byte

            'The output from this isn't actually used but it forces the Api
            'to evaluate the modifiers for the key code
            Dim ModifierState As Integer
            GetKeyState(ModifierState)

            Dim KeyStateStatus As Boolean = GetKeyboardState(KeyState)

            If Not KeyStateStatus Then
                Return ""
            End If

            ToUnicodeEx(CUInt(lParam.vkCode),
                        CUInt(lParam.scanCode),
                        KeyState, SB, SB.Capacity, 0,
                        InputLanguage.CurrentInputLanguage.Handle)
            Return SB.ToString()
    End Select
End Function

Public Function KeyboardCallback(ByVal Code As Integer,
                                 ByVal wParam As Integer,
                                 ByRef lParam As KBDLLHOOKSTRUCT) As Integer
    Try
        Dim Key As String = Nothing
        If (lParam.flags And LLKHF_EXTENDED) = 0 Then
            If (lParam.flags And LLKHF_UP) = 0 Then
                Key = ConvertToUnicode(lParam)
            End If
        Else
            Dim KeyCode = DirectCast(lParam.vkCode, System.Windows.Forms.Keys)
            If KeyCode <> System.Windows.Forms.Keys.RShiftKey And
                KeyCode <> System.Windows.Forms.Keys.LShiftKey Then
                If (lParam.flags And LLKHF_UP) = 0 Then
                    'Special Key pressed
                    Key = "" & KeyCode.ToString & "+"
                Else
                    'Special Key released
                    Key = "" & KeyCode.ToString & "-"
                End If
            End If
        End If

        If Key IsNot Nothing Then
            Debug.WriteLine(Key)
        End If

    Catch ex As Exception
        'Do something!    
    End Try

    Return CallNextHookEx(KeyboardHandle, Code, wParam, lParam)
End Function

【讨论】:

以上是关于解释 SetWindowsHookEx / WH_KEYBOARD_LL 的正确逻辑的主要内容,如果未能解决你的问题,请参考以下文章

SetWindowsHookEx WH_KEYBOARD_LL 问题锁屏

SetWindowsHookEx 其他进程的 记录

SetWindowsHookEx 与 Qt4 中的 WH_JOURNALRECORD 挂钩

带有 WH_KEYBOARD 的 SetWindowsHookEx 卡在循环/队列中

SetWindowsHookEx WH_MOUSE 在 Win7 中的断点处冻结

SetWindowsHookEx WH_KEYBOARD_LL 在右移时没有响应