C#.NET Winform 注册使用全局快捷键详解

借助于全局快捷键,用户可以在任何地方操控程序,触发对应的功能。但 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 CRForm());

}

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 CRForm());

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

窗体的消息处理函数探秘

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

internal class CRForm : Form

{

protected override void WndProc(ref Message m)

{

Console.WriteLine("CRForm 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 CRForm_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("CRForm 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 CRForm_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 CRForm());

}

}

internal class CRForm : Form

{

/// <summary>

/// 定义用于改变窗体显示状态热键的标识符。

/// </summary>

const int ChangeVisibleHotKeyId = 1;

public CRForm()

{

this.Load += CRForm_Load;

this.FormClosed += CRForm_FormClosed;

}

private void CRForm_FormClosed(object sender, FormClosedEventArgs e)

{

NativeMethods.UnregisterHotKey(this.Handle, ChangeVisibleHotKeyId);

}

private void CRForm_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("CRForm 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

[本文作者:张赐荣]

C#Winform 注册使用全局快捷键详解的更多相关文章

  1. C# WinForm 中 MessageBox的使用详解

    1.C# WinForm 中 MessageBox的使用详解:http://www.cnblogs.com/bq-blog/archive/2012/07/27/2611810.html

  2. Navicat Mac 快捷键详解

    Navicat 是数据库管理工具,满足了大家对数据库的存储过程.事件.触发器.函数.视图等功能,并且支持MySQL.MariaDB.SQL Server.SQLite.Oracle 和 Postgre ...

  3. Admin注册和路由分发详解

    Admin注册和路由分发详解 1.启动 #autodiscover_modules('admin', register_to=site) 2.注册 1.单例对象 admin.site = AdminS ...

  4. 在Form Load中设置showInTaskBar =false 或 隐藏窗口 this.Hide()时会导致注册的全局快捷键无效

    在Form Load中设置showInTaskBar =false   或 隐藏窗口 this.Hide() 会导致注册的全局快捷键无效.  反正是其中一个,有点记不清了. 在Form Shown中s ...

  5. winform打包发布安装包详解..

    winform打包发布安装包详解..   使用VS 自带的打包工具,制作winform安装项目 开发环境:VS 2008 Access 操作系统:Windows XP 开发语言:C# 项目名称:**管 ...

  6. MyBatis 全局配置文件详解(七)

    MyBatis 配置文件作用 MyBatis配置文件包含影响 MyBatis 框架正常使用的功能设置和属性信息.它的作用好比手机里的设置图标,点击这个图标就可以帮助我们查看手机的属性信息和设置功能.其 ...

  7. 【Android应用开发】Android Studio - MAC 版 - 快捷键详解

    博客地址 : http://blog.csdn.net/shulianghan/article/details/47321177 作者 : 韩曙亮 要点总结 : -- 熟练使用快捷键 : 在任何编程环 ...

  8. 转载:Chrome调试折腾记_(1)调试控制中心快捷键详解!!!

    转载:http://blog.csdn.net/crper/article/details/48098625 大多浏览器的调试功能的启用快捷键都一致…按下F12;还是熟悉的味道;  或者直接 Ctrl ...

  9. Chrome调试折腾记_(1)调试控制中心快捷键详解!!!

    转载:http://blog.csdn.net/crper/article/details/48098625 大多浏览器的调试功能的启用快捷键都一致-按下F12;还是熟悉的味道;  或者直接 Ctrl ...

随机推荐

  1. Linux上天之路(六)之Linux文件管理

    文件与文件夹的操作 1) 新建 2)改名 3)查看 4)删除 5)拷贝 6)移动 1. 文件的操作 文件的新建:touch filename 文件的改名:mv 文件的查看:ls 文件内容的查看:cat ...

  2. Go语言系列之标准库log

    Go语言内置的log包实现了简单的日志服务.本文介绍了标准库log的基本使用. 使用Logger log包定义了Logger类型,该类型提供了一些格式化输出的方法.本包也提供了一个预定义的" ...

  3. Spark应用的结构的学习

    关注公众号:分享电脑学习回复"百度云盘" 可以免费获取所有学习文档的代码(不定期更新) 承接上一篇文档<Standalone集群搭建和Spark应用监控> 需要了解的概 ...

  4. Maven+ajax+SSM实现编辑修改

    转载自:https://www.cnblogs.com/kebibuluan/p/9017754.html 3.尚硅谷_SSM高级整合_使用ajax操作实现修改员工的功能 当我们点击编辑案例的时候,我 ...

  5. C#进程调用FFmpeg操作音视频

    项目背景 因为公司需要对音视频做一些操作,比如说对系统用户的发音和背景视频进行合成,以及对多个音视频之间进行合成,还有就是在指定的源背景音频中按照对应的规则在视频的多少秒钟内插入一段客户发音等一些复杂 ...

  6. 小白也能看懂的Redis教学基础篇——做一个时间窗限流就是这么简单

    不知道ZSet(有序集合)的看官们,可以翻阅我的上一篇文章: 小白也能看懂的REDIS教学基础篇--朋友面试被SKIPLIST跳跃表拦住了 书接上回,话说我朋友小A童鞋,终于面世通过加入了一家公司.这 ...

  7. CentOS 8安装Docker报错(Problem: package docker-ce-3:19.03.8-3.el7.x86_64 requires containerd.io >= 1.2.)

    CentOS8安装docker和docer-conpose 报错如下Problem: package docker-ce-3:19.03.8-3.el7.x86_64 requires contain ...

  8. tomcat容器启动失败疑难问题解决方案

    严重: 子容器启动失败java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: 初始化组件[or ...

  9. 在DigitalOcean vps中安装vnstat监控流量,浏览器打开php代码。。。

    由于DigitalOcean中没有发现可以观察已用流量的功能,有想知道自己的流量使用情况,所以安装了vnstat. 安装过程十分简单,见百度经验,官方主页等. 1.安装完vnstat后,直接命令vns ...

  10. 字符串自实现(一)(mystrcpy,mystrcat,mystrcmp)

    char* mystrcpy(char* str_one,const char* str_two) { char* tmp = str_one; while (*str_one++ = *str_tw ...