MFC应用程序框架入门(转)
1 MFC概述
顾名思意,MFC应用程序框架是以MFC作为框架基础的,以此程序框架模式搭建起来的应用程序在程序结构组织上是完全不同于以前的Win32 SDK编程方式的。自20世纪90年代初问世以来,MFC一直试图把Windows API函数封装到类库中个各个逻辑类中。MFC的这种封装并非简单地对API函数进行分组与打包,而是更多地通过类来试图实现全部的系统策略。随着越来越多系统功能的加入,MFC的规模也在不断拓展,目前已包括有200多个类,涵盖了通用Windows 类、文档/视框架、OLE、数据库、Internet以及分布式功能等多方面的基本内容。这样一个坚实的程序开发基础无疑从很大程度上方便了程序设计人员对Windows 程序的开发。
MFC提供了相当多不同功能的类以适合尽可能广泛的需求。这里绝大多数的MFC类都是直接或间接从CObject类派生出来的,CObject类为其派生类提供了三个重要的特性支持:持久性(Serialization)支持、运行时(Run-time)类信息支持和诊断(Diagnostic)调试支持等。其中持久性是以流的方式将某个类对象中的持久性数据输出或输入到外部存储介质如磁盘文件等的过程;运行时类信息(Run-time Class Information,RTCI)则可以重新获取一个对象的类名及其他一些有关对象在运行时的信息。RTCI也是C++中除运行时类型信息(Run-time Type Information,RTTI)机制外的另一个重要工具;诊断和调试支持作为CObject类的一个组成部分,可以在实现CObject派生类时执行有效性检查并可向调试窗口输出状态信息。
并非MFC提供的所有函数都是类成员函数,MFC也提供了一系列以Afx为前缀的全局函数。类成员函数只能在其所属类对象所在的上下文中使用,但是这些AFX函数却可以在任何时候的任何地方直接使用。下表列出的是几个比较重要AFX函数:
函数名 | 函数说明 |
AfxAbout | 无条件终止一个应用程序;通常在发生无法回复的错误时使用 |
AfxBeginThread | 创建一个新的线程并开始执行 |
AfxEndThread | 终止当前正在执行的线程 |
AfxMessageBox | 显示一个Windows 消息窗口 |
AfxGetApp | 返回一个指向应用程序对象的指针 |
AfxGetAppName | 返回应用程序名 |
AfxGetMainWnd | 返回一个指向应用程序主窗口的指针 |
AfxGetInstanceHandle | 返回一个标识当前应用程序实例的句柄 |
AfxRegisterWndClass | 为一个MFC应用程序注册一个用户自定义的窗口类 |
2 MFC对API函数的封装
如果读者曾经有过SDK的开发经历,一定会对其烦琐的编程方式和大量的Win32 API函数调用深有感触。所有不同功能的API函数均是以全局函数的形式放在一起的,由于API函数数目比较庞大,因此无论是学习还是使用都是有一定难度的。相比而言,建立在API函数基础之上的MFC类库则通过把相关API函数的分类封装而可以大大简化编程的难度,用MFC类编写的Windows 应用程序完成相同的任务只需要进行少量的工作。
众多的API函数根据功能的不同而被MFC封装到200多个类中,这些类基本涵盖了进行Windows 编程大部分可能用到的功能。由于封装后的MFC类太多,这里不能一一介绍,下面就以其中比较重要的CObject类和CWnd类为例对API函数的封装情况做一简要介绍。
CObject类是MFC中最主要也是最基本的类之一,该类不支持多重继承,派生的类只能有一个CObject基类。CObject类是位于类层次结构最顶层的,绝大多数MFC类都是从CObject类派生出来的。CObject类包含了所有MFC类必须具备的几个基本功能:持久性支持、运行时类信息支持和诊断调试支持。其中持久性支持功能由成员函数IsSerializable()和Serialize()提供。前者用于检测对象是否支持序列化。如果一个类能够被序列化,就必须在声明时包含DECLARE_SERIAL宏、在实现时包含IMPLEMENT_SERIAL宏。Serialize()函数则可以将对象写入档案文件(Archive)或从档案文件读出对象。成员函数GetRuntimeClass()可以获取到一个指向CruntimeClass类对象的指针,通过该指针可以得到对象的运行时类信息。CObject类在诊断调试支持方面提供了成员函数AssertValid()和Dump(),前者可对对象内存状态的有效性进行检查,后者负责将对象的内容转储到一个CdumpContext对象中,并可以提供诊断服务及一些有用的调试信息。
在MFC中,CWnd类提供了所有窗口类的基本功能,是一个非常重要的类,大约三分之一的MFC类都是以此为基类。该类主要对创建、操纵窗口类的API函数进行了封装,而且通过消息映射机制隐藏了SDK编程中使用相当不便的窗口处理函数,是消息的分发处理更加方便。
CWnd类最重要的一个封装是对API函数CreateWindow()的封装,该函数被封装为CWnd类成员函数Create()。从VC提供的MFC源文件WinCore.cpp中可以清楚看出CWnd类对CreateWindow()函数的封装过程,下面给出相关部分的实现清单:
BOOL CWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext) { // can't use for desktop or pop-up windows (use CreateEx instead) ASSERT(pParentWnd != NULL); ASSERT((dwStyle & WS_POPUP) == 0); return CreateEx(0, lpszClassName, lpszWindowName, dwStyle | WS_CHILD, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pParentWnd->GetSafeHwnd(), (HMENU)nID, (LPVOID)pContext); } |
可以看出,主要工作是在CreateEx()成员函数中完成的,而该函数又是对API函数CreateWindowEx()的封装。封装后的代码在调用CreateWindowEx()前构造并填充了一个非常类似于WNDCLASS结构的CREATESTRUCT结构,并调用了PreCreateWindow()。
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.8Xn", 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; } |
看上去经过封装的窗口创建函数要比原API函数复杂许多,但这并不说明MFC的封装将导致编程的效率低下,恰恰相反,由于CWnd在绝大多数场合中是以基类的形式出现的,因此可在派生类中添加代码完成对CWnd::Create()的调用而比较方便的实现对派生类窗口的创建。
MFC应用程序框架可以看作是MFC基本类库的一个超集(Superset),类库是众多可在任何程序中使用的类的集合,而应用程序框架则定义了程序自身的结构。下面给出一个使用了MFC应用程序框架的简单例子,通过这段例程可以比较清楚地了解MFC应用程序框架的一般结构。
// Sample01.h文件 // 应用程序类 class CSample01App : public CWinApp { public: virtual BOOL InitInstance(); }; // 框架窗口类 class CSample01Frame : public CFrameWnd { public: CSample01Frame(); protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() }; // Sample01.cpp文件 #include <afxwin.h> #include "Sample01.h" // 应用程序对象 CSample01App theApp; // 初始化应用程序实例 BOOL CSample01App::InitInstance() { m_pMainWnd = new CSample01Frame(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } // 消息映射 BEGIN_MESSAGE_MAP(CSample01Frame, CFrameWnd) ON_WM_PAINT() END_MESSAGE_MAP() // 构造函数 CSample01Frame::CSample01Frame() { Create(NULL, "MFC应用程序框架程序"); } // WM_PAINT消息响应函数 void CSample01Frame::OnPaint() { CPaintDC dc(this); dc.TextOut(100, 100, "Hello World!"); } |
仍象编写Sample00程序一样建立一个Win32应用程序工程Sample01(配套程序见"光盘配套程序Sample01"),然后分别向工程添加头文件Sample01.h和源文件Sample01.cpp,并将上述代码写入相应的文件。为了能顺利编译,还需要修改一下编译命令,通过"Alt+F7"快捷键呼出【Project Settings】对话框,在【Preprocessor definitions】栏的最后添加选项"_AFXDLL",前面用逗号分隔。接下来还需要在【Project Options】栏的最后添加命令行"/MD",用空格同其他命令行参数进行分隔。编译运行,可以看出效果同SDK方式编写的Sample00程序是一样的,但在代码实现上更加结构化,编写过程也更加简单。
接下来,对上述应用程序框架代码进行分析。首先从MFC应用程序的核心--CWinApp类的派生类CSample01App谈起。CWinApp类提供了可以获取消息并将获取到的消息分发到应用程序窗口的消息循环和一些关键的虚函数,通过对这些虚函数的重载可使开发人员对应用程序的一些固有行为进行扩展。当把头文件Afxwin.h包含进来后,就可以在程序中使用包括CWinApp在内的一些MFC类了。一个MFC应用程序有且只能有一个应用程序对象而且必须被以全局方式进行声明,所以该对象自程序开始运行起就一直驻留在内存。
由于使用了MFC应用程序框架的程序在本质上仍是Windows 应用程序,因此必然需要在程序中存在作为Windows 应用程序入口的WinMain()函数。在前面的示例代码中之所以没有看到WinMain()函数是由于该函数已经通过封装的手段隐藏到应用程序框架中了。除WinMain()外,CWinApp类成员函数Run()也是隐含执行的,这个函数也是非常重要的,它负责把消息放进应用程序窗口的消息循环中,由WinMain()函数完成对Run()的调用 。
当WinMain()函数寻找到应用程序对象后将立即调用CWinApp类的虚函数InitInstance()。由于CWinApp基类是不知道究竟需要何种主框架窗口的,因此在使用时必须在CWinApp的派生类中对InitInstance()函数进行重载。InitInstance()函数是在应用程序已经开始运行但窗口尚未创建时被调用的,若非由InitInstance()创建了窗口,应用程序是无法拥有窗口的,这也就意味着缺少了InitInstance()函数的应用程序将无法接收、处理消息,对Windows程序而言这也就失去了存在的意义。由此可见,从CWinApp类中进行派生,并且对InitInstance()函数进行重载是编写MFC应用程序框架程序的必要条件。
除应用程序类外,从CFrameWnd派生的CSample01Frame类还对应用程序的主框架窗口做了描述。在构造函数中调用基类的CFrameWnd成员函数Create(),由Windows 负责创建出实际的窗口结构,并由应用程序框架将其链接到C++对象。
本示例程序的大部分功能实际是在MFC的CWinApp和CFrameWnd等基类中完成的,在编程时,只需在派生类中编写少量功能代码,C++允许以这样的方式借用基类中的大量代码而无须复制代码。应用程序框架负责提供程序的结构框架,开发人员在此基础上为其添加相应的实现代码,从而可以非常方便地完成一个完整的应用程序。应用程序框架不仅定义了应用程序的结构安排,实际上还包含了更多的C++基类。
4 小结
SDK的API编程方法、MFC的编程方法以及本系列讲座后面将要介绍的ATL编程方法是VC++程序设计中比较常用的几种编程方法,其中MFC以其强大的功能和灵活的编程方式而成为大多数程序开发人员最经常使用的一种编程方式。本文从基础问题入手对MFC及其框架程序做了较为详细的论述,使读者能够对MFC编程有一个基本的认识。