现在的位置: 首页 > 综合 > 正文

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

2013年08月03日 ⁄ 综合 ⁄ 共 12040字 ⁄ 字号 评论关闭

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

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

抱歉!评论已关闭.