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.