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