See the question and my original answer on StackOverflow

To create a global Windows hotkey we can use the RegisterHotKey function but it's not that easy with WinUI3 since this function works by sending a window message and the WinUI3 window message loop is not exposed.

We can work around that with "subclassing" the WinUI3 window message loop, that's done in the WindowMessageHook utility below.

Here is how you can use it in a WinUI3 window:


<Window x:Class="WinUI3App.MainWindow" xmlns="" xmlns:x="">
        <Button x:Name="myButton" Click="myButton_Click">Click</Button>


using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Windows.System;

public partial class MainWindow : Window
    public MainWindow()

        // hook this window's message
        var hook = new WindowMessageHook(this);
        Closed += (s, e) => hook.Dispose(); // unhook on close
        hook.Message += (s, e) =>
            const int WM_HOTKEY = 0x312;
            if (e.Message == WM_HOTKEY)
                // click on the button using UI Automation
                var pattern = (ButtonAutomationPeer)FrameworkElementAutomationPeer.FromElement(myButton).GetPattern(PatternInterface.Invoke);

        // register CTRL + B as a global hotkey
        var hwnd = Win32Interop.GetWindowFromWindowId(AppWindow.Id);
        var id = 1; // some arbitrary hotkey identifier
        if (!RegisterHotKey(hwnd, id, MOD.MOD_CONTROL, VirtualKey.B))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        Closed += (s, e) => UnregisterHotKey(hwnd, id); // unregister hotkey on window close

    private void myButton_Click(object sender, RoutedEventArgs e)
        myButton.Content = "hello";

    // interop code for Windows API hotkey functions
    [DllImport("user32", SetLastError = true)]
    private static extern bool RegisterHotKey(nint hWnd, int id, MOD fsModifiers, VirtualKey vk);

    [DllImport("user32", SetLastError = true)]
    private static extern bool UnregisterHotKey(nint hWnd, int id);

    private enum MOD
        MOD_ALT = 0x1,
        MOD_CONTROL = 0x2,
        MOD_SHIFT = 0x4,
        MOD_WIN = 0x8,
        MOD_NOREPEAT = 0x4000,

Message hooking utility:

public class WindowMessageHook : IEquatable<WindowMessageHook>, IDisposable
    private delegate nint SUBCLASSPROC(nint hWnd, uint uMsg, nint wParam, nint lParam, nint uIdSubclass, uint dwRefData);

    private static readonly ConcurrentDictionary<nint, WindowMessageHook> _hooks = new();
    private static readonly SUBCLASSPROC _proc = SubclassProc;

    public event EventHandler<MessageEventArgs> Message;
    private nint _hWnd;

    public WindowMessageHook(Window window) : this(GetHandle(window)) { }
    public WindowMessageHook(nint hWnd)
        if (hWnd == 0)
            throw new ArgumentException(null, nameof(hWnd));

        _hWnd = hWnd;
        _hooks.AddOrUpdate(hWnd, this, (k, o) =>
            if (Equals(o)) return o;
            return this;
        if (!SetWindowSubclass(hWnd, _proc, 0, 0))
            throw new Win32Exception(Marshal.GetLastWin32Error());

    protected virtual void OnMessage(object sender, MessageEventArgs e) => Message?.Invoke(sender, e);
    protected virtual void Dispose(bool disposing)
        if (!disposing) return;
        var hWnd = Interlocked.Exchange(ref _hWnd, IntPtr.Zero);
        if (hWnd != IntPtr.Zero)
            RemoveWindowSubclass(hWnd, _proc, 0);
            _hooks.Remove(hWnd, out _);

    ~WindowMessageHook() { Dispose(disposing: false); }
    public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); }

    [DllImport("comctl32", SetLastError = true)]
    private static extern bool SetWindowSubclass(nint hWnd, SUBCLASSPROC pfnSubclass, uint uIdSubclass, uint dwRefData);

    [DllImport("comctl32", SetLastError = true)]
    private static extern nint DefSubclassProc(nint hWnd, uint uMsg, nint wParam, nint lParam);

    [DllImport("comctl32", SetLastError = true)]
    private static extern bool RemoveWindowSubclass(nint hWnd, SUBCLASSPROC pfnSubclass, uint uIdSubclass);

    private static nint GetHandle(Window window)
        return Win32Interop.GetWindowFromWindowId(window.AppWindow.Id);

    private static nint SubclassProc(nint hWnd, uint uMsg, nint wParam, nint lParam, nint uIdSubclass, uint dwRefData)
        if (_hooks.TryGetValue(hWnd, out var hook))
            var e = new MessageEventArgs(hWnd, uMsg, wParam, lParam);
            hook.OnMessage(hook, e);
            if (e.Result.HasValue)
                return e.Result.Value;
        return DefSubclassProc(hWnd, uMsg, wParam, lParam);

    public override int GetHashCode() => _hWnd.GetHashCode();
    public override string ToString() => _hWnd.ToString();
    public override bool Equals(object obj) => Equals(obj as Window);
    public virtual bool Equals(WindowMessageHook other) => other != null && _hWnd.Equals(other._hWnd);

public class MessageEventArgs : EventArgs
    public MessageEventArgs(nint hWnd, uint uMsg, nint wParam, nint lParam)
        HWnd = hWnd;
        Message = uMsg;
        WParam = wParam;
        LParam = lParam;

    public nint HWnd { get; }
    public uint Message { get; }
    public nint WParam { get; }
    public nint LParam { get; }
    public virtual nint? Result { get; set; }