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

 

[本文作者:張賜榮]