除了传统的SDK方式编写Windows应用程序外,还可以使用微软的基础类库MFC。它采用C++程序设计语言对SDK函数进行包装,使Windows下的C语言程序设计,成为面向对象的MFC。以下为MFC窗口应用程序框架示例及解析。
1.建立Win32项目FirstMFC
打开Visual Studio 2005à文件à新建à项目àVisual C++àwin32àwin32项目àwin32应用程序à空项目(默认使用Unicode字符集)。然后在“项目属性à常规àMFC的使用”中选择“在共享 DLL 中使用MFC”。如果运行时提示找不到mfc80d.dll或msvcr80d.dll文件,在“项目属性à配置属性à清单工具à常规”中的“使用FAT32解决办法”处选择“是”,再重新生成解决方案。
2.编写源文件FirstMFC.cpp
3.MFC程序框架浅析
3.1 afxwin.h
afxwin.h为MFC标准头文件,它包含了afx.hà afxver_.hàafxv_w32.hàwindows.h,等头文件。该头文件包含了常用的窗口类CWnd、应用程序类CWinApp、线程类CWinThread、文档类CDoc、视图类CView、菜单类CMenu、对话框类Cdialog、设备描述表类CDC及常见绘图工具类,因此每一个MFC应用程序都必须加载它。
3.2 框架程序的启动
上面的程序中,我们首先由CFrameWnd派生CMainFrame,并编写了其鼠标单击消息响应;由CWinApp派生CMainApp,并重载CWinApp::InitInstance,在其中new一个CMainFrame对象赋给CWinApp::m_pMainWnd;最后声明了CMainApp一个全局对象实例theApp。鉴于MFC框架的封装性,初学者可能会云里雾里。MFC既然只是对Windows GUI程序的一个封装,其筋骨内核还是SDK那一套:创建窗口+消息循环。你肯定会问,MFC框架程序是怎么启动的?入口函数WinMain在哪里呢?关于Windows应用程序的运行机制,我们在《Windows编程之从控制台到SDK窗口》中已做过初步探讨。我们还是F10,断点将停留在appmodul.cpp中的_tWinMain处,其调用堆栈如下:
// CRTEXE.C
wmainCRTStartupà__tmainCRTStartupà(w)WinMain
_tWinMain为WinMain的平台适应宏(_t),其定义如下:
// TCHAR.H
#ifdef _UNICODE
#define _tWinMain wWinMain
#else /* ndef _UNICODE */
#define _tWinMain WinMain
至此,我们寻找到了基于MFC框架的Windows GUI程序的启动点。然而,_tWinMain函数极其简单,其只是简单的调用AfxWinMain。
// APPMODUL.CPP
extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
__in LPTSTR lpCmdLine, int nCmdShow)
{
// call shared/exported WinMain
return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
我们继续F11深入其其幕后。AfxWinMain的代码如下:
// WINMAIN.CPP
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread(); // CMainApp theApp;
CWinApp* pApp = AfxGetApp(); // CMainApp theApp;
// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// 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();
InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE1("Warning: Temp map lock count non-zero (%ld)./n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif
AfxWinTerm();
return nReturnCode;
}
在这里,我们发现了MFC框架的心脏——CWinThread!
3.3 CWinApp
应用程序实例类CMainApp:CWinApp:CWinThread,在MFC中CWinApp:CWinThread类实现了SDK应用程序的WinMain函数体。传统SDK应用程序中WinMain函数所完成的工作由CWinApp的InitApplication、InitInstance和Run成员函数承担。CMainApp theApp;创建了应用程序唯一的全局对象,它代表了应用程序的主线程,使得程序得以执行。
应用程序主框架窗口为CMainFrame。CMainFrame:CFrameWnd:CWnd。CWnd类代表了MFC中最基本的GUI对象,它提供包括注册窗口类、创建窗口及子窗口、获得窗口、管理窗口、访问窗口及控件、控制窗口光标、创建和使用句柄等功能。CWnd封装了SDK中的RegisterClass、CreateWindow等操作。CFrameWnd类的对象是一个框架窗口,它包含边框、标题栏、菜单、最大/小化按钮和一个激活的视图,m_pMainWnd = new CMainFrame();则创建了应用程序的主窗口。
入口函数AfxWinMain通过AfxGetThread()(AfxGetApp())获取应用程序示例theApp,然后调用theApp.InitApplication()进行全局初始化,进而调用theApp.InitInstance()。
CWinThread::InitInstance()函数是CWinApp派生类唯一需要重载的函数,它负责应用程序的初始化,如初始化数据、创建文档模板、处理命令行以及应用程序主窗口的创建和显示。一般在其中完成主窗口的创建(CreateWindowEx)、显示(ShowWindow)和刷新(UpdateWindow)。
AfxWinMain紧接着调用theApp.Run()。CWinThread::Run()是线程消息收发功能的控制函数,完成GetMessage和DispatchMessage。
MFC应用程序的启动过程如下图所示:
相关源码参考APPMODUL.CPP、WINMAIN.CPP、APPCORE.CPP、THRDCORE.CPP。
CWnd::Create函数调用CreateExàPreCreateWindow/AfxHookWindowCreate/::CreateWindowEx,参考WINCORE.CPP。
(1)PreCreateWindow-窗口类的注册
PreCreateWindow函数调用AfxDeferRegisterClassàAfxEndDeferRegisterClass [à_AfxRegisterWithIcon]àAfxRegisterClassà::RegisterClass注册窗口类(.lpfnWndProc = DefWindowProc)。用户可调用AfxRegisterWndClass/AfxRegisterClass注册自己的窗口类,然后将返回值(窗口类名称)作为Create的第一参数。
如果传递给Create函数的第一个参数lpszClassName为NULL,则使用系统默认类(afxRegisteredClasses):
CWnd::PreCreateWindowßàAFX_WND_REGßà_afxWnd
CFrameWnd::PreCreateWindowßàAFX_WNDFRAMEORVIEW_REG ßà_afxWndFrameOrView
CView::PreCreateWindowßàAFX_WNDFRAMEORVIEW_REGßà_afxWndFrameOrView
AFXIMPL.H中定义了AFX_WND*_REG 、AFX_WNDCLASS等;WINCORE.CPP中定义了_afxWnd*等窗口类名称字符串(const TCHAR[])。
用户可以重载虚函数PreCreateWindow(末尾需要调用基类的该函数),设置其CREATESTRUCT参数,以改变CreateWindowEx的部分参数。
(2)CreateWindow-消息钩子及窗口的创建
因为在CreateWindowEx返回窗口句柄之前,窗口函数就开始处理诸如WM_GETMINMAXINFO、WM_NCCREATE、WM_CREATE等消息,所以必须在CreateWindowEx之前安装WH_CBT类型的钩子,以便截获CreateWindowEx返回之前调用过程中发送的消息。 AfxHookWindowCreate函数调用::SetWindowsHookEx(WH_CBT,_AfxCbtFilterHook, NULL, ::GetCurrentThreadId());安装钩子。钩子函数_AfxCbtFilterHook中调用SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)afxWndProc);将创建hWnd时所使用的WNDCLASS窗口模板的过程函数由DefWindowProc更改为AfxWndProc。
(3)消息循环
CWinThread::Run()函数调用::PeekMessage(&m_msgCur, …)和PumpMessage()来查询和获取消息,如果有消息则将其记录到CWinThread::m_msgCur成员中。在PumpMessage()中调用::GetMessage(&m_msgCur, …),如果Get到的Message为WM_QUIT,则PumpMessage()返回FALSE。CWinThread::Run()函数调用CWinThread::ExitInstance()函数做线程退出清理。如果Get到的Message非WM_QUIT或WM_KICKIDLE(dialog equivalent of OnIdle),则先调用PreTranslateMessage(&m_msgCur)对要处理的消息进行过滤,再调用::TranslateMessage(&m_msgCur);对消息进行“翻译”,再调用::DispatchMessage(&m_msgCur);将消息传送给指定窗口的窗口函数,即AfxWndProc。
AfxWndProcàAfxCallWndProc:
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
AfxCallWndProc将消息交由具体窗口对象的WindowProc处理:CWnd::WindowProcàCWnd::OnWndMsg。在OnWndMsg中,对消息进行分流处理:WM_COMMANDàOnCommand,WM_NOTIFYàOnNotify。对于常规窗口消息,OnWndMsg调用GetMessageMap()函数获得DECLARE_MESSAGE_MAP声明的消息映射表AFX_MSGMAP,根据nMessage和nCode调用AfxFindMessageEntry函数从消息映射表中查找AFX_MSGMAP_ENTRY,根据函数签名nSig从MessageMapFunctions中查询正确的函数指针,完成消息函数(AFX_PMSG pfn)的正确调用。
CreateWindowEx函数完成窗口的创建。返回hWnd之前,其WM_CREATE消息将被_AfxCbtFilterHook截获(wParam参数即为hWnd)。这样WM_CREATE消息就会被_AfxCbtFilterHook设置的AfxWndProc窗口函数处理。这也是为什么在窗口创建之前就需要安装消息钩子的原因。
用户可以重载虚函数PreTranslateMessage(末尾需要调用基类的该函数),对感兴趣的消息进行过滤预处理。
(4)消息映射
现在再来看看MFC中的消息处理,传统的SDK Windows应用程序消息处理方式采用switch-case分支结构实现消息的分发。而在MFC中采用消息映射机制(Message Map)。这种消息映射机制包括一组消息映射宏,一条消息映射宏把一个Windows消息和其他消息处理函数联系起来。
//声明消息映射
BEGIN_MESSAGE_MAP(theClass, theBaseClass)
//{{AFX_MSG_MAP
ON_WM_LBUTTONDOWN() //单击鼠标左键的映射宏
ON_MESSAGE(messageID,memberFun)
……
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
在类定义文件中声明消息处理函数,例如Windows预定义的单击鼠标左键消息的处理函数为:
afx_msg void OnLButtonDown(UINT nFlags,CPoint point);
为了使用消息映射宏,还需在头文件中的类末尾声明使用消息映射宏DECLARE_MESSAGE_MAP(),该宏为类声明消息映射AFX_MSGMAP。
在类实现文件中,BEGIN_MESSAGE_MAP宏为消息映射AFX_MSGMAP的定义开始({),END_MESSAGE_MAP()宏为消息映射的定义结束(})。这两个宏之间的内容为AFX_MSGMAP. lpEntries,即AFX_MSGMAP_ENTRY _messageEntries[]。用户向其中添加关注的消息映射,并编写其消息响应函数。
4.MFC程序框架的研究之道
探究MFC内部机制,需参阅MFC源码,我们可以对一个简单的MFC框架程序进行F5,、F10、F11以管中窥豹其内部运行流程。
(1)MFC巧妙地使用了精致而又强大的宏,这些宏会给阅读源码会带来一定的困难。
(2)框架的状态信息也是理解的难点,包括模块状态AFX_MODULE_STATE、线程状态_AFX_THREAD_STATE和模块线程状态AFX_MODULE_THREAD_STATE,其中涉及到线程局部存储(TLS)的知识。例如消息队列即是一种线程私有数据,GetMessage、TranslateMessage、DispatchMessage等操作都是线程相关的。源码参考AFXTLS_.H/AFXTLS.CPP、AFXSTAT_.H/AFXSTATE.CPP、APPMODUL.CPP。
(3)MFC类库体系建立在动态类型识别和动态创建的基础上,CObject为对象之根,由一个重要的结构体CRuntimeClass将对象帝国体系化。其中涉及到两组重要的宏:DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC和DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE。源码参考AFX.H、OBJCORE.CPP。
(4)MFC框架中的窗口管理基于窗口句柄映射CHandleMap,其中CWnd::Attach函数Attaches a Windows handle to a CWnd object;CWnd::Detach函数Detaches a Windows handle from a CWnd object。static CWnd::FromHandlePermanent函数Returns a pointer to a CWnd object when given a handle to a window;CWnd::GetSafeHwnd函数Returns CWnd::m_hWnd。源码参考AFXCOLL.H、AFXWIN.H、WINCORE.CPP、VIEWCORE.CPP、WINFRM.CPP。
(5)MFC中的消息映射将消息和消息处理函数关联起来,例如ON_WM_CREATE() :WM_CREATEßàOnCreate,这样大大简化了switch-case庞大的分支处理体系。其中牵涉到类成员函数指针、函数签名(AfxSig)及函数类型转换。涉及到的数据结构有AFX_MSGMAP_ENTRY 、AFX_MSGMAP;涉及到的宏有DECLARE_MESSAGE_MAP、BEGIN_MESSAGE_MAP/END_MESSAGE_MAP。源码参考AFXMSG_.H、AFXWIN.H。
(6)面向对象中基于虚函数的多态性在MFC框架窗口消息处理流程中扮演着极其重要的角色。通过基类对象指针调用虚函数将在运行期调用实际指向的派生类对象的相应函数,动态绑定以改变处理流向。窗口的虚函数接口使得我们可以根据实际需要编写自己的处理过程,以取代基类的默认处理。
除了以上手动编写MFC框架程序外,我们也可以利用MFC向导生成一个通用应用程序框架:打开Visual Studio 2005à文件à新建à项目àMFCàMFC应用程序à单文档项目;编译生成可执行文件即可运行。
至此,我们通过对比传统SDK应用程序和MFC应用程序格式,对各自的框架有了初步的认识了。后面我们将通过应用实例来一步一步深入MFC/windows应用程序编程。
参考:
《深入浅出MFC》候俊杰
《MFC深入浅出》李进久
《Windows程序设计》王艳平
《深入解析MFC》George Shepherd, Scot Wingo