ATL對窗口消息處理函數的封裝
在本節開始部分談到的封裝窗口的兩個難題,其中第一個問題是怎樣解決將窗口函數的消息轉發到HWND相對應的類的實例中的相應函數。
下面我們來看一下,ATL採用的是什麼辦法來實現的。
我們知道每個Windows的窗口類都有一個窗口函數。
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
在類CWindowImplBaseT中,定義了兩個類的靜態成員函數。
template <class TBase = CWindow, class TWinTraits = CControlWinTraits> class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase > { public: … … static LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); … … } |
它們都是窗口函數。之所以定義爲靜態成員函數,是因爲每個類必須只有一個窗口函數,而且,窗口函數的申明必須是這樣的。
在前面介紹的消息處理邏輯過程中,我們知道怎樣通過宏生成虛函數ProcessWindowsMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID)。
現在的任務是怎樣在窗口函數中把消息傳遞給某個實例(窗口)的ProcessWindowsMessage()。這是一個難題。窗口函數是類的靜態成員函數,因此,它不象類的其它成員函數,參數中沒有隱含this指針。
注意,之所以存在這個問題是因爲ProcessWindowsMessage()是一個虛函數。而之所以用虛函數是考慮到類的派生及多態性。如果不需要實現窗口類的派生及多態性,是不存在這個問題的。
通常想到的解決辦法是根據窗口函數的HWND參數,尋找與其對應的類的實例的指針。然後,通過該指針,調用該實例的消息邏輯處理函數ProcessWindowsMessage()。
這樣就要求存儲一個全局數組,將HWND和該類的實例的指針一一對應地存放在該數組中。
ATL解決這個問題的方法很巧妙。該方法並不存儲這些對應關係,而是使窗口函數接收C++類指針作爲參數來替代HWND作爲參數。
具體步驟如下:
· 在註冊窗口類時,指定一個起始窗口函數。
· 創建窗口類時,將this指針暫時保存在某處。
· Windows在創建該類的窗口時會調用起始窗口函數。它的作用是創建一系列二進制代碼(thunk)。這些代碼用this指針的物理地址來取代窗口函數的HWND參數,然後跳轉到實際的窗口函數中。這是通過改變棧來實現的。
· 然後,用這些代碼作爲該窗口的窗口函數。這樣,每次調用窗口函數時都對參數進行轉換。
· 在實際的窗口函數中,只需要將該參數cast爲窗口類指針類型。
詳細看看ATL的封裝代碼。
1. 註冊窗口類時,指定一個起始窗口函數。
在superclass中,我們分析到窗口註冊時,指定的窗口函數是StartWindowProc()。
2. 創建窗口類時,將this指針暫時保存在某處。
template <class TBase, class TWinTraits> HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName, DWORD dwStyle, DWORD dwExStyle, UINT nID, ATOM atom, LPVOID lpCreateParam) { ATLASSERT(m_hWnd == NULL); if(atom == 0) return NULL; _Module.AddCreateWndData(&m_thunk.cd, this); if(nID == 0 && (dwStyle & WS_CHILD)) nID = (UINT)this; HWND hWnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szWindowName, dwStyle, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, hWndParent, (HMENU)nID, _Module.GetModuleInstance(), lpCreateParam); ATLASSERT(m_hWnd == hWnd); return hWnd; } |
該函數用於創建一個窗口。它做了兩件事。第一件就是通過_Module.AddCreateWndData(&m_thunk.cd, this);語句把this指針保存在_Module的某個地方。
第二件事就是創建一個Windows窗口。
3. 一段奇妙的二進制代碼
下面我們來看一下一段關鍵的二進制代碼。它的作用是將傳遞給實際窗口函數的HWND參數用類的實例指針來代替。
ATL定義了一個結構來代表這段代碼:
#pragma pack(push,1) struct _WndProcThunk { DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) DWORD m_this; // BYTE m_jmp; // jmp WndProc DWORD m_relproc; // relative jmp }; |
#pragma pack(pop)
#pragma pack(push,1)的意思是告訴編譯器,該結構在內存中每個字段都緊緊挨着。因爲它存放的是機器指令。
這段代碼包含兩條機器指令:
mov dword ptr [esp+4], pThis jmp WndProc |
MOV指令將堆棧中的HWND參數(esp+0x4)變成類的實例指針pThis。JMP指令完成一個相對跳轉到實際的窗口函數WndProc的任務。注意,此時堆棧中的HWND參數已經變成了pThis,也就是說,WinProc得到的HWND參數實際上是pThis。
上面最關鍵的問題是計算出jmp WndProc的相對偏移量。
我們看一下ATL是怎樣初始化這個結構的。
class CWndProcThunk { public: union { _AtlCreateWndData cd; _WndProcThunk thunk; }; void Init(WNDPROC proc, void* pThis) { thunk.m_mov = 0x042444C7; //C7 44 24 0C thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); // write block from data cache and // flush from instruction cache FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); } }; |
ATL包裝了一個類並定義了一個Init()成員函數來設置初始值的。在語句thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); 用於把跳轉指令的相對地址設置爲(int)proc - ((int)this+sizeof(_WndProcThunk))。
上圖是該窗口類的實例(對象)內存映象圖,圖中描述了各個指針及它們的關係。很容易計算出相對地址是(int)proc - ((int)this+sizeof(_WndProcThunk))。
4. StartWindowProc()的作用
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData(); ATLASSERT(pThis != NULL); pThis->m_hWnd = hWnd; pThis->m_thunk.Init(pThis->GetWindowProc(), pThis); WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); #ifdef _DEBUG // check if somebody has subclassed us already since we discard it if(pOldProc != StartWindowProc) ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n")); #else pOldProc; // avoid unused warning #endif return pProc(hWnd, uMsg, wParam, lParam); } |
該函數做了四件事:
一是調用_Module.ExtractCreateWndData()語句,從保存this指針的地方得到該this指針。
二是調用m_thunk.Init(pThis->GetWindowProc(), pThis)語句初始化thunk代碼。
三是將thunk代碼設置爲該窗口類的窗口函數。
WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); |
這樣,以後的消息處理首先調用的是thunk代碼。它將HWND參數改爲pThis指針,然後跳轉到實際的窗口函數WindowProc()。
四是在完成上述工作後,調用上面的窗口函數。
由於StartWindowProc()在創建窗口時被Windows調用。在完成上述任務後它應該繼續完成Windows要求完成的任務。因此在這裏,就簡單地調用實際的窗口函數來處理。
5. WindowProc()窗口函數
下面是該函數的定義:
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; // set a ptr to this message and save the old value MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } }; const MSG* pOldMsg = pThis->m_pCurrentMsg; pThis->m_pCurrentMsg = &msg; // pass to the message map to process LRESULT lRes; BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0); // restore saved value for the current message ATLASSERT(pThis->m_pCurrentMsg == &msg); pThis->m_pCurrentMsg = pOldMsg; // do the default processing if message was not handled if(!bRet) { if(uMsg != WM_NCDESTROY) lRes = pThis->DefWindowProc(uMsg, wParam, lParam); else { // unsubclass, if needed LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC); lRes = pThis->DefWindowProc(uMsg, wParam, lParam); if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnWndProc) ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc); // clear out window handle HWND hWnd = pThis->m_hWnd; pThis->m_hWnd = NULL; // clean up after window is destroyed pThis->OnFinalMessage(hWnd); } } return lRes; } |
首先,該函數把hWnd參數cast到一個類的實例指針pThis。
然後調用pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);語句,也就是調用消息邏輯處理,將具體的消息處理事務交給ProcessWindowMessage()。
接下來,如果ProcessWindowMessage()沒有對任何消息進行處理,就調用缺省的消息處理。
注意這裏處理WM_NCDESTROY的方法。這和subclass有關,最後恢復沒有subclass以前的窗口函數。
前面講到過subclass的原理,這裏看一下是怎麼封裝的。
template <class TBase, class TWinTraits> BOOL CWindowImplBaseT< TBase, TWinTraits >::SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd == NULL); ATLASSERT(::IsWindow(hWnd)); m_thunk.Init(GetWindowProc(), this); WNDPROC pProc = (WNDPROC)&(m_thunk.thunk); WNDPROC pfnWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); if(pfnWndProc == NULL) return FALSE; m_pfnSuperWindowProc = pfnWndProc; m_hWnd = hWnd; return TRUE; } |
沒什麼好說的,它的工作就是初始化一段thunk代碼,然後替換原先的窗口函數。