C#Winform 註冊使用全局快捷鍵詳解
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
//docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
UnregisterHotKey
//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
//docs.microsoft.com/zh-cn/windows/win32/learnwin32/window-messages
RegisterHotKey function (winuser.h) – Win32 apps | Microsoft Docs
//docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
UnregisterHotKey function (winuser.h) – Win32 apps | Microsoft Docs
//docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey
WM_HOTKEY 消息 (Winuser.h) – Win32 apps | Microsoft Docs
//docs.microsoft.com/zh-cn/windows/win32/inputdev/wm-hotkey
Application 類 (System.Windows.Forms) | Microsoft Docs
//docs.microsoft.com/zh-cn/dotnet/api/system.windows.forms.application?view=netframework-4.8
[本文作者:張賜榮]