How to use Global HotKeys on Windows
TLDR;
To receive global HotKeys in Windows, your application needs to process Window Messages and therefore will need a message loop. To do that, I've written a very small library that you can use or take a look at here. Works for applicationTypes incl. AvaloniaUI, Console, WinForms and Wpf.
Who needs Global HotKeys?
In my last blog post I started writing a small Example Application named 'Timeboxing'. I wanted to extend it now with a couple of features, one of which is starting and stopping the timer with a HotKey.
Handling keyboard input is easy in any application, but only if the application has focus. If it has not, you usually need to give it focus first.
Getting Focus is usually not handled by the application itself, but by the Window Manager of the platform, so if we want to add that feature, we will need to make use of some platform specific APIs. As my knowledge about other Desktop APIs except Windows is limited at best, I will only touch on windows.
If we take getting focus into account for starting an action, the following options come to mind:
- ALT+TAB + Keyboard Input
- Win+[0-9] + Keyboard Input
- Create an AutoHotKey script
- Use an existing HotKey library
- Start a second application instance that notifies the first and then exits
- Use the Windows API to register a HotKey the proper way
Given those options, you can probably already guess what I have opted for, but let's try to keep the facade where I first list perfectly good options, before I find somewhat contrived reasons that help me rationalize my decision to write some code use the best approach.
ALT-TAB
The most prominent UX concept of Windows for switching between applications is of course ALT-Tabbing between applications. It allows you to quickly switch between the most recent applications. It works well, if you are continuously switching between two or so applications.
So the idea is, you are working on a thing, you hit ALT+TAB, you hit an application wide HotKey to say, start the timer and then you hit ALT+TAB again and continue working.
The thing is, Timeboxing is not an application that you have in the foreground, you probably don't even have it as a recent application. That's because it's basically a countdown timer that you start and forget about for almost half an hour before you come back to it.
So it's unlikely to be your most recent application, it's just somewhere in that deep stack of IDEs, text editors, console windows and the many, many separate instances of browser windows that for whatever reason must not be allowed to mingle.
And with that what the user actually needs to do is:
- Press ALT+TAB,TAB,TAB,TAB,TAB,Shift+Tab
- Press the application-wide HotKey
- ALT+TAB to continue working
Win Key + Number
Another option is that the user pins the application to the taskbar and then uses Windows Key and the 1-based index of the taskbar icon to start the application - or - brings it to the front in case the application is already started.
In comparison to the ALT-TAB approach, this one has the advantage that the HotKey is not context dependant, that is, it does not change depending on what app was active before. The disadvantage is, that it only works for Win+[0-9] so there are only 10 HotKeys and it vies with other applications for a spot.
An additional issue that it shares with the ALT-TAB approach is, that you can only activate the window - you cannot transport any additional information with it. It's not a general HotKey, it's just a "Focus it" Action and you need to tell the application what you want it do it in an additional step.
That's fine, if it's your IDE, but if it's just a single action that you want to start, let's say a Timer that you want to start, what could have been a single HotKey is now a sequence of:
- Press Win+Number
- Press the application-wide HotKey
- Press ALT+TAB to continue working
Shortcut + Single Instance
Another option is some combination of Shortcut Link with arguments, parsing said arguments a single instance check and some inter-process communication.
An example of what we would need to do could be:
- Parse the command-line arguments at startup of the application
- Check if another instance of the app is already running
- If not, start normally and execute the actions specified in he arguments
- If there is another instance, use some form of IPC to tell the other instance what to do, then exit.
- Setup a shortcut that starts the app with the arguments.
- Make the shortcut executable via a hotkey, for example pinning it to the taskbar or create the Shortcut on the Desktop and set it's Shortcut key in the property page, which is basically a CTRL+ALT+[a-z0-9] Key combination.
This approach is more flexible than just activating your application - you can get specific information to the application and act on it. One downside is the dependency on external links and the limited number of usable HotKeys.
AutoHotKey
For applications that I often use, I'd like to create a AutoHotKey script that focuses them when I press a custom HotKey, e.g.:
; active windows terminal on ctrl-;
Ctrl & `;::
WinActivate, ahk_class CASCADIA_HOSTING_WINDOW_CLASS
Once you have it active, you can extend the script to click the buttons that you want and then switch back to the original application that had the focus earlier.
That's a nice solution! You can cut down the sequence you need to do to a single HotKey combination. It works great for third party applications that you use yourself. I would like to provide a similar experience for my little pet application, but preferable without actually bundling it with an AutoHotKey script.
Looking at HotKey Libraries
To integrate that functionality in your app, you need to receive a global HotKey event somehow and after a quick web search, I found two libraries which promised to do what I wanted.
But upon closer inspection I found that they didn't work for my scenario: one library depended on Wpf (and had a restrictive license) and the other one was dependent on either WinForms or Wpf.
That means that if you don't want either of those dependencies - say because you are using an alternative UI Framework or a Console App, you are out of luck.
Why WinForms or Wpf?
The question then becomes: Why do those libraries depend on WinForms or Wpf?
The answer lies in the API for handling HotKeys on Windows - which consists of two Win32 functions, one for registering and one for "unregistering" a hotkey (RegisterHotKey / UnregisterHotKey).
After calling RegisterHotKey()
and supplying the function with a Window Handle and the HotKey in question, the system sends a WM_HOTKEY Windows Message to the Window specified by the Handle every time the HotKey is pressed until UnregisterHotKey()
is called.
How do you create a Window?
So to actually receive the notification Message, you need a Window that handles the incoming HotKey Window messages.
To create a Window, you need to call either CreateWindow()
or CreateWindowEx()
and supply it with a couple of arguments, the most important being the Class of the Window that should be created.
The followup-question then becomes how do you create a Window Class?
How do you register a Window Class?
By calling either RegisterClass()
or the RegisterClassEx()
function and supplying it with a reference to an instance of the WNDCLASSW or the WNDCLASSEX struct respectively.
As one of it's most important fields, the struct holds the Windows Procedure that gets called when a Window Messages is dispatched to a window of that class.
So before we can fill the struct, we need to implement the Windows Procedure in question, which is a delegate of type WNDPROC.
How do you write a Windows Procedure?
Even though a Window Procedure needs to handle a lot of different messages - not only the WM_HOTKEY
one that we are interested in - the implementation is quite easy, because we can delegate (hehe) most the work to a default implementation called DefWindowProc.
So our implementation of the Windows Procedure will only handle WM_HOTKEY
messages and fire a HotKeyPressed
event if that is the case and forward any other kind of Window Message to DefWindowProc
.
Phew, that's almost all we need. There is one more thing that we need to do - we need to have message loop that reads messages from the message queue and dispatches it to the windows procedure.
How do you implement a Message Loop?
Luckily the process is very well documented - the message Loop itself is very simple, it basically consists of an while loop that reads messages from the message queue by calling GetMessage()
and dispatches them to the Window Procedure by using DispatchMessage()
.
But that complicates matters somewhat - a message loop needs to handle Window messages continuously and combined with the fact that GetMessage()
blocks as long no messages are available and that it can only retrieve messages that are associated with the same thread, we will need a dedicated thread.
So to summarize, what we need is the following:
- a Message Loop
- a Window Procedure
- call RegisterClass
- call CreateWindow
- register HotKeys
- unregister HotKeys
- raise an event on pressed HotKey
...and most of that needs to happen on the same, dedicated thread.
I think that kind of answers our previous question why the libraries take a dependency on Wpf or WinForms - they want to reuse the creation of Windows and the MessageLoop implementation.
How can we put it together?
Now that we finally have all the parts, we can assemble them into the separate operations that we need to handle, I came up with the following operations:
- Initialization
- Register a HotKey
- Unregister a HotKey
- Receiving a HotKey
- Cleanup
What do those Operations do?
Initialization consists of spawning a dedicated thread, creating a Window Procedure, registering a class, create a window and enter the message loop.
Registering the HotKey is basically just calling RegisterHotKey()
. The only complication is, that it must happen on the thread that created the window.
Unregistering the HotKey means calling UnregisterHotKey()
with the same caveat that it must be done by the thread that created the window.
Receiving the HotKey means reacting to the WM_HOTKEY
message and raising an event.
Cleanup means Unregistering all Registered HotKey, destroying the Window and unregistering the class.
How does the Implementation look like?
Initialization
As for the API, I chose to write a class named HotKeyManager
, which in it's constructor handles the initialization as shown in the following diagram:

HotKey Registration
Registering the HotKey is exposed by a member called Register()
. To satisfy the requirement that only the thread that created the window can call RegisterHotKey()
, the Register()
member does not call RegisterHotKey()
directly, instead it sends a custom Windows Message containing the details of the HotKey (VirtualKeyCode and Modifiers). Calling the RegisterHotKey()
function is then done by the Window Procedure running on the Message Loop Thread.
HotKey Unregistration
Unregistering the HotKey also happens by sending a custom Windows Message. But instead of a corresponding Unregister()
member on the HotKeyManager class, I chose to return an IDisposable
instance as the return value of the Register()
member, which simplifies the API and the implementation.
HotKeyPressed event
In addition to the aforementioned custom Window Messages, the Window Procedure also handles the WM_HOTKEY
message, that the HotKeyManager
exposes over the HotKeyPressed
event.
Visualizing the Message Loop
I've tried to visualize the general behavior of the message loop in the following image:

Where is the code?
Glad you asked! I've uploaded the code of the mini-library to github here. The library code is mostly a single F# file - only the definitions used for interfacing with the Win32 API are living in separate files.
How can we use the code?
The following is the code for a simple C# Console Application that registers 2 hotkeys and prints a message to the Console whenever a HotKey is pressed:
Example Program.cs
using System;
using GlobalHotKeys;
using GlobalHotKeys.Native.Types;
void HotKeyPressed(HotKey hotKey) =>
Console.WriteLine($"HotKey Pressed: Id = {hotKey.Id}, Key = {hotKey.Key}, Modifiers = {hotKey.Modifiers}");
using var hotKeyManager = new HotKeyManager();
using var subscription = hotKeyManager.HotKeyPressed.Subscribe(HotKeyPressed);
using var shift1 = hotKeyManager.Register(VirtualKeyCode.KEY_1, Modifiers.Shift);
using var ctrl1 = hotKeyManager.Register(VirtualKeyCode.KEY_1, Modifiers.Control);
Console.WriteLine("Listening for HotKeys...");
Console.ReadLine();
Example Applications
There are also some example applications that show off the HotKey registration:
References
- GlobalHotKey (https://github.com/8/GlobalHotKey)
- AutoHotKey (https://www.autohotkey.com/)
- AvaloniaUI (https://avaloniaui.net)
- RegisterHotKey (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey)
- UnregisterHotKey (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey)
- Blog Post "Desktop Apps with AvaloniaUI and FSharp" (./Desktop-Apps-with-AvaloniaUI-and-FSharp)
- WM_HOTKEY (https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-hotkey)
- CreateWindow (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowa)
- CreateWindowEx (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw)
- RegisterClass (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassw)
- RegisterClassEx (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw)
- WNDCLASSW (https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassw)
- WNDCLASSEX (https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw)
- DefWindowProc (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw)
- Message Loop (https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues)
- GetMessage() (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage)
- DispatchMessage() (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-dispatchmessage)