In the Outlook add-in project I'm working on there is a feature that if the users enters '@' character in the body of mail editor window a dropdown popup will show with some options.
Because this feature is on Outlook window we implemented the feature by capturing keyboard events1 and waiting for "shift + 2" keycode. We check for "shift + 2" because the captured windows keyboard message contains keycode and not the actual character.
In a beta phase an European client reported that this functionality doesn't work, a quick session with the client revealed he used European keyboard layout, something new to me as I used to US keyboard layout.
The bug
Comparing keyboard message keycode with "shift+2" to handle '@' character ignores the different keyboard layouts, handling only US keyboard layout, European and Asian keyboard layout will fail.
How to handle different keyboard layouts
.NET framework provide a very useful class: InputLanguage with static CurrentInputLanguage and InstalledInputLanguages properties allowing to get the current and all installed culture/keyboard layout pairs.
But by itself the InputLanguage class doesn't provide a way to convert character to keycode and vice versa. To accomplish this we need to resort to Win32 API in user32 dll, ToUnicodeEx and VkKeyScanEx are able to convert keycode to character and character to keycode respectively (sample code can be found below).
The solution
Using InstalledInputLanguages with GetKeyboardShortcutForChar method (see below) to get all the keycodes for all installed culture/keyboard layout pairs and then including the CurrentInputLanguage in comparing the keyboard message keycode to the collection of keycodes collected for specific character.
I have chosen this approach and not converting the keyboard message keycode into character because then I will have to call ToUnicodeEx for each keyboard event, an overhead my approach lacks.
Sample code
[DllImport("user32.dll")] public static extern short VkKeyScanEx(char ch, IntPtr dwhkl); public static void GetKeyboardShortcutForChar(char c, InputLanguage lang, out Keys key, out bool shift) { var keyCode = VkKeyScanEx(c, lang.Handle); key = (Keys) (keyCode & 0xFF); shift = (keyCode & 0x100) == 0x100; } [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int ToUnicodeEx(int wVirtKey, uint wScanCode, byte[] lpKeyState, StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl); public static char? FromKeys(int keys, bool shift, bool capsLock) { var keyStates = new byte[256]; if (shift) keyStates[16] = 0x80; if (capsLock) keyStates[20] = 0x80; var sb = new StringBuilder(10); int ret = User32.ToUnicodeEx(keys, 0, keyStates, sb, sb.Capacity, 0, InputLanguage.CurrentInputLanguage.Handle); return ret == 1 ? (char?)sb[0] : null; }