浅谈HOOK
摘要: 本文針對HOOK技術在VC編程中的應用進行討論,并著重對應用比較廣泛的全局HOOK做了闡述。
一、引言
Windows操作系統是建立在事件驅動機制之上的,系統各部分之間的溝通也都是通過消息的相互傳遞而實現的。但在通常情況下,應用程序只能處理來自進程內部的消息或是從其他進程發過來的消息,如果需要對在進程外傳遞的消息進行攔截處理就必須采取一種被稱為HOOK(鉤子)的技術。鉤子是Windows操作系統中非常重要的一種系統接口,用它可以輕松截獲并處理在其他應用程序之間傳遞的消息,并由此可以完成一些普通應用程序難以實現的特殊功能。基于鉤子在消息攔截處理中的強大功能,本文即以VC++ 6.0為編程背景對鉤子的基本概念及其實現過程展開討論。為方便理解,在文章最后還給出了一個簡單的有關鼠標鉤子的應用示例。
二、鉤子的基本原理
鉤子的本質是一段用以處理系統消息的程序,通過系統調用,將其掛入到系統。鉤子的種類有很多,每一種鉤子負責截獲并處理相應的消息。鉤子機制允許應用程序截獲并處理發往指定窗口的消息或特定事件,其監視的窗口即可以是本進程內的也可以是由其他進程所創建的。在特定的消息發出,并在到達目的窗口之前,鉤子程序先行截獲此消息并得到對其的控制權。此時在鉤子函數中就可以對截獲的消息進行各種修改處理,甚至強行終止該消息的繼續傳遞。
任何一個鉤子都由系統來維護一個指針列表(鉤子鏈表),其指針指向鉤子的各個處理函數。最近安裝的鉤子放在鏈的開始,最早安裝的鉤子則放在最后,當鉤子監視的消息出現時,操作系統調用鏈表開始處的第一個鉤子處理函數進行處理,也就是說最后加入的鉤子優先獲得控制權。在這里提到的鉤子處理函數必須是一個回調函數(callback function),而且不能定義為類成員函數,必須定義為普通的C函數。在使用鉤子時可以根據其監視范圍的不同將其分為全局鉤子和線程鉤子兩大類,其中線程鉤子只能監視某個線程,而全局鉤子則可對在當前系統下運行的所有線程進行監視。顯然,線程鉤子可以看作是全局鉤子的一個子集,全局鉤子雖然功能強大但同時實現起來也比較煩瑣:其鉤子函數的實現必須封裝在動態鏈接庫中才可以使用。
鉤子的安裝與卸載
由于全局鉤子具有相當的廣泛性而且在功能上完全覆蓋了線程鉤子,因此下面就主要對應用較多的全局鉤子的安裝與使用進行討論。前面已經提過,操作系統是通過調用鉤子鏈表開始處的第一個鉤子處理函數而進行消息攔截處理的。因此,為了設置鉤子,只需將回調函數放置于鏈首即可,操作系統會使其首先被調用。在具體實現時由函數SetWindowsHookEx()負責將回調函數放置于鉤子鏈表的開始位置。SetWindowsHookEx()函數原型聲明如下:
HHOOK SetWindowsHookEx(int idHook;
HOOKPROC lpfn;
HINSTANCE hMod;
DWORD dwThreadId);
其中:參數idHook 指定了鉤子的類型,總共有如下13種:
WH_CALLWNDPROC 系統將消息發送到指定窗口之前的"鉤子"
WH_CALLWNDPROCRET 消息已經在窗口中處理的"鉤子"
WH_CBT 基于計算機培訓的"鉤子"
WH_DEBUG 差錯"鉤子"
WH_FOREGROUNDIDLE 前臺空閑窗口"鉤子"
WH_GETMESSAGE 接收消息投遞的"鉤子"
WH_JOURNALPLAYBACK 回放以前通過WH_JOURNALRECORD"鉤子"記錄的輸入消息
WH_JOURNALRECORD 輸入消息記錄"鉤子"
WH_KEYBOARD 鍵盤消息"鉤子"
WH_MOUSE 鼠標消息"鉤子"
WH_MSGFILTER 對話框、消息框、菜單或滾動條輸入消息"鉤子"
WH_SHELL 外殼"鉤子"
WH_SYSMSGFILTER 系統消息"鉤子"
參數lpfn為指向鉤子處理函數的指針,即回調函數的首地址;參數hMod則標識了鉤子處理函數所處模塊的句柄;第四個參數dwThreadId 指定被監視的線程,如果明確指定了某個線程的ID就只監視該線程,此時的鉤子即為線程鉤子;如果該參數被設置為0,則表示此鉤子為監視系統所有線程的全局鉤子。此函數在執行完后將返回一個鉤子句柄。
雖然對于線程鉤子并不要求其象全局鉤子一樣必須放置于動態鏈接庫中,但是推薦其也在動態鏈接庫中實現。因為這樣的處理不僅可使鉤子可為系統內的多個進程訪問,也可以在系統中被直接調用,而且對于一個只供單進程訪問的鉤子,還可以將其鉤子處理過程放在安裝鉤子的同一個線程內,此時SetWindowsHookEx()函數的第三個參數也就是該線程的實例句柄。
在SetWindowsHookEx()函數完成對鉤子的安裝后,如果被監視的事件發生,系統馬上會調用位于相應鉤子鏈表開始處的鉤子處理函數進行處理,每一個鉤子處理函數在進行相應的處理時都要考慮是否需要把事件傳遞給下一個鉤子處理函數。如果要傳遞,就通過函數CallNestHookEx()來解決。盡管如此,在實際使用時還是強烈推薦無論是否需要事件傳遞而都在過程的最后調用一次CallNextHookEx( )函數,否則將會引起一些無法預知的系統行為或是系統鎖定。該函數將返回位于鉤子鏈表中的下一個鉤子處理過程的地址,至于具體的返回值類型則要視所設置的鉤子類型而定。該函數的原型聲明如下:
LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);
其中,參數hhk為由SetWindowsHookEx()函數返回的當前鉤子句柄;參數nCode為傳給鉤子過程的事件代碼;參數wParam和lParam 則為傳給鉤子處理函數的參數值,其具體含義同設置的鉤子類型有關。
最后,由于安裝鉤子對系統的性能有一定的影響,所以在鉤子使用完畢后應及時將其卸載以釋放其所占資源。釋放鉤子的函數為UnhookWindowsHookEx(),該函數比較簡單只有一個參數用于指定此前由SetWindowsHookEx()函數所返回的鉤子句柄,原型聲明如下:
BOOL UnhookWindowsHookEx(HHOOK hhk);
三、鼠標鉤子的簡單示例
最后,為更清楚展示HOOK技術在VC編程中的應用,給出一個有關鼠標鉤子使用的簡單示例。在鉤子設置時采用的是全局鉤子。下面就對鼠標鉤子的安裝、使用以及卸載等過程的實現進行講述:
由于本例程需要使用全局鉤子,因此首先構造全局鉤子的載體--動態鏈接庫。考慮到 Win32 DLL與Win16 DLL存在的差別,在Win32環境下要在多個進程間共享數據,就必須采取一些措施將待共享的數據提取到一個獨立的數據段,并通過def文件將其屬性設置為讀寫共享:
#pragma data_seg("TestData")
HWND glhPrevTarWnd=NULL; // 窗口句柄
HWND glhHook=NULL; // 鼠標鉤子句柄
HINSTANCE glhInstance=NULL; // DLL實例句柄
#pragma data_seg()
……
SECTIONS // def文件中將數據段TestData設置為讀寫共享
TestData READ WRITE SHARED
在安裝全局鼠標鉤子時使用函數SetWindowsHookEx(),并設定鼠標鉤子的處理函數為MouseProc(),安裝函數返回的鉤子句柄保存于變量glhHook中:
void StartHook(HWND hWnd)
{
……
glhHook=(HWND)SetWindowsHookEx(WH_MOUSE,MouseProc,glhInstance,0);
}
鼠標鉤子安裝好后,在移動、點擊鼠標時將會發出鼠標消息,這些消息均經過消息處理函數MouseProc()的攔截處理。在此,每當捕獲到系統各線程發出的任何鼠標消息后首先獲取當前鼠標所在位置下的窗口句柄,并進一步通過GetWindowText()函數獲取到窗口標題。在處理函數完成后,通過CallNextHookEx()函數將事件傳遞到鉤子列表中的下一個鉤子處理函數:
LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam)
{
LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam;
if(nCode>=0)
{
HWND glhTargetWnd=pMouseHook->hwnd;
//取目標窗口句柄
HWND ParentWnd=glhTargetWnd;
while(ParentWnd !=NULL)
{
glhTargetWnd=ParentWnd;
//取應用程序主窗口句柄
ParentWnd=GetParent(glhTargetWnd);
}
if(glhTargetWnd!=glhPrevTarWnd)
{
char szCaption[100];
//取目標窗口標題
GetWindowText(glhTargetWnd,szCaption,100);
……
}
}
//繼續傳遞消息
return CallNextHookEx((HHOOK)glhHook,nCode,wParam,lParam);
}
最后,調用UnhookWindowsHookEx()函數完成對鉤子的卸載:
void StopHook()
{
……
UnhookWindowsHookEx((HHOOK)glhHook);
}
現在完成的是鼠標鉤子的動態鏈接庫,經過編譯后需要經應用程序的調用才能實現對當前系統下各線程間鼠標消息的攔截處理。這部分同普通動態鏈接庫的使用沒有任何區別,在將其加載到進程后,首先調用動態鏈接庫的StartHook()函數安裝好鉤子,此時即可對系統下的鼠標消息實施攔截處理,在動態鏈接庫被卸載即終止鼠標鉤子時通過動態鏈接庫中的StopHook()函數卸載鼠標鉤子。
經上述編程,在安裝好鼠標鉤子后,鼠標在移動到系統任意窗口上時,馬上就會通過對鼠標消息的攔截處理而獲取到當前窗口的標題。實驗證明此鼠標鉤子的安裝、使用和卸載過程是正確的。
四、小結
鉤子,尤其是系統鉤子具有相當強大的功能,通過這種技術可以對幾乎所有的Windows系統消息和事件進行攔截處理。這種技術廣泛應用于各種自動監控系統對進程外消息的監控處理。本文只對鉤子的一些基本原理和一般的使用方法做了簡要的探討,感興趣的讀者完全可以在本文所述代碼基礎之上用類似的方法實現對諸如鍵盤鉤子、外殼鉤子等其他類型鉤子的安裝與使用。本文所述代碼在Windows 98下由Microsoft Visual C++ 6.0編譯通過。
?
?
Hook機制
??????
一、基本概念:
?
??? 鉤子(Hook),是Windows消息處理機制的一個平臺,應用程序可以在上面設置子程以監視指定窗口的某種消息,而且所監視的窗口可以是其他進程所創建的。當消息到達后,在目標窗口處理函數之前處理它。鉤子機制允許應用程序截獲處理window消息或特定事件。
?
??? 鉤子實際上是一個處理消息的程序段,通過系統調用,把它掛入系統。每當特定的消息發出,在沒有到達目的窗口前,鉤子程序就先捕獲該消息,亦即鉤子函數先得到控制權。這時鉤子函數即可以加工處理(改變)該消息,也可以不作處理而繼續傳遞該消息,還可以強制結束消息的傳遞。
?
二、運行機制:
?
1、鉤子鏈表和鉤子子程:
?
??? 每一個Hook都有一個與之相關聯的指針列表,稱之為鉤子鏈表,由系統來維護。這個列表的指針指向指定的、應用程序定義的、被Hook子程調用的回調函數,也就是該鉤子的各個處理子程。當與指定的Hook類型關聯的消息發生時,系統就把這個消息傳遞到Hook子程。一些Hook子程可以只監視消息,或者修改消息,或者停止消息的前進,避免這些消息傳遞到下一個Hook子程或者目的窗口。最近安裝的鉤子放在鏈的開始,而最早安裝的鉤子放在最后,也就是后加入的先獲得控制權。
?
?????? Windows 并不要求鉤子子程的卸載順序一定得和安裝順序相反。每當有一個鉤子被卸載,Windows 便釋放其占用的內存,并更新整個Hook鏈表。如果程序安裝了鉤子,但是在尚未卸載鉤子之前就結束了,那么系統會自動為它做卸載鉤子的操作。
?
??? 鉤子子程是一個應用程序定義的回調函數(CALLBACK Function),不能定義成某個類的成員函數(#add static 成員函數則可),只能定義為普通的C函數。用以監視系統或某一特定類型的事件,這些事件可以是與某一特定線程關聯的,也可以是系統中所有線程的事件。
?
?? ?鉤子子程必須按照以下的語法:
??? LRESULT CALLBACK HookProc
?????? (
?????? ?????? int nCode,
???? ???? WPARAM wParam,
???? ???? LPARAM lParam
???? );
HookProc是應用程序定義的名字。
?
nCode參數是Hook代碼,Hook子程使用這個參數來確定任務。這個參數的值依賴于Hook類型,每一種Hook都有自己的Hook代碼特征字符集。
wParam和lParam參數的值依賴于Hook代碼,但是它們的典型值是包含了關于發送或者接收消息的信息。
?
2、鉤子的安裝與釋放:
?
??? 使用API函數SetWindowsHookEx()把一個應用程序定義的鉤子子程安裝到鉤子鏈表中。SetWindowsHookEx函數總是在Hook鏈的開頭安裝Hook子程。當指定類型的Hook監視的事件發生時,系統就調用與這個Hook關聯的Hook鏈的開頭的Hook子程。每一個Hook鏈中的Hook子程都決定是否把這個事件傳遞到下一個Hook子程。Hook子程傳遞事件到下一個Hook子程需要調用CallNextHookEx函數。
???
HHOOK SetWindowsHookEx(
int idHook,????? // 鉤子的類型,即它處理的消息類型
HOOKPROC lpfn,?? // 鉤子子程的地址指針。如果dwThreadId參數為0
???????????????????? ?? // 或是一個由別的進程創建的線程的標識,
???????????????????? ?? // lpfn必須指向DLL中的鉤子子程。
???????????????????? ?? // 除此以外,lpfn可以指向當前進程的一段鉤子子程代碼。
???????????????????? ?? // 鉤子函數的入口地址,當鉤子鉤到任何消息后便調用這個函數。
HINSTANCE hMod,?// 應用程序實例的句柄。標識包含lpfn所指的子程的DLL。
???????????????????? ?? // 如果dwThreadId 標識當前進程創建的一個線程,
???????????????????? ?? // 而且子程代碼位于當前進程,hMod必須為NULL。
???????????????????? ?? // 可以很簡單的設定其為本應用程序的實例句柄。
DWORD dwThreadId // 與安裝的鉤子子程相關聯的線程的標識符。
???????????????????? ?? // 如果為0,鉤子子程與所有的線程關聯,即為全局鉤子。
??????????? );
函數成功則返回鉤子子程的句柄,失敗返回NULL。
?
以上所說的鉤子子程與線程相關聯是指在一鉤子鏈表中發給該線程的消息同時發送給鉤子子程,且被鉤子子程先處理。
?
??? 在鉤子子程中調用得到控制權的鉤子函數在完成對消息的處理后,如果想要該消息繼續傳遞,那么它必須調用另外一個SDK中的API函數CallNextHookEx來傳遞它,以執行鉤子鏈表所指的下一個鉤子子程。這個函數成功時返回鉤子鏈中下一個鉤子過程的返回值,返回值的類型依賴于鉤子的類型。這個函數的原型如下:
?
LRESULT CallNextHookEx
???????????????????? (
??????????????????????????? HHOOK hhk;
??????????????????????????? int nCode;
??????????????????????????? WPARAM wParam;
??????????????????????????? LPARAM lParam;
???????????????????? ?);
????????????????????
hhk為當前鉤子的句柄,由SetWindowsHookEx()函數返回。
NCode為傳給鉤子過程的事件代碼。
wParam和lParam 分別是傳給鉤子子程的wParam值,其具體含義與鉤子類型有關。
?????????????
??? 鉤子函數也可以通過直接返回TRUE來丟棄該消息,并阻止該消息的傳遞。否則的話,其他安裝了鉤子的應用程序將不會接收到鉤子的通知而且還有可能產生不正確的結果。
?
??? 鉤子在使用完之后需要用UnHookWindowsHookEx()卸載,否則會造成麻煩。釋放鉤子比較簡單,UnHookWindowsHookEx()只有一個參數。函數原型如下:
?
UnHookWindowsHookEx
????????????? (
????????????? HHOOK hhk;
????????????? );
函數成功返回TRUE,否則返回FALSE。
?
3、一些運行機制:
?
??? 在Win16環境中,DLL的全局數據對每個載入它的進程來說都是相同的;而在Win32環境中,情況卻發生了變化,DLL函數中的代碼所創建的任何對象(包括變量)都歸調用它的線程或進程所有。當進程在載入DLL時,操作系統自動把DLL地址映射到該進程的私有空間,也就是進程的虛擬地址空間,而且也復制該DLL的全局數據的一份拷貝到該進程空間。也就是說每個進程所擁有的相同的DLL的全局數據,它們的名稱相同,但其值卻并不一定是相同的,而且是互不干涉的。
??????
?????? 因此,在Win32環境下要想在多個進程中共享數據,就必須進行必要的設置。在訪問同一個Dll的各進程之間共享存儲器是通過存儲器映射文件技術實現的。也可以把這些需要共享的數據分離出來,放置在一個獨立的數據段里,并把該段的屬性設置為共享。必須給這些變量賦初值,否則編譯器會把沒有賦初始值的變量放在一個叫未被初始化的數據段中。
?
?????? #pragma data_seg預處理指令用于設置共享數據段。例如:
#pragma data_seg("SharedDataName")
HHOOK hHook=NULL;
#pragma data_seg()
?????? 在#pragma data_seg("SharedDataName")和#pragma data_seg()之間的所有變量 將被訪問該Dll的所有進程看到和共享。
?
??? 當進程隱式或顯式調用一個動態庫里的函數時,系統都要把這個動態庫映射到這個進程的虛擬地址空間里(以下簡稱"地址空間")。這使得DLL成為進程的一部分,以這個進程的身份執行,使用這個進程的堆棧。
?
4、系統鉤子與線程鉤子:
?
??? SetWindowsHookEx()函數的最后一個參數決定了此鉤子是系統鉤子還是線程鉤子。
???
?? ?線程勾子用于監視指定線程的事件消息。線程勾子一般在當前線程或者當前線程派生的線程內。
???
??? 系統勾子監視系統中的所有線程的事件消息。因為系統勾子會影響系統中所有的應用程序,所以勾子函數必須放在獨立的動態鏈接庫(DLL) 中。系統自動將包含"鉤子回調函數"的DLL映射到受鉤子函數影響的所有進程的地址空間中,即將這個DLL注入了那些進程。
?
幾點說明:
?(1)如果對于同一事件(如鼠標消息)既安裝了線程勾子又安裝了系統勾子,那么系統會自動先調用線程勾子,然后調用系統勾子。
?
?(2)對同一事件消息可安裝多個勾子處理過程,這些勾子處理過程形成了勾子鏈。當前勾子處理結束后應把勾子信息傳遞給下一個勾子函數。
?
?(3)勾子特別是系統勾子會消耗消息處理時間,降低系統性能。只有在必要的時候才安裝勾子,在使用完畢后要及時卸載。
?
三、鉤子類型
?
??? 每一種類型的Hook可以使應用程序能夠監視不同類型的系統消息處理機制。下面描述所有可以利用的Hook類型。
?
1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
?
??? WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以監視發送到窗口過程的消息。系統在消息發送到接收窗口過程之前調用WH_CALLWNDPROC Hook子程,并且在窗口過程處理完消息之后調用WH_CALLWNDPROCRET Hook子程。
?
??? WH_CALLWNDPROCRET Hook傳遞指針到CWPRETSTRUCT結構,再傳遞到Hook子程。CWPRETSTRUCT結構包含了來自處理消息的窗口過程的返回值,同樣也包括了與這個消息關聯的消息參數。
?
2、WH_CBT Hook
?
??? 在以下事件之前,系統都會調用WH_CBT Hook子程,這些事件包括:
??? 1. 激活,建立,銷毀,最小化,最大化,移動,改變尺寸等窗口事件;
??? 2. 完成系統指令;
??? 3. 來自系統消息隊列中的移動鼠標,鍵盤事件;
??? 4. 設置輸入焦點事件;
??? 5. 同步系統消息隊列事件。
???
??? Hook子程的返回值確定系統是否允許或者防止這些操作中的一個。
?
3、WH_DEBUG Hook
?
??? 在系統調用系統中與其他Hook關聯的Hook子程之前,系統會調用WH_DEBUG Hook子程。你可以使用這個Hook來決定是否允許系統調用與其他Hook關聯的Hook子程。
?
4、WH_FOREGROUNDIDLE Hook
?
??? 當應用程序的前臺線程處于空閑狀態時,可以使用WH_FOREGROUNDIDLE Hook執行低優先級的任務。當應用程序的前臺線程大概要變成空閑狀態時,系統就會調用WH_FOREGROUNDIDLE Hook子程。
?
5、WH_GETMESSAGE Hook
?
??? 應用程序使用WH_GETMESSAGE Hook來監視從GetMessage or PeekMessage函數返回的消息。你可以使用WH_GETMESSAGE Hook去監視鼠標和鍵盤輸入,以及其他發送到消息隊列中的消息。
?
6、WH_JOURNALPLAYBACK Hook
?
??? WH_JOURNALPLAYBACK Hook使應用程序可以插入消息到系統消息隊列。可以使用這個Hook回放通過使用WH_JOURNALRECORD Hook記錄下來的連續的鼠標和鍵盤事件。只要WH_JOURNALPLAYBACK Hook已經安裝,正常的鼠標和鍵盤事件就是無效的。WH_JOURNALPLAYBACK Hook是全局Hook,它不能象線程特定Hook一樣使用。WH_JOURNALPLAYBACK Hook返回超時值,這個值告訴系統在處理來自回放Hook當前消息之前需要等待多長時間(毫秒)。這就使Hook可以控制實時事件的回放。WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。
?
7、WH_JOURNALRECORD Hook
?
??? WH_JOURNALRECORD Hook用來監視和記錄輸入事件。典型的,可以使用這個Hook記錄連續的鼠標和鍵盤事件,然后通過使用WH_JOURNALPLAYBACK Hook來回放。WH_JOURNALRECORD Hook是全局Hook,它不能象線程特定Hook一樣使用。WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
?
8、WH_KEYBOARD Hook
?
??? 在應用程序中,WH_KEYBOARD Hook用來監視WM_KEYDOWN and WM_KEYUP消息,這些消息通過GetMessage or PeekMessage function返回。可以使用這個Hook來監視輸入到消息隊列中的鍵盤消息。
?
9、WH_KEYBOARD_LL Hook
?
??? WH_KEYBOARD_LL Hook監視輸入到線程消息隊列中的鍵盤消息。
?
10、WH_MOUSE Hook
?
??? WH_MOUSE Hook監視從GetMessage 或者 PeekMessage 函數返回的鼠標消息。使用這個Hook監視輸入到消息隊列中的鼠標消息。
?
11、WH_MOUSE_LL Hook
?
??? WH_MOUSE_LL Hook監視輸入到線程消息隊列中的鼠標消息。
?
12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
?
??? WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我們可以監視菜單,滾動條,消息框,對話框消息并且發現用戶使用ALT+TAB or ALT+ESC 組合鍵切換窗口。WH_MSGFILTER Hook只能監視傳遞到菜單,滾動條,消息框的消息,以及傳遞到通過安裝了Hook子程的應用程序建立的對話框的消息。WH_SYSMSGFILTER Hook監視所有應用程序消息。
???
??? WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我們可以在模式循環期間過濾消息,這等價于在主消息循環中過濾消息。
???
通過調用CallMsgFilter function可以直接的調用WH_MSGFILTER Hook。通過使用這個函數,應用程序能夠在模式循環期間使用相同的代碼去過濾消息,如同在主消息循環里一樣。
?
13、WH_SHELL Hook
?
?? ?外殼應用程序可以使用WH_SHELL Hook去接收重要的通知。當外殼應用程序是激活的并且當頂層窗口建立或者銷毀時,系統調用WH_SHELL Hook子程。
????? WH_SHELL 共有5鐘情況:
1. 只要有個top-level、unowned 窗口被產生、起作用、或是被摧毀;
2. 當Taskbar需要重畫某個按鈕;
3. 當系統需要顯示關于Taskbar的一個程序的最小化形式;
4. 當目前的鍵盤布局狀態改變;
5. 當使用者按Ctrl+Esc去執行Task Manager(或相同級別的程序)。
?
??? 按照慣例,外殼應用程序都不接收WH_SHELL消息。所以,在應用程序能夠接收WH_SHELL消息之前,應用程序必須調用SystemParametersInfo function注冊它自己。
?
?
?
關于鉤子HOOK函數的詳細說明
?
鉤子HOOK函數是Windows消息處理機制的一部分,通過設置“鉤子”,應用程序可以在系統級對所有消息、事件進行過濾,訪問在正常情況下無法訪問的消息。當然,這么做也是需要付出一定的代價的。由于多了這么一道處理過程,系統性能會受到一定的影響,所以大家在必要的時候才使用“鉤子”,并在使用完畢及時將其刪除。
首先讓我們看看HOOK函數是怎么安裝、調用和刪除的。應用程序通常是調用SetWindowsHookEx()函數來進行安裝的,其函數的原型如下:
?
SetWindowsHookEx(
?
Int idHook;
?
HOOKPROC lpfn;
?
HINSTANCE hMod;
?
DWORD dwThreadId;
?
);
?
參數說明:
?
idHook 是”鉤子”的類型,”鉤子”的類型一共有13種,具體如下表:
?
“鉤子”類型
解釋
?
WH_CALLWNDPROC
系統將消息發送到指定窗口之前的“鉤子”
?
WH_CALLWNDPROCRET
消息已經在窗口中處理的“鉤子”
?
WH_CBT
基于計算機培訓的“鉤子”
?
WH_DEBUG
差錯“鉤子”
?
WH_FOREGROUNDIDLE
前臺空閑窗口“鉤子”
?
WH_GETMESSAGE
接收消息投遞的“鉤子”
?
WH_JOURNALPLAYBACK
回放以前通過WH_JOURNALRECORD“鉤子”記錄的輸入消息
?
WH_JOURNALRECORD
輸入消息記錄“鉤子”
?
WH_KEYBOARD
鍵盤消息“鉤子”
?
WH_MOUSE
鼠標消息“鉤子”
?
WH_MSGFILTER
對話框、消息框、菜單或滾動條輸入消息“鉤子”
?
WH_SHELL
外殼“鉤子”
?
WH_SYSMSGFILTER
系統消息“鉤子”
?
?
lpfn 指向“鉤子”過程的指針。
?
hMod “鉤子”過程所在模塊的句柄。
?
dwThreadId “鉤子”相關線程的標識。
?
通常我們都是把”鉤子”做成動態鏈接庫,這樣的好處是可以是系統內的每個進程訪問。但是也可以在系統中直接調用,我的建議還是用動態庫。如果用動態庫的話,那么SetWindowsHookEx()中的第三個參數就是該動態鏈接庫模塊的句柄;對于一個只供單個進程訪問的”鉤子”,可以將其”鉤子”過程放在安裝”鉤子”的同一個線程內,此時SetWindowsHookEx()中的第三個參數為該線程的hInstance。安裝”鉤子”有兩種方法:1.你可以把他做成動態連接庫文件,和程序一起編譯。2.你可以在程序的任何地方直接調用。第2種的方法太麻煩,我不建議用,在這里我就不詳細介紹啦。相比之下第1種比較簡單。其”鉤子”的過程都在動態鏈接庫內完成。SetWindowsHookEx()函數是一個安裝函數,如故一個由某種類型的”鉤子”監視的事件發生,系統就會調用相應類型的”鉤子”鏈開始處的”鉤子”過程,”鉤子”鏈的每個”鉤子”過程都要考慮是否把事件傳遞給下一個”鉤子”過程。如果要傳遞的話,就要調用CallNestHookEx()函數。這個函數成功時返回”鉤子”鏈中下一個”鉤子”過程的返回值,返回值的類型依賴于”鉤子”的類型。這個函數的原型如下:
?
LRESULT CallNextHookEx(
?
HHOOK hhk;
?
int nCode;
?
WPARAM wParam;
?
LPARAM lParam;
?
);
?
其中hhk為當前”鉤子”的句柄,由SetWindowsHookEx()函數返回。NCode為傳給”鉤子”過程的事件代碼。wParam和lParam 分別是傳給”鉤子”過程的wParam值,其具體含義與”鉤子”類型有關。
?
釋放”鉤子”
?
釋放”鉤子”比較簡單,他只有一個參數。當不在需要”鉤子”時,應及時將其釋放。他是調用UnhookWindowsHookEx()函數來實現的,函數原型如下:
?
UnhookWindowsHookEx(
?
HHOOK hhk;
?
);
?
函數成功返回TRUE,否則返回FALSE。
?
如果我這樣講您還是不明白的話,請看下面給出的一些典型“鉤子”代碼和說明。
?
LRESULT WINAPI CallWndProc(int nCode,WPARAM wParam,LPARAM lParam)
?
{
?
if(nCode<0)
?
return CallNextHookEx(NULL,nCode,wParam,lParam);
?
switch(nCode)
?
{
?
case HC_ACTION:
?
//”鉤子”程序要處理什么的代碼
?
break;
?
default:
?
break;
?
}
?
return CallNextHookEx(NULL,nCode,wParam,lParam);
?
}
?
這是WH_CALLWNDPROC”鉤子”的代碼,此”鉤子”允許程序監視由函數SendMessage發送給窗口過程的消息。系統將消息發送到目的窗口之前調用WH_CALLWNDPROC “鉤子”過程。
?
LRESULT WINAPI CallwndProc(int nCode,WPARAM,wParam,LPARAM lParam)
?
{
?
if(nCode<0) return callNextHookEx(NULL,nCode,wParam,lParam);
?
switch(nCode)
?
{
?
case HC_ACTION:
?
switch(wParam)
?
{
?
Case PM_REMOVE:
?
//某個應用程序調用了GetMessage函數或者是帶PM_REMOVE參數的//PeekMessage函數,從消息隊列中移去一個消息。
?
Break;
?
Case PM_NOREMOVE:
?
//某個應用程序以PM_NOREMOVE為參數調用PeekMessage函數
?
break;
?
default:
?
break;
?
}
?
break;
?
default:
?
break;
?
}
?
return CallNextHookEx(NULL,nCode,wParam,lParam);
?
}
?
這是調用WH_GETMESSAGE的函數,此函數允許應用程序監視函數GetMessage和 PeekMessage返回的消息。應用程序可以用鉤子WH_GETMESSAGE來監視鼠標和鍵盤的輸入以及其他系統發送到消息隊列中的消息。
?
LRESULT CALLBACK CBTProc(int nCode,WPARAM wParam,LPARAM lParam)
?
{
?
If(nCode<0) Return callNextHookEx(NULL,nCode,wParam,lParam);
?
Switch(nCode)
?
{
?
case HCBT_ACTIVATE:
?
//系統將激活一個窗口
?
break;
?
case HCBT_CLICKSKIPPED:
?
//系統從系統消息隊列中移去一個鼠標消息
?
break;
?
case HCBT_CREATEWND:
?
//系統將創建一個窗口
?
break;
?
case HCBT_DESTROYWND:
?
//系統將關閉一個窗口
?
break;
?
case HCBT_KEYSKIPPED:
?
//系統從系統消息隊列中移去一個鍵盤消息
?
break;
?
case HCBT_MINMAX:
?
//系統將最大化或最小化一個窗口
?
break;
?
case HCBT_MOVESIZE:
?
//系統將移動一個窗口或改變一個窗口的大小
?
break;
?
case HCBT_QS:
?
//系統在系統消息隊列中檢索到WM_QUEUESYNC消息
?
break;
?
case HCBT_SETFOCUS:
?
//系統設置鍵盤輸入窗口
?
break;
?
case HCBT_SYSCOMMAND:
?
//將要執行一個系統命令
?
break;
?
default:
?
//可以添加其他代碼
?
break;
?
}
?
return CallNextHookEx(NULL,nCode,wParam,lParam);
?
}
?
每種”鉤子”類型都有其對應的函數,這些函數的參數都是一樣的,有興趣的朋友可以在MSDN中找的他們的詳細說明。
?
下面我給出一個完整的”鉤子”安裝和刪除的過程的代碼。
?
#include "stdafx.h"
?
#include "hook.h"
?
HINSTANCE hInstance;
?
HHOOK hhkKeyboard;
?
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
?
{
?
switch (ul_reason_for_call)
?
{
?
case DLL_PROCESS_ATTACH:
?
case DLL_THREAD_ATTACH:
?
case DLL_THREAD_DETACH:
?
case DLL_PROCESS_DETACH:
?
break;
?
}
?
hInstance=(HINSTANCE)hModule;
?
return TRUE;
?
}
?
LRESULT KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
?
{
?
MessageBeep(-1);
?
return CallNextHookEx(hhkKeyboard,nCode,wParam,lParam);
?
}
?
HOOK_API BOOL EnableKeyboardCapture()
?
{
?
if(!(hhkKeyboard=SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)KeyboardProc,hInstance,0)))
?
return FALSE;
?
return TRUE;
?
}
?
HOOK_API BOOL DisableKeyboardCapture()
?
{
?
return UnhookWindowsHookEx(hhkKeyboard);
?
}
?
注意:這是一個動態鏈接庫文件。
?
在程序中要想調用“鉤子”的時候,有EnableKeyboardCapture()函數就可以啦,但你按鍵的時候就回發出聲音。
?
?
?
Win32的Hook一些要點
?
?
如果你看msdn中的setwindowshookex函數說明,你會發現,ms告訴你如果你想
hook全局數據,你必須把hook代碼寫在dll中,如果你的hook代碼在.exe中,
則你只能hook本進程的數據.
?
你想過這是為什么嗎?
?
win32拋棄了win16的全局內存的概念,每個進程有自己獨立的內存空間,
并且不受其他進程影響.這樣一來所有代碼都只能訪問局部資源,但很顯然有些
應用必須是全局的,比如你的hook,所以ms必須提供一種折衷的安全的方法.好
在windows中的dll正好可以解決這個問題.
?
dll是一種代碼摸塊,它可以被映射進不同的進程空間,具體方法就不多談了,如果
你有興趣我們以后討論.
?
基于以上原因,ms要求我們把全局hook放進dll,以便由win32映射進不同的進程
空間,每個進程空間中的hook代碼只負責處理該進程中的數據.所有的局部
之和就是全局,這樣既維護了進程空間的獨立性,又可以處理全局數據.
?
你的問題是想hook一個進程的數據,但又不是本進程,所以你必須讓win32幫你把
hook代碼放到其他進程空間,又要有所選擇.
win32在映射hook代碼到其他進程空間中的方法很簡單,就是在另一個進程中調用
Loadlibrary
?
只有Loadlibrary成功以后才能hook到該進程的數據.
如果某個進程執行loadlibrary失敗,那么我們將無法hook到該進程的數據.
?
loadlibrary失敗除了系統原因外,還有個重要因素就是dllmain沒有返回TRUE!
?
根據以上理論,我們只要在 非指定程序 裝載hook dll時讓dllmain返回false,即可達到
你的目的了.
?
所以你需要在hook dll中添加下面的代碼"
?
?
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
switch(dwReason) {
case DLL_PROCESS_ATTACH:
??? ............
?? if(不是指定程序)
???? return FALSE
break;
.................
........
}
??? return TRUE;
}
?
?
?
這里有個例子:
ftp://pub:pub@211.157.101.157/Hook和hotKey.rar
ftp://pub:pub@211.157.101.157/Hook和hotKey源碼.rar
?
利用鍵盤鉤子開發按鍵發音程序
作者:GDGF
?
一、前言
一日,看見我媽正在用電腦練習打字,頻頻低頭看鍵盤,我想:要是鍵盤能發音的話,不就可以方便她養成"盲打"的好習慣嗎?光想不做可不行,開始行動(您可千萬別急著去拿工具箱啊^_^)...
按鍵能發音,其關鍵就是讓程序能夠知道當前鍵盤上是哪個鍵被按下,并播放相應的聲音,自己的程序當然不在話下,那么其它程序當前按下哪個鍵如何得知呢?利用鍵盤鉤子便可以很好地解決。
?
下載本文的全部源代碼 大小:552K
?
二、掛鉤(HOOK)的基本原理
WINDOWS調用掛接的回調函數時首先會調用位于函數鏈首的函數,我們只要將自己的回調函數置于鏈首,該回調函數就會首先被調用。那么如何將我們自己的回調函數置于函數鏈的鏈首呢?函數SetWindowsHookEx()實現的就是該功能。我們首先來看一下SetWindowsHookEx函數的原型:
?
?
HHOOK SetWindowsHookEx(
?int idHook,??????
?HOOKPROC lpfn,????
?HINSTANCE hMod,???
?DWORD dwThreadId?
);
第一個參數:指定鉤子的類型,有WH_MOUSE、WH_KEYBOARD等十多種(具體參見MSDN)
第二個參數:標識鉤子函數的入口地址
第三個參數:鉤子函數所在模塊的句柄;
第四個參數:鉤子相關函數的ID用以指定想讓鉤子去鉤哪個線程,為0時則攔截整個系統的消息。
?
另外需要注意的是為了捕獲所有事件,掛鉤函數應該放在動態鏈接庫DLL中。
?
三、具體實現
理論的話就不多說了,運行VC++6.0,新建一個MFC AppWizard(dll)工程,命名為Hook,使用默認的創建DLL類型的選項,也就是使用共享MFC DLL,點擊完成后開始編寫代碼:
?
(1)在Hook.h中定義全局函數
BOOL installhook(); //鉤子安裝函數
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);//掛鉤函數
?
(2)在Hook.cpp文件的#endif下添加定義全局變量Hook的代碼:
static HHOOK hkb=NULL;
HINSTANCE hins; //鉤子函數所在模塊的句柄
(3)添加核心代碼
BOOL installhook()
{
??? hkb=SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)KeyboardProc,hins,0);
??? return TRUE;
}
第一個參數指定鉤子的類型,因為我們只用到鍵盤操作所以設定為WH_KEYBOARD;第二個參數將鉤子函數的入口地址指定為KeyboardProc,當鉤子鉤到任何消息后便調用這個函數,即當不管系統的哪個窗口有鍵盤輸入馬上會引起KeyboardProc的動作;第三個參數是鉤子函數所在模塊的句柄;最后一個參數是鉤子相關函數的ID用以指定想讓鉤子去鉤哪個線程,為0時則攔截整個系統的消息;
現在,就開始定義當鍵盤上的鍵按下時程序要做什么了~
KeyboardProc動作:
?
LRESULT CALLBACK KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
{
??? if(((DWORD)lParam&0x40000000) && (HC_ACTION==nCode))
??? {
?????? switch(wParam) //鍵盤按鍵標識
??????? {
??????? case ''1'':sndPlaySound("1.wav",SND_ASYNC);break; //當數字鍵1被按下
???????? case ''2'':sndPlaySound("2.wav",SND_ASYNC);break;
??????? case ''3'':sndPlaySound("3.wav",SND_ASYNC);break;
??????? case ''4'':sndPlaySound("4.wav",SND_ASYNC);break;
??????? ....
??????? case ''A'':sndPlaySound("a.wav",SND_ASYNC);break; //當字母鍵A被按下
???????? case ''B'':sndPlaySound("b.wav",SND_ASYNC);break;
??????? case ''C'':sndPlaySound("c.wav",SND_ASYNC);break;
??????? case ''D'':sndPlaySound("d.wav",SND_ASYNC);break;
??????? ....
??????? }
???? }
???? LRESULT RetVal = CallNextHookEx( hkb, nCode, wParam, lParam );
???? return RetVal;
}
上面的代碼中我們用播放聲音做為按鍵被按下后的動作,API函數sndPlaySound的第一個參數定義的聲音文件的絕對路徑(比如要播放C盤下的a.wav,就定義成"C:\\a.wav");第二參數定義播放模式,SND_ASYNC模式可以及時地釋放正在播放的聲音文件,立刻停止當前聲音的播放轉去播放新的聲音,這樣在我們連續擊鍵時就不會有阻塞感了.為了執行sndPlaySound函數,必須在Hook.cpp的文件頭加上:?#include "mmsystem.h"
并且點擊VC++菜單上的“工程”-“設置”進入Link屬性頁,在L對象/庫模塊下輸入:winmm.lib后確定即可.
?
(4)添加輸出標識
在Hook.def的末尾添加
?
?
installhook
KeyboardProc
短短的四步,鍵盤鉤子的制作算是完成了,編譯生成后的DLL文件就可以自由的用別的程序來調用了.
在程序中如何調用DLL呢?那就簡單了.再用VC++6.0新建一個MFC AppWizard(exe)工程,命名為KeySound,點擊"確定"后選擇程序類型為對話框,直接點擊確定即可.
在KeySoundDlg.cpp文件中的OnInitDialog()初始化函數的CDialog::OnInitDialog();下面添加:
?
//阻止程序反復駐留內存,也為了防止有兩個程序同時讀取DLL而發生錯誤.
?
?
CreateMutex(NULL, FALSE, "KeySound");
if(GetLastError()==ERROR_ALREADY_EXISTS)
?? OnOK();
?
//讀取DLL
static HINSTANCE hinstDLL;
typedef BOOL (CALLBACK *inshook)();
inshook instkbhook;
if(hinstDLL=LoadLibrary((LPCTSTR)"Hook.dll"))
{
??? instkbhook=(inshook)GetProcAddress(hinstDLL,"installhook");?
??? instkbhook();
}
else
{
??? MessageBox("當前目錄找不到Hook.dll文件,程序初始化失敗");
??? OnOK();
}
將編譯生成后的KeySound.exe和Hook.dll放在同一目錄下,定義好聲音文件,運行KeySound.exe后打開記事本或寫字板,體驗一下系統為您即時快速地朗讀您按下的每一個鍵的快感吧^-^
?
有一點必須說明,標準鍵盤有101個鍵,您想讓多少鍵發聲音,就必須在上面的KeyboardProc動作里定義多少個鍵,常用的10個數字鍵和26個英文字母不會給您帶來太大的困難,只要相應的''A''對應A鍵,''1''對應1鍵就可以,但如果您希望能讓更多的鍵都有各種特色音樂的話,很可能會遇到一些鍵盤編碼上的麻煩,比如ESC鍵就不能簡單的用''ESC''來搞定了,得用VK_ESCAPE,又比如Alt鍵得用VK_MENU來定義,沒有個鍵盤編碼表的話會令人相當頭疼,這里我介紹一種讓程序來告訴您鍵盤按鍵名稱的方法:
為一個工程添加PreTranslateMessage映射,添加如下代碼:
?
?
char KeyName[50];
ZeroMemory(KeyName,50);
if(pMsg -> message == WM_KEYDOWN)
{
?? GetKeyNameText(pMsg->lParam,KeyName,50);
?? MessageBox(KeyName);
}
那么當程序窗口顯示在面前時按下某個鍵,就會彈出一個消息顯示該鍵的名稱,然后用''''包起來就可以了,比如逗號句號,就是'',''和''.'',簡單吧:)
到此就全部完成了按鍵發音程序的編寫,通過改變聲音文件的名稱而不用改動程序本身就可以達到更換按鍵聲音的目的了,只是有個遺憾,聲音文件在硬盤中的位置不能變更,從C盤換移動D盤程序就不能播放了,怎么樣才能靈活的讀取聲音文件呢?可以用API函數GetModuleFileName來得到程序所在的目錄,具體實現方法如下:
(1)在Hook.h的public:下面添加:
?
?
BOOL InitInstance(); //初始化函數
(2)在Hook.cpp的#endif下添加定義全局變量的代碼:
?
?
char szBuf[256];
char *p;
CString msg;
(3)在Hook.cpp中適當位置添加:
?
?
BOOL CHookApp::InitInstance ()
{
?? hins=AfxGetInstanceHandle();
?? GetModuleFileName(AfxGetInstanceHandle( ),szBuf,sizeof(szBuf));
?? p = szBuf;
?? while(strchr(p,''\\''))
?? {
??????? p = strchr(p,''\\'');
??????? p++;
?? }
?? *p = ''\0'';
?? msg=szBuf;
?? return TRUE;
}
(4)新建一個文件夾并命名為Sound;
?
(5)改變聲音文件物理位置定義方式
case ''1'':sndPlaySound(msg+"sound\\1.wav",SND_ASYNC);break;
msg是得到程序當前所在目錄,加上后面的代碼就是指播放當前目錄下的Sound目錄里的1.wav文件,這樣就將聲音文件的絕對路徑改成了靈活的相對路徑.您只要把KeySound.exe,Hook.dll和Sound文件夾放在同一個文件夾下,以后只要搬動整個文件夾就能實現聲音文件的任意移動了。
?
調試時需要注意:將Hook.dll、Sound目錄放在KeySound.exe的執行目錄下。假如編譯鏈接的時候出現unresolved external symbol __imp__sndPlaySoundA@8 這樣的信息,請在Project Settings中加入Winmm.lib 。
?
//
《魔高一丈2.0》開發實例
作者:濟南 宋悅
?
下載本文程序與代碼
http://www.vckbase.com/code/downcode.asp?id=1578
一、開發背景:
?
我想大家都有過忙手忙腳最小化窗口(或關閉窗口)的經歷吧!原因很簡單——不想讓突如其來的老板、老媽、老婆看到我們電腦屏幕上正在顯示的游戲、日記、MM:-) 等屬于個人隱私的東東。 如果能做一個程序在后臺運行,當我們發出一個特殊的輸入事件(我選擇了鼠標左、右鍵同時按下)時,該程序就迅速隱藏正在顯示的窗口,免去人工瞄準并按下每個窗口右上方的那個小得可憐的的最小化按扭之苦了。當危險解除再利用這個特殊事件使隱藏的窗口恢復。這對于像我這樣小腦不太發達、心理素質又不過硬而又經常在老板的眼皮底下“懸崖騎馬”的同志們來說是絕對有實戰意義的。于是我做了這個“魔高一丈”以實現上述功能!
?
?
二、程序原理:
?
首先,我們得能截獲鼠標左、右鍵同時按下去這個事件——這并不難——設一個標志變量當鼠標發出WM_LBUTTONDOWN并且又有WM_RBUTTONDOWN消息發出時把它置“1”罷了。而我要說明的是,這個“同時按下”只是一種宏觀上的概念,鼠標是不會同時發出兩個消息的。其次就是解決不管鼠標位于任何窗口之上都能在程序里截獲(或者稱為監聽更準確)到鼠標發出的消息并加以過濾的問題了,這是很關鍵的。我用了鉤子船長的那只鉤子(Hook),而且是全局的鼠標鉤子,它給了我們跟操作系統溝通的一個機會。許多比較有神秘感的程序(比如金山詞霸的鼠標取詞)都是用它實現的,稍后我將詳細解釋。最后就是剩下能得到可見的窗口的句柄(HANDLE)并根據其句柄顯示、隱藏窗口的問題了,這也沒什么難的有現成的API函數——EnumWindows和ShowWindow。你可以先運行一下我的程序(那個大五星,需要把它跟那個Mousehook.dll文件放在一個文件夾下)。當鼠標左右鍵一起按下時所有的窗口都隱藏了;再一次同時按下左右鍵又可恢復隱藏窗口;單擊任務欄右下角(托盤)的圖標可隱藏或顯示本程序窗口。
?
三、開發步驟:
?
第0步、選用VC 6.0集成開發環境。
第1步、由于建立全局鉤子必須把鉤子函數放在DLL里面,所以我們選擇MFC AppWizard(DLL)創建一個新的項目,命名為“Mousehook”,再選擇選擇MFC Extension DLL類型(為了方便嘛!)。為什么必須把全局鉤子函數放在DLL里呢?這是因為系統會動態地調用你所添加的全局鼠標鉤子,所有窗口消息數都會由于你添加了鼠標鉤子而引起系統處理(何為處理?調用鉤子函數也。)這必然需要操作系統能夠從一個東東里動態地加入這段處理程序,而這個東東非DLL莫屬。
第2步、在項目中加入Mousehook.h文件用以構造一個鉤子類——CMousehook,具體如下:
?
class AFX_EXT_CLASS CMousehook:public CObject
{
public:
?CMousehook();
?~CMousehook();
?BOOL starthook();//封裝SetWindowsHookEx( int idHook, HOOK_PROC lpfn, HINSTANCE hMod,DWORD dwThreadID)用來安裝鉤子
?BOOL stophook(); //封裝UnhookWindowsHookEx( HHOOK hhk )用來卸載鉤子
?VOID SetCheck1(UINT i);//處理對話框的選擇鉤選框1
?VOID SetCheck2(UINT i);//處理對話框的選擇鉤選框2
?VOID SetCheck3(UINT i);//處理對話框的選擇鉤選框3
?static BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam);//系統回調的鉤子函數
?VOID UseForExit();//退出程序時恢復所有隱藏窗口
};
這里我想特別地提一下EnumWindowsProc函數前的CALLBACK跟static,對于CALLBACK我想給大家一個特別江湖的解釋其就是:凡是由你設計而卻由Windows系統調用的函數,統稱callback函數。這些函數都有一定的類型,以配合Windows的調用操作。——引用臺灣侯師傅的話。他還說,某些Windows API函數會要求以callback函數(的函數地址)作為其參數之一。我們這里用到的又比如 SetWindowsHookEx( int idHook, HOOK_PROC lpfn, HINSTANCE hMod,DWORD dwThreadID)的第二個參數。這種API通常會在進行某種行為之后或滿足某種狀態的情況下調用其參數中的callback函數。又由于系統在調用callback函數的時候并不會借助任何對象去調用該callback函數,所以在用類來封裝callback函數時,需要用static來使callback函數能夠獨立于對象而又屬于類的成員函數。明白了不?(啊?地球人都知道呀!太傷自尊了!)
?
第3步、在項目中加入Mousehook.cpp文件在CMousehook里封裝其中加入必要的共享數據以及SetWindowsHookEx、UnhookWindowsHookEx等函數——這些API函數具體的參數的類型跟作用解釋在程序代碼的注釋里有(網上也到處都有,我也是從網上摳下來的。一個聲音高叫著——當然MSDN里也有。),而把它們寫在文章里就不免有騙取稿費之嫌了。我只是想解釋一下為什么需要使用一個共享的數據段,如下: #pragma data_seg("mydata")????????????? //編譯器識別的指令用以在虛擬內存中開辟一個數據段存放該指令下面的數據
?
HINSTANCE glhInstance=NULL;???????????? //DLL實例(或者說模塊)的句柄。
HHOOK glhHook=NULL;???????????????????? //鼠標鉤子的句柄。
HWND?GlobalWndHandle[100]={NULL,.....};//用來存放被隱藏的窗口的句柄,以數組的形式保存。
??????????????????????????????????????? //該數組必須初始化,原因見下文。我以“......”省略。
UINT?Global_i=0;?????????????????????? //用以在循環中序列化窗口數組的變量。
BOOL?Condition1=0;???????????????????? //用以記錄左鍵按下或釋放的標志變量。
BOOL?Condition2=0;???????????? ????????//用以記錄右鍵按下或釋放的標志變量。
BOOL?HideOrVisitableFlag=0;??????????? //用以標識當再次有左、右鍵同時按下的情況發生時是隱藏還是顯示窗口。
BOOL?Check1=0;???????????????????????? //用來表示控件Check1狀態的標志變量。
BOOL?Check2=0;???????????????????????? //用來表示控件Check2狀態的標志變量。
BOOL?Check3=0;??????????? ?????????????//用來表示控件Check3狀態的標志變量。
?
#pragma data_seg()????????????????????? //與#pragma data_seg("mydata") 首尾呼應表示該數據段的結束。
加入上述數據段以后還應在項目里插入一個“Mousehook.def”文件,用:"SECTIONS mydata READ WRITE SHARED"將mydata數據段設置為一個可讀寫的共享段。在程序里加入預編譯指令,或在開發環境的項目設置里也可以達到設置數據段屬性的目的,我就不一一贅述了。
我前面講過,系統通過調用放在DLL中的鉤子回調函數來實現全局鉤(鉤取所有窗口的鼠標消息),操作系統對DLL的操作僅僅是把DLL映射到需要它的進程的虛擬地址空間里去。也就是說,DLL函數中的代碼所創建的任何對象(包括變量)都歸調用它的線程或進程所有。“DLL在WIN32中什么都不擁有”——這句話很重要。比如我們在DLL里建立了一個變量a,而我們的這個DLL文件又被兩個進程所調用,這兩個進程的中都用到了a可這絕對是兩個不同存儲單元中存儲的兩個a,它們之間沒有絲毫的聯系。給其中一個賦值也絕對不會影響到另一個。而對于本程序的一些數據是需要在不同的進程中保持唯一的(也可以說是一致),比方說: HWND GlobalWndHandle[100]它是用來保存程序做了隱藏的窗口之句柄的數組。當程序運行,我在任意窗口A中同時按下了鼠標左、右鍵,由于設置了鼠標鉤子,系統會調用DLL中的鉤子處理函數截獲消息并加以處理,即把目前的可見窗口隱藏并把窗口句柄保存到GlobalWndHandle[100]數組中以備將來顯示之用。如果不把GlobalWndHandle[100]放到一個共享的數據段里,系統就會在目前我們截獲鼠標消息的A窗口的進程的地址空間里開辟HWND GlobalWndHandle[100]來存儲窗口句柄。這樣對于其他進程就不能方便地得到這個進程存入GlobalWndHandle[100]數組的數據了。這時只能將GlobalWndHandle[100]等需要跨進程訪問的變量數據放在一個共享的數據段里了。另外,需要特別注意——必須給這些變量賦初值(就象我在程序代碼里傻呼呼地寫了100個NULL一樣。你可以不初始化這個數組試驗一下,有助于你理解我上面的話),否則編譯器會把沒有賦初始值的變量放在一個叫未被初始化的數據段中。
?
第4步:編譯生成dll文件,并用MFC AppWizard(exe)建立一個基于對話框的項目,在里面添加一個名為“Mousehook.h”的頭文件其內容與dll項目中的“Mousehook.h”文件一致,打開菜單的“Project Settings”對話框在“Link”選項標簽的“Object/library modules”編輯框里填入Mousehook.lib(此文件是與dll一起生成的,當編譯一個隱式調用dll的exe時,lib文件起到提供dll引出函數接口地址的作用,如果此路徑設置不正確程序是無法進行連接的)文件的存放路徑。這樣就可以放心使用dll里定義的CMousehook類的成員了。如下:
1 在HideWindowDlg.h中加入#include "MouseHook.h"并在CHideWindowDlg中定義一個CMousehook類對象hook。
2 在CHideWindowDlg::OnInitDialog()函數中加入hook.starthook()并初始化相關變量,這樣當對話框初始時就會啟動鼠標鉤子。
3 在CHideWindowDlg::~CHideWindowDlg()函數中加入hook.stophook()。用以釋放對話框對象時解除鼠標鉤。
為了不忽略讀者的智力水平我只對主要的代碼進行了說明,其余有關托盤、Check控件的部分代碼都比較傳統也沒什么好說明的。最后,編譯成exe文件以后還須把Mousehook.dll文件拷貝到同exe相同的目錄下才能正確運行exe。
?
臨了,希望大家能對我上文中含糊、混沌的地方提出批評指正,也歡迎大家來信(me@sanlian.com.cn)切磋。
轉載于:https://www.cnblogs.com/yefengmeander/archive/2011/08/03/2888013.html
總結
- 上一篇: 遗传算法和神经网络结合在税收中的运用
- 下一篇: 自娱自乐的FreeRTOS——confi