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

來源:互聯網
上載者:User
window   一. 簡介

  貓和嬰兒有很多共同之處。他們都喜歡吃家中養植的植物,都非常討厭關門。他們也都愛玩弄你的鍵盤,結果是,你正發送給你的老闆的電子郵件可能是以半截句子發送出去的,你的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類的執行個體時),如下所示:

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
  既然KeyboardHook實現IDisposable,那麼這可以在Dispose方法中完成。

public void Dispose()



相關文章

E-Commerce Solutions

Leverage the same tools powering the Alibaba Ecosystem

Learn more >

11.11 Big Sale for Cloud

Get Unbeatable Offers with up to 90% Off,Oct.24-Nov.13 (UTC+8)

Get It Now >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。