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. SYCOJ#111、吉祥物

    题目-吉祥物 (shiyancang.cn) 1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,x; 4 int pos(i ...

  2. JAVA8-STREAM 使用说明

    概述 本人在java开发过程中,有些知识点需要记录整理,我尽量严谨的叙述我学习的经过和心得,以便备份和和大家一起进步学习,此篇文章是在网上多出搜集整理验证,结尾会注明出处,今天学习一个java8新的功 ...

  3. k8s的应用包管理工具helm的部署和使用

    1.概述 我们一般是在k8s里面部署一些简单的应用,比如用deployment,daemonset,statefuleset的方式来部署应用,但是如果要部署一些复杂的应用,那么整个配置的编写.部署的过 ...

  4. 前端——JSON学习总结

    学习网址: https://www.bilibili.com/video/BV1Pt411u7R3 什么是JSON?(以下有关概念内容为视频中学习文档相关内容,代码为本人学习时使用的有关代码) JSO ...

  5. 【转载】ASP.NET 内联代码、内联表达式、数据绑定表达式使用方法罗列(形式就是常说的尖括号 百分号 等于号 井号)

    ASP.NET 内联代码.内联表达式.数据绑定表达式使用方法罗列(形式就是常说的尖括号 百分号 等于号 井号) 今天在做渭南电脑维修网的一个小功能时遇到了一些问题,因此特别列出,以备他日之用. 首先对 ...

  6. Servlet Listener(监听器)

    监听器 Listener 是一个实现特定接口的 Java 程序,这个程序专门用于监听另一个 Java 对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即自动执行.监听器的相关概 ...

  7. JS、jQuery 刷新 iframe 的方法

    1.JavaScript 刷新 iframe 可以使用以下方法: document.getElementById('some_frame_id').contentWindow.location.rel ...

  8. 为什么ConcurrentHashMap是线程安全的?

    ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题.数据覆盖等问题.而这些问题,只要使用 ConcurrentHashMap ...

  9. ansible command和shell的区别

    1.command模块不支持管道符和变量等,如果要使用这些,需要shell模块. 2.在使用ansible中的时候,默认的模块是-m command,从而模块的参数不需要填写,直接使用即可

  10. 如何使用iconfont的CDN

    如何使用iconfont的CDN iconfont作为阿里的图标库,在开发过成功用的已经是非常广泛了,但iconfont并不需要将图标下载后使用,而是可以直接用cdn引入使用,至于使用流程,请看下文. ...