進入MFC講壇的前言(三)
MFC中的窗口創建及窗口消息映射
我經常碰到有人問我有關窗口創建的問題,他們經常把用HWND描述的系統窗口對象和用CWnd描述的MFC的窗口對象混淆不清。這兩者之間是緊密聯系在一起的,但是MFC為了自身的管理,在CWnd中加了一些額外的內容,包括如何從HWND生成CWnd。
在MFC中,有幾種典型的窗口對象,CWnd描述的一般窗口對象,CView描述的視圖對象,CFrameWnd描述的SDI框窗對象,CMDIFrameWnd描述的MDI框窗對象等等。在這一章中,主要討論下述內容:
MFC中窗口的創建
MFC的消息映射機制(MESSAGE MAP)
對于上面兩點MFC的設計者們使用了很高的技巧來確保應用程序的代碼盡可能小,其中的技巧和隱藏在它們背后的思想值得我們學習。下面對各項內容進行討論。
MFC中窗口的創建
在Window下,創建窗口可以使用兩個函數,CreateWindow()和CreateWindowEx(),它們都需要一個參數,這個參數是標識窗口類的字符串。所以,如果要創建窗口,一般的做法是,先使用RegisterClass()或RegisterClassEx()注冊一個窗口類,然后使用該窗口類來創建窗口。在前面我也提到過,注冊窗口類的最主要目的是為系統提供窗口函數的地址,以便被DispatchMessage()之類的函數回。
在MFC中,創建窗口的函數是CWnd或其派生類的Create()或CreateEx方法,注冊窗口類一般使用AfxRegisterWndClass(),在這個全局函數中,并沒有發現窗口函數地址這樣的參數,因此腦子里自然就會有這樣的問題:窗口函數在哪里?它是如何同窗口關聯的?下面我們將對MFC的一些與此有關的代碼進行仔細分析,回答上述兩個問題。
窗口函數
在MFC中,有一個全局的函數AfxWndProc(),正如下面的注釋所示,它就是CWnd及所有從它派生的窗口類的窗口函數,它的實現如下:
// The WndProc for all CWnd's and derived classes
LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
// special message which identifies the window as using AfxWndProc
if (nMsg == WM_QUERYAFXWNDPROC)
return 1;
// all other messages route through message map
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == hWnd);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
AfxCallWndProc()調用pWnd對象的虛擬函數WindowProc(),它的代碼如下:
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
// OnWndMsg does most of the work, except for DefWindowProc call
LRESULT lResult = 0;
if (!OnWndMsg(message, wParam, lParam, &lResult))
lResult = DefWindowProc(message, wParam, lParam);
return lResult;
}
上面的代碼中,OnWndMsg()是用來處理該窗口消息的函數,如果某條消息沒有被OnWndMsg()處理,也就是該窗口沒有提供處理該消息的函數,它就調用DefWindowProc()進行處理,DefWindowProc()也是一個虛擬函數,看看它的代碼:
LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
if (m_pfnSuper != NULL)
return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
WNDPROC pfnWndProc;
if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam);
else
return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
}
DefWindowProc()的策略很簡單,調用基類的窗口函數m_pfnSuper來處理該消息。
通過上面的分析,可以得出這樣的結論:與其說AfxWndProc()是MFC的唯一窗口函數,還不如說AfxWndProc()是MFC的窗口消息分發中心。正是由于有了這個消息分發中心,才使得MFC的應用程序能夠用有限的幾個窗口類,作出各種形形色色的窗口,使得在應用程序中,增加CWnd的派生類,并不增加系統中窗口類的個數,將對系統資源的使用控制在一個穩定的范圍之內。
注冊窗口類除了提供窗口函數外,還指定該窗口的一些外觀,如是否有標題條,窗口缺省背景等等。在MFC框架中,有框窗、視圖和控制條(CControlBar)等,它們除了操作行為不同外,外觀等也不相同,所以MFC注冊了幾種缺省的窗口類。在MFC中,有一個全局函數AfxEndDeferRegisterClass(LONG fToRegister),它用來注冊MFC預定義的窗口類,包括同框窗、視圖所對應的窗口類。由于它的代碼占的篇幅很長,而且實現也很簡單,所以就不列出它的代碼了,如果你有興趣,可以在wincore.cpp中找到它的實現代碼。
掛接窗口函數
如果你考察過AfxEndDeferRegisterClass()的實現代碼,你會對一行代碼感到迷惑,下面列出的是AfxEndDeferRegisterClass()的部分代碼,帶陰影部分的是那一行令人迷惑的代碼:
BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister)
{
。。。。。。
wndcls.lpfnWndProc = DefWindowProc;
。。。。。。
}
MFC將所有預定義的窗口類的窗口函數都設置成DefWindowProc。大家都知道,DefWindowProc是Window下為一般窗口提供消息缺省處理的API,它肯定不是應用程序所需要的窗口函數,所以,MFC肯定在某個地方置換了它,置換DefWindowProc的代碼在哪里呢?
前面說過,在MFC中創建窗口時是用CWnd的兩個虛擬函數Create()和CreateEx(),Create()是通過調用CreateEx()實現的,所以最終窗口的創建都要歸結到CreateEx()函數上。因此,我們可以推斷MFC在CreateEx()中置換了DefWindowProc。為了證實這一點,看看CWnd::CreateEx()的代碼:
BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
// allow modification of several common create parameters
CREATESTRUCT cs;
cs.dwExStyle = dwExStyle;
cs.lpszClass = lpszClassName;
cs.lpszName = lpszWindowName;
cs.style = dwStyle;
cs.x = x;
cs.y = y;
cs.cx = nWidth;
cs.cy = nHeight;
cs.hwndParent = hWndParent;
cs.hMenu = nIDorHMenu;
cs.hInstance = AfxGetInstanceHandle();
cs.lpCreateParams = lpParam;
if (!PreCreateWindow(cs)){
PostNcDestroy();
return FALSE;
}
AfxHookWindowCreate(this);
HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
if (!AfxUnhookWindowCreate()) PostNcDestroy(); // cleanup if CreateWindowEx fails too soon
if (hWnd == NULL) return FALSE;
ASSERT(hWnd == m_hWnd); // should have been set in send msg hook
return TRUE;
}
從上面的代碼看不出任何顯式的置換DefWindowProc的代碼,其實,它隱藏在AfxHookWindowCreate(this)之中,順藤摸瓜,再看看AfxHookWindowCreate()的代碼:
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (pThreadState->m_pWndInit == pWnd)
return;
if (pThreadState->m_hHookOldCbtFilter == NULL)
{
pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
_AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
if (pThreadState->m_hHookOldCbtFilter == NULL)
AfxThrowMemoryException();
}
ASSERT(pThreadState->m_hHookOldCbtFilter != NULL);
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == NULL); // only do once
ASSERT(pThreadState->m_pWndInit == NULL); // hook not already in progress
pThreadState->m_pWndInit = pWnd;
}
AfxHookWindowCreate()設置了一個線程級的CBT Hook,該Hook的入口地址為_AfxCbtFilterHook,_AfxCbtFilterHook是一個全局的MFC函數,_AfxCbtFilterHook通過調用SetWindowLong()用AfxWndProc()的入口地址置換掉DefWindowProc。
小結:
1、在MFC中,創建一個窗口的過程是:
1、生成一個對應窗口類的對象
2、調用該對象的Create()或CreateEx()方法
3、在CreateEx()中(Create()是調用CreateEx()實現的),先調用虛擬函數PreCreateWindow(),讓應用程序有一個改變窗口行為的機會,同時,在PreCreateWindow中,還作了一件很重要的事情,就是如果指向窗口類的字符串指針為NULL,就調用AfxDeferRegisterClass()注冊一個合適的窗口類,將該注冊的窗口類作為創建窗口的類型參數。AfxDeferRegisterClass()就是AfxEndDeferRegisterClass(),后者根據參數注冊相應的窗口類,并將DefWindowProc作為該窗口類的臨時窗口函數。
4、CreateEx()在調用CreateWindowEx()創建真正的窗口對象之前,設置一個線程級的CBT Hook,該hook在窗口創建完成后被調用,MFC在hook函數中調用SetWindowLong()將該窗口的窗口函數置換成AfxWndProc。
2、從Window的角度看,任何一個MFC應用程序都只有一個窗口函數AfxWndProc。
3、AfxWndProc的作用是截獲所有的發送給窗口消息,并將這些消息發送給相應的窗口對象的窗口函數WindowProc處理,所以它實質上是一個窗口消息分發器,注意,非窗口消息不被AfxWndProc所分發,它們在AfxWndProc被調用之前就被CWinThread::PreTranslateMessage()處理過了。
4、同PreTranslateMessage()不同的地方在于,AfxWndProc()能夠截獲所有的來自于消息隊列的和非消息隊列的窗口消息(如調用SendMessage()發送的窗口消息)。