當看完孫鑫老師的《VC++深入詳解》以後,對MFC有了一個大致的把握以後,這次我們自己剖析一個最精簡的MFC程式,看看他和WindosAPI寫法的區別:這個程式不使用類嚮導建立的,只有一個標頭檔和一個源檔案:
//標頭檔class CMyApp : public CWinApp{public: virtual BOOL InitInstance ();};class CMainWindow : public CFrameWnd{public: CMainWindow ();protected: afx_msg void OnPaint (); DECLARE_MESSAGE_MAP ()};//源檔案#include <afxwin.h>#include "Hello.h"CMyApp myApp;/////////////////////////////////////////////////////////////////////////// CMyApp member functionsBOOL CMyApp::InitInstance (){ m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE;}/////////////////////////////////////////////////////////////////////////// CMainWindow message map and member functionsBEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_PAINT ()END_MESSAGE_MAP ()CMainWindow::CMainWindow (){ Create (NULL, _T ("The Hello Application"));}void CMainWindow::OnPaint (){ CPaintDC dc (this); CRect rect; GetClientRect (&rect); dc.DrawText (_T ("Hello, MFC"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
我們看到,程式有兩個類,一個是CMyApp,他是從CWinApp派生下來的;另一個是CMainWindow,它是從CFrameWnd派生下來的。程式還有一個CMyApp類型的全域對象,myApp它代表了應用程式本身。
第一個問題,程式是如何啟動並執行?首先,MFC是對WindowsAPI的封裝,肯定符合API那一套的規律:
WinMain函數->建立視窗類別->註冊視窗類別->建立視窗->顯示視窗->更新視窗->訊息迴圈。而在訊息響應函數中處理訊息。
但是MFC有一個很奇怪的特點(也是為什麼MFC學起來很彆扭的原因),它使用一個半物件導向的語言(C#中就沒有main函數,得先有對象,才有函數;而C++必須有main函數,而且程式是順著main函數走),把一套面向過程的函數(WindowsAPI)封裝成一個看起來全物件導向的東西(這個程式直接看不出來入口在哪裡)。我們看不出來那兩個成員函數InitInstance和OnPaint是何時被調用的。
我們的線索在於:1.全域對象myApp肯定是先於main(WinMain)建立的。
2.在InitInstance中,new了一個CMainWindow對象,接著是顯示視窗,更新視窗。
第二條比較明顯,我們先看第二條,對與m_pMainWnd->ShowWindow (m_nCmdShow);我們進入這個函數的調用,可以看到:
BOOL CWnd::ShowWindow(int nCmdShow){ASSERT(::IsWindow(m_hWnd));if (m_pCtrlSite == NULL)return ::ShowWindow(m_hWnd, nCmdShow);elsereturn m_pCtrlSite->ShowWindow(nCmdShow);}
而且單步調試,發現就是走的if這條路徑,調用全域函數ShowWindow,也就是API裡面的那個,來完成顯示。
對於m_pMainWnd->UpdateWindow ();,單步調試,進入函數可以看到:
_AFXWIN_INLINE void CWnd::UpdateWindow(){ ASSERT(::IsWindow(m_hWnd)); ::UpdateWindow(m_hWnd); }
調用的是API的函數完成的。但是其他的內容卻不得而知。無奈之下,我們只能看看調用棧,發現是AfxWinMain函數調用的InitInstance。一看到這裡,又柳暗花明了:
1.AfxWinMain看上去跟WinMain很像啊!我們通過調用棧可以看到:
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow){// call shared/exported WinMainreturn AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);}
而_tWinMain是一個宏,它就是WinMain!我們終於找到頭了!
2.在AfxWinMain中有幾句非常重要的代碼:
// Perform specific initializationsif (!pThread->InitInstance()){if (pThread->m_pMainWnd != NULL){TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");pThread->m_pMainWnd->DestroyWindow();}nReturnCode = pThread->ExitInstance();goto InitFailure;}nReturnCode = pThread->Run();
其中InitInstance是虛函數,會調用我們自己寫的InitInstance:
BOOL CMyApp::InitInstance (){ m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE;}
第一句會調用CMainWindow的建構函式:
CMainWindow::CMainWindow (){ Create (NULL, _T ("The Hello Application"));}
我們跳過調用基類的部分,直接看衍生類別的,其中的Create調用的是:
BOOL CFrameWnd::Create(LPCTSTR lpszClassName,LPCTSTR lpszWindowName,DWORD dwStyle,const RECT& rect,CWnd* pParentWnd,LPCTSTR lpszMenuName,DWORD dwExStyle,CCreateContext* pContext){HMENU hMenu = NULL;if (lpszMenuName != NULL){// load in a menu that will get destroyed when window gets destroyedHINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU);if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL){TRACE0("Warning: failed to load menu for CFrameWnd.\n");PostNcDestroy(); // perhaps delete the C++ objectreturn FALSE;}}m_strTitle = lpszWindowName; // save title for laterif (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext)){TRACE0("Warning: failed to create CFrameWnd.\n");if (hMenu != NULL)DestroyMenu(hMenu);return FALSE;}return TRUE;}
其中CreateEx調用的是CWnd的函數:
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 parametersCREATESTRUCT 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);#ifdef _DEBUGif (hWnd == NULL){TRACE1("Warning: Window creation failed: GetLastError returns 0x%8.8X\n",GetLastError());}#endifif (!AfxUnhookWindowCreate())PostNcDestroy(); // cleanup if CreateWindowEx fails too soonif (hWnd == NULL)return FALSE;ASSERT(hWnd == m_hWnd); // should have been set in send msg hookreturn TRUE;}
在其中CREATESTRUCT結構體裡的東西與WNDCLASS中的東西非常相似。關鍵的,調用PreCreateWindow:
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs){if (cs.lpszClass == NULL){VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));cs.lpszClass = _afxWndFrameOrView; // COLOR_WINDOW background}if ((cs.style & FWS_ADDTOTITLE) && afxData.bWin4)cs.style |= FWS_PREFIXTITLE;if (afxData.bWin4)cs.dwExStyle |= WS_EX_CLIENTEDGE;return TRUE;}
在AfxEndDeferRegisterClass中,我們終於找到了視窗類別:
BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister){// mask off all classes that are already registeredAFX_MODULE_STATE* pModuleState = AfxGetModuleState();fToRegister &= ~pModuleState->m_fRegisteredClasses;if (fToRegister == 0)return TRUE;LONG fRegisteredClasses = 0;// common initializationWNDCLASS wndcls;memset(&wndcls, 0, sizeof(WNDCLASS)); // start with NULL defaultswndcls.lpfnWndProc = DefWindowProc;wndcls.hInstance = AfxGetInstanceHandle();wndcls.hCursor = afxData.hcurArrow;INITCOMMONCONTROLSEX init;init.dwSize = sizeof(init);// work to register classes as specified by fToRegister, populate fRegisteredClasses as we goif (fToRegister & AFX_WND_REG){// Child windows - no brush, no icon, safest default class styleswndcls.style = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;wndcls.lpszClassName = _afxWnd;if (AfxRegisterClass(&wndcls))fRegisteredClasses |= AFX_WND_REG;}if (fToRegister & AFX_WNDOLECONTROL_REG){// OLE Control windows - use parent DC for speedwndcls.style |= CS_PARENTDC | CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;wndcls.lpszClassName = _afxWndOleControl;if (AfxRegisterClass(&wndcls))fRegisteredClasses |= AFX_WNDOLECONTROL_REG;}if (fToRegister & AFX_WNDCONTROLBAR_REG){// Control bar windowswndcls.style = 0; // control bars don't handle double clickwndcls.lpszClassName = _afxWndControlBar;wndcls.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);if (AfxRegisterClass(&wndcls))fRegisteredClasses |= AFX_WNDCONTROLBAR_REG;}//下面代碼省略
我們看到:整個程式不是一次性完成註冊的,而是先用一個if語句判斷你的視窗到底是什麼類型,然後調用相應的註冊函數,我們這裡走的是_AfxRegisterWithIcon:
AFX_STATIC BOOL AFXAPI _AfxRegisterWithIcon(WNDCLASS* pWndCls,LPCTSTR lpszClassName, UINT nIDIcon){pWndCls->lpszClassName = lpszClassName;HINSTANCE hInst = AfxFindResourceHandle(MAKEINTRESOURCE(nIDIcon), RT_GROUP_ICON);if ((pWndCls->hIcon = ::LoadIcon(hInst, MAKEINTRESOURCE(nIDIcon))) == NULL){// use default iconpWndCls->hIcon = ::LoadIcon(NULL, IDI_APPLICATION);}return AfxRegisterClass(pWndCls);}
其中的LoadIcon已經是API函數了。最後調用AfxRegisterClass函數完成註冊:
BOOL AFXAPI AfxRegisterClass(WNDCLASS* lpWndClass){WNDCLASS wndcls;if (GetClassInfo(lpWndClass->hInstance, lpWndClass->lpszClassName,&wndcls)){// class already registeredreturn TRUE;}if (!::RegisterClass(lpWndClass)){TRACE1("Can't register window class named %s\n",lpWndClass->lpszClassName);return FALSE;}//以下代碼省略
在裡面,我們終於切切實實的見到了視窗類別和RegisterClass函數。
我們的思路扯得有點遠,現在回到CWnd::CreateEx中去。裡面除了PreCreateWindow來完成視窗的註冊之外,還有CreateWindowEx來建立視窗。這個函數的用法與CreateWindow基本類似,而使用的實參,正是前面說的與WNDCLASS類似的CREATESTRUCT。
再回到AfxWinMain,看Run函數,它調用的是CWinApp的Run函數:
int CWinApp::Run(){if (m_pMainWnd == NULL && AfxOleGetUserCtrl()){// Not launched /Embedding or /Automation, but has no main window!TRACE0("Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application.\n");AfxPostQuitMessage(0);}return CWinThread::Run();}
最終調用CWinThread的Run函數:
int CWinThread::Run(){ASSERT_VALID(this);// for tracking the idle time stateBOOL bIdle = TRUE;LONG lIdleCount = 0;// acquire and dispatch messages until a WM_QUIT message is received.for (;;){// phase1: check to see if we can do idle workwhile (bIdle &&!::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)){// call OnIdle while in bIdle stateif (!OnIdle(lIdleCount++))bIdle = FALSE; // assume "no idle" state}// phase2: pump messages while availabledo{// pump message, but quit on WM_QUITif (!PumpMessage())return ExitInstance();// reset "no idle" state after pumping "normal" messageif (IsIdleMessage(&m_msgCur)){bIdle = TRUE;lIdleCount = 0;}} while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));}ASSERT(FALSE); // not reachable}
完成訊息迴圈。
至此,我們把WinMain函數是如何封裝的基本討論完了。但是還有一大塊沒有討論,就是WndProc 。在討論之前,我們先要理解MFC架構師的設計思想:應用程式是很複雜的,弄不好就會寫錯,導致死機等不可預料的情況,所以,架構師們希望:如果你非常清楚代碼要怎麼寫,那麼你可以在自己餓衍生類別中完成你的設計,如果你不太清楚該怎麼寫,那你就不要寫,MFC會在基類中幫你完成一個最基本的處理(這個處理功能雖然很單一,但是能確保程式不會死機、崩潰之類的)。
按理說,C++的虛函數本來應該是設計這種架構的首選,應用程式的開發人員只需要重寫這些虛函數就可以了。但是也不知道是為什麼,MFC卻沒有採用虛函數,而是採用了一種極其古怪的方式實現了這套機制。秘密就在於宏
DECLARE_MESSAGE_MAP ()
和
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
ON_WM_PAINT ()
END_MESSAGE_MAP ()
之間。
我們先把DECLARE_MESSAGE_MAP宏展開,然後調整格式:
class CMainWindow : public CFrameWnd{public: CMainWindow ();protected: afx_msg void OnPaint ();//下面是宏展開的內容private: static const AFX_MSGMAP_ENTRY _messageEntries[]; protected: static AFX_DATA const AFX_MSGMAP messageMap; static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); virtual const AFX_MSGMAP* GetMessageMap() const; //宏展開結束};
1,先看看void OnPaint前面的afx_msg是幹什麼的?我們轉到它的定義,發現它什麼也不是,只起一個預留位置的作用。也就是強調這個函數是訊息的響應函數。
2.static const AFX_MSGMAP_ENTRY _messageEntries[];是一個靜態結構體數組,結構體類型是:
struct AFX_MSGMAP_ENTRY{UINT nMessage; // windows messageUINT nCode; // control code or WM_NOTIFY codeUINT nID; // control ID (or 0 for windows messages)UINT nLastID; // used for entries specifying a range of control id'sUINT nSig; // signature type (action) or pointer to message #AFX_PMSG pfn; // routine to call (or special value)};
3.static AFX_DATA const AFX_MSGMAP messageMap;:
AFX_DATA一路轉到定義,發現是__declspec(dllimport),也就是聲明這個函數是從動態連結程式庫中調用的。
AFX_MSGMAP是一個結構體,
struct AFX_MSGMAP{#ifdef _AFXDLLconst AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();#elseconst AFX_MSGMAP* pBaseMap;#endifconst AFX_MSGMAP_ENTRY* lpEntries;};
其中有2個變數。注意,因為有#ifdef和#else的緣故。
4.static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); 是一個函數,函數的傳回型別就是指向3中的AFX_MSGMAP類型的指標
5.virtual const AFX_MSGMAP* GetMessageMap() const; 是虛函數,傳回型別與4相同。
類的聲明我們看的差不多了,再看類的定義中的:
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_PAINT ()END_MESSAGE_MAP ()
我們依舊把宏展開、對齊、並將用宏定義的函數帶入實參:
//BEGIN_MESSAGE_MAP宏展開//函數定義1const AFX_MSGMAP* PASCAL CMainWindow::_GetBaseMessageMap() { return &CFrameWnd::messageMap;} //函數定義2const AFX_MSGMAP* CMainWindow::GetMessageMap() const {return &CMainWindow::messageMap;} //變數賦值AFX_COMDAT AFX_DATADEF const AFX_MSGMAP CMainWindow::messageMap = {&CMainWindow::_GetBaseMessageMap, &CMainWindow::_messageEntries[0]}; //變數賦值AFX_COMDAT const AFX_MSGMAP_ENTRY CMainWindow::_messageEntries[] = {//ON_WM_PAINT ()宏展開{ WM_PAINT, 0, 0, 0, AfxSig_vv, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))&OnPaint },//END_MESSAGE_MAP()宏展開{ 0,0, 0, 0, AfxSig_end, (AFX_PMSG)0 } };
看樣子就順眼多了。PS:我之前一直不知道C++;裡面的“\”續行號有什麼作用,這下我算是見識到了:因為#define的東西必須在一行中,所以如果這個東西比較長,必須分開寫的話,\就派上用場了。
言歸正傳。我們先看最後一個變數_messageEntrie數組的賦值:AFX_MSGMAP_ENTRY中的第一個成員就是訊息名,這裡填入的是WM_PAINT;最後一個成員是void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);類型的函數指標:這裡填入的訊息響應函數。而這個數組的第二元素的成員基本都是0,用來指明它是數組中的最後一個元素。(跟\0結尾的字串很像)。
下面看messageMap成員變數的賦值。這個成員是結構類型的,我們把它再次列出:
struct AFX_MSGMAP{#ifdef _AFXDLLconst AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();#elseconst AFX_MSGMAP* pBaseMap;#endifconst AFX_MSGMAP_ENTRY* lpEntries;};
實際上,這裡使用的是&CMainWindow::pfnGetBaseMap = _GetBaseMessageMap;lpEntries = _messageEntries。其中_GetBaseMessageMap返回的是基類CFrameWnd的messageMap。
而函數定義2則使用的是衍生類別的messageMap。
而我們注意到,CFrameWnd中也有一套這樣的宏。串連著CFrameWnd和和它的基類,依次向前。
大概理順了程式,我們就可以討論一下MFC到底是如何使用宏來實現“准多態”的效果了:
首先,在類的聲明中:
static const AFX_MSGMAP_ENTRY _messageEntries[]; protected: static AFX_DATA const AFX_MSGMAP messageMap; static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); virtual const AFX_MSGMAP* GetMessageMap() const;
也就是說,如果衍生類別寫了,那麼就調用virtual const AFX_MSGMAP* GetMessageMap() const; ,否則就調用static const AFX_MSGMAP* PASCAL _GetBaseMessageMap();使用基類的函數。
而衍生類別是否有定義,是通過messageMap記錄的,它的第一個成員是函數名,第二個成員是訊息入口,訊息入口中記錄了訊息和訊息響應函數的關係。
而且在更上層的基類中,也有這樣的宏,也有這樣的機制,所以可以如果在基類的函數中沒有找到,可以通過這套機制尋找基類的基類……。
到這裡我們不禁感歎MFC訊息映射宏的巧妙。我們只要在宏之間加上訊息就可以了。
講完了機制,我們看看訊息響應程式運行時是如何調用的。首先調用的是AfxCallWndProc,在其中調用
lResult = pWnd->WindowProc(nMsg, wParam, lParam);,在if語句中其中調用OnWndMsg,看看在訊息是否在裡面。這裡走的是:
case AfxSig_vv:(this->*mmf.pfn_vv)();break;
而它將會引起我們的OnPaint函數的調用。
講到這裡,就差不多講完了,如果有錯誤的地方,還望賜教。