How to get keyboard scancode of pressed Key? - wpf

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.

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);
}

Related

WPF Ctrl+C Hotkey Registration Overrides/Stops Typical Copy Functionality

I have tried looking into this for a while now and have not been able to come up with an answer that has worked. I am trying to register my WPF C# application to listen for Ctrl+C from anywhere while the application is running. The code below is part of my application and does trigger when the user presses Ctrl+C, however, the contents of the Clipboard are stale (the contents are from a previous copy before the application was running). The AnimateWindow function is being called so I know my hotkey registration is working.
So, my issue is that it appears registering for the hotkey below is overriding the copy functionality. I would like to keep this functionality intact as it is part of how my application works. I need to take what was copied and use that data for a database query and to fill a TextBox.
This is just a snippet of the entire project, please let me know if additional example code is needed to help answer. Thank you!
[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// CTRL + C Hotkey
private const int COPY_ID = 9001;
private const uint MOD_CONTROL = 0x0002; // Modifier: Control
private const uint VK_C = 0x43; // 'C' key
/* Registering the hotkeys to be monitored */
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_windowHandle = new WindowInteropHelper(this).Handle;
_source = HwndSource.FromHwnd(_windowHandle);
_source.AddHook(HwndHook);
RegisterHotKey(_windowHandle, COPY_ID, MOD_CONTROL, VK_C); // 'C' key
}
/* The hook for pressing a hotkey */
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_HOTKEY = 0x0312;
switch (msg)
{
case WM_HOTKEY:
switch (wParam.ToInt32())
{
case COPY_ID:
AnimateWindow();
break;
}
}
}

How to stop FilterMessage to proceed further?

I am looking forward to find a way to stop my message to proceed further or in other words to make it handled.
Say, cursor location is at textbox and I am scanning a barcode "#Save#". I recognized using WindowProc that this is barcode text and I handled Save text (by saving my form) but now I don't want to write this text("#Save#") in textbox.
So in short sentence I am looking for property or method to say a window this text (input) is handled, don't do any further action.
HwndSource source = (HwndSource)PresentationSource.FromDependencyObject(this);
source.AddHook(WindowProc);
private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if(device == barcode)
{
......
.....
//Did some action
//tried to stop msg to follow further in loop, but not working
handled ==true;
}
return IntPtr.Zero;
}
Is that a typo, extra equals sign? Your code reads:
handled ==true;
that's not an assignment, although, I don't think that should even compile...
did you mean:
handled = true;

How to change which item is “outlined” in a ListBox without changing the selection?

How do you change which item is “outlined” in a ListBox? See the following screenshot for clarification:
Background: I want to have a standard multi-select listbox that works normally. Unfortunately, the Windows Forms ListBox (with SelectionMode.MultiExtended) is not fully functional. The missing functionality is that it doesn’t let you select a disjoint set of items by using Ctrl+Arrow keys and Ctrl+Space. (Try it in Windows Explorer to see how it’s supposed to work.) I am trying to subclass ListBox and add this missing functionality. To this end, I intend to respond to Ctrl+Arrow keys in the OnKeyDown protected method, but in order to do so, I need to be able to move the outline without changing the set of selected items. How do I do that?
You can do this by sending the LB_SETCARETINDEX message to the list box:
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern uint SendMessage(IntPtr hWnd, uint msg, uint wParam, uint lParam);
public const uint LB_SETCARETINDEX = 0x019E;
public const uint LB_GETCARETINDEX = 0x019F;
[...]
public int OutlineIndex
{
get
{
return (int) WinAPI.SendMessage(Handle, WinAPI.LB_GETCARETINDEX, 0, 0);
}
set
{
if (value < 0 || value >= Items.Count)
throw new ArgumentException("OutlineIndex cannot be negative or greater than the size of the collection.", "value");
WinAPI.SendMessage(Handle, WinAPI.LB_SETCARETINDEX, (uint) value, 0);
}
}

How to sort Windows by z-index?

If I'm enumerating windows in Application.Current.Windows, how can I tell, for any two windows, which one is "closer" (i.e. has greater z-index)?
Or, to say the same in other words, how can I sort those windows by z-index?
You cannot get a Window's Z Order information from WPF so you must resort to Win32.
Something like this ought to do the trick:
var topToBottom = SortWindowsTopToBottom(Application.Current.Windows.OfType<Window>());
...
public IEnumerable<Window> SortWindowsTopToBottom(IEnumerable<Window> unsorted)
{
var byHandle = unsorted.ToDictionary(win =>
((HwndSource)PresentationSource.FromVisual(win)).Handle);
for(IntPtr hWnd = GetTopWindow(IntPtr.Zero); hWnd!=IntPtr.Zero; hWnd = GetWindow(hWnd, GW_HWNDNEXT)
if(byHandle.ContainsKey(hWnd))
yield return byHandle[hWnd];
}
const uint GW_HWNDNEXT = 2;
[DllImport("User32")] static extern IntPtr GetTopWindow(IntPtr hWnd);
[DllImport("User32")] static extern IntPtr GetWindow(IntPtr hWnd, uint wCmd);
The way this works is:
It uses a dictionary to index the given windows by window handle, using the fact that in the Windows implementation of WPF, Window's PresentationSource is always HwndSource.
It uses Win32 to scan all unparented windows top to bottom to find the proper order.
Ah this was a really fun one:
[DllImport("user32.dll")]
static extern IntPtr GetActiveWindow();
public static Window ActiveWindow
{
get
{
return HwndSource.FromHwnd(GetActiveWindow()).RootVisual as Window;
}
}
It will give you the active window in your app (which is usually the foremost).

How can I invoke the dialog to set printer options manually?

I'm using WPF and need to let users set some print related options like printer and printer properties (e.g. papertray, landscape/portrait, duplex, etc). I'm aware of the PrintDialog class to get a PrintQueue and PrintTicket object. However I need to create I custom solution and can not show the PrintDialog.
I manage to get the available PrintQueue objects and let users select a printer. I'm struggling with the printer properties.
My question is: how can I show the dialog in which a user can set the printer properties for the selected PrintQueue (the dialog that is shown when a user clicks on the Properties button in the WPF PrintDialog).
The following code was found here (minus the Window_Loaded event). I tested it and it seems to work like a charm. Obviously you'll have to set the printer name in the PrinterSettings object before displaying the dialog.
Hope this works for you:
[DllImport("kernel32.dll")]
static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll")]
static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll")]
static extern bool GlobalFree(IntPtr hMem);
[DllImport("winspool.Drv", EntryPoint = "DocumentPropertiesW", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter, [MarshalAs(UnmanagedType.LPWStr)] string pDeviceName, IntPtr pDevModeOutput, IntPtr pDevModeInput, int fMode);
private const Int32 DM_OUT_BUFFER = 14;
public void OpenPrinterPropertiesDialog(PrinterSettings printerSettings, System.IntPtr pHandle) {
IntPtr hDevMode = printerSettings.GetHdevmode();
IntPtr pDevMode = GlobalLock(hDevMode);
Int32 fMode = 0;
int sizeNeeded = DocumentProperties(pHandle, IntPtr.Zero, printerSettings.PrinterName, pDevMode, pDevMode, fMode);
IntPtr devModeData = Marshal.AllocHGlobal(sizeNeeded);
fMode = DM_OUT_BUFFER;
DocumentProperties(pHandle, IntPtr.Zero, printerSettings.PrinterName, devModeData, pDevMode, fMode);
GlobalUnlock(hDevMode);
printerSettings.SetHdevmode(devModeData);
printerSettings.DefaultPageSettings.SetHdevmode(devModeData);
GlobalFree(hDevMode);
Marshal.FreeHGlobal(devModeData);
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
OpenPrinterPropertiesDialog(new PrinterSettings(), new WindowInteropHelper(this).Handle);
}
If you target x86 compilation and run from a x64 machine, the code from Pwninstein will not work: when allocating devModeData, DocumentPropreties will always fail and returns a sizeNeeded of -1, with a LastError code 13.
To solve the problem, either make sure you target AnyCPU or just change the call to DocumentPropreties to the following:
int sizeNeeded = DocumentProperties(pHandle,
IntPtr.Zero,
printerSettings.PrinterName,
IntPtr.Zero, // This solves it
pDevMode,
fMode);
Using IntPtr.Zero instead of a proper pointer to a DevMode structure looks wrong, but that first call to DocumentProperties does not attempt to modify the memory at that position. The only data returned by the call is the memory size needed to store the device mode data that represents the internal parameters of the print driver.
Reference:
PInvoke.Net page on DocumentProperties

Resources