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.
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:
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.
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:
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:
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:
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.
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.
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.
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.
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?
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.
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.
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:
…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.
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 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.
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:
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.
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.
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.
I’ve tried to visualize the general behavior of the message loop in the following image:
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.
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:
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();
There are also some example applications that show off the HotKey registration: