
五. 處理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()
新聞熱點
疑難解答