当看完孙鑫老师的《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 functions BOOL CMyApp::InitInstance () { m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_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); else return 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 WinMain return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow); }
而_tWinMain是一个宏,它就是WinMain!我们终于找到头了!
2.在AfxWinMain中有几句非常重要的代码:
// Perform specific initializations if (!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 destroyed HINSTANCE 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++ object return FALSE; } } m_strTitle = lpszWindowName; // save title for later if (!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 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); #ifdef _DEBUG if (hWnd == NULL) { TRACE1("Warning: Window creation failed: GetLastError returns 0x%8.8X\n", GetLastError()); } #endif 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; }
在其中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 registered AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); fToRegister &= ~pModuleState->m_fRegisteredClasses; if (fToRegister == 0) return TRUE; LONG fRegisteredClasses = 0; // common initialization WNDCLASS wndcls; memset(&wndcls, 0, sizeof(WNDCLASS)); // start with NULL defaults wndcls.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 go if (fToRegister & AFX_WND_REG) { // Child windows - no brush, no icon, safest default class styles wndcls.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 speed wndcls.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 windows wndcls.style = 0; // control bars don't handle double click wndcls.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 icon pWndCls->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 registered return 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 state BOOL 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 work while (bIdle && !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)) { // call OnIdle while in bIdle state if (!OnIdle(lIdleCount++)) bIdle = FALSE; // assume "no idle" state } // phase2: pump messages while available do { // pump message, but quit on WM_QUIT if (!PumpMessage()) return ExitInstance(); // reset "no idle" state after pumping "normal" message if (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 message UINT nCode; // control code or WM_NOTIFY code UINT nID; // control ID (or 0 for windows messages) UINT nLastID; // used for entries specifying a range of control id's UINT 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 _AFXDLL const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); #else const AFX_MSGMAP* pBaseMap; #endif const 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宏展开 //函数定义1 const AFX_MSGMAP* PASCAL CMainWindow::_GetBaseMessageMap() { return &CFrameWnd::messageMap; } //函数定义2 const 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 _AFXDLL const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); #else const AFX_MSGMAP* pBaseMap; #endif const 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函数的调用。
讲到这里,就差不多讲完了,如果有错误的地方,还望赐教。