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.
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.
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