← Blog

Whid — The Origin

Chapter 5: Add global hot key in WinUI 3

13 May 2022

I had to realize that I’m getting old, when Paul said, WPF is not the coolest and latest tool anymore to create a user interface.

Paul had to realize that he is getting old, when we were searching around for creating a UWP project, and the internet said, UWP is also not the coolest and latest tool anymore. (By the way, Paul is like 20.)

Although neither of us have ever used it, we have decided to go with WinUI 3 for the time tracker. It’s so awesome, what could go wrong?

TLDR; Issues addressed

Registering a global hot key for the WinUI 3 application

The goal was to set up the main window in the following way: you can press a shortcut, and then the window opens (regardless of what is in the foreground), and after entering a task and pressing Enter, pressing Escape, or pressing the key combination again the window closes.

The first thing we learned is that you can add keyboard shortcuts to your application using keyboard accelerators, but those are not global. This means they can be handled only if your window is active.

Hot keys can be registered using the Win32 API both in WPF and UWP (although the latter needs a bit of a workaround). This hasn’t changed in WinUI 3, although it definitely has a better support for the Win32 interop. Currently there are two NuGet packages which can be used:

We installed CsWin32 using the NuGet Package Manager (currently it’s only a prerelease), and then added a NativeMethods.txt file in the root folder of our project, in which we can list the functions we will need. For this we have to add only one method in our NativeMethods.txt:

RegisterHotKey

Then we had to trigger building the project which generated the InterOp classes and then register a hot key to our window handle.

As on MacOS we are using the Cmd + . (period) shortcut, on Windows we decided to go with Ctrl + Win + . (period) as Win + . (period) is already used to display an emoji keyboard. The key modifiers can be found under Windows.Win32.UI.Input.KeyboardAndMouse.HOT_KEY_MODIFIERS and for the rest you can use Microsoft’s virtual key code table (0xBE for the period). You can also do a check if registering the hot key was successful, as RegisterHotKey has a boolean return value.

public sealed partial class MainWindow : Window
{
    private const uint DOT_KEY = 0xBE;

    public MainWindow()
    {
        this.InitializeComponent();
        Windows.Win32.Foundation.HWND hwnd = new Windows.Win32.Foundation.HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
        var success = PInvoke.RegisterHotKey(hwnd, 0, Windows.Win32.UI.Input.KeyboardAndMouse.HOT_KEY_MODIFIERS.MOD_WIN | Windows.Win32.UI.Input.KeyboardAndMouse.HOT_KEY_MODIFIERS.MOD_CONTROL, DOT_KEY);
    }
}

We registered the hot key to our main window, so any time someone presses the specified key combination, the window will get a hot key message. The next step is to process this.

Overriding the window procedure of the WinUI 3 application

For this step we still stick to the Win32 API, but we have to extend the NativeMethods.txt file with the following methods:

SetWindowLongPtr
CallWindowProc

We have to rebuild the project in order to have the newly generated source code.

First we have to implement the new window procedure. It has to have the signature of Windows.Win32.UI.WindowsAndMessaging.WNDPROC, and it’s also good practice to handle only the hot key message, and delegate everything else to the original window procedure using CallWindowProc. The hot key message is described with the uint 0x0312. The message can also have parameters, but we will ignore those, as we registered only one hot key.

public sealed partial class MainWindow : Window
{
    private const uint WM_HOTKEY = 0x0312;
    private Windows.Win32.UI.WindowsAndMessaging.WNDPROC origPrc;

    private Windows.Win32.Foundation.LRESULT HotKeyPrc(Windows.Win32.Foundation.HWND hwnd,
        uint uMsg,
        Windows.Win32.Foundation.WPARAM wParam,
        Windows.Win32.Foundation.LPARAM lParam)
    {
        if (uMsg == WM_HOTKEY)
        {
            Debug.WriteLine("Hot key pressed");
            return (Windows.Win32.Foundation.LRESULT)IntPtr.Zero;
        }

        return PInvoke.CallWindowProc(origPrc, hwnd, uMsg, wParam, lParam);
    }
    // ... rest of the class
}

Now we will override the original window procedure using SetWindowLong. This requires a bit of converting back and forth between delegates, ints and pointers. In the code snippet below you can see that the SetWindowLong call returns the original window procedure that we can store and reuse for delegation.

public sealed partial class MainWindow : Window
{
    private const uint WM_HOTKEY = 0x0312;
    private Windows.Win32.UI.WindowsAndMessaging.WNDPROC origPrc;
    private Windows.Win32.UI.WindowsAndMessaging.WNDPROC hotKeyPrc;

    public MainWindow()
    {
        //... initialize window and register hot key

        hotKeyPrc = HotKeyPrc;
        var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(hotKeyPrc);
        origPrc = Marshal.GetDelegateForFunctionPointer<Windows.Win32.UI.WindowsAndMessaging.WNDPROC>((IntPtr)PInvoke.SetWindowLongPtr(hwnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));

    }

    // ... rest of the class
}

Note that you actually have to store the hotkeyPrc as a class member, otherwise the garbage collector would clean it up, and you would bump into weird memory errors. If you set a breakpoint in the HotKeyPrc and open the application for debugging, you can see that we step into it when you press Ctrl + Win + . (period).

Showing and hiding the window of the WinUI 3 application

If you would like to hide your window without closing it, you know the solution already: Win32. Let’s extend our NativeMethods.txt further!

ShowWindow
SetForegroundWindow
SetFocus
SetActiveWindow

After generating the sources again, you can call the following command to hide your window:

PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);

Showing it is pretty similar, but in our case we would also like to make it a focused, activated top window:

PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_SHOW);
PInvoke.SetForegroundWindow(hwnd);
PInvoke.SetFocus(hwnd);
PInvoke.SetActiveWindow(hwnd);

Final code snippet where everything works like a dream

public sealed partial class MainWindow : Window
{
    private const uint DOT_KEY = 0xBE;
    private const uint WM_HOTKEY = 0x0312;
    private Windows.Win32.UI.WindowsAndMessaging.WNDPROC origPrc;
    private Windows.Win32.UI.WindowsAndMessaging.WNDPROC hotKeyPrc;

    private Windows.Win32.Foundation.LRESULT HotKeyPrc(Windows.Win32.Foundation.HWND hwnd,
        uint uMsg,
        Windows.Win32.Foundation.WPARAM wParam,
        Windows.Win32.Foundation.LPARAM lParam)
    {
        if (uMsg == WM_HOTKEY)
        {
            if (!this.Visible)
            {
                PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_SHOW);
                PInvoke.SetForegroundWindow(hwnd);
                PInvoke.SetFocus(hwnd);
                PInvoke.SetActiveWindow(hwnd);
            }
            else
            {
                PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
            }

            return (Windows.Win32.Foundation.LRESULT)IntPtr.Zero;
        }

        return PInvoke.CallWindowProc(origPrc, hwnd, uMsg, wParam, lParam);
    }

    public MainWindow()
    {
        this.InitializeComponent();
        Windows.Win32.Foundation.HWND hwnd = new Windows.Win32.Foundation.HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
        PInvoke.RegisterHotKey(hwnd, 0, Windows.Win32.UI.Input.KeyboardAndMouse.HOT_KEY_MODIFIERS.MOD_WIN | Windows.Win32.UI.Input.KeyboardAndMouse.HOT_KEY_MODIFIERS.MOD_CONTROL, DOT_KEY);

        hotKeyPrc = HotKeyPrc;
        var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(hotKeyPrc);
        origPrc = Marshal.GetDelegateForFunctionPointer<Windows.Win32.UI.WindowsAndMessaging.WNDPROC>((IntPtr)PInvoke.SetWindowLongPtr(hwnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));

    }
}