3

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.

BionicCode
  • 1
  • 4
  • 28
  • 44
OoDeLally
  • 552
  • 1
  • 5
  • 21
  • So you're telling your french users that top left key they use to input a is actually really a q? Why would you do that? Not only does this ignore windows design principles but it seems likely to be confusing. – Andy Aug 29 '20 at 15:14
  • 1
    @Andy as I mentioned what matters is the physical location of key on the keyboard. Imagine for example that you implement a piano keyboard. You will map the first key to the first note. The key letter is totally irrelevant. – OoDeLally Aug 29 '20 at 15:32
  • It doesn't seem to make much sense to read the key location instead of the actual key pressed. Layout is totally not relevant if all you do is process pressed keys e.g. hotkeys. When 'Q' quits the application it will be 'Q' no matter the user's layout. You (the application) only need the input 'Q'. The user no matter if he's using French, English, German or Greek keyboard just cares about 'Q'. When the Greek user asks the French which hotkey to press they don't care if they are using different layouts. The French user answers 'Q'. – BionicCode Sep 03 '20 at 17:14
  • He doesn't have to ask for the used layout to transform the key to 'P' (or what ever). This is the reason why there is no API for WPF to support this. What counts is the input and not the location of the physical key.Really doesn't make sense. – BionicCode Sep 03 '20 at 17:15
  • 3
    A real world example of this is HotKeys in a game like Age of Empires. The hotkeys are arrange in a grid in the top left of the keyboard that correspond to a grid in the game. QWER, ASDF, and ZXCV. If you are using a different keyboard, AOE automatically converts the keys for you. So for Dovark, instead of saying "Press Q for blah" it will say "Press ' for blah" (because ' is the top left key on Dvorak). This is INCREDIBLY helpful because you don't need to manually update all the key bindings just because you use a different keyboard layout. – Ben Randall Sep 20 '22 at 15:36

1 Answers1

2

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.

Custom mapping

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.

Scancode Based Solution #1: 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;
}

Scancode based solution #2: 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);
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • 1
    Thank you. This is what I wanted to avoid to do, since I would have to create such mapping for every existant layout. Sure I would of course limit myself to the 5-6 commonly used. But really, isnt there any way to get those damn key scancodes? – OoDeLally Sep 03 '20 at 20:51
  • Of course there is. I have provided a solution to get the scancode of a pressed key. Since scancodes are a low-level concept (hardware <-> OS), you have to use Win32 API to access the Windows message loop. – BionicCode Sep 04 '20 at 02:02
  • The second solution provided by **BionicCode** is what you should use. @BionicCode, could you complement your answer with an example of its use, let's say in the Window.PreviewKeyDown (...) handler? – EldHasp Sep 04 '20 at 05:10
  • @EldHasp This is a Win32 keyboard hook. It's a low level version of the Windows messages, which are converted to WPF keyboard events by the `InputManager` and can be handled by observing the `Keyboard` events. These same generated events are also delegated to `UIElement`. I have updated the code - please check it again. Now the first scancode solution shows how to discriminate the different key events (key up and key down) while the second scancode solution handles key press event. – BionicCode Sep 04 '20 at 10:04
  • 1
    @EldHasp The first scancode solution is the low level equivalent of `Keyboard.KeyUp` and `Keyboard.KeyDown`. The example shows how to register a handler for those events (Window messages). – BionicCode Sep 04 '20 at 10:04
  • @OoDeLally I have update my answer once more to show a more convenient way to handle key up and key down separately (if required). – BionicCode Sep 04 '20 at 10:05
  • @OoDeLally Now that you have the scancode (and its current `Key` value), note that you still need to do some mapping, if you want to allow lookup like getting the current layout's `Key` of a scancode or reverse outside the message handler. – BionicCode Sep 04 '20 at 11:18