Correct logic for interpreting SetWindowsHookEx /

2019-05-30 06:32发布

问题:

What's the correct way to convert a message from SetWindowsHookEx with WH_KEYBOARD_LL into a useful representation of the key pressed?

I'm aware this will most likely involve understanding the mappings of the local keyboard. (NB: I'm only considering when a key is pressed, not when it's released for simplicity)

Broadly speaking there seem to be three scenarios:

  • Special key pressed (Ctrl/Escape/Shift/Alt)
  • Standard key pressed (A-Z, 0-9, etc... Note that a and A both read as A)
  • Some hard to define cases like number pad and F1-F12

Special keys can be dealt with as required and there are some useful lookups in System.Windows.Forms.Keys

but if I were to do (on a UK-English Keyboard) an exclamation mark, it's detected as Shift-Down1Shift up

Since I'm hooking at too low a level (I believe) to get the codes after they've been through the keyboard "conversion" layer, I'm curious how I'd go about correctly interpreting them.

As to why I'm doing this. It started off as a way to give convenient shortcuts to a media player I've written that will work anywhere, even inside games (some games seem to intercept key strokes and prevent them propagating to the OS). I've actually got enough for what I need, as long as it's me using the app (only likely user) but my curiosity is piqued as to how I could take this further if I wanted to.

回答1:

Depending on the language (and its use of dead keys), this can be extremely complicated. ToAsciiEx works for the simple situations, but if there are dead keys involved, or an IME, things get very complicated very quickly.

Michael Kaplan's blog has an extensive series of articles talking about keyboard layouts and the Keyboard Layout Creator, which is a tool for creating keyboard layouts (the simplest way of mapping keys to characters). The Keyboard Layout Creator is used when a single set of keystrokes maps to a single character. In these cases, a mapping can be done, but it becomes somewhat tricky to do when more than one key is needed to determine a character.

Languages such as Japanese, Chinese, or Korean do not have a single mapping from keystrokes to characters. For these languages, an Input Method Editor is needed; these typically use Text Services Framework, which is an extensive set of APIs that input method editors (and other text services, such as handwriting recognition and speech recognition) use to communicate with the application to determine a mapping from keystrokes to characters. Because the mapping from keystrokes to characters with an IME is defined by code, the low-level keyboard hook often cannot perform this mapping at all.



回答2:

Eric's absolutely correct that there is not a simple way to do this. I'm attaching my best-effort code below which gets me 95% of the way there (Using ToUnicodeEx).

It doesn't catch special (IME/Dead key) characters correctly (AltGr-4 for a on a UK-English Keyboard). It does, however, preserve the state of the kernel-mode keyboard buffer (so it doesn't interfere with advanced key combos being typed).

Note that this code is not production-ready and needs proper error handling, etc, etc. It's also only tested on a few keyboard layouts. 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