我是荔园微风,作为一名在IT界整整25年的老兵,今天来看一下微软MFC技术简明介绍
Visual C++ 与 MFC
微软公司于1992年上半年推出了C/C++ 7.0 产品时初次向世人介绍了MFC 1.0,这个产品包含了20,000行C++原始代码,60个以上的Windows相关类,以及其它的一般类如时间、数据处理、文件、内存、诊断、字符串等等等。它所提供的,其实是一个 "thin and efficient C++ transformation of the Windows API"。其32位版亦在1992年下半年随着 Win32 SDK 推出。
MFC 1.0 获得的回响带给 AFX 小组不少鼓舞。他们的下一个目标放在更高阶的架构支持和组件用户接口上。前者成就了Document/View 架构,后者成就了工具栏、状态栏、打印、预览等极受欢迎的UI性质。当然,他们并没有忘记兼容性与移植性。虽然 AFX 小组并未承诺MFC可以跨不同操作系统如 UNIX XWindow、OS/2 PM、Mac System 7,但在其本家(Windows 产品线)身上,在16位Windows 3.x和32位Windows 95 与Windows NT之间的移植性是无庸置疑的。虽然其16位产品和32位产品是分别包装销售,你的原始代码通常只需重新编译链接即可。
Visual C++ 1.0(也就是 C/C++ 8.0)搭配 MFC 2.0 于1993/03 推出,这是针对Windows 3.x 的16位产品。接下来又在1993/08 推出在Windows NT上的 Visual C++ 1.1 for Windows NT,搭配的是MFC 2.1。这两个版本有着相同的基本性质。MFC 2.0 内含近60,000 行 C++ 程序代码,分散在 100 个以上的类中。Visual C++ 整合环境的数个重要工具(如Wizards)本身即以 MFC 2.0 设计完成,它们的出现对于软件生产效率的提升有极大贡献。
微软在 1993年又推出了 16 位的 Visual C++ 1.5,搭配 MFC 2.5。这个版本最大的进步是多了 OLE2 和 ODBC 两组类。整合环境也为了支持这两组类而做了些微改变。
1994年,微软推出Visual C++ 2.0,搭配MFC 3.0,这个32位版本主要的特征在于配合目标操作系统(Windows NT 和Windows 95),支持多线程。所有类都是thread-safe。UI 对象方面,加入了属性表(Property Sheet)、miniframe 窗口、可随处停驻的工具栏。MFC collections 类改良为 template-based。链接器有重大突破,原使用的Segmented Executable Linker 改为 Incremental Linker,这种链接器在对 OBJ 文件做链接时,并不每次从头到尾重新来过,而只是把新数据往后加,旧数据加记作废。想当然耳,EXE 文件会累积许多不用的垃圾,那没关系, 透过Win32 memory-mapped file, 操作系统(Windows NT 及 Windows 95)只把欲使用的部分加载,丝毫不影响执行速度。必要时程序员也可选用传统方式链接,这些垃圾自然就不见了。对我们这些终日受制于edit-build-run-debug 轮回的程序员, Incremental Linker 可真是个好礼物。
1995年,微软又加上了MAPI(Messaging API)和 WinSock 支持,推出 MFC 3.1(32 位元版),并供应13个通用控制组件,也就是 Windows 95 所提供的tree、tooltip、spin、slider、progress、RTF edit 等等控制组件。
1995年,MFC 有了3.2 版,那是不值一提的小改版。
然后就是1995年的32 位MFC 4.0。这个版本纳入了DAO 数据库类、多线程同步控制类,并允许制作 OCX containers。搭配推出的 Visual C++ 4.0 编译器,也终于支持了 template、RTTI 等 C++ 语言特性。IDE 整合环境有重大的改头换面行动,Class View、Resource View、File View 都使得项目的管理更直觉更轻松,Wizardbar 则活脱脱是一个简化的ClassWizard。此外,多了一个极好用的Components Gallery,并允许程序员订制AppWizard。
1996 年上半年又推出了MFC 4.1,最大的焦点在ISAPI(Internet Server API)的支持,提供五个新类 , 分别是CHttpServer 、CHttpFilter 、 CHttpServerContext 、CHttpFilterContext、CHtmlStream,用以建立交互式 Web 应用程序。整合环境方面也对应地提供了一个ISAPI Extension Wizard。在附加价值上,Visual C++ 4.1提供了Game SDK,帮助开发Windows 95上的高效率游戏软件。Visual C++ 4.1 还提供不少个由协力公司完成的OLE控制组件(OCXs),这些 OLE 控制组件技术很快就要全面由桌上跃到网上,称为 ActiveX 控制组件。不过,遗憾的是,Visual C++ 4.1 的编译器有些臭虫,不能够制作 VxD(虚拟装置驱动程序)。
1996 年下半年推出的 MFC 4.2,提供对 ActiveX 更多的技术支持,并整合 Standard C++ Library。它封包一组新的 Win32 Internet 类(统称为 WinInet),使 Internet 上的程序开发更容易。它提供 22 个新类和 40 个以上的新成员函数。它也提供一些控制元件,可以系结(binding)近端和远程的数据源(data sources)。整合环境方面,Visual C++ 4.2提供新的Wizard给ActiveX 程序开发使用,改善了影像编辑器,使它能够处理在Web 服务器上的两个标准图文件格式:GIF 和 JPEG。
1997 年推出的Visual C++ 5.0,主要诉求在编译器的速度改善,并将Visual C++ 合并到微软整个Visual Tools的终极管理软件Visual Studio 97 之中。所有的微软虚拟开发工具,包括 Visual C++、Visual Basic、Visual J++、Visual InterDev、Visual FoxPro、都在Visual Studio 97 的整合之下有更密切的彼此奥援。至于程序设计方面,MFC 本身没有什么变化(4.21 版),但附了一个 ATL(Active Template Library)2.1 版,使 ActiveX 控制组件的开发更轻松些。
我想你会发现,微软正不断地为为什么要使用MFC加上各式各样的强烈理由,并强烈导引它成为Windows 程序设计的 C++ 标准接口。你会看到愈来愈多的 MFC/C++ 程序代码。
MFC概况
MFC非常巨大(其它application framework也不差),MFC 类主要可分为下列数大群组:
-
General Purpose classes——提供字符串类、数据处理类(如数组与串列),异常情况处理类、文件类...等等。
-
Windows API classes - 用来封包Windows API,例如窗口类、对话框类、DC 类...等等。
-
Application framework classes——组成应用程序骨干者,即此组类,包括Document/View、消息捕获、消息映射、消息循环、动态生成、文件读写等等。
-
High level abstractions - 包括工具栏、状态栏、分裂窗口、卷动窗口等等。
-
operation system extensions - 包括OLE、ODBC、DAO、MAPI、WinSock、ISAPI等等。
General Purpose classes
也许你使用 MFC 的第一个目标是为了写 Windows 程序,但并不是整个 MFC 都只为此目的而被发明出来。下面这些类适用于 Windows,也适用于 DOS。
CObject
绝大部分类库,往往以一个或两个类,做为其它绝大部分类的基础。MFC 亦复如此。CObject是万类之首,凡类派生自CObject者,得以继承数个面向对象重要性质,包括RTTI(运行时期类型识别)、Persistence(对象保存)、Dynamic Creation(动态生成)、Diagnostic(错误诊断)。本书第3章对于这些技术已有了一份 DOS 环境下的模拟,第8章另有 MFC 相关原始代码的探讨。其中,对象保存又牵扯到 CArchive,诊断又牵扯到 CDumpContext,运行时期类型识别以及动态生成又牵扯到CRuntimeClass。
数据处理类(collection classes)
所谓collection,意指用来管理一群对象或标准类型的数据。这些类像是Array或List 或 Map 等等,都内含针对元素的加入或删除或巡访等成员函数。Array (数组)和 List(串列)是数据结构这门课程的重头戏,大家比较熟知,Map(可视之为表格)则是由成双成对的两两对象所构成,使你很容易由某一对象得知成对的另一对象;换句话说一个对象是另一个对象的键值(key)。例如,你可以使用 String-to-String Map,管理一个电话-人名数据库;或者使用 Word-to-Ptr Map,以 16 位数值做为一个指针的键值。
最令人侧目的是,由于这些类都支持 Serialization,一整个数组或串列或表格可以单一一进程序代码就写到文件中(或从文件读出)。第8章的 Scribble Step1范例程序中你就会看到它的便利。
MFC 支持的collection classes有:
杂项类
-
Crect——封装Windows 的RECT 结构。这个类在Windows环境中特别有用,因为CRect常常被用作MFC类成员函数的参数。
-
Csize——封装Windows的SIZE结构。
-
Cpoint——封装Windows的POINT 结构。这个类在Windows环境中特别有用,因为CPoint常常被用作MFC类成员函数的参数。
-
Ctime——表现绝对时间,提供许多成员函数,包括取得目前时间 ( static GetCurrentTime)、将时间数据格式化、抽取特定字段(时、分、秒)等等。它对于 +、-、+=、-+ 等运算符都做了重载动作。
-
CtimeSpan——以秒数表现时间,通常用于计时秒表。提供许多成员函数,包括把秒数转换为日、时、分、秒等等。
-
Cstring——用来处理字符串。支持标准的运算符如 =、+=、< 和 >。
异常处理类(exception handling classes)
所谓异常情况(exception),是发生在你的程序运行时期的不正常情况,像是文件打不开、内存不足、写入失败等等等。与异常处理有关的MFC类一共有以下11种:
Windows API classes
这是MFC声名最大的一群类。如果你去看看原始代码,就会看到这些类的成员函数所对应的各个Windows API函数。
-
CwinThread——代表MFC程序中的一个线程。自从3.0 版之后,所有的MFC类就都已经是thread-safe了。SDK 程序中标准的消息循环已经被封装在此一类之中(你会在第6章看到我如何把这一部分开膛剖肚)。
-
CwinApp——代表你的整个MFC应用程序。此类派生自CWinThread;要知道,任何32位Windows程序至少由一个线程构成。CWinApp 内含有用的成员变 数 如 m_szExeName,放置执行文件文件名 ,以及有用的成员函数如ProcessShellCommand,处理命令列选项。
-
CWnd——所有窗口,不论是框架窗口、子框窗口、对话框、控制组件、view 窗口,都有一个对应的C++ 类,你可以想象窗口handle和C++ 对象结盟。这些C++ 类统统派生自 CWnd,也就是说,凡派生自 CWnd 之类才能收到 WM_ 窗口消息(WM_COMMAND 除外)。
所谓窗口handle和C++ 对象结盟,实际上是CWnd 对象有一个成员变量m_hWnd,就放着对应的窗口handle。所以,只要你手上有一个CWnd 对象或CWnd 对象指针,就可以轻易获得其窗口handle:
HWND hWnd = pWnd->m_hWnd;
-
CcmdTarget——CWnd的父类。派生自它 ,类才能够处理命令消息WM_COMMAND。这个类是消息映射以及命令消息循环的大部分关键,我将在第9章推敲这两大神秘技术。
-
GDI 类、DC 类、Menu 类。
Application framework classes
这一部分最为人认知的便是Document/View,这也是使MFC跻身 application framework的关键。Document/View 的观念是希望把数据的本体,和数据的显示分开处理。由于文件产生之际,必须动态生成 Document/View/Frame 三种对象,所以又必须有所谓的Document Template 管理之。
CDocTemplate、CSingleDocTemplate、CmultiDocTemplate——Document Template 扮演黏胶的角色,把 Document 和 View 和其 Frame(外框窗口)胶黏在一块儿。CSingleDocTemplate 一次只支持一种文件类型,CMultiDocTemplate 可同时支持多种文件类型。注意,这和 MDI 程序或 SDI 程序无关,换句话说,MDI 程序也可以使用CSingleDocTemplate,SDI 程序也可以使用CMultiDocTemplate。
但是,逐渐地,MDI 这个字眼与它原来的意义有了一些出入(要知道,这个字眼早在SDK时代即有了)。因此,你可能会看到有些书籍这么说:MDI 程序使用CMultiDocTemplate,SDI程序使用CSingleDocTemplate。
Cdocument——当你为自己的程序由CDocument 派生出一个子类后,应该在其中加上成员变量,以容纳文件数据;并加上成员函数,负责修改文件内容以及
读写文件。读写文件由虚函数Serialize 负责。第8章的Scribble Step1 范例程序有极佳的示范。
Cview——此类负责将文件内容呈现到显示装置上:也许是屏幕,也许是打印机。文件内容的呈现由虚函数OnDraw负责。由于这个类实际上就是你在屏幕上所看到的窗口(外再罩一个外框窗口),所以它也负责用户输入的第一线服务。例如第8章的 Scribble Step1 范例,其 View 类便处理了鼠标的按键动作。
High level abstractions
视觉性UI对象属于此类,例如工具栏CToolBar、状态栏CStatusBar、对话框列CDialogBar。加强型的View也属此类,如可卷动的ScrollView、以对话框为基础的CFormView、小型文字编辑器CEditView、树状结构的CTreeView,支持RTF文件格式的CRichEditView 等等。
Afx 全局函数
还记得吧,C++ 并不是纯种的面向对象语言(SmallTalk 和Java才是)。所以,MFC之中得以存在有不属于任何类的全局函数,它们统统在函数名称开头冠以Afx。
下面是几个常见的 Afx 全局函数:
函数名称 说明
MFC宏(macros)
CObject和CRuntimeClass之中封装了数个所谓的object services,包括取得运行时期的类信息(RTTI)、Serialization(文件读写)、动态产生对象...等等。所有派生自CObject的类,都继承这些机能。我想你对这些名词及其代表的意义已经不再陌生 -- 如果你没有错过第3章的「MFC 六大技术模拟」的话。
取得运行时期的类信息(RTTI),使你能够决定一个运行时期的对象的类信息,这样的能力在你需要对函数参数做一些额外的类型检验,或是当你要针对对象属于某种类而做特别的动作时,份外有用。
Serialization 是指将对象内容写到文件中,或从文件中读出。如此一来对象的生命就可以在程序结束之后还延续下去,而在程序重新启动之后,再被读入。
这样的对象可说是 "persistent"(永续存在)。
所谓动态的对象生成(Dynamic object creation),使你得以在运行时期产生一个特定的对象。例如document、view、和frame对象就都必须支持动态对象生成,因为framework 需要在运行时期产生它们。
此外,OLE 常常需要在运行时期做对象的动态生成动作。例如一个OLE server 程序必须能够动态产生 OLE items,用以反应 OLE client 的需求。
MFC 数据类型(data types)
下面所列的这些数据类型,常常出现在 MFC 之中。其中的绝大部分都和一般的Win32程序(SDK 程序)所用的相同。
下面这些是和 Win32 程序(SDK 程序)共同使用的数据类型:
数据类型 意义
BOOL Boolean值(布尔值,不是TRUE就是FALSE)
BSTR 32-bit字符指针
BYTE 8-bit整数,未带正负号
COLORREF 32-bit 数值,代表一个颜色值
DWORD 32-bit 整数,未带正负号
LONG 32-bit 整数,带正负号
LPARAM 32-bit 数值,做为窗口函数或 callback 函数的一个参数
LPCSTR 32-bit 指针,指向一个常数字符串
LPSTR 32-bit 指针,指向一个字符串
LPCTSTR 32-bit 指针,指向一个常数字符串。此字符串可移植到Unicode和DBCS(双字节字集)
LPTSTR 32-bit 指针,指向一个字符串。此字符串可移植到 Unicode和DBCS(双位组字集)
LPVOID 32-bit 指针,指向一个未指定类型的数据
LPRESULT 32-bit数值,做为窗口函数或callback函数的回返值
UINT 在Win16中是一个16-bit未带正负号整数,在Win32中是一个 32-bit未带正负号整数。
WNDPROC 32-bit 指针,指向一个窗口函数
WORD 16-bit 整数,未带正负号
WPARAM 窗口函数的callback函数的一个参数。在Win16中是 16 bits,在Win32中是32 bits。
下面这些是MFC独特的数据类型:
数据类型 意义
POSITION 一个数值,代表 collection 对象(例如数组或串列)中的元素位置。常使用于MFC collection classes。
LPCRECT 32-bit指针,指向一个不变的 RECT 结构。
前面所说那些MFC数据类型与C++语言数据类型之间的对应,定义于 WINDEF.H中。我列出其中一部分,并且将不符合(_MSC_VER >= 800) 条件式的部分略去。
#define NULL 0
#define far // 侯俊杰注:Win32 不再有 far 或 near memory model,
#define near // 而是使用所谓的 flat model。pascall 函数调用习惯
#define pascal __stdcall // 也被 stdcall 函数调用习惯取而代之。
#define cdecl _cdecl
#define CDECL _cdecl
#define CALLBACK __stdcall//侯俊杰注:在Windows programming演化过程中
#define WINAPI __stdcall // 曾经出现的 PASCAL、CALLBACK、WINAPI、
#define WINAPIV __cdecl// APIENTRY,现在都代表相同的意义,就是 stdcall
#define APIENTRY WINAPI// 函数调用习惯。
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define FAR far
#define NEAR near
#define CONST const
typedef unsigned long DWORD;
typedef int BOOL;
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef float FLOAT;
typedef FLOAT *PFLOAT;
typedef BOOL near *PBOOL;
typedef BOOL far *LPBOOL;
typedef BYTE near *PBYTE;
typedef BYTE far *LPBYTE;
typedef int near *PINT;
typedef int far *LPINT;
typedef WORD near *PWORD;
typedef WORD far *LPWORD;
typedef long far *LPLONG;
typedef DWORD near *PDWORD;
typedef DWORD far *LPDWORD;
typedef void far *LPVOID;
typedef CONST void far *LPCVOID;
typedef int INT;
typedef unsigned int UINT;
typedef unsigned int *PUINT;
/* Types use for passing & returning polymorphic values */
typedef UINT WPARAM;
typedef LONG LPARAM;
typedef LONG LRESULT;
typedef DWORD COLORREF;
typedef DWORD *LPCOLORREF;
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;
typedef const RECT FAR* LPCRECT;
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;
typedef struct tagSIZE
{
LONG cx;
LONG cy;
} SIZE, *PSIZE, *LPSIZE;
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。