C# + 低級Windows API鉤子攔截鍵盤輸入

 





  一. 簡介



  貓和嬰兒有很多共同之處。他們都喜歡喫家中養植的植物,都非常討厭關門。他們也都愛玩弄你的鍵盤,結果是,你正發送給你的老闆的電子郵件可能是以半截句子發送出去的,你的Excel帳戶也被加入了一些亂七八糟的內容,並且你還沒有注意到,當打開Windows資源管理器時,若干文件已經被移到了回收站!



  其解決方案是,開發一個應用程序實現如下功能:只要鍵盤處於"威脅狀態"你就可以進行切換,並確保任何鍵盤輸入活動都不會造成危害。本文想展示如何使用一種低級Windows API鉤子在一個C#應用程序中實現鍵盤"控制"。下圖是本文示例程序的一個運行快照。





  二. 背景



  其實,已經存在許多有關於Windows鉤子的文章和示例代碼,並且已經有人編寫過與本文幾乎一樣的C++示例程序。然而,當我搜索相應的C#應用程序的源碼時,卻找到極少的.NET示例,而且沒有一個程序能夠提供一個方便的自包含的C#類。



  .NET框架能夠使你以託管方式來存取你最常使用的鍵盤事件(通過KeyPress,KeyUp和KeyDown)。遺憾的是,這些事件都不能被用來停止Windows組合鍵(如Alt+Tab或Windows"開始"鍵),從而允許用戶"遠離"某一個應用程序。



  本文的想法在操作系統級上捕獲鍵盤事件而不是通過框架級來實現。爲此,應用程序需要使用Windows API函數來把它自身添加到應用程序"鉤子鏈"中以監聽來自操作系統的鍵盤消息。當它收到這種類型的消息時,該應用程序能夠選擇性地傳遞消息,或者進行正常處理,或者"鎮壓"它以便不再有其它應用程序(包括Windows)來影響它。本文正是想解釋其實現機理。



  然而,請注意,本文中的代碼僅適用於基於NT版本的Windows(NT,2000和XP),並且無法使用這個方法來停用Ctrl+Alt+Delete。有關於如何實現這一點,你可以參考MSDN有關資料。



  三. 使用代碼



  爲了易於使用,我在本文中提供了兩個獨立的zip文件。一個僅包含KeyboardHook類,這是本文介紹的重點。另一個是一個完整的微軟Visual C# 2005 Express Edition應用程序工程,名叫"Baby Keyboard Bash",它實現顯示擊鍵的名字或彩色的形狀以響應於擊鍵。



  四. 實例化類



  鍵盤鉤子是通過keyboard.cs中的KeyboardHook類來建立和管理的。這個類實現了IDisposable接口,因此,實例化它的最簡單的方法是在應用程序的Main()方法中使用using關鍵字來封裝Application.Run()調用。這將確保只要該應用程序開始即建立鉤子並且,更重要的是,當該應用程序結束時立即使這個鉤子失效。



  這個類引發一個事件來警告應用程序已經有鍵被按下,因此主表單能夠存取在Main()方法中創建的KeyboardHook實例就顯得非常重要;最簡單的方法是把這個實例存儲在一個公共成員變量中。



  KeyboardHook提供了三種構造器來啓用或禁用某些設置:



  €€ KeyboardHook():捕獲所有擊鍵,沒有任何內容傳遞到Windows或另外的應用程序。



  €€ KeyboardHook(string param):把參數串轉換爲Parameters枚舉中的值之一,然後調用下面的構造器:



  €€ KeyboardHook(KeyboardHook.Parameters enum):根據從Parameters枚舉中選擇的值的不同,分別啓動下列設置:



   o Parameters.AllowAltTab:允許用戶使用Alt+Tab切換到另外的應用程序。



   o Parameters.AllowWindowsKey:允許用戶使用Ctrl+Esc或一種Windows鍵存取任務欄和開始菜單。



   o Parameters.AllowAltTabAndWindows:啓用Alt+Tab,Ctrl+Esc和Windows鍵。



   o Parameters.PassAllKeysToNextApp:如果該參數爲true,那麼所有的擊鍵將被傳遞給任何其它監聽應用程序(包括Windows)。



  當擊鍵繼續被鍵盤鉤子捕獲時,啓用Alt+Tab和/或Windows鍵允許實際使用該計算機者切換到另一個應用程序並且使用鼠標與之交互。PassAllKeysToNextApp設置有效地禁用了擊鍵捕獲;這個類也是建立一個低級鍵盤鉤子並且引發它的KeyIntercepted事件,但是它還負責把鍵盤事件傳遞到另一個監聽程序。



  因此,實例化該類以捕獲所有擊鍵的方法如下:



public static KeyboardHook kh;

[STAThread]

static void Main()

{

 //其它代碼

 using (kh = new KeyboardHook())

 {

  Application.Run(new Form1());

 }

  五. 處理KeyIntercepted事件



  當一外鍵被按下時,這個KeyboardHook類激活一個包含一些KeyboardHookEventArgs的KeyIntercepted事件。這是通過一個KeyboardHookEventHandler類型的方法使用以下方式來實現的:



kh.KeyIntercepted += new KeyboardHook.KeyboardHookEventHandler(kh_KeyIntercepted);

  這個KeyboardHookEventArgs返回關於被按下鍵的下列信息:



  €€ KeyName:鍵名,通過把捕獲的鍵代碼強制轉換爲System.Windows.Forms.Keys而獲得。



  €€ KeyCode:由鍵盤鉤子返回的原來的鍵代碼



  €€ PassThrough:指出是否這個KeyboardHook實例被配置以允許該擊鍵傳遞到其它應用程序。如果你想允許一用戶使用Alt+Tab或 Ctrl+Esc/Windows鍵切換到其它的應用程序的話,那麼對之進行檢查是很有用的。 



  然後,使用一個具有適當簽名的方法來執行擊鍵所調用的任何任務。下面是一個示例片斷:



void kh_KeyIntercepted(KeyboardHookEventArgs e)

{

 //檢查是否這個鍵擊事件被傳遞到其它應用程序並且停用TopMost,以防他們需要調到前端

 if (e.PassThrough)

 {

  this.TopMost = false;

 }

 ds.Draw(e.KeyName);

}

  本文的剩下部分將解釋低級鍵盤鉤子是如何在KeyboardHook中實現的。



  六. 實現一個低級Windows API鍵盤鉤子



  在user32.dll中,Windows API包含三個方法來實現此目的:



  €€ SetWindowsHookEx,它負責建立鍵盤鉤子



  €€ UnhookWindowsHookEx,它負責移去鍵盤鉤子



  €€ CallNextHookEx,它負責把擊鍵信息傳遞到下一個監聽鍵盤事件的應用程序



  創建一個能夠攔截鍵盤的應用程序的關鍵是,實現前面兩個方法,而"放棄"第三個。結果是,任何擊鍵都只能傳遞到這個應用程序中。



  爲了實現這一目標,第一步是包括System.Runtime.InteropServices命名空間並且導入API方法,首先是SetWindowsHookEx:



using System.Runtime.InteropServices

...

//在類內部:

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]

private static extern IntPtr SetWindowsHookEx(int idHook,

LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

  導入UnhookWindowsHookEx和CallNextHookEx的代碼請見後面的討論。



  下一步是調用SetWindowsHookEx來建立鉤子,這時需要傳遞下列四個參數:



  €€ idHook:



  這個數字決定了要建立的鉤子的類型。例如,SetWindowsHookEx可以被用於鉤住鼠標事件(當然還有其它事件)。在本文情況下,我們僅對13有興趣,這是鍵盤鉤子的id。爲了使代碼更易讀些,我們把它賦值給一個常數WH_KEYBOARD_LL。



  €€ Lpfn:



  這是一個指向函數的長指針,該函數將負責處理鍵盤事件。在C#中,"指針"是通過傳遞一個代理類型的實例而獲得的,從而使之引用一個適當的方法。這是我們在每次使用鉤子時所調用的方法。



  這裏值得注意的是,這個代理實例需要被存儲於這個類的一個成員變量中。這是爲了防止一旦第一個方法調用結束它會被作爲垃圾回收。



  €€ hMod:



  建立鉤子的應用程序的一個實例句柄。我找到的絕大多數實例僅把它設置爲IntPtr.Zero,理由是不大可能存在該應用程序的多個實例。然而,這部分代碼使用了來自於kernel32.dll的GetModuleHandle來標識準確的實例從而使這個類更具靈活性。



  €€ dwThreadId:



  當前進程的id。把它設置爲0可以使這個鉤子成爲全局構子,這是相應於一個低級鍵盤鉤子的正確設置。



  SetWindowsHookEx返回一個鉤子id,這個id將被用於當應用程序結束時從鉤子鏈中脫鉤,因此它需要存儲在一個成員變量中以備將來使用。KeyboardHook類中的相關代碼如下:



private HookHandlerDelegate proc;

private IntPtr hookID = IntPtr.Zero;

private const int WH_KEYBOARD_LL = 13;

public KeyboardHook()

{

 proc = new HookHandlerDelegate(HookCallback);

 using (Process curProcess = Process.GetCurrentProcess())

 using (ProcessModule curModule = curProcess.MainModule)

 {

  hookID = SetWindowsHookEx(WH_KEYBOARD_LL, proc,GetModuleHandle(curModule.ModuleName), 0);

 }

} 



 



  七. 處理鍵盤事件



  如前面所提及,SetWindowsHookEx需要一個到被用來處理鍵盤事件的回調函數的指針。它期望有一個使用如下簽名的函數:



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

  其實,建立一個函數指針的C#方法使用了一個代理,因此,向SetWindowsHookEx指出它需要的內容的第一步是使用正確的簽名來聲明一個代理:



private delegate IntPtr HookHandlerDelegate(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);

  然後,使用相同的簽名編寫一個回調方法;這個方法將包含實際上處理鍵盤事件的所有代碼。在KeyboardHook的情況下,它檢查是否擊鍵應該被傳遞給其它應用程序並且接下來激發KeyIntercepted事件。下面是一個簡化版本的不帶有擊鍵處理代碼的情況:



private const int WM_KEYDOWN = 0x0100;

private const int WM_SYSKEYDOWN = 0x0104; 

private IntPtr HookCallback(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)

{

 //僅爲KeyDown事件過濾wParam,否則該代碼將再次執行-對於每一次擊鍵(也就是,相應於KeyDown和KeyUp)

 //WM_SYSKEYDOWN是捕獲Alt相關組合鍵所必需的

 if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))

 {

  //激發事件

  OnKeyIntercepted(new KeyboardHookEventArgs(lParam.vkCode, AllowKey));

  //返回一個"啞"值以捕獲擊鍵

  return (System.IntPtr)1;

 }

 //事件沒有被處理,把它傳遞給下一個應用程序

 return CallNextHookEx(hookID, nCode, wParam, ref lParam);

}

  接下來,一個到HookCallback的參考被指派給HookHandlerDelegate的一個實例並且被傳遞到SetWindowsHookEx的調用,正如前一節所展示的。



  無論何時一個鍵盤事件發生,下列參數將被傳遞給HookCallBack:



  €€ nCode:



  根據MSDN文檔,回調函數應該返回CallNextHookEx的結果,如果這個值小於零的話。正常的鍵盤事件將返回一個大於或等於零的nCode值。



  €€ wParam:



  這個值指示發生了什麼類型的事件:鍵被按下還是鬆開,以及是否按下的鍵是一個系統鍵(左邊或右邊的Alt鍵)。



  €€ lParam:



  這是一個存儲精確擊鍵信息的結構,例如被按鍵的代碼。在KeyboardHook中聲明的這個結構如下:



private struct KBDLLHOOKSTRUCT

{ 

 public int vkCode;

 int scanCode;

 public int flags;

 int time;

 int dwExtraInfo;

}

  其中的這兩個公共參數是在KeyboardHook中的回調方法所使用的僅有的兩個參數。vkCoke返回虛擬鍵代碼,它能夠被強制轉換爲System.Windows.Forms.Keys以獲得鍵名,而flags顯示是否這是一個擴展鍵(例如,Windows Start鍵)或是否同時按下了Alt鍵。有關於Hook回調方法的完整代碼展示在每一種情況下要檢查哪些flags值。



  如果flags提供的信息和KBDLLHOOKSTRUCT的其它組成元素不需要,那麼這個回調方法和代碼的簽名可以按如下進行修改:



private delegate IntPtr HookHandlerDelegate(

int nCode, IntPtr wParam, IntPtr lParam);

  在這種情況中,lParam將僅返回vkCode。 



 



  八. 把擊鍵傳遞到下一個應用程序



  一個良好的鍵盤鉤子回調方法應該以調用CallNextHookEx函數並且返回它的結果結束。這可以確保其它應用程序能夠有機會處理針對於它們的擊鍵。



  然而,KeyboardHook類的主要功能在於,阻止擊鍵被傳播到任何其它更多的應用程序。因此它無論在何時處理一次擊鍵,HookCallback都將返回一個啞值:



return (System.IntPtr)1;

  另一方面,它確實調用CallNextHookEx-如果它不處理該事件,或如果重載的構造器中的使用KeyboardHook傳遞的參數允許某些組合鍵通過。



  CallNextHookEx被啓用-通過從user32.dll導入該函數,如下列代碼所示:



[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]

private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,

IntPtr wParam, ref KeyInfoStruct lParam);

  然後,被導入的方法被HookCallMethod所調用,這可以確保所有的通過鉤子接收到的參數被繼續傳遞到下一個應用程序中:



CallNextHookEx(hookID, nCode, wParam, ref lParam);

  如前面所提及,如果在lParam中的flags是不相關的,那麼可以修改導入的CallNextHookEx的簽名以把lParam定義爲System.IntPtr。



  九. 移去鉤子



  處理鉤子的最後一步是使用從user32.dll中導入的UnhookWindowsHookEx函數移去它(當破壞KeyboardHook類的實例時)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章