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