作者:陈剑,杨晓
MFC是一个十分精密的类库,设计者经历了AFX项目的失败后,从面向对象的角度封装了Windows庞大的API。 然而这些封装对于我们这些接触了一些Win32的MFC初学者来说有点过于隐秘。 比如说写MFC程序的过程中我们无法看到main函数,也不知道App,Frame,View这三个类是如何结合起来的。这些困惑会让我们感觉到很迷茫,而且感觉到程序会一不小心就出现一些奇怪的错误。
要写出好的MFC程序,我们需要对MFC类库的各种功能的实现有所研究。MFC的消息是一个让人叹为观止的设计,通过各种宏以及一些易被人们忽略的基础C语言知识把本来用消息循环实现的Windows程序完美地封装成了一个用面向对象观念实现的MFC程序。为了对MFC有更深入的了解,我研究了一下它的消息的封装过程。
先从我们能够看到的地方开始在自动生成的所有头文件(除了Resource.h)都写着: DECLARE_MESSAGE_MAP()
在cpp文件里面则有
BEGIN_MESSAGE_MAP(CMessageMechanismApp, CWinAppEx)
ON_COMMAND(ID_APP_ABOUT, &CMessageMechanismApp::OnAppAbout) ………
END_MESSAGE_MAP()
通过寻找这些奇怪的宏的定义我们在afxwin.h里面看到 #define DECLARE_MESSAGE_MAP() \\ protected: \\
static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \\ virtual const AFX_MSGMAP* GetMessageMap() const; \\
BEGIN_MESSAGE_MAP的定义如下:
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \\ PTM_WARNING_DISABLE \\
const AFX_MSGMAP* theClass::GetMessageMap() const \\ { return GetThisMessageMap(); } \\
const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \\ { \\
typedef theClass ThisClass; \\ typedef baseClass TheBaseClass; \\ static const AFX_MSGMAP_ENTRY _messageEntries[] = \\ {
END_MESSAGE_MAP的定义为: #define END_MESSAGE_MAP() \\
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \\ }; \\
static const AFX_MSGMAP messageMap = \\
{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \\ return &messageMap; \\ } \\ PTM_WARNING_RESTORE
显然,宏DECLARE_MESSAGE_MAP()在头文件中为我们的类声明了两个成员函数GetThisMessageMap(),GetMessageMap();这两个函数的定义则由BEGIN_MESSAGE_MAP和END_MESSAGE_MAP在对应的源文件中提供。可以看到GetMessageMap()直接调用了自己类的GetThisMessageMap函数,BEGIN_MESSAGE_MAP和END_MESSAGE_MAP以及中间的ON_COMMAND等宏合力定义了GetThisMessageMap函数,在GetThisMessageMap中先利用ON_COMMAND定义的东西生成一个AFX_MSGMAP_ENTRY对象数组,然后利用这个对象生成AFX_MSGMAP对象。以上两个类的定义为: 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_PTR nSig; // signature type (action) or pointer to message # AFX_PMSG pfn; // routine to call (or special value) };
struct AFX_MSGMAP {
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); const AFX_MSGMAP_ENTRY* lpEntries; };
从AFX_MSGMAP的定义可以知道,它是一个存储消息信息的数据结构,其中pfn存的就是那个消息要调用的函数。上面定义了一个数组,怎么知道那个数组有多长呢?仔细看会发现{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 },这个就是数组结束的标志。再看看AFX_MSGMAP结构体它包含了一个指向他的对象所在类的基类的GetThisMessageMap的指针和一个指向上面声明的消息记录数组的指针。指向基类的函数的指针有什么作用呢?随着深入了解,我们会发现它与消息的传递有关。也就是我们常常发现一个消息,无论一个消息我们在App类还是Doc类还是View类中定义,我们都可以响应它,这是消息传递带来的好处。在_messageEntry的定义体里面,我们可以通过ON_COMMAND或者其它的如ON_UPDATE_COMMAND_UI这样的宏生成的对象填充这个数组。
走到这一步,我们发现无法再倒推下去了,没办法,只好搜索资料。要了解消息的注册过程,我们还要了解整个MFC程序的启动过程,MFC程序的启动过程可以粗略概括如下: 程序启动时最先启动的是以下位于crt0.c的函数 int WinMainCRTStartup(void);
在调用这个函数后所有的全局对象,静态对象都会被初始化。在这些对象的初始化中有一个对象的初始化起了关键作用,它就是位于和你项目同名字那个cpp文件里面的theApp对象。theApp对象初始化时,它的构造函数把自己的信息存到进程和应用程序里面: AfxGetThread() = this AfxGetApp() = this
theApp的其它大多数成员初始化为NULL
WinMainCRTStartup继续运行,调用在appmodul.cpp中的WinMain函数(这就是我们熟悉的程序入口),WinMain函数直接调用AfxWinMain(应该算是MFC的程序入口点了)函数AfxWinMain通过
CWinThread* pThread = AfxGetThread(); CWinApp* pApp = AfxGetApp(); 取得theApp的指针
然后分别调用函数:
AfxWinInit,pApp->InitApplication,pThread->InitInstance,pThread->Run
AfxWinInit通过
CWinApp* pApp = AfxGetApp();
取得theApp指针并初始化theApp与命令行相关的成员变量
然后调用的pApp->InitApplication用途暂时还不明白
消息的设置从下面这个函数开始 pThread->InitInstance() 调用基类的函数 CWinApp::InitInstance(); 通过下面代码连接文档,视图等
pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CMainFrame), // 主 SDI 框架窗口 RUNTIME_CLASS(CTestView));
对于单文档的程序在ProcessShellCommand执行过程中下面一段代码生成了一个CMainFrame的对象,这段代码位于docteml.cpp中
CFrameWnd* pFrame = (CFrameWnd*)m_pOleFrameClass->CreateObject(); if (pFrame == NULL) {
TRACE(traceAppMsg, 0, \"Warning: Dynamic create of frame %hs failed.\\n\", m_pOleFrameClass->m_lpszClassName); return NULL; }
// create new from resource (OLE frames are created as child windows) if (!pFrame->LoadFrame(m_nIDServerResource,
WS_CHILD|WS_CLIPSIBLINGS, pParentWnd, &context)) {
TRACE(traceAppMsg, 0, \"Warning: CDocTemplate couldn't create an OLE frame.\\n\"); // frame will be deleted in PostNcDestroy cleanup return NULL; }
然后,在docsingl.cpp中这样赋值: pThread->m_pMainWnd = pFrame; pThread就是那个传说中的theApp.
如果是多文档或者是对话框的话应该在InitInstance里面直接有 pMainFrame->LoadFrame(IDR_MAINFRAME)
在LoadFrame函数中调用了:
CFrameWndEx::LoadFrame(nIDResource, dwDefaultStyle, pParentWnd, pContext) 在上面那个函数中再调用:
CFrameWnd::LoadFrame(nIDResource, dwDefaultStyle, pParentWnd, pContext)
终于到达了我们消息设置的起点:
AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)
在上面那个函数中定义了窗口类WNDCLASS wndcls;然后把窗口类的消息处理函数初始化为DefWindowProc,我们知道DefWindowProc是Win32自带的消息处理函数,它并不知道我们要怎样处理我们的消息,这个处理函数将来会被我们mfc的处理函数代替,我们继续看下去吧。
紧接着AfxDeferRegisterClass,LoadFrame函数调用了 Create(lpszClass, strTitle, dwDefaultStyle, rectDefault,
pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext)
Create函数调用
CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext)
在CreatEx中一个MFC的钩子函数被调用 AfxHookWindowCreate(this);
这个钩子程序中调用Win32 API函数
SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
通过这个函数在窗口被激活,创建,撤销,最小化时将调用_AfxCbtFilterHook函数。在_AfxCbtFilterHook函数中用AfxGetAfxWndProc取得旧的消息处理函数后,然后用SetWindowLongPtr吧原来的消息处理函数换为mfc的afxWindProc WNDPROC afxWndProc = AfxGetAfxWndProc();
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (DWORD_PTR)afxWndProc);
现在我们来看看afxWndProc干了什么事情,它首先 CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
取得CWnd的指针,这个指针指向我们继承自CWnd的程序的主类的对象,然后调用 AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam)
我们再看AfxCallWndProc函数的工作,它首先调用的关键函数是 pWnd->WindowProc(nMsg, wParam, lParam)
在wincore.cpp中WindowProc的定义如下
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) {
// OnWndMsg does most of the work, except for DefWindowProc call LRESULT lResult = 0;
if (!OnWndMsg(message, wParam, lParam, &lResult)) lResult = DefWindowProc(message, wParam, lParam); return lResult; }
它首先调用了
OnWndMsg(message, wParam, lParam, &lResult)
转到OnWndMsg的定义,发现这个成员函数是一个长达500行的巨型函数
它处理了WM_COMMAND, WM_NOTIFY, WM_ACTIVATE, WM_SETCURSOR以及一些特殊的ActiveX消息,对于WM_COMMAND消息,它调用了OnCommand(wParam, lParam),也是CWnd中的函数通过研究发现OnCommand做一些我没有考虑过的事情。
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam) // return TRUE if command invocation was attempted {
UINT nID = LOWORD(wParam); HWND hWndCtrl = (HWND)lParam; int nCode = HIWORD(wParam);
// default routing for command messages (through closure table) if (hWndCtrl == NULL) {
// zero IDs for normal commands are not allowed if (nID == 0) return FALSE;
// make sure command has not become disabled before routing
CTestCmdUI state; state.m_nID = nID;
OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL); if (!state.m_bEnabled) {
TRACE(traceAppMsg, 0, \"Warning: not executing disabled command %d\\n\", nID); return TRUE; }
// menu or accelerator nCode = CN_COMMAND; } else {
// control notification
ASSERT(nID == 0 || ::IsWindow(hWndCtrl));
if (_afxThreadState->m_hLockoutNotifyWindow == m_hWnd) return TRUE; // locked out - ignore control notification // reflect notification to child window control if (ReflectLastMsg(hWndCtrl)) return TRUE; // eaten by child
// zero IDs for normal commands are not allowed if (nID == 0) return FALSE; } #ifdef _DEBUG
if (nCode < 0 && nCode != (int)0x8000)
TRACE(traceAppMsg, 0, \"Implementation Warning: control notification = $%X.\\n\", nCode); #endif
return OnCmdMsg(nID, nCode, NULL, NULL); }
首先,函数把参数wParam的高低字分离,低位表示的是控件的ID或者我们定义的消息,把lParam看作一个控件句柄。控件的句柄为空而wParam的低位不为0则把这个消息交给OnCmdMsg。如果控件的句柄不为空,,则通过ReflectLastMsg把消息交给控件处理。最后如果消息没有被处理的话就调用OnCmdMsg把wParam的高低字传进去。现在我们看看OnCmdMsg干了什么。在寻找OnCmdMsg的定义过程中,我发现这个函数不是CWnd自己的OnCmdMsg函数,而是CFrameWnd重写了的OnCmdMsg的函数,找了很久终于在winfrm.cpp中找到它了,它的定义如下: BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) {
CPushRoutingFrame push(this); // pump through current view FIRST CView* pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// then pump through frame
if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE;
// last but not least, pump through app CWinApp* pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE; return FALSE; }
先取得View对象的指针,然后调用View对象的OnCmdMsg,如果消息被View处理了则退出,View无法处理则调用CWnd的OnCmdMsg,再无法处理则调用App对象的OnCmdMsg,最后都不能处理就只能返回FALSE了,从这里我们可以看出消息的路由先后顺序是View->FrameWnd->App,我们先看CFrameWnd::OnCmdMsg吧,我只贴出我认为重要的代码,把我要说的话写在注释里面: const AFX_MSGMAP* pMessageMap; const AFX_MSGMAP_ENTRY* lpEntry; if (nCode != CN_UPDATE_COMMAND_UI) {
nMsg = HIWORD(nCode); nCode = LOWORD(nCode); }
if (nMsg == 0)
nMsg = WM_COMMAND;
//从最高层的MessageMap开始搜索对应的消息,一直搜索到最底层,也就是CObject for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)()) {
//搜索匹配的消息,听说下面这个函数用了汇编的代码来加快速度
lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID); if (lpEntry != NULL) {
// 发现了匹配的消息
//调用下面的函数调用记录在lpEntry匹配的元素中的消息处理函数
return _AfxDispatchCmdMsg(this, nID, nCode, lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo); } }
为了看到底,我们接着看_AfxDispatchCmdMsg函数的代码,它位于cmdtarg.cpp
AFX_STATIC BOOL AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode, AFX_PMSG pfn, void* pExtra, UINT_PTR nSig, AFX_CMDHANDLERINFO* pHandlerInfo) // return TRUE to stop routing {
ENSURE_VALID(pTarget);
UNUSED(nCode); // unused in release builds union MessageMapFunctions mmf; mmf.pfn = pfn;
BOOL bResult = TRUE; // default is ok if (pHandlerInfo != NULL) {
pHandlerInfo->pTarget = pTarget; pHandlerInfo->pmf = mmf.pfn; return TRUE; }
switch (nSig) {
default: // illegal ASSERT(FALSE); return 0; break; case AfxSigCmd_v:
// normal command or control notification
ASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKED ASSERT(pExtra == NULL); (pTarget->*mmf.pfnCmd_v_v)(); break; ……………………………
其中上面的union MessageMapFunctions是所有的可用消息处理函数的一个联合,我们可以先把一个成员函数的相对地址传给它,再根据nSig得到函数的类型进行函数的调用,这里用到了成员函数指针的概念。View的,App的消息路由也差不多。
上面是消息的路由过程,那么消息等待和分发发生在哪里呢?在MFC的main函数AfxWinMain中,它有这样一个语句
nReturnCode = pThread->Run();
pThread->Run()的源代码是: int CWinApp::Run() {
if (m_pMainWnd == NULL && AfxOleGetUserCtrl()) {
// Not launched /Embedding or /Automation, but has no main window!
TRACE(traceAppMsg, 0, \"Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application.\\n\");
AfxPostQuitMessage(0); }
return CWinThread::Run();
}
它又调用了CWinThread的Run,这个Run的代码缩短版如下 int CWinThread::Run() {
ASSERT_VALID(this);
_AFX_THREAD_STATE* pState = AfxGetThreadState(); BOOL bIdle = TRUE; LONG lIdleCount = 0; for (;;) {
while (bIdle &&
!::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)) {
if (!OnIdle(lIdleCount++))
bIdle = FALSE; // assume \"no idle\" state } do {
if (!PumpMessage()) return ExitInstance();
if (IsIdleMessage(&(pState->m_msgCur))) {
bIdle = TRUE; lIdleCount = 0; }
} while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)); } }
然后轮到PumpMessage出场了 BOOL CWinThread::PumpMessage() {
return AfxInternalPumpMessage(); }
还要调用AfxInternalPumpMessage,这个函数的源代码中有一段大家期待已久的代码 if (pState->m_msgCur.message != WM_KICKIDLE && !AfxPreTranslateMessage(&(pState->m_msgCur))) {
::TranslateMessage(&(pState->m_msgCur)); ::DispatchMessage(&(pState->m_msgCur)); }
终于看到经典的TranslateMessage,DispatchMessage了,这就是MFC消息的全过程!
经过这次探究,我体会到了MFC的复杂,以及C++那些不常用的功能的强大。
首先,在MFC程序启动之初,MFC抓住了全局函数会首先初始化这个特点,在theApp这个全局变量的初始化过程中为其它的全局变量设定了值,使得一系列的afx函数可以取得指向theApp的指针。这种做法非常巧妙。
其次就是宏的不可思议的用法,BEGIN_MSG_MAP 和 END_MSG_MAP就像汉堡的两块面包一样夹这中间的ON_COMMAND等宏定义形成了一个函数,虽然宏不可以滥用,但是正确使用真是威力强大。
在AFX_MSGMAP的构造也十分巧妙,我们都知道结构体里面有那个结构体的指针的话可以连成一个链表,但没想到,指向一个基类的成员函数的话可以也可以形成一个从子类到基类的消息链表,值得注意的是AFX_MSGMAP指向的基类成员函数是静态的。
在MFC中多态的运用也是展现的淋漓尽致,调试跟踪过程中常常会发现从一个类的函数调用本类的函数结果跳到了另外一个类的函数,这样虽然很混乱,但是的确实现了很多没有多态无法实现的功能。
在看到AfxInternalPumpMessage这个union的定义和用法后,我发现了原来union不单节省空间,还可以起到强制转换的效果。比如说有如下定义 union Mix { int i; char c; };
当写下Mix m; m.i = 31H时,写m.c就可以得到一个char ’1’了。
MFC的匈牙利命名法同样给我很大的震撼,平时看别人的代码时,即使代码很短,我都会看得十分痛苦,但是看MFC的代码我发现,只要看到变量名就可以知道变量的属性,类型和用途了。看了那么一大段MFC代码后觉得感觉还是不错的。
在寻找消息的源头的过程中,我遇到了很多的困难,刚开始是用右键->转到定义这种方法去跟踪,后来发现,这样的跟踪法不可以找到cpp中的定义,没办法只好上百度,发现了一篇名为《MFC技术内幕系列之四》的文章,我就对着那篇文章一步一步地前进,当进行到theApp.InitInstance时,我发现找不到文章中贴出来对的关键代码pThread->m_pMainWnd = pFrame,这让我很困惑,找了好久才发现原因是多文档,对话款和单文档三种模式的的InitInstance有比较大的差别,在发现这个原因的过程中我在网上看到了一个看源码的方法—---调试程序,经过无数次的插入断点,打开了不少与30个MFC的cpp源代码文件,终于发现了那段关键的代码。通过发现那段关键代码,我觉得通过调试来了解程序的运行是一个很好的方法。
最后,在探究的过程中在csdn论坛上的一个MVP的话引起了我的思考,他说:“把怎么造钉子搞清楚了未必就能把机器组装出来”。他说的非常对,即使我们把MFC的各种构造都知道得非常清楚了,我们还是不一定可以写出好的程序的,只有我们多思考,多动手才能取得进步。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- baoaiwan.cn 版权所有 赣ICP备2024042794号-3
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务