Introduction
There are two common ways to implement global hotkeys, each comes with its own set of pros and cons. It's important to understand what your code is doing when your code climbs out of your program's context and into the user's desktop.
Background
A global hotkey refers to a system-wide key press event, i.e., a case in which your code reacts to a key press regardless of where keyboard focus is.
The Windows Desktop API exposes a RegisterHotkey function, which you should prefer over the alternative method of using a low-level keyboard hook.
RegisterHotkey
, however, has three significant limitations:
- When you register a key, you basically override its original functionality. For example, if you override the F5 key, your users will be irritated when the key will no longer refresh web pages on their browser.
- After point #1, it goes without saying that no two programs can have the same global hotkey combination. If you try to register a hotkey that already belongs to another running program, you'll run into a WinAPI error.
- Some combinations cannot be registered at all (e.g. Ctrl+Alt+Del)
A low-level keyboard hook defeats those limitations, but has two consequences that you need to keep in mind:
- Your code is likely to be labeled as spyware (for key logging) by antiviruses (or people who decompile your code and see your use of
SetWindowsHookEx
) - It has the potential of slowing down keyboard input processing for the entire system (this will be explained in detail below)
There's very little we can do with the first limitation, but the latter can be prevented if you understand what you're doing.
First, let's see what happens when you hit a key on your keyboard:
(a similar flow is applicable for the case of KEYUP
)
From the chart above, you learn that your global keyboard hooks process keyboard input before the keyboard input reaches its final destination (the focused application).
The hooks run synchronously. If a hook runs slowly, then input will be delayed and irritate the user.
Assuming you do not intend to prevent any keys from reaching other hooks and programs, the intuitive solution to this problem is to process the input in a separate thread.
This is the LowLevelKeyboardProc
code taken from the open source NonInvasiveKeyboardHook library:
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var vkCode = Marshal.ReadInt32(lParam);
ThreadPool.QueueUserWorkItem
(this.HandleSingleKeyboardInput, new KeyboardParams(wParam, vkCode));
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
A naive approach could've been somewhat reverting the order, such as:
var returnValue = CallNextHookEx(_hookId, nCode, wParam, lParam);
ProcessInputInSameThread();
return returnValue;
While it is true that the early invocation of CallNextHookEx
will resume the current key's flow down the message queue, the next key press will not be processed until the blocking code finishes and the function returns.
Using the Code
So if it's global hotkeys you're after and the standard WinAPI hotkey system does not meet your needs, then the open source NonInvasiveKeyboardHook library is the answer.
It exposes a KeyboardHookManager
, which exposes the functionality of registering specific key combinations (i.e., you cannot use this for spyware).
* It is recommended that you only use one KeyboardHookManager
instance.
Basics
Instantiate and start a KeyboardHookManager
:
var keyboardHookManager = new KeyboardHookManager();
keyboardHookManager.Start();
Stopping it (can be resumed by calling .Start
again later).
keyboardHookManager.Stop();
Register hotkeys
Register a hotkey
without modifiers:
keyboardHookManager.RegisterHotkey(0x60, () =>
{
Debug.WriteLine("NumPad0 detected");
});
Register a hotkey
with a single modifier:
keyboardHookManager.RegisterHotkey(NonInvasiveKeyboardHookLibrary.ModifierKeys.Control, 0x60, () =>
{
Debug.WriteLine("Ctrl+NumPad0 detected");
});
Register a hotkey
with multiple modifiers:
keyboardHookManager.RegisterHotkey(NonInvasiveKeyboardHookLibrary.ModifierKeys.Control |
NonInvasiveKeyboardHookLibrary.ModifierKeys.Alt, 0x60, () =>
{
Debug.WriteLine("Ctrl+Alt+NumPad0 detected");
});
keyboardHookManager.RegisterHotkey(new[]
{NonInvasiveKeyboardHookLibrary.ModifierKeys.Control,
NonInvasiveKeyboardHookLibrary.ModifierKeys.Alt}, 0x60, () =>
{
Debug.WriteLine("Ctrl+Alt+NumPad0 detected");
});
Unregister Hotkeys
A hotkey
can be unregistered based on its unique key combination, or using the globally unique identifier returned by RegisterHotKey
.
keyboardHookManager.RegisterHotkey(0x60, () => { Debug.WriteLine("NumPad0 detected"); });
keyboardHookManager.UnregisterHotkey(0x60);
OR
var hotkeyId = keyboardHookManager.RegisterHotkey(0x60, () => { Debug.WriteLine("NumPad0 detected"); });
keyboardHookManager.UnregisterHotkey(hotkeyId);
It is also possible to unregister all hotkey
s:
keyboardHookManager.UnregisterAll();