借助于全局快捷键,用户可以在任何地方操控程序,触发对应的功能。但 WinForms 框架并没有提供全局快捷键的功能。想要实现全局快捷键需要跟 Windows API 打交道。本文就交你如何使用 Windows API 使用全局快捷键。

了解消息循环机制

消息机制简要介绍

  一个窗体到底是如何工作的呢?它是如何响应用户的操作的呢?不妨先让我们搞明白一个程序的运行机制吧。

  在 Windows 上面,一个桌面应用程序是通过消息机制驱动的。消息(Message)携带着对应窗体发生了什么的信息。如,用户按下了按键、鼠标移动或者点击等等。

  那么工作流程是怎样的呢?

  1. 首先,用户做出了一些操作或者一些其他的事情发生了,系统就会创建一条消息出来。接着,把消息投送到当前对应的窗体的线程消息队列。等待应用程序处理消息。消息会携带一个窗体的句柄、一个消息号、以及一些额外信息。这些信息可以告诉应用程序,到底发生了什么事情。

  2. 应用程序完成初始化之后,就开始建立消息处理机制。通过不断循环从消息队列获取消息。对于那些有对应目标窗体的消息,将消息转发到对应窗体的窗体处理函数。

  3. 窗体处理函数负责处理消息。

在 Win Forms 中,消息的派发机制

  在 Win Forms 中, Application.Run 方法就实现了消息处理机制。我们看一下 Program.cs 中的以下代码。这段代码就是创建一个窗体,接着,把窗体传入 Application.Run 方法。而 Application.Run 方法,首先显示这个窗体,接着就开始循环从消息队列获取消息并派发消息了。

[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}

  Application.Run 方法的描述如下:

在当前线程上开始运行标准应用程序消息循环,并使指定窗体可见。

  那么,能不能直观的看到有哪些消息放到了咱们的消息队列里面呢?通过查看 Application 类的文档,我们找到了如下方法:

public static void AddMessageFilter (System.Windows.Forms.IMessageFilter value);

添加消息筛选器以便在向目标传送 Windows 消息时监视这些消息。

  很显然,想要查看到消息需要我们实现一个 IMessageFilter 接口的类。我们来编写一个这样的类:如下:

internal class MyMessageFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
Console.WriteLine("MyMessageFilter: {0}", m.ToString());
return false;
}
}

  代码非常的易懂。不过值得说到的是,返回 false 的含义是允许这条消息继续向下传递,如果返回 true,则该条消息就不会往下继续传递。

  下面,我们把这个消息处理器注册到 Application 中去。

  Main 方法下编写如下的代码:

Application.AddMessageFilter(new MyMessageFilter()); ;
Application.Run(new Form1());

  第一行就是我们新增加的代码。接着为了能出现控制台窗口,我们应该把程序的目标平台选为 Windows 控制台程序。最后开始执行应用程序。应该就可以在控制台中看到有信息输出了。

窗体的消息处理函数探秘

  通过 Application 建立的消息派发机制,消息会被发送到下一站,也就是窗体的消息处理函数。在 Win Forms 中,我们可以通过重写消息的处理函数,来窥探这些消息内容。请看如下代码:

internal class Form1 : Form
{
protected override void WndProc(ref Message m)
{
Console.WriteLine("Form1 WndProc: {0}", m.ToString());
base.WndProc(ref m);
}
}

消息机制小结

  通过以上代码,你应该对消息机制有了一个直观的描述。那么,下面会说到我们的今天的主角——热键。由于热键被触发的时候,也是通过消息机制告知应用程序的,因此我们当然要会处理热键消息啦。相信你现在已经可以写出对应的代码了。

导入相关 API

  注册全局热键和撤销全局热键的 API 文档如下,共你去查阅。

RegisterHotKey

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey

UnregisterHotKey

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey

  为了能把这两个函数引入我们的程序,我们需要定义一个枚举类。如下:

/// <summary>
/// 为热键提供修饰键选项的枚举。
/// </summary>
[Flags]
public enum KeyModifiers
{
/// <summary>
/// 没有修饰键。
/// </summary>
None = 0X00, /// <summary>
/// ALT 键。
/// </summary>
Alt = 0X01, /// <summary>
/// CTRL 键。
/// </summary>
Control = 0X02, /// <summary>
/// SHIFT 键。
/// </summary>
Shift = 0X04, /// <summary>
/// Windows 徽标键。
/// </summary>
Windows = 0X08, /// <summary>
/// 热键按下时禁止重复发出消息。
/// </summary>
NoRepeat = 0X4000
}

  接着我们引入两个API 函数和一个常量。如下:

/// <summary>
/// 导入和定义 Windows SDK 中关于全局热键函数及常量的静态类。
/// </summary>
internal static class NativeMethods
{
/// <summary>
/// 定义使用 <see cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/> 注册的热键触发的消息的消息号。
/// </summary>
public const int WM_HOTKEY = 0X0312; /// <summary>
/// 注册系统全局热键。
/// </summary>
/// <param name="hWnd">关联的窗口句柄。如果此值为零,则与当前县城关联, WM_HOTKEY 消息会放到当前县城的消息队列。</param>
/// <param name="id">用来标识热键的标识符。</param>
/// <param name="fsModifiers">修饰键和选项的值。</param>
/// <param name="vk">虚拟键代码。</param>
/// <returns>成功返回 true, 失败返回 false。如需错误信息可调用 <see cref="Marshal.GetLastWin32Error"/> 方法。</returns>
/// <seealso cref="UnregisterHotKey(IntPtr, int)"/>
/// <remarks>
/// 当键被按下时,系统会寻找匹配的已注册的全局热键,如果该全局热键与一个窗体关联,则 <see cref="WM_HOTKEY"/> 消息会放到该窗体的消息队列,若未与一个窗体关联,则将 <see cref="WM_HOTKEY"/> 消息发送到对应的线程消息队列。
/// 该函数无法将全局热键与另一个线程创建的窗体关联。
/// 如果将要注册的全局热键已被注册,调用该函数将失败。
/// 如果已注册的全局热键具有与将要注册的全局热键相同的窗体句柄 (hWnd) 和标识符 (id), 则新注册的全局热键与旧全局热键一起维护。 如果就全局热键需要被新全局热键替换,应该先显示地调用 <see cref="UnregisterHotKey(IntPtr, int)"/> 函数以撤销注册的全局热键, 接着调用该函数注册新的全局热键。
/// 在 Windows Server 2003 上: 新全局热键与以注册的全局热键具有相同的窗体句柄 (hWnd) 和标识符 (id) 时, 旧全局热键将被新的全局热键替换。
/// F12 应当保留给调试器使用。
/// 应用程序必须指定 0x0000 到 0xBFFF之间的值, 共享类库必须指定 0xC000 到 0xFFFF 之间的值给 id 参数。
/// </remarks>
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, KeyModifiers fsModifiers, Keys vk); /// <summary>
/// 撤销已经注册的系统全局热键。
/// </summary>
/// <param name="hWnd">关联的窗口句柄。如果没有与任何窗口关联,则必须为零。</param>
/// <param name="id">需要撤销的热键的标识符。</param>
/// <returns>成功返回 true, 失败返回 false。如需错误信息可调用 <see cref="Marshal.GetLastWin32Error"/>方法。</returns>
/// <seealso cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/>
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
}

  以上,我们就准备好了相关的类型和平台调用的定义代码。

使用热键的流程

  使用热键的流程如下:

  1. 在必要的时候注册需要的热键。

  2. 在必要的时候释放注册的热键。

  3. 处理好热键消息。

关联到窗体的热键实例

注册热键

  下面我们通过注册一个 Ctrl + Shift + H 这一热键,演示关联到窗体的热键的工作流程。首先,区分不同热键的方法是指定不同的 id 标识符。我们首先定义一个常量,规定我们这个热键的标识符:

/// <summary>
/// 定义用于改变窗体显示状态热键的标识符。
/// </summary>
const int ChangeVisibleHotKeyId = 1;

  接着我们在窗体的 Load 事件下编写如下代码,注册我们需要的热键。

private void Form1_Load(object sender, EventArgs e)
{
NativeMethods.RegisterHotKey(this.Handle, ChangeVisibleHotKeyId, KeyModifiers.Control | KeyModifiers.Shift, Keys.H);
}

处理热键

  为了使该热键能实现对应的功能。我们应该重写窗体的处理函数,并且,把 WM_HOTKEY 消息拿出来,并且派遣到另外一个方法实现具体的功能。代码如下:

    protected override void WndProc(ref Message m)
{
Console.WriteLine("Form1 WndProc: {0}", m.ToString()); // 根据消息 id 处理消息。
switch (m.Msg)
{
case NativeMethods.WM_HOTKEY:
// 我们把热键的 id 取出来,调用处理热键的方法。
this.ProcessHotKeyMessage(m.WParam.ToInt32());
break;
default:
base.WndProc(ref m);
break;
}
} /// <summary>
/// 处理热键消息。我们在这里实现热键对应的功能。
/// </summary>
/// <param name="hotKeyId">热键的标识符。</param>
private void ProcessHotKeyMessage(int hotKeyId)
{
// 根据不同的id 区分不同的热键。
switch (hotKeyId)
{
case ChangeVisibleHotKeyId:
this.Visible = !this.Visible;
break;
}
}

撤销热键

  最后,我们在窗体销毁时撤销我们注册的热键,代码如下:

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
NativeMethods.UnregisterHotKey(this.Handle, ChangeVisibleHotKeyId);
}

  以上,就完成了我们的热键注册工作了。可以执行程序试一下是否能正常工作。

更进一步

  本文只是展示了关联到窗体的热键的处理流程。还有一种情况是这样的,我们的程序并不需要窗体,那么显然就不需要创建出来一个窗体。那么应该如何处理这个热键呢?没错,你可以在 MessageFilter 中对热键消息进行处理。

完整代码

以下是本程序的完整代码:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms; namespace HotKeyApp
{
internal class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.AddMessageFilter(new MyMessageFilter()); ;
Application.Run(new Form1());
}
} internal class Form1 : Form
{
/// <summary>
/// 定义用于改变窗体显示状态热键的标识符。
/// </summary>
const int ChangeVisibleHotKeyId = 1; public Form1()
{
this.Load += Form1_Load;
this.FormClosed += Form1_FormClosed;
} private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
NativeMethods.UnregisterHotKey(this.Handle, ChangeVisibleHotKeyId);
} private void Form1_Load(object sender, EventArgs e)
{
NativeMethods.RegisterHotKey(this.Handle, ChangeVisibleHotKeyId, KeyModifiers.Control | KeyModifiers.Shift, Keys.H);
} protected override void WndProc(ref Message m)
{
Console.WriteLine("Form1 WndProc: {0}", m.ToString()); // 根据消息 id 处理消息。
switch (m.Msg)
{
case NativeMethods.WM_HOTKEY:
// 我们把热键的 id 取出来,调用处理热键的方法。
this.ProcessHotKeyMessage(m.WParam.ToInt32());
break;
default:
base.WndProc(ref m);
break;
}
} /// <summary>
/// 处理热键消息。我们在这里实现热键对应的功能。
/// </summary>
/// <param name="hotKeyId">热键的标识符。</param>
private void ProcessHotKeyMessage(int hotKeyId)
{
// 根据不同的id 区分不同的热键。
switch (hotKeyId)
{
case ChangeVisibleHotKeyId:
this.Visible = !this.Visible;
break;
}
} } internal class MyMessageFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
Console.WriteLine("MyMessageFilter: {0}", m.ToString());
return false;
}
} /// <summary>
/// 为热键提供修饰键选项的枚举。
/// </summary>
[Flags]
public enum KeyModifiers
{
/// <summary>
/// 没有修饰键。
/// </summary>
None = 0X00, /// <summary>
/// ALT 键。
/// </summary>
Alt = 0X01, /// <summary>
/// CTRL 键。
/// </summary>
Control = 0X02, /// <summary>
/// SHIFT 键。
/// </summary>
Shift = 0X04, /// <summary>
/// Windows 徽标键。
/// </summary>
Windows = 0X08, /// <summary>
/// 热键按下时禁止重复发出消息。
/// </summary>
NoRepeat = 0X4000
} /// <summary>
/// 导入和定义 Windows SDK 中关于全局热键函数及常量的静态类。
/// </summary>
internal static class NativeMethods
{
/// <summary>
/// 定义使用 <see cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/> 注册的热键触发的消息的消息号。
/// </summary>
public const int WM_HOTKEY = 0X0312; /// <summary>
/// 注册系统全局热键。
/// </summary>
/// <param name="hWnd">关联的窗口句柄。如果此值为零,则与当前县城关联, WM_HOTKEY 消息会放到当前县城的消息队列。</param>
/// <param name="id">用来标识热键的标识符。</param>
/// <param name="fsModifiers">修饰键和选项的值。</param>
/// <param name="vk">虚拟键代码。</param>
/// <returns>成功返回 true, 失败返回 false。如需错误信息可调用 <see cref="Marshal.GetLastWin32Error"/> 方法。</returns>
/// <seealso cref="UnregisterHotKey(IntPtr, int)"/>
/// <remarks>
/// 当键被按下时,系统会寻找匹配的已注册的全局热键,如果该全局热键与一个窗体关联,则 <see cref="WM_HOTKEY"/> 消息会放到该窗体的消息队列,若未与一个窗体关联,则将 <see cref="WM_HOTKEY"/> 消息发送到对应的线程消息队列。
/// 该函数无法将全局热键与另一个线程创建的窗体关联。
/// 如果将要注册的全局热键已被注册,调用该函数将失败。
/// 如果已注册的全局热键具有与将要注册的全局热键相同的窗体句柄 (hWnd) 和标识符 (id), 则新注册的全局热键与旧全局热键一起维护。 如果就全局热键需要被新全局热键替换,应该先显示地调用 <see cref="UnregisterHotKey(IntPtr, int)"/> 函数以撤销注册的全局热键, 接着调用该函数注册新的全局热键。
/// 在 Windows Server 2003 上: 新全局热键与以注册的全局热键具有相同的窗体句柄 (hWnd) 和标识符 (id) 时, 旧全局热键将被新的全局热键替换。
/// F12 应当保留给调试器使用。
/// 应用程序必须指定 0x0000 到 0xBFFF之间的值, 共享类库必须指定 0xC000 到 0xFFFF 之间的值给 id 参数。
/// </remarks>
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, KeyModifiers fsModifiers, Keys vk); /// <summary>
/// 撤销已经注册的系统全局热键。
/// </summary>
/// <param name="hWnd">关联的窗口句柄。如果没有与任何窗口关联,则必须为零。</param>
/// <param name="id">需要撤销的热键的标识符。</param>
/// <returns>成功返回 true, 失败返回 false。如需错误信息可调用 <see cref="Marshal.GetLastWin32Error"/>方法。</returns>
/// <seealso cref="RegisterHotKey(IntPtr, int, KeyModifiers, VirtualKeys)"/>
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
}
}

最后

  最后,希望本文对于你有些许帮助。

参考资料

窗口消息 (入门与 Win32 和 c + + 一起) - Win32 apps | Microsoft Docs

https://docs.microsoft.com/zh-cn/windows/win32/learnwin32/window-messages

RegisterHotKey function (winuser.h) - Win32 apps | Microsoft Docs

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey

UnregisterHotKey function (winuser.h) - Win32 apps | Microsoft Docs

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey

WM_HOTKEY 消息 (Winuser.h) - Win32 apps | Microsoft Docs

https://docs.microsoft.com/zh-cn/windows/win32/inputdev/wm-hotkey

Application 类 (System.Windows.Forms) | Microsoft Docs

https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.forms.application?view=netframework-4.8

在 WinForms 项目中使用全局快捷键的更多相关文章

  1. [ionic开源项目教程] - 第5讲 如何在项目中使用全局配置

    第5讲 如何在项目中使用全局配置? Q:ionic开发,说纯粹一点,用的就是html+css+js,那么无疑跟web开发的方式是类似的.在这里给大家分享一个小技巧,如何在项目中使用全局配置? A:我的 ...

  2. 在WPF中使用全局快捷键

    今天写一个小程序中使用到了全局快捷键,找到了我之前写的文章在c#中使用全局快捷键翻了一下,发现它是WinForm版本的,而我现在大部分写WPF程序了,便将其翻译了为WPF版本的了. static cl ...

  3. vue-cli项目中使用全局过滤器及传参(日期格式化)

    // 过滤日期格式,传入时间戳,根据参数返回不同格式 const formatTimer = function(val, hours) { if (val) { ); var y = dateTime ...

  4. Delphi 中的全局快捷键+给指定窗体发送按键

    [背景] 公司做视频影像采集,平时采集图像的时候都需要打开采集窗口,然后需要开着采集窗口来进行图像采集.同事问我能不能做一个全局快捷键,哪怕我没有操作也可以采集图像.说干就干,一直想做全局快捷键了,网 ...

  5. ASP.NET项目中引用全局dll

    在ASP.NET项目中,有些dll是全局dll,也就是说,没有放在单个项目的引用中.它们一般存放在如下目录C:\Windows\assembly中 这个时候,我们需要在单个项目中引用他们,应该如何做呢 ...

  6. WEB 项目中的全局异常处理

    在web 项目中,遇到异常一般有两种处理方式:try.....catch....:throw 通常情况下我们用try.....catch.... 对异常进行捕捉处理,可是在实际项目中随时的进行异常捕捉 ...

  7. 如何在.net项目中使用全局程序集GAC

    在解决已有.net网站问题过程中(之前的同事写的),发现出现dll不存在的情况,build报错 在bin目录下找不到该dll,后来发现是全局程序集,存储在C:\Windows\assembly目录下 ...

  8. vue项目中设置全局引入scss,使每个组件都可以使用变量

    在Vue项目中使用scss,如果写了一套完整的有变量的scss文件.那么就需要全局引入,这样在每个组件中使用. 可以在mian.js全局引入,下面是使用方法. 1: 安装node-sass.sass- ...

  9. 在vue-cli项目中定义全局 filter、method 方法

    1.创建 filters.js(methods.js) 文件: 2.filters.js(methos.js) 中定义全局过滤方法: 1 export default { 2 /** 时间戳转换 */ ...

随机推荐

  1. snort规则

    一.Snort规则分为两个部分 二.规则头的基本格式 动作: 动作描述一个数据包的"谁,在何处,什么"的问题,并指明规则被激发后,在事件中应当做什么.在编写规则时,你可以从下面的关 ...

  2. golang实现WebSocket的商业化使用的开发逻辑(1)

    WebSocket是什么 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议.其最大特点之一就是:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对 ...

  3. spring-boot-learning-配置文件相关

      Spring-boot的配置文件   springboot通过那个下面的依赖去读取配置文件的上下文: <!-- https://mvnrepository.com/artifact/org. ...

  4. We're sorry but demo3 doesn't work properly without JavaScript enabled. Please enable it to continue.

    今天遇到一个问题为 vue请求得到的响应为 We're sorry but demo3 doesn't work properly without JavaScript enabled. Please ...

  5. 学习Docker(二)

    一.Docker快速入门 Docker 改变了什么? 1.简化配置 2.流水线管理 3.应用隔离 4.提高开发效率 5.快速部署 6.面向产品:产品交付 7.面向开发:简化环境配置 8.面向测试:多版 ...

  6. 学习 Haproxy (四)

    一. haproxy 的安装配置 # cat /etc/redhat-release CentOS release 6.6 (Final) # uname -r 2.6.32-504.el6.i686 ...

  7. Numpy对数组按索引查询

    Numpy对数组按索引查询 三种索引方法: 基础索引 神奇索引 布尔索引 基础索引 一维数组 和Python的List一样 二维数组 注意:切片的修改会修改原来的数组 原因:Numpy经常要处理大数组 ...

  8. Azure DevOps 中 Dapr项目自动部署流程实践

    注:本文中主要讨论 .NET6.0项目在 k8s 中运行的 Dapr 的持续集成流程, 但实际上不是Dapr的项目部署到K8s也是相同流程,只是k8s的yaml配置文件有所不同 流程选择 基于 Dap ...

  9. 使用 Vuex + Vue.js 构建单页应用

    鉴于该篇文章阅读量大,回复的同学也挺多的,特地抽空写了一篇 vue2.0 下的 vuex 使用方法,传送门:使用 Vuex + Vue.js 构建单页应用[新篇] ------------------ ...

  10. 用纯CSS美化radio和checkbox

    Radio和checkbox需要美化吗?答案是必须的,因为设计风格一直都会变化,原生的样式百年不变肯定满足不了需求. 先看看纯CSS美化过后的radio和checkbox效果:查看. 项目地址:mag ...