Reputation: 609
I make an WPF app where hotkeys are chosen for their physical position on the keyboard, rather than for what letter they represent. Because different users use different layout (qwerty, azerty, etc), my app must be smart enough to be layout-agnostic.
I designated QWERTY as the layout of reference.
So for example a French user uses AZERTY. Therefore what the Q
key is in QWERTY is A
in AZERTY.
So in order to be agnostic I imagine I should do the following operation:
// Represents, in QWERTY, the top-left letter key.
Key myKey = Key.Q;
// ScanCode represent the physical key on the keyboard.
var scanCode = GetScanCodeFromKey(myKey, new CultureInfo("en-US"));
// Would be "A" if the user uses AZERTY.
Key agnosticKey = getKeyFromScanCode(scanCode, CultureInfo.CurrentCulture);
This seems feasible, but I cannot find a function that does GetScanCodeFromKey
and getKeyFromScanCode
.
The only relevant post I've found is Is it possible to create a keyboard layout that is identical to the keyboard used?
but unfortunately it seems to be made for win32, and not WPF. MapVirtualKeyEx
is not accessible from WPF.
Upvotes: 3
Views: 5806
Reputation: 29008
I have to remark that writing high level code (a WPF appliacation) that has to depend on low level concepts (hardware/machine details) is never a good idea. High level applications should depend on high level abstractions (input) of the low level data output. You always want to add an extra abstraction layer/interface between high level and low level or software and hardware.
You can of course do the mapping yourself. Using Win32 API allows you to get the current active input language and deal with the low level concept of a keyboard layout.
First chose the base layout of your application e.g. en-GB. Then create a lookup table based on the supported layout languages.
Each entry of this table is another table used as conversion table: from current layout to application's internal base layout.
To simplify lookup table creation you can create a minimal table by focusing on the supported keys only.
You can create the conversion table from a file(s) e.g XML or JSON to the lookup table. This provides simple extensibility to an already deployed application.
private Dictionary<int, Dictionary<Key, Key>> LanguageLayouts { get; set; }
public MainWindow()
{
InitializeComponent();
this.LanguageLayouts = new Dictionary<int, Dictionary<Key, Key>>
{
{
CultureInfo.GetCultureInfo("fr-FR").LCID, new Dictionary<Key, Key>
{
{Key.A, Key.Q},
{Key.Z, Key.W},
{Key.E, Key.E},
{Key.R, Key.R},
{Key.T, Key.T},
{Key.Y, Key.Y}
}
}
};
}
private void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
int lcid = GetCurrentKeyboardLayout().LCID;
if (this.LanguageLayouts.TryGetValue(lcid, out Dictionary<Key, Key> currentLayoutMap))
{
if (currentLayoutMap.TryGetValue(e.Key, out Key convertedKey))
{
HandlePressedKey(convertedKey);
}
}
}
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, IntPtr proccess);
[DllImport("user32.dll")] static extern IntPtr GetKeyboardLayout(uint thread);
public CultureInfo GetCurrentKeyboardLayout()
{
IntPtr foregroundWindow = GetForegroundWindow();
uint foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
int keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
return new CultureInfo(keyboardLayout);
}
Since keyboard scancodes are a low-level concept (hardware <-> OS), you have to use Win32 API to hook onto the Windows message loop and filter keystroke messages.
Windows keyboard drivers usually use the scancode set 1 code table to convert scancodes to actual key values.
Windows API scancodes are decimal based i.e presented as integer, while codes in reference tables are documented hex based.
System.Windows.Interop.HwndSource
(Recommended)HwndSource
is a WPF ready wrapper that provides access to the Win32 window handle.
Note that HwndSource
implements IDisposable
.
You can listen to the Windows message loop by registering a callback of type HwndSourceHook
by calling HwndSource.AddHook
.
Also don't forget to unregister the callback by calling HwndSource.RemoveHook
.
To handle keystroke messages, you have to monitor the loop for WM_KEYUP
and WM_KEYDOWN
messages. See Keyboard Input Notifications for a list of keyboard input messages.
See System-Defined Messages for an overview of all system messages e.g., Clipborad messages.
private async void OnLoaded(object sender, RoutedEventArgs e)
{
var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
if (hwndSource != null)
{
hwndSource.AddHook(MainWindow.OnWindowsMessageReceived);
}
// Define filter constants (see docs for message codes)
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private static IntPtr OnWindowsMessageReceived(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
// Equivalent of Keyboard.KeyDown event (or UIElement.KeyDown)
case MainWindow.WM_KEYDOWN:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html)
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
// Equivalent of Keyboard.KeyUp event (or UIElement.KeyUp)
case MainWindow.WM_KEYUP:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
}
return IntPtr.Zero;
}
SetWindowsHookExA
private void Initialize()
{
// Keep a strong reference to the delegate.
// Otherwise it will get garbage collected (Win32 API won't keep a reference to the delegate).
this.CallbackDelegate = OnKeyPressed;
// Argument '2' (WH_KEYBOARD) defines keystroke message monitoring (message filter).
// Argument 'OnKeyPressed' defines the callback.
SetWindowsHookEx(2, this.CallbackDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId());
}
protected delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
private HookProc CallbackDelegate { get; set; }
[DllImport("user32.dll")]
protected static extern IntPtr SetWindowsHookEx(int code, HookProc func, IntPtr hInstance, int threadID);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr OnKeyPressed(int code, IntPtr wParam, IntPtr lParam)
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
// TODO::Handle scancode
// Let other callbacks handle the message too
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}
Upvotes: 2