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