在 WinForms 项目中使用全局快捷键
借助于全局快捷键,用户可以在任何地方操控程序,触发对应的功能。但 WinForms 框架并没有提供全局快捷键的功能。想要实现全局快捷键需要跟 Windows API 打交道。本文就交你如何使用 Windows API 使用全局快捷键。
了解消息循环机制
消息机制简要介绍
一个窗体到底是如何工作的呢?它是如何响应用户的操作的呢?不妨先让我们搞明白一个程序的运行机制吧。
在 Windows 上面,一个桌面应用程序是通过消息机制驱动的。消息(Message)携带着对应窗体发生了什么的信息。如,用户按下了按键、鼠标移动或者点击等等。
那么工作流程是怎样的呢?
首先,用户做出了一些操作或者一些其他的事情发生了,系统就会创建一条消息出来。接着,把消息投送到当前对应的窗体的线程消息队列。等待应用程序处理消息。消息会携带一个窗体的句柄、一个消息号、以及一些额外信息。这些信息可以告诉应用程序,到底发生了什么事情。
应用程序完成初始化之后,就开始建立消息处理机制。通过不断循环从消息队列获取消息。对于那些有对应目标窗体的消息,将消息转发到对应窗体的窗体处理函数。
窗体处理函数负责处理消息。
在 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);
}
以上,我们就准备好了相关的类型和平台调用的定义代码。
使用热键的流程
使用热键的流程如下:
在必要的时候注册需要的热键。
在必要的时候释放注册的热键。
处理好热键消息。
关联到窗体的热键实例
注册热键
下面我们通过注册一个 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 项目中使用全局快捷键的更多相关文章
- [ionic开源项目教程] - 第5讲 如何在项目中使用全局配置
第5讲 如何在项目中使用全局配置? Q:ionic开发,说纯粹一点,用的就是html+css+js,那么无疑跟web开发的方式是类似的.在这里给大家分享一个小技巧,如何在项目中使用全局配置? A:我的 ...
- 在WPF中使用全局快捷键
今天写一个小程序中使用到了全局快捷键,找到了我之前写的文章在c#中使用全局快捷键翻了一下,发现它是WinForm版本的,而我现在大部分写WPF程序了,便将其翻译了为WPF版本的了. static cl ...
- vue-cli项目中使用全局过滤器及传参(日期格式化)
// 过滤日期格式,传入时间戳,根据参数返回不同格式 const formatTimer = function(val, hours) { if (val) { ); var y = dateTime ...
- Delphi 中的全局快捷键+给指定窗体发送按键
[背景] 公司做视频影像采集,平时采集图像的时候都需要打开采集窗口,然后需要开着采集窗口来进行图像采集.同事问我能不能做一个全局快捷键,哪怕我没有操作也可以采集图像.说干就干,一直想做全局快捷键了,网 ...
- ASP.NET项目中引用全局dll
在ASP.NET项目中,有些dll是全局dll,也就是说,没有放在单个项目的引用中.它们一般存放在如下目录C:\Windows\assembly中 这个时候,我们需要在单个项目中引用他们,应该如何做呢 ...
- WEB 项目中的全局异常处理
在web 项目中,遇到异常一般有两种处理方式:try.....catch....:throw 通常情况下我们用try.....catch.... 对异常进行捕捉处理,可是在实际项目中随时的进行异常捕捉 ...
- 如何在.net项目中使用全局程序集GAC
在解决已有.net网站问题过程中(之前的同事写的),发现出现dll不存在的情况,build报错 在bin目录下找不到该dll,后来发现是全局程序集,存储在C:\Windows\assembly目录下 ...
- vue项目中设置全局引入scss,使每个组件都可以使用变量
在Vue项目中使用scss,如果写了一套完整的有变量的scss文件.那么就需要全局引入,这样在每个组件中使用. 可以在mian.js全局引入,下面是使用方法. 1: 安装node-sass.sass- ...
- 在vue-cli项目中定义全局 filter、method 方法
1.创建 filters.js(methods.js) 文件: 2.filters.js(methos.js) 中定义全局过滤方法: 1 export default { 2 /** 时间戳转换 */ ...
随机推荐
- springcloud学习03-spring cloud eureka(下)
7.配置服务提供者(生产者) 7.1.配置resources/application.yml. 值eureka.client.service-url(或serviceUrl).defaultZone是 ...
- Android 12(S) 图形显示系统 - Surface 一点补充知识(十二)
必读: Android 12(S) 图形显示系统 - 开篇 一.前言 因为个人工作主要是Android多媒体播放的内容,在工作中查看源码或设计程序经常会遇到调用API: static inline i ...
- ctf之计算器
题目信息如下: 打开环境,发现是一道简单的计算题 只能输入一位数字 F12查看源码发现作者将最大长度设置为1,我们将最大长度修改了即可 输入答案即可得到flag
- 羽夏笔记——Hook攻防基础
写在前面 本笔记是由本人独自整理出来的,图片来源于网络.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你 ...
- C++获取设备 PID,VID 信息
可直接编译(设置成:使用多字节字符集) 转来的,代码: /* http://www.experts-exchange.com/Programming/Editors_IDEs/Q_24506125.h ...
- redis主从复制和哨兵机制
redis主从复制和哨兵机制 技术标签: redis 1.redis主从复制(master/slave模式) 主数据库可以进行读写操作,当写操作导致数据发生变化时会自动将数据同步给从数据库.而一般情况 ...
- Intellij IDEA实现SpringBoot项目多端口启动的两种方法
有时候使用springboot项目时遇到这样一种情况,用一个项目需要复制很多遍进行测试,除了端口号不同以外,没有任何不同.遇到这种情况怎么办呢?这时候可以使用Intellij IDEA解决 前言 有时 ...
- JVM的小总结(转)
ref:http://www.cnblogs.com/ityouknow/p/6482464.html 注1:看了大神:纯洁的微笑的JVM系列篇,发现好多地方还是似懂非懂,理解的并不透彻,jvm的调优 ...
- Java中带参数的方法和JavaScript中带参数的函数有什么不同?
javascript是动态语言,是弱类型语言,其参数的使用很灵活:java则是强类型语言,参数的类型必须明确的
- vue钩子函数的妙用之“created()和activated()”
一.created() 在创建vue对象时,当html渲染之前就触发: 但是注意,全局vue.js不强制刷新或者重启时只创建一次, 也就是说,created()只会触发一次: 二.activated( ...