windows桌面編程--監聽全局鍵盤鼠標事件

2020-12-29

關鍵字:.NET framework、.NET CORE、.NET、WPF、windows forms、SetWindowsHookEx、鉤子函數


 

1、如何捕獲鍵鼠事件?

 

在windows桌面編程中,要想捕獲應用內的鍵鼠事件還是非常簡單的。直接在XAML上對應window或控件的對應事件上註冊回調就可以了。

 

但全局鍵鼠事件就沒這麼容易了。

 

全局鍵鼠事件需要用到“鉤子函數”--向系統註冊一個自己的鉤子函數以“鉤取”來自底層的鍵鼠事件。

 

這個關鍵的向系統註冊鉤子函數的API原型如下:

HHOOK SetWindowsHookExA(
  int       idHook,
  HOOKPROC  lpfn,
  HINSTANCE hmod,
  DWORD     dwThreadId
);

這是一段C++代碼。我們來解讀一下這個函數。

 

早期的Windows軟件開發用的是C++,即使現在微軟主推 .NET 也依然有很多系統級功能和接口是用非C#語言實現的,正如這個註冊鉤子函數的系統接口。

 

不過我們大可不必擔心C#和C++兩門語言之間的兼容性問題,微軟早就搞定一切了。只需要我們老老實實引入相應的DLL和聲明函數原型就可以直接調用了。

 

另外,SetWindowsHookEx 函數似乎是有兩個變種:SetWindowsHookExA 與 SetWindowsHookExW 。這三者之間是完全通用的,一般直接寫 SetWindowsHookEx 即可。

 

回到這個接口的原型解讀。它的返回值我們理解成是一個指針變量就可以了,當我們成功向系統註冊鉤子函數時會返回一個地址用於標識我們的鉤子。這個返回值最好好好保存,因爲在註銷鉤子函數時需要用到。如果實在是因爲“不小心”弄丟了返回值,也不要緊。系統會在你退出應用時註銷你的鉤子函數的。

 

接下來看看它的四個參數。

參數1:idHook。表示我們需要鉤取哪種類型的事件。數值13表示全局鍵盤事件,數值14表示全局鼠標事件,其它事件值不在本文討論範圍內,有需要的同學請自行查閱官方文檔。

參數2:lpfn。在C#中就是一個委託類型值,填入要註冊的鉤子函數名。具體的委託類型會在後面說明。

參數3:hmod。無須過多理會,表示持有鉤子函數的進程號,填0再強轉爲IntPtr即可。

參數4:dwThreadId。無須過多理會,直接填0即可。

 

這個接口更詳細的解釋還得查閱微軟官方文檔,相關鏈接如下:

  https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa 

 

上述參數2的鉤子函數類型在C#中的委託原型如下所示:

public delegate int HookProc(int code, IntPtr wParam, IntPtr lParam);

只要是使用 SetWindowsHookEx 註冊的鉤子函數都可以使用這種形式。

 

鉤子函數的原理大致是當鍵鼠設備產生了一個事件後首先上報到驅動,再上報到系統層,系統層會檢測是否有應用註冊了相應鉤子函數,如果有,則將事件逐個回調給應用,待應用處理完後再根據其返回值來決定事件的後續處理方式。因此,千萬不要在鉤子函數內做耗時操作,否則系統會因爲事件傳遞過程被阻塞而出問題的,聽說嚴重的情況下系統會主動註銷你的鉤子。

 

2、捕獲全局鍵盤事件

 

全局鍵盤事件的 idHook 值是13,其實還有另一個值2也表示鍵盤事件。但數值2的會多出一些限制,導致部分情況下的鍵盤事件接收不到,因爲我們都是直接使用數值13的。

 

接下來要重點討論的就是鍵盤事件的鉤子函數的定義了。

 

鍵盤事件鉤子函數的詳細說明可以查閱下方鏈接:

  https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644985(v=vs.85)

 

這裏作個簡要的中文解釋。其原型如下所示:

LRESULT CALLBACK LowLevelKeyboardProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

這同樣是個C++函數,不過同樣不要緊。

 

首先它的返回值是一個整型數。返回數值0表示允許該事件繼續傳播,返回大於0的數表示此事件到此爲止,其它尚未接收到該事件的鉤子或系統函數將不再能接收到了。同時,微軟還重點指出,如果回調函數中的參數 nCode 的值小於0,則必須將這一事件交由 CallNextHookEx 函數去處理,並返回這一函數的返回值。

 

其次是它的參數。

參數1:nCode。事件狀態碼,當值爲0時處理按鍵事件,小於0時最好將事件交由 CallNextHookEx 函數處理。

參數2:wParam。按鍵事件碼,有四個可能值:1、普通鍵按下:0x100;2、普通鍵擡起:0x101;3、系統鍵按下:0x104;4、系統鍵擡起:0x105。在本文中我們只需關心前兩個事件。

參數3:lParam。事件詳細信息結構體的地址,下面展開聊聊。

 

上述參數3 lParam 所指向的結構體原型如下:

typedef struct tagKBDLLHOOKSTRUCT {
  DWORD     vkCode;
  DWORD     scanCode;
  DWORD     flags;
  DWORD     time;
  ULONG_PTR dwExtraInfo;
} KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

vkCode 表示按鍵碼,即被按下按鍵的鍵碼。其值有效範圍爲 1 ~ 254。具體的鍵值對應關係參見: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes

 

scanCode 是掃描碼,本文不關心這一數值。

 

flags 用於記載一些額外信息,本文同樣不關心這一數值。

 

time 則是事件的發生時間,單位爲毫秒,表示的是系統啓動以來的相對時間值。

 

dwExtraInfo 是額外信息,無須關心。

 

3、捕獲全局鼠標事件

 

全局鼠標事件與全局鍵盤事件幾無差別。這裏主要聊聊鉤子函數的委託類型。它的原型定義如下:

LRESULT CALLBACK LowLevelMouseProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

函數返回值與參數nCode與上一節鍵盤鉤子函數一樣。

 

參數 wParam 表示鼠標事件類型。幾個主要的數值如下表所示:

鼠標移動 0x200
鼠標左鍵按下 0x201
鼠標左鍵擡起 0x202
鼠標右鍵按下 0x204
鼠標右鍵擡起 0x205
鼠標滾輪滾動 0x20a
鼠標側鍵按下 0x20b
鼠標側健擡起 0x20c
鼠標水平滾輪滾動 0x20e
   

參數 lParam 同樣是事件詳細信息結構體的地址,該結構體的原型如下所示:

typedef struct tagMSLLHOOKSTRUCT {
  POINT     pt;
  DWORD     mouseData;
  DWORD     flags;
  DWORD     time;
  ULONG_PTR dwExtraInfo;
} MSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;

成員 pt 表示事件的座標值結構體,其原型如下所示:

typedef struct tagPOINT {
  LONG x;
  LONG y;
} POINT, *PPOINT;

需要注意的是,雖然它被聲明爲 'LONG' 類型,但它實際上只有4個字節長度。另外,如果你對結構體的瞭解足夠深刻,一定能理解在實際開發中直接用一個 long 型來替代 POINT 類型是完全可行的,只需要知道Windows桌面編程使用的是小端序就可以了,當然,如果你理解不了這句話,那老老實實再創建一個POINT結構體來套進去就是了。

 

成員mouseData 不太需要關注。當事件是滾輪滾動時,它的高16位記錄的是滾動方向及距離。正值表示遠離用戶的滾動,負值表示靠近用戶的滾動,其數值恆定爲120,可以理解爲表示一格滾動。

 

成員 flags 不需要理會。

 

成員 time 表示事件發生時間,單位爲毫秒,自系統啓動以來的相對時間值。

 

成員 dwExtraInfo 不需要理會。 

 

4、實現

 

本小節我們直接貼上一個示例代碼,用於捕獲Windows系統的全局鍵鼠事件。

 

我們的需求是實現一個應用,其中有兩個按鈕,一個用於註冊鉤子事件,另一個用於註銷鉤子事件,使用的框架是 .NET core 3.1,軟件界面如下圖所示:

 

程序運行並註冊鉤子函數後操作鼠標時的打印信息如下:

 

操作鍵盤後的打印信息如下:

 

 

具體的源碼如下所示:

using System;
using System.Runtime.InteropServices;
using System.Windows;


namespace KMHook
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        internal struct Keyboard_LL_Hook_Data
        {
            public UInt32 vkCode;
            public UInt32 scanCode;
            public UInt32 flags;
            public UInt32 time;
            public IntPtr extraInfo;
        }

        internal struct Mouse_LL_Hook_Data
        {
            internal long yx;
            internal readonly int mouseData;
            internal readonly uint flags;
            internal readonly uint time;
            internal readonly IntPtr dwExtraInfo;
        }

        private static IntPtr pKeyboardHook = IntPtr.Zero;
        private static IntPtr pMouseHook = IntPtr.Zero;
        //鉤子委託聲明
        public delegate int HookProc(int code, IntPtr wParam, IntPtr lParam);
        private static HookProc keyboardHookProc;
        private static HookProc mouseHookProc;

        //安裝鉤子
        [DllImport("user32.dll")]
        public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr pInstance, int threadID);
        //卸載鉤子
        [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern bool UnhookWindowsHookEx(IntPtr pHookHandle);
        [DllImport("user32.dll")]
        public static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); //parameter 'hhk' is ignored.

        private static int keyboardHookCallback(int code, IntPtr wParam, IntPtr lParam)
        {
            if (code < 0)
            {
                return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
            }

            Keyboard_LL_Hook_Data khd = (Keyboard_LL_Hook_Data)Marshal.PtrToStructure(lParam, typeof(Keyboard_LL_Hook_Data));
            System.Diagnostics.Debug.WriteLine($"key event:{wParam}, key code:{khd.vkCode}, event time:{khd.time}");

            return 0;
        }

        private static int mouseHookCallback(int code, IntPtr wParam, IntPtr lParam)
        {
            if (code < 0)
            {
                return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
            }

            Mouse_LL_Hook_Data mhd = (Mouse_LL_Hook_Data)Marshal.PtrToStructure(lParam, typeof(Mouse_LL_Hook_Data));
            System.Diagnostics.Debug.WriteLine($"mouse event:{wParam}, ({mhd.yx & 0xffffffff},{mhd.yx >> 32})");

            return 0;
        }

        internal static bool InsertHook()
        {
            bool iRet;
            iRet = InsertKeyboardHook();
            if (!iRet)
            {
                return false;
            }

            iRet = InsertMouseHook();
            if (!iRet)
            {
                removeKeyboardHook();
                return false;
            }

            return true;
        }

        //安裝鉤子方法
        private static bool InsertKeyboardHook()
        {
            if (pKeyboardHook == IntPtr.Zero)//不存在鉤子時
            {
                //創建鉤子
                keyboardHookProc = keyboardHookCallback;
                pKeyboardHook = SetWindowsHookEx(13, //13表示全局鍵盤事件。
                    keyboardHookProc,
                    (IntPtr)0,
                    0);

                if (pKeyboardHook == IntPtr.Zero)//如果安裝鉤子失敗
                {
                    removeKeyboardHook();
                    return false;
                }
            }

            return true;
        }

        private static bool InsertMouseHook()
        {
            if (pMouseHook == IntPtr.Zero)
            {
                mouseHookProc = mouseHookCallback;
                pMouseHook = SetWindowsHookEx(14, //14表示全局鼠標事件
                    mouseHookProc,
                    (IntPtr)0,
                    0);

                if (pMouseHook == IntPtr.Zero)
                {
                    removeMouseHook();
                    return false;
                }
            }

            return true;
        }

        internal static bool RemoveHook()
        {
            bool iRet;
            iRet = removeKeyboardHook();
            if (iRet)
            {
                iRet = removeMouseHook();
            }

            return iRet;
        }

        private static bool removeKeyboardHook()
        {
            if (pKeyboardHook != IntPtr.Zero)
            {
                if (UnhookWindowsHookEx(pKeyboardHook))
                {
                    pKeyboardHook = IntPtr.Zero;
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        private static bool removeMouseHook()
        {
            if (pMouseHook != IntPtr.Zero)
            {
                if (UnhookWindowsHookEx(pMouseHook))
                {
                    pMouseHook = IntPtr.Zero;
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        private void Button_Install_Click(object sender, RoutedEventArgs e)
        {
            InsertHook();
        }

        private void Button_Remove_Click(object sender, RoutedEventArgs e)
        {
            RemoveHook();
        }
    }
}

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章