How to make a global keyboard accelerator/hotkey for a button?
See the question and my original answer on StackOverflowTo 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:
MainWindow.xaml:
<Window x:Class="WinUI3App.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button x:Name="myButton" Click="myButton_Click">Click</Button>
</StackPanel>
</Window>
MainWindow.xaml.cs:
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()
{
InitializeComponent();
// 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);
pattern.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);
[Flags]
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;
o.Dispose();
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)
{
ArgumentNullException.ThrowIfNull(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; }
}