windows程序设计(19):剖析MFC机制

来源:互联网 发布:摄影师修图用什么软件 编辑:程序博客网 时间:2024/06/06 02:10

当看完孙鑫老师的《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函数的调用。


讲到这里,就差不多讲完了,如果有错误的地方,还望赐教。