65.9K
CodeProject 正在变化。 阅读更多。
Home

桌面应用程序中的全局热键

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2018年12月30日

MIT

3分钟阅读

viewsIcon

31625

了解如何在 C# 桌面应用程序(例如 Windows Forms 或 WPF)中正确创建全局热键

引言

实现全局热键有两种常见方法,每种方法都有其自身的优缺点。当您的代码脱离程序的上下文并进入用户的桌面时,了解您的代码正在做什么非常重要。

背景

全局热键是指系统范围内的按键事件,即,无论键盘焦点在哪里,您的代码都会对按键做出反应的情况。

Windows 桌面 API 公开了 RegisterHotkey 函数,您应该优先使用它,而不是使用 低级键盘钩子的替代方法。

但是,RegisterHotkey三个主要的限制

  1. 当您注册一个键时,您基本上会覆盖其原始功能。例如,如果您覆盖了 F5 键,当该键不再在他们的浏览器上刷新网页时,您的用户会感到恼火。
  2. 在第 1 点之后,不言而喻的是,没有两个程序可以具有相同的全局热键组合。如果您尝试注册已属于另一个正在运行的程序的热键,您将遇到 WinAPI 错误。
  3. 某些组合根本无法注册(例如 Ctrl+Alt+Del)

低级键盘钩子克服了这些限制,但您需要记住两个后果

  1. 您的代码很可能会被杀毒软件(或对您的代码进行反编译并看到您使用 SetWindowsHookEx 的人)标记为间谍软件(用于键盘记录)
  2. 它有可能减慢整个系统的键盘输入处理速度(这将在下面详细解释)

对于第一个限制,我们几乎无能为力,但如果您了解自己在做什么,则可以防止后者发生。

首先,让我们看看当您按下键盘上的一个键时会发生什么

Keyboard Input Flow Chart

(对于 KEYUP 也是类似的过程)

从上面的图表中,您了解到您的全局键盘钩子会在键盘输入到达其最终目的地(焦点应用程序)之前处理键盘输入。

钩子同步运行。如果钩子运行缓慢,则输入将被延迟并惹恼用户。

假设您不打算阻止任何键到达其他钩子和程序,则此问题的直观解决方案是在单独的线程中处理输入。

这是从开源 NonInvasiveKeyboardHook 库中提取的 LowLevelKeyboardProc 代码

private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0)
    {
        var vkCode = Marshal.ReadInt32(lParam);

        // To prevent slowing keyboard input down, we use handle keyboard inputs in a separate thread
        ThreadPool.QueueUserWorkItem
           (this.HandleSingleKeyboardInput, new KeyboardParams(wParam, vkCode));
    }
     
    return CallNextHookEx(_hookId, nCode, wParam, lParam);
}

一种幼稚的方法可能有点颠倒顺序,例如

var returnValue = CallNextHookEx(_hookId, nCode, wParam, lParam);
ProcessInputInSameThread();
return returnValue;

虽然尽早调用 CallNextHookEx 确实会恢复 当前 键在消息队列中的流程,但只有在阻塞代码完成并且该函数返回后,才会处理 下一个 按键。

Using the Code

因此,如果您想要全局热键,并且标准的 WinAPI 热键系统不能满足您的需求,那么开源 NonInvasiveKeyboardHook 库就是答案。

它公开了一个 KeyboardHookManager,它公开了注册特定键组合的功能(即,您不能将其用于间谍软件)。

* 建议您仅使用 一个 KeyboardHookManager 实例。

基础

实例化并启动一个 KeyboardHookManager

var keyboardHookManager = new KeyboardHookManager();
keyboardHookManager.Start();

停止它(稍后可以通过再次调用 .Start 来恢复)。

keyboardHookManager.Stop();

注册热键

注册一个没有修饰符的 hotkey

// 0x60 = NumPad0
keyboardHookManager.RegisterHotkey(0x60, () =>
{
    Debug.WriteLine("NumPad0 detected");
});

注册一个带单个修饰符的 hotkey

keyboardHookManager.RegisterHotkey(NonInvasiveKeyboardHookLibrary.ModifierKeys.Control, 0x60, () => 
{ 
    Debug.WriteLine("Ctrl+NumPad0 detected");
});

注册一个带有多个修饰符的 hotkey

// Multiple modifiers can be specified using the bitwise OR operation
keyboardHookManager.RegisterHotkey(NonInvasiveKeyboardHookLibrary.ModifierKeys.Control | 
NonInvasiveKeyboardHookLibrary.ModifierKeys.Alt, 0x60, () => 
{ 
    Debug.WriteLine("Ctrl+Alt+NumPad0 detected");
});

// Or as an enum of modifiers
keyboardHookManager.RegisterHotkey(new[]
{NonInvasiveKeyboardHookLibrary.ModifierKeys.Control, 
NonInvasiveKeyboardHookLibrary.ModifierKeys.Alt}, 0x60, () =>
{
    Debug.WriteLine("Ctrl+Alt+NumPad0 detected");
});

取消注册热键

可以根据其唯一的键组合或使用 RegisterHotKey 返回的全局唯一标识符来注销 hotkey

keyboardHookManager.RegisterHotkey(0x60, () => { Debug.WriteLine("NumPad0 detected"); });
keyboardHookManager.UnregisterHotkey(0x60);

或者

var hotkeyId = keyboardHookManager.RegisterHotkey(0x60, () => { Debug.WriteLine("NumPad0 detected"); });
keyboardHookManager.UnregisterHotkey(hotkeyId);

也可以注销 所有 hotkey

keyboardHookManager.UnregisterAll();
© . All rights reserved.