Windows掛鉤的簡單使用

最近手機上的短信存儲器快滿了,應該刪除一些短信以留出一些空間,但是有好多短信是各個MM發過來的,捨不得就這麼刪除了,想導出到電腦裏面保存起來。萬一哪天MM成了我女朋友了,有機會的時候可以給她看看,說明我是這麼珍惜跟她相關的點點滴滴。^_^於是用數據線把手機連接到電腦上,打開EasyGPRS軟件,讀取手機中的短信到列表窗口中。但是可惜的是EasyGPRS軟件沒有提供導出短信內容的功能,於是只好自己想辦法了。當然最簡單的方法是,把各條短信的接收時間,對方號碼,內容等信息在電腦上輸入一遍,保存到文件中。但是近200條短信,我可以沒有那個耐心。我當然得想跟簡單快捷的方法。我想到的方法就是,寫一個程序,把EasyGPRS已經從手機中讀取的短信內容,從列表窗口複製並保存到一個文件中。想到了就做!原本以爲比較簡單的東西,沒想到也花了我6,7個小時時間,當然是晚上下班後和早晨上班前的空餘時間。:)
   先對照程序界面說說其基本功能:


Windows掛鉤的簡單使用
 
    點擊並拖動查找工具圖標到EasyGPRS軟件界面上的短信列表窗口上,釋放圖標,程序會把短信列表的內容複製到自己的列表窗口中;在窗口中央的格式文本框中輸入導出的格式;點擊【導出】按鈕,程序就按照指定的格式(當然只是比較簡單的格式定義)將短信內容輸出到下面的文本框中,並複製到剪貼板上;這時打開一個文本編輯軟鍵,如記事本,執行粘貼命令後保存就可以了。複製短信內容並執行導出後的界面如下(事關個人隱私,我不得不做了點簡單的處理^_^):
 
Windows掛鉤的簡單使用
 
    寫這個程序的難點,和本文的重點就在於,怎麼把另一個程序(進程)中一個列表控件中內容,複製到自己的程序(進程)中?我採用了Windows掛鉤的實現方式。下面就依編程的順序進行講述。
    要將另一個進程中的列表控件的內容複製到自己的進程中,當然首先得找到這個列表控件了,方法比較簡單,按下鼠標左鍵,拖動窗口上的查找工具圖標到目標列表窗口上,釋放鼠標鍵就可以了。從程序實現上來講,在查找工具圖標上按下鼠標左鍵時,開始鼠標捕獲;移動鼠標時,需要在當前鼠標位置下的窗口邊框上繪製一個矩形框,表示當前的目標窗口,方法是使用WindowFromPoint()函數取得鼠標光標下的窗口句柄,使用GetWindowRect()取得其相對於屏幕座標系的矩形,使用GetDC(NULL)取得屏幕設備描述表,使用Rectangle()在目標窗口上繪製外邊框;釋放鼠標鍵時,停止鼠標捕捉。
   找到列表控件窗口後,就可以向它發送消息取得其列數,行數和各行數據了,我剛開始時就是這麼做的。但是,發送消息取得列數和行數是正確的,卻不能取得各行的數據。這是爲什麼呢?原來,Windows編程中,允許跨越進程邊界向另一個進程中的窗口發送消息(跨進程發送消息當然也是跨線程發送消息,處理方式同在線程內部發送消息的處理方式有一些不同,詳細情況可以參考《Windows核心編程》第26章《窗口消息》)。但是,跨進程邊界發送的消息不能帶有指針類型的參數,因爲指針不能跨越進程邊界。在Windows系統中,各個進程是相互隔離的,有自己的獨立地址空間,一個進程中有效的指針,傳遞到另一個進程中不一定有效;即使有效,它也是指向另一個進程的地址空間中的某個位置了,負責處理消息的目標窗口(線程)會從那個位置取得輸入參數,或者將處理結果存放到那裏去,發送消息的進程無法正確地傳遞消息參數,或者利用指針取得消息處理時返回的結果。跨進程傳遞指針需要特殊的處理,例如使用COM+,或者Java RMI等分佈式處理系統。
    由於需要一個指針作爲其參數,所以不能用普通的SendMessage()函數向另一個進程中的列表控件發送消息,取得控件的某個項目的內容。那麼如何解決這個問題,取得另一個進程中的列表控件的內容呢?我想到了前段時間學習的《Windows核心編程》一書中講到過的高級DLL操作技術,是否可以用某種DLL技術解決這個問題呢?趕緊找出書來,翻開第22章《插入DLL和掛接API》,再次閱讀第3節《使用Windows掛鉤來插入DLL》,我就找到解決辦法了。作者在這一節介紹了使用windows掛鉤在某進程中插入DLL的方法,並給出了實際的應用示例,即用Windows掛鉤向資源管理器進程Explorer.exe插入DLL,來實現保存和恢復桌面圖標位置的功能。我就稍微變通一下,使用Windows掛鉤向EasyGPRS進程插入DLL,來實現取得EasyGPRS進程中的短信列表窗口內容的功能。
    首先簡單介紹以下設置Windows掛鉤的函數SetWindowsHookEx:
 

HHOOK SetWindowsHookEx( int idHook,

                        HOOKPROC lpfn,

                        HINSTANCE hMod,

                        DWORD dwThreadId );

 參數的簡單說明:
     idHook 掛鉤的種類,在我這個程序裏面使用的是WH_GETMESSAGE標誌,表示安裝一個監測投遞到目標線程的消息的掛鉤
     lpfn  掛鉤函數,一個應用程序定義的回調函數,當掛鉤截取到消息時,系統將調用此函數
     hMod  包含掛鉤函數的DLL模塊句柄
     dwThreadId  要安裝掛鉤的目標線程
   Windows掛鉤函數必須是包含在Dll中的。因爲掛鉤截取到消息時,系統使用目標線程調用掛鉤函數,而安裝掛鉤的線程可能和目標線程不在同一個進程中,不共享同一個地址空間。這樣,目標線程沒辦法執行位於另一個進程中的掛鉤函數,因爲即使知道掛鉤函數的入口地址,函數的可執行代碼也是在另一個進程中的。Windows要求掛鉤函數位於Dll中,這樣,因爲Dll可以被映射到不同的進程中。安裝掛鉤的時候,Windows可以將原本已經映射到源進程的Dll,再映射到目標進程中。這樣,位於目標進程中的目標線程就可以調用掛鉤函數了。需要注意的是:掛鉤函數在兩個進程中的入口地址很可能是不一樣的,因爲包含它的Dll的在不同進程中被映射到的地址很可能是不一樣的。
     安裝掛鉤函數後,系統在截取到相應類型的消息時,就會利用目標線程調用掛鉤函數,這時候就可以直接用SendMessage()函數向目標列表控件發送消息,取得其某一行的內容了。但是問題是,取得的內容是在目標進程中的,還是無法傳遞到源進程中。我對這個問題的解決方法是在Dll中設置一個帶共享屬性的節,把取得的內容放到共享節中。這樣目標線程取得內容後,源進程也可以通過位於共享節的變量得到內容。注意:這裏共享節是位於Dll中的,而Dll是被同時映射到兩個進程中的。
    最後一個問題是:源線程和目標線程需要以某種方式來進行通信和同步它們的操作。源線程必須告訴目標線程,要取得哪個列表窗口的哪一行的內容;目標線程瞭解了這個信息之後,執行掛鉤函數,取得列表項內容,之後源線程才能利用共享節定義的變量,將列表項內容複製到自己的列表中。我採用的通信方式是線程消息和事件。
    寫了這麼多,才發現自己的講述比較亂,好像沒太多條理,這裏簡單總結一下。我要解決的問題是,把另一個程序中的一個列表窗口的內容,複製自己的程序中。因爲跨進程發送消息時,消息參數不能含有指針,所以不能簡單地用SendMessage()發送相關消息給另一個程序中的列表窗口,來取得其內容。最終對這個問題的解決涉及到3個方面:
1 利用Windows掛鉤機制,將含有掛鉤函數的dll插入到目標進程。目標線程在調用掛鉤函數時,可以簡單地用SendMessage()函數取得列表項內容。
2 目標線程取得列表項內容後,如何傳回源線程。我採用的方法是利用dll中的共享數據節。
3 對於目標線程和源線程的通信和同步,我採用了線程消息和事件的機制。
 
    現在該看具體的代碼了。這是含有掛鉤函數的dll程序代碼:
 1    #include <windows.h>
2    #include <CommCtrl.h>
3    #include <shlwapi.h>
4   
5    #define UM_GET_COL_INFO   (WM_USER + 0x1000)
6    #define UM_GET_ITEM_INFO  (WM_USER + 0x2000)
7   
8    #pragma comment(lib,"shlwapi.lib")
9    #pragma comment(lib,"comctl32.lib")
10  
11   #pragma data_seg("Shared")
12   HHOOK    g_hhook=NULL;
13   extern "C" __declspec(dllexport) LVCOLUMN g_colinfo={0};
14   extern "C" __declspec(dllexport) LVITEM   g_iteminfo={0};
15   extern "C" __declspec(dllexport) TCHAR    g_szText[500]={0};
16   #pragma data_seg()
17  
18   #pragma comment(linker,"/section:Shared,rws")
19  
20   static HINSTANCE g_hDll;
21  
22   BOOL WINAPI DllMain(HINSTANCE hInst,DWORD fdwReason,PVOID fImpLoad)
23   {
24      switch(fdwReason)
25      {
26      case DLL_PROCESS_ATTACH:
27          g_hDll = hInst;
28          break;
29      }
30      return TRUE;
31   }
32  
33   static LRESULT WINAPI  GetMsgProc(int ncode,WPARAM wParam,LPARAM lParam)
34   {
35      MSG* pMsg;
36      pMsg = (MSG*)lParam;
37      /* 取得列(ListView頭部)信息 */
38      if (UM_GET_COL_INFO == pMsg->message)
39      {
40          HWND hList = (HWND)(pMsg->wParam);
41          HANDLE hEvent;
42  
43          ZeroMemory(g_szText,sizeof(g_szText));
44          g_colinfo.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
45          g_colinfo.fmt  = LVCFMT_CENTER;
46          g_colinfo.pszText = g_szText;
47          g_colinfo.cchTextMax = 500;
48     
49          ListView_GetColumn(hList,pMsg->lParam,&g_colinfo);
50  
51          hEvent = OpenEvent(EVENT_ALL_ACCESS,FALSE,"MsgProcessed");
52          SetEvent(hEvent);
53          CloseHandle(hEvent);
54      }
55      /* 取得數據項信息 */
56      else if (UM_GET_ITEM_INFO == pMsg->message)
57      {
58          HWND hList = (HWND)(pMsg->wParam);
59          HANDLE hEvent;
60  
61          ZeroMemory(g_szText,sizeof(g_szText));
62          g_iteminfo.mask = LVIF_TEXT;
63  
64          g_iteminfo.iItem = HIWORD(pMsg->lParam);
65          g_iteminfo.iSubItem = LOWORD(pMsg->lParam);
66  
67          g_iteminfo.pszText = g_szText;
68          g_iteminfo.cchTextMax = 500;
69         
70          ListView_GetItem(hList,&g_iteminfo);
71         
72          hEvent = OpenEvent(EVENT_ALL_ACCESS,FALSE,"MsgProcessed");
73          SetEvent(hEvent);
74          CloseHandle(hEvent);
75      }
76      return CallNextHookEx(g_hhook,ncode,wParam,lParam);
77   }
78  
79   extern "C"  __declspec(dllexport) BOOL WINAPI SetMyHook(DWORD dwThreadId)
80   {
81      if (0 != dwThreadId)
82          g_hhook = SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,g_hDll,dwThreadId);
83      else
84          UnhookWindowsHookEx(g_hhook);
85      return TRUE;
86   }
87  
對代碼說明如下:
1 第6,7行定義了兩個自定義消息,源線程給目標線程發送這兩個消息,分別表示要求取得列表控件的列信息或者某個項目的信息。消息的WPARAM參數是列表控件的窗口句柄,LPARAM參數是列序號或者項目序號(高16位是項目序號,低16位是子項目序號)
2 第9,10行定義了dll需要額外鏈接的庫。這樣用預編譯指令在源文件裏面寫,就不需要改工程屬性了,比較方便。
3 第12至17行在Dll中定義了一個名爲Shared的節。注意節中定義的變量必須初始化,否則變量不是被放在指定的節,而是放在默認的未初始化全局變量節。(編譯時編譯器會給出警告信息)。節中的後三個變量加了extern "C" __declspec(dllexport)修飾,其中__declspec(dllexport)表示需要導出指定的變量,extern "C"表示以C語言方式導出,即不給修飾名,這樣即使Dll是用C++語言編寫的,C語言編寫的客戶端程序也可以調用它。第19行定義了Shared節的屬性爲rws。r = readable,w=writeable,s=shared,即在多個進程間共享本節。
共享節的意義是:Dll被多次映射時,共享節中的變量只有一份,而不是通常的對每個映射dll的進程有一份,變量在多個進程間共享,類似於C++類中的靜態成員變量被類的多個實例共享一樣。
4 第34行的GetMsgProc()是掛鉤函數,它將被目標線程調用。此函數對6,7行定義的消息進行處理,取得列表的列信息或者項目信息,放到共享節定義的變量中;然後打開用於進程間(源進程和目標進程)通信的事件,設置事件。注意:這裏可以安全地對事件句柄調用CloseHandle()函數,因爲事件的引用計數是2,不僅目標進程引用了它,創建這個事件的源進程也引用了它。這裏調用CloseHandle()使事件引用計數減1,變成1,系統內核的事件對象並不會被銷燬。
5 第80行的SetMyHook()函數供源進程調用以設置或者取消掛鉤。函數比較簡單,只是簡單地調用一個API函數。不導出這個函數,直接在源進程中進行調用也是可以的,只是我在源進程採用了簡單的隱含鏈接方式,而不是動態加載方式,這樣做要方便一點(不導出這個函數時安裝掛鉤的方法是,調用GetModuleHandle("dll模塊名")取得dll模塊句柄,然後在調用SetWindowsHookEx時傳遞這個模塊句柄)。
6 代碼中導出函數和變量用extern "C" __declspec(dllexport)修飾,顯得比較累贅,更通常採用的方法是添加一個頭文件,在其中添加以下代碼
#ifdef MYDLL
#define MYAPI extern "C __declspec(dllexport)
#else
#define MYAPI extern "C" __declspec(dllimport)
#endif
這樣定義後,編譯dll和客戶端程序時就可以使用同一個頭文件。這也是各種相關書籍介紹的方法。各種相關書籍上一般還會介紹使用def文件的方法,這個更簡便一些。我這裏只是一個簡單的dll,所以沒有采用這兩種方法。
 
   下面該介紹客戶端程序代碼了。由於代碼比較多,這裏只給出與本文主題相關的,主要部分的代碼如下:
1    #include <shlwapi.h>
2    #pragma comment(lib,"shlwapi.lib")
3   
4    #pragma comment(lib,"MsgHookDll.lib")
5   
6    extern "C" __declspec(dllimport) BOOL WINAPI SetMyHook(DWORD);
7    extern "C" __declspec(dllimport) LVCOLUMN g_colinfo;
8    extern "C" __declspec(dllimport) LVITEM   g_iteminfo;
9    extern "C" __declspec(dllimport) TCHAR    g_szText[500];
10  
11   #define UM_GET_COL_INFO   (WM_USER + 0x1000)
12   #define UM_GET_ITEM_INFO  (WM_USER + 0x2000)
13  
14   void CExportSMSDlg::CopyDataFromList()
15   {
16       if(NULL == m_hDestWnd) return;
17  
18       HWND hList,hHeader;
19       int  x,y,rows,cols;
20       DWORD dwThreadId;
21       HANDLE hEvent;
22  
23       //取得行列數,清空客戶端程序列表內容的代碼省略
24  
25       hEvent = CreateEvent(NULL,FALSE,FALSE,"MsgProcessed");
26       dwThreadId = GetWindowThreadProcessId(m_hDestWnd,NULL);
27       SetMyHook(dwThreadId);
28      
29       // 複製ListView頭部信息的代碼省略
30  
31       //複製各行數據的代碼類似於取得列表
32       for(y = 0; y < rows; y++)
33           for(x = 0; x < cols; x++)
34           {
35               PostThreadMessage(dwThreadId,UM_GET_ITEM_INFO,(WPARAM)m_hDestWnd,MAKELPARAM(x,y));
36               WaitForSingleObject(hEvent,INFINITE);
37              
38  
39               g_iteminfo.pszText = g_szText;
40               g_iteminfo.mask  |= LVIF_STATE;
41               g_iteminfo.state |= LVIS_SELECTED;
42               if (0 == x)
43                   ListView_InsertItem(hList,&g_iteminfo);
44               else
45                   ListView_SetItem(hList,&g_iteminfo);
46               ListView_EnsureVisible(hList,y,FALSE);
47               ProcessMessage();
48           }
49  
50       CloseHandle(hEvent);
51       SetMyHook(0);
52   }

說明如下:
1 第4行隱含鏈接含有鉤子函數的dll;第6到第9行聲明dll導入函數和變量
2 第11,12行定義兩種自定義消息類型
3 第25行創建用於源進程和目標進程通信的事件對象
3 第26,27行取得目標窗口線程ID,調用導出函數SetMyHook安裝掛鉤
4 第35行,客戶端發送消息給目標線程,要求取得某行的數據
5 第36行,客戶端等待目標線程取得列表指定行數據,放入dll共享節的相關變量中之後,觸發事件。關於這一點上文已有說明。關於事件對象採用全局命名空間,一個命名的事件可以在系統中的所有進程間共享這一點,我就不用詳細解釋了。
6 第39行是不是有點多此一舉呢? 鉤子函數中已經執行了g_iteminfo.pszText = g_szText;從列表中取得某項內容,這裏還有必要寫這一句嗎?這一句不多餘,而且還很重要。Dll映射到兩個進程中的起始地址很可能不一樣,則g_szText這個數組類型的全局變量在兩個進程中的邏輯地址是不一樣的。目標進程中執行g_iteminfo.pszText = g_szText;使g_iteminfo.pszText的值等於目標進程中g_szText的地址,這個地址大多數時候是不等於源進程中的g_szText地址值的。源進程如果不再次執行這個語句,則下面的43,45行代碼企圖在一個很可能只相對於目標進程有效的地址(g_szText的地址)處取得項目的文本信息,這很可能造成拒絕訪問異常。關於dll被映射到兩個進程中的不同地址處,造成g_szText在兩個進程中的邏輯地址不一樣這一點,可以參看操作系統原理類教材對地址重定位的講解。
7 第40,41行設置新添加的項目爲被選中狀態
8 第47行是一個自定義函數,用於處理消息隊列中積壓的消息,其代碼如下:
static void ProcessMessage()
{
    MSG msg;
    while(PeekMessage(&msg,0,0,0,PM_REMOVE))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}
9 第50行關閉事件對象句柄是很好的習慣
10 卸載掛鉤,請參看上面給出的dll源代碼
 
   最後說明兩點:
1 這裏給出的取得列表控件的內容是通用的。雖然我是用它來取得EasyGPRS軟件的短信列表框的內容,但是它完全也可以取得其他各種列表框的內容。(本文提到的列表框是指ListView控件)
2 本文提到的dll和客戶程序的源代碼,可以到我的網絡硬盤上下載。地址:http://www.800disk.com/,用戶名:yaozijian110,密碼:123456(這個帳號只具有下載文件的權限)
 
    這篇文章終於寫完了。這才發現自己的表達能力,對內容的組織能力還是有所欠缺的,以後還要多鍛鍊纔是。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章