Globale Hotkeys in WPF mit .NET 8

Globale Hotkeys in WPF mit .NET 8

13 Jan 2025 - Matthias Voigt

Immer wieder brauche ich in einem WPF-Programm globale Hotkeys und jedes Mal suche ich im Netz danach. Da nichts im Internet für immer bleibt, veröffentliche ich hier meine Variante, die unter .NET 8 funktioniert.

Interessante Punkte im Code

  • Verwendung von HwndSource In WPF nutze ich HwndSource, um ein Fenster-Handle (hWnd) zu erhalten und eine WndProc-Methode zu registrieren.

  • Unterstützung mehrerer Modifikator-Tasten
    Die ModifierKeys-Enumeration ist mit dem FlagsAttribute versehen, sodass mehrere Tasten wie Ctrl + Shift kombiniert werden können.

  • Ereignisbasierte Umsetzung
    Sobald der Hotkey gedrückt wird, löst KeyboardHook ein Ereignis aus, das in der MainWindow-Klasse abgefangen wird.

Implementierung der KeyboardHook-Klasse

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Input;

public sealed class KeyboardHook : IDisposable
{
    [DllImport("user32.dll")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
 
    [DllImport("user32.dll")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);

    private const int WM_HOTKEY = 0x0312;
    private HwndSource? _hwndSource;
    private int _currentId;
    private IntPtr _windowHandle;

    public KeyboardHook(Window window)
    {
        window.SourceInitialized += (_, __) =>
        {
            var helper = new WindowInteropHelper(window);
            _windowHandle = helper.Handle; 
            _hwndSource = HwndSource.FromHwnd(_windowHandle);
            _hwndSource?.AddHook(WndProc);
        };
    }

    public void RegisterHotKey(ModifierKeys modifier, Key key)
    {
        if (_hwndSource == null)
            throw new InvalidOperationException("Fenster-Handle ist noch nicht initialisiert!");

        _currentId++;
        uint virtualKeyCode = (uint)KeyInterop.VirtualKeyFromKey(key);

        if (!RegisterHotKey(_windowHandle, _currentId, (uint)modifier, virtualKeyCode))
            throw new InvalidOperationException("Konnte Hotkey nicht registrieren.");
    }

    public event EventHandler<KeyPressedEventArgs>? KeyPressed;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_HOTKEY)
        {
            var key = KeyInterop.KeyFromVirtualKey(((int)lParam >> 16) & 0xFFFF);
            var modifier = (ModifierKeys)((int)lParam & 0xFFFF);

            KeyPressed?.Invoke(this, new KeyPressedEventArgs(modifier, key));
            handled = true;
        }
        return IntPtr.Zero;
    }


    public void Dispose()
    {
        if (_hwndSource != null)
        {
            for (int i = _currentId; i > 0; i--)
            {
                UnregisterHotKey(_windowHandle, i);
            }
            _hwndSource.RemoveHook(WndProc);
            _hwndSource = null;
        }
    }
}

public class KeyPressedEventArgs(ModifierKeys modifier, Key key) : EventArgs
{
    public ModifierKeys Modifier { get; } = modifier;
    public Key Key { get; } = key;
}

Nutzung der Klasse

Die Verwendung der KeyboardHook-Klasse in einer WPF-Anwendung:

public partial class MainWindow : Window
{
    private readonly KeyboardHook _keyboardHook;
    public MainWindow()
    {
        InitializeComponent();
        _keyboardHook = new KeyboardHook(this);
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _keyboardHook.KeyPressed += KeyboardHook_KeyPressed;
        _keyboardHook.RegisterHotKey(ModifierKeys.Control, Key.F1);
    }


    private void KeyboardHook_KeyPressed(object? sender, KeyPressedEventArgs e)
    {
        MessageBox.Show($"Hotkey {e.Modifier} + {e.Key} wurde gedrückt!");
    }
}