深入剖析WTL框架(五)

 

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以前的窗口函數。

WTL對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代碼,然後替換原先的窗口函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章