Introduction
I was working on an app that needed hotkey
support and found out technically how to do it, but did not see any very clean solutions, so I wrote my own. This code provides a dead-simple way to attach/detach a snippet of code to a hotkey
in a WPF app.
Background
Hotkey
s are a relic from early versions of Windows, so we need to use interop functionality to get to it. All of this is abstracted in the HotKeyHelper
class, however the main trick is to get the relic window handle (hwnd
) of the main window of your WPF application. The hwnd
is not available at construction time, so we need to hook an event that occurs at a point where the handle is known.
Using the Code
The hotkey
code is implemented to be "fire and forget", so you can add the key without having to explicitly remove it, but that is available if needed. As I mentioned in the background, it is necessary to create the HotKeyHelper
at a time when the window has a valid hwnd
we can access. OnSourceInitialized
is a good place to do this:
HotKeyHelper _hotKeys;
int _throwConfettiKeyId;
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_hotKeys = new HotKeyHelper(this);
_throwConfettiKeyId = _hotKeys.ListenForHotKey(
Key.C,
HotKeyModifiers.Alt | HotKeyModifiers.Control,
() => { this.ThrowConfetti(); }
);
}
void DoSomeStuffLater()
{
_hotKeys.StopListeningForHotKey(_throwConfettiKeyId);
}
Here is the actual code for the helper class:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
namespace HotKeyTools
{
[Flags]
public enum HotKeyModifiers
{
None = 0,
Alt = 1,
Control = 2,
Shift = 4,
WindowsKey = 8,
}
public interface IHotKeyTool : IDisposable
{
int ListenForHotKey(System.Windows.Input.Key key, HotKeyModifiers modifiers, Action keyAction);
void StopListeningForHotKey(int id);
}
public class HotKeyHelper : IHotKeyTool
{
[DllImport("user32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
protected static extern bool RegisterHotKey(IntPtr hwnd, int id, uint fsModifiers, uint vk);
[DllImport("user32", SetLastError = true)]
protected static extern int UnregisterHotKey(IntPtr hwnd, int id);
protected const int WM_HOTKEY = 0x312;
int _idSeed;
private IntPtr _windowHandle;
Dictionary<int, Action> _hotKeyActions = new Dictionary<int, Action>();
public HotKeyHelper(Window handlerWindow)
{
_idSeed = (int)((DateTime.Now.Ticks % 0x60000000) + 0x10000000);
_windowHandle = new WindowInteropHelper(handlerWindow).Handle;
if(_windowHandle == null)
{
throw new ApplicationException("Cannot find window handle.
Try calling this on or after OnSourceInitialized()");
}
var source = HwndSource.FromHwnd(_windowHandle);
source.AddHook(HwndHook);
}
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_HOTKEY)
{
var hotkeyId = wParam.ToInt32();
if (_hotKeyActions.ContainsKey(hotkeyId))
{
_hotKeyActions[hotkeyId]();
handled = true;
}
}
return IntPtr.Zero;
}
public int ListenForHotKey
(System.Windows.Input.Key key, HotKeyModifiers modifiers, Action doThis)
{
var formsKey = (Keys)KeyInterop.VirtualKeyFromKey(key);
var hotkeyId = _idSeed++;
_hotKeyActions[hotkeyId] = doThis;
RegisterHotKey(_windowHandle, hotkeyId, (uint)modifiers, (uint)formsKey);
return hotkeyId;
}
public void StopListeningForHotKey(int hotkeyId)
{
UnregisterHotKey(_windowHandle, hotkeyId);
}
public void Dispose()
{
foreach(var hotkeyId in _hotKeyActions.Keys)
{
StopListeningForHotKey(hotkeyId);
}
}
}
}
History
- 13th November, 2018 - Initial version