I have a WPF application that is using the Win32 RegisterHotKey method and an HwndSource.AddHook message handler.
I am trying to add functionality to not handle the hotkey if I don't take any action by using the HwndSourceHook's handled parameter.
However, setting handled to false or true does not change the behavior, which always intercepts the registered hotkeys and does not pass them on to the applications they were pressed in.
I have another application that's using the Windows Forms Application.AddMessageFilter handler, and in that case it's working fine with the same hotkeys when returning false to indicate the message is unhandled.
The source can be found here: https://github.com/trevorlloydelliott/eve-switcher/blob/master/HotkeyHandler.cs
I have gone so far as to simplify my logic like so:
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_HOTKEY = 0x0312;
if (msg == WM_HOTKEY)
{
handled = false;
return IntPtr.Zero;
}
}
I have also tried not using a Window and an HwndSource hook, and instead using the approach of the ComponentDispatcher.ThreadFilterMessage
event to intercept all WM_HOTKEYs and set handled to false like so:
private void ComponentDispatcher_ThreadFilterMessage(ref MSG msg, ref bool handled)
{
const int WM_HOTKEY = 0x0312;
if (msg .message == WM_HOTKEY)
{
handled = false;
}
}
This also fails to pass the hotkey on to the underlying application. In my tests, I was trying the Tab hotkey with Notepad open. My app would register the Tab hotkey with RegisterHotKey, then set handled to false, but the hotkey never reaches Notepad. As soon as I close my app, it reaches Notepad.
I am using RegisterHotKey the same way as in my other Windows Forms app.
Win32 RegisterHotKey()
defines a system-wide hotkey. Hotkeys are global and are processed before regular keyboard input processing which means that if you register a hotkey successfully, pressing that key will result you in getting hotkey message instead of normal keyboard input messages. If a hotkey press event has occurred, then other applications will not see that key press. That's by design.
RegisterHotKey
is the proper way to use global hotkeys. The whole point is that by specifying a hotkey for an application, you ensure that the key is unique among all applications running on you system and that your application will always receive the keypress event for that key.
Using a simple key (e.g. Tab
) as global hotkey poses a problem of conflicting with local hotkeys of an application that has a focus. Therefore, global hotkeys should be such that the user can avoid collisions with applications he commonly uses.
However, there is another way to handle hotkeys in WPF. You can use low level WH_KEYBOARD_LL
keyboard hook by installing it with SetWindowsHookEx
method call. It enables you to monitor keyboard input events in the input queue.
Here is the class utilizing the hook:
using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Input;
namespace KeyboardHooks
{
public delegate void HotkeyPressedEventHandler(object sender, HotkeyPressedEventArgs e);
public class HotkeyPressedEventArgs : EventArgs
{
public Key Key { get; }
public ModifierKeys Modifiers { get; }
public bool Handled { get; set; }
public HotkeyPressedEventArgs(Key key, ModifierKeys modifiers)
{
Key = key;
Modifiers = modifiers;
}
}
public class HotkeyManager
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
[StructLayout(LayoutKind.Sequential)]
private class KeyboardHookStruct
{
/// <summary>
/// Specifies a virtual-key code. The code must be a value in the range 1 to 254.
/// </summary>
public int vkCode;
/// <summary>
/// Specifies a hardware scan code for the key.
/// </summary>
public int scanCode;
/// <summary>
/// Specifies the extended-key flag, event-injected flag, context code, and transition-state flag.
/// </summary>
public int flags;
/// <summary>
/// Specifies the time stamp for this message.
/// </summary>
public int time;
/// <summary>
/// Specifies extra information associated with the message.
/// </summary>
public int dwExtraInfo;
}
private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
private static extern int UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern int CallNextHookEx(
int idHook,
int nCode,
int wParam,
IntPtr lParam);
private int keyboardHook = 0;
public HotkeyManager()
{
}
public event HotkeyPressedEventHandler HotkeyPressed;
public void Start()
{
// install Keyboard hook only if it is not installed and must be installed
if (keyboardHook == 0)
{
// Create an instance of HookProc.
keyboardHook = SetWindowsHookEx(
WH_KEYBOARD_LL,
KeyboardHookProc,
Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]),
0);
// If SetWindowsHookEx fails.
if (keyboardHook == 0)
{
// Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set.
int errorCode = Marshal.GetLastWin32Error();
// do cleanup
Stop(false);
// Initializes and throws a new instance of the Win32Exception class with the specified error.
throw new Win32Exception(errorCode);
}
}
}
public void Stop(bool throwExceptions)
{
// if keyboard hook set and must be uninstalled
if (keyboardHook != 0)
{
// uninstall hook
int retKeyboard = UnhookWindowsHookEx(keyboardHook);
// reset invalid handle
keyboardHook = 0;
// if failed and exception must be thrown
if (retKeyboard == 0 && throwExceptions)
{
//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set.
int errorCode = Marshal.GetLastWin32Error();
//Initializes and throws a new instance of the Win32Exception class with the specified error.
throw new Win32Exception(errorCode);
}
}
}
private int KeyboardHookProc(int nCode, int wParam, IntPtr lParam)
{
bool handled = false;
if (nCode >= 0 && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
{
if (HotkeyPressed != null)
{
// read structure KeyboardHookStruct at lParam
KeyboardHookStruct khStruct =
(KeyboardHookStruct) Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));
if (khStruct != null)
{
Key key = KeyInterop.KeyFromVirtualKey(khStruct.vkCode);
HotkeyPressedEventArgs args = new HotkeyPressedEventArgs(key, Keyboard.Modifiers);
HotkeyPressed.Invoke(this, args);
handled = args.Handled;
}
}
}
// if event handled in application do not handoff to other listeners
if (handled)
{
return 1;
}
return CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}
}
}
Example of use:
HotkeyManager hotkeyManager = new HotkeyManager();
hotkeyManager.HotkeyPressed += HotkeyManagerOnHotkeyPressed;
hotkeyManager.Start();
private void HotkeyManagerOnHotkeyPressed(object sender, HotkeyPressedEventArgs e)
{
if (e.Key == Key.Tab && e.Modifiers == ModifierKeys.None)
{
Console.WriteLine("Tab pressed!");
//e.Handled = true;
}
}
As regards the difference between WinForm's PreFilterMessage
and WPF's HwndSourceHook
, I guess that the first is called before the message is passed to any event handler while the second is itself an event handler. So they behave differently.
Bonus. There is another way to pass unhandled hotkey presses further to other applications, but it is less reliable. In case the global hotkey is unhandled you can unregister it, send system keyboard event with this key and then register the hotkey again. Basically, you do the following:
if (!handled)
{
UnregisterHotkey(hotkey.Gesture);
KeyboardMessage.Send(hotkey.Gesture.Key);
RegisterHotkey(hotkey.Gesture);
}
KeyboardMessage
class you can find here.