MFC功能已經非常強大,自己做界面庫也許沒什麼意思,但是這個過程中卻能學到很多東西。比如說:
窗口類的封裝,從全局窗口消息處理到窗口對象消息處理的映射方法:
對界面進行封裝,一般都是一個窗口一個類,比如實現一個最基本的窗口類CMyWnd,你一定會把窗口過程作為這個類的成員函數,但是使用WINAPI創建窗口時必須注冊類WNDCLASS,裡面有個成員數據lpfnWndProc需要WNDPROC的函數指針,一般想法就是把窗口類的消息處理函數指針傳過去,但是類成員函數除非是靜態的,否則無法轉換到WNDPROC,而全局的消息處理函數又無法得到窗口類對象的指針。這裡有幾種解決辦法:
一種解決方法是用窗口列表,開一個結構數組,窗口類對象創建窗口的時候把窗口HWND和this指針放入數組,全局消息處理函數遍歷數組,利用HWND找出this指針,然後定位到對象內部的消息處理函數。這種方法查找對象的時間會隨著窗口個數的增多而增長。
另一種方法比較聰明一點,WNDCLASS裡面有個成員數據cbWndExtra一般是不用的,利用這點,注冊類時給該成員數據賦值,這樣窗口創建時系統會根據該值開辟一塊內存與窗口綁定,這時把創建的窗口類的指針放到該塊內存,那麼在靜態的窗口消息循環函數就能利用GetWindowLong(hWnd,GWL_USERDATA)取出該指針,return (CMyWnd*)->WindowProc(...),這樣就不用遍歷窗口了。但是這樣一來就有個致命弱點,對窗口不能調用SetWindowLong(hWnd,GWL_USERDATA,數據),否則就會導致程序崩潰。幸好這個函數(特定這幾個參數)是調用幾率極低的,對於窗口,由於創建窗口都是調用窗口類的Create函數,不用手工注冊WNDCLASS類,也就不會調用SetWindowLong函數。但是畢竟缺乏安全性,而且當一秒鐘內處理的窗口消息很多時,這種查找速度也可能不夠快。
還有一種就是比較完美的解決辦法,稱之為thunk技術。thunk是一組動態生成的ASM指令,它記錄了窗口類對象的this指針,並且這組指令可以當作函數,既也可以是窗口過程來使用。thunk先把窗口對象this指針記錄下來,然後轉向到靜態stdProc回調函數,轉向之前先記錄HWND,然後把堆棧裡HWND的內容替換為this指針,這樣在stdProc裡就可以從HWND取回對象指針,定位到WindowProc了。
我們先來看看窗口過程函數定義:
系統調用m_thunk時的堆棧:
ret HWND MSG WPARAM LPARAM
-------------------------------------------
棧頂
棧底
系統把參數從右到左依次壓棧,最後把返回地址壓棧,我們只要在系統調用窗口過程時修改堆棧,把其中的hWnd參數替換掉就行了。這時thunk技術就有用武之地了,我們先定義一個結構:
類定義:
private:
WNDPROC m_thunk
}
在創建窗口的時候把窗口過程設定為this->m_thunk,m_thunk的類型是WNDPROC,因此是完全合法的,當然這個m_thunk還沒有初始化,在創建窗口前必須初始化:
//call Offset
//調用code[0],call執行時會把下一條指令壓棧,即把Proc壓棧
thunk->Call = 0xE8;
// call [rel]32
thunk->Offset = (size_t)&(((Thunk*)0)->Code)-(size_t)&(((Thunk*)0)->Proc);
// 偏移量,跳過Proc到Code[0]
thunk->Proc = CMyWnd::stdProc; //靜態窗口過程
這樣m_thunk雖然是一個結構,但其數據是一段可執行的代碼,而其類型又是WNDPROC,系統就會忠實地按窗口過程規則調用這段代碼,m_thunk就把Window字段裡記錄的this指針替換掉堆棧中的hWnd參數,然後跳轉到靜態的stdProc:
這樣就把窗口過程轉向到了類成員函數WindowProc,當然這樣還有一個問題,就是窗口句柄hWnd還沒來得及記錄,因此一開始的窗口過程應該先定位到靜態的InitProc,CreateWindow的時候給最後一個參數,即初始化參數賦值為this指針:
在InitProc裡面取出該指針:
LRESULT WINAPI CMyWnd::InitProc(HWND hWnd,UINT uMsg,UINT
wParam,LONG lParam)
{
if(uMsg == WM_NCCREATE)
{
CMyWnd *w = NULL;
w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams;
if(w)
{
//記錄hWnd
w->m_hWnd = hWnd;
//改變窗口過程為m_thunk
SetWindowLong(hWnd,GWL_WNDPROC,(LONG)w-CreateThunk());
return (*(WNDPROC)(w->GetThunk()))(hWnd,uMsg,wParam,lParam);
}
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
這樣就大功告成。
窗口過程轉發流程:
假設已建立CMyWnd類的窗口對象 CMyWnd *window,初始化完畢後調用window->Create,這時Create的窗口其窗口過程函數是靜態CMyWnd::InitWndProc
題外話:thunk的匯編代碼全部寫在注釋裡了,把這段匯編轉成數據可費了不少勁,當時手頭沒有合適的工具,只有一本《8086/8088匯編語言程序設計》,根據附錄中的指令碼匯總表轉成機器碼數據,那裡面根本沒有EAX,ECX,ESP等的概念,只能連蒙帶猜加調試,非法操作了n(n>10)回才得到那些數據,當時真是長出了一口氣:TNND,終於搞定了!:-)