第5章 总观 Application Framework

带艺术气息的软件创作行为将在Application Framework 出现后逐渐成为工匠技术,而我们都将成为软件IC装配厂里的男工女工。

但,不是亨利福特,我们又如何能够享受大众化的汽车?

或许以后会出现「纯手工精制」的软件,可我自己从来不嫌机器馒头难吃。

什么是 Application Framework?

还没有学习任何一套Application Framework的使用之前,就给你近乎学术性的定义,我可以想象对你而言绝对是「形而上的」(超物质的无形哲理),尤其如果你对面向对象(Object Oriented)也还没有深刻体会的话。形而上者谓之道,形而下者谓之器,我想能够舍器而直接近道者,几稀!但是,「定义」这种东西又似乎宜开宗明义摆在前头。我诚挚地希望你在阅读后续的技术章节时能够时而回来看看这些形而上的叙述。当你有所感受,技术面应该也进入某个层次了。

侯捷怎么说

首先我们看看侯捷在其无责任书评中是怎么说的:

演化(evolution)永远在进行,但这个世界却不是每天都有革命性(revolution) 的事物发生。动不动宣称自己(或自己的产品)是划时代的革命性的,带来的影响就像时下满街跑的大 师一样使 我们渐渐 无动于衷(大师不可能满街跑)! 但是Application Framework 的的确确在我们软件界称得上具有革命精神。

什么是Application Framework?Framework这个字眼有组织、框架、体制的意思,Application Framework不仅是一般性的泛称,它其实还是面向对象领域中的一个专有名词。

基本上你可以说,Application Framework是一个完整的程序模型,具备标准应用软件所需的一切基本功能,像是文件存取、打印预览、数据交换...,以及这些功能的使用接口(工具栏、状态栏、菜单、对话框)。如果更以术语来说,Application Framework 就是由一整组合作无间的「对象」架构起来的大模型。喔不不,当它还没有与你的程序产生火花的时候,它还只是有形无体,应该说是一组合作无间的「类」架构起来的大模型。

这带来什么好处呢?程序员只要带个购物袋到「类超级市场」采买,随你要买MDI或OLE或ODBC或Printing Preview,回家后就可以轻易拼凑出一个色香味俱全的大餐。

「类超级市场」就是C++ 类库,以产品而言,在Microsoft 是MFC,在Borland 是OWL,在IBM则是OpenClass。这个类库不只是类库而已,传统的函数库(C Runtime 或 Windows API)乃至于一般类库提供的是生鲜超市中的一条鱼一支葱一颗大白菜,彼此之间没有什么关联,主掌中馈的你必须自己选材自己调理。能够称得上Application Framework 者,提供的是火锅拼盘(就是那种带回家通通丢下锅就好的那种),依你要的是白菜火锅鱼头火锅或是麻辣火锅,菜色带调理包都给你配好。当然这样的火锅拼盘是不能够就地吃的,你得给它加点能量。放把火烧它吧,这火就是所谓的application object(在 MFC 程序中就是派生自 CWinApp 的一个全局对象)。是这个对象引起了连锁反应(一连串的 'new'),使每一个形(类)有了真正的体(对象),把应用程序以及 Application Framework 整个带动起来。一切因缘全由是起。

Application Framework 带来的革命精神是,程序模型已经存在,程序员只要依个人需求加料就好:在派生类中改写虚函数,或在派生类中加上新的成员函数。这很像你在火锅拼盘中依个人口味加盐添醋。

由于程序代码的初期规模十分一致(什么样风格的程序应该使用什么类,是一成不变的),而修改程序以符合私人需要的基本动作也很一致(我是指像「开辟一个空的骨干函数」这种事情),你动不了 Application Framework 的大架构,也不需要动。这是福利不是约束。

应用程序代码骨干一致化的结果,使优越的软件开发工具如 CASE(Computer Aid Software Engineering)tool 容易开发出来。你的程序代码大架构掌握在 Application Framework 设计者手上,于是他们就有能力制作出整合开发环境( Integrated Development Environment,IDE)了。这也是为什么 Microsoft、Borland、Symantec、Watcom、IBM 等公司的集成开发环境进步得如此令人咋舌的原因了。

有人说工学院中唯一保有人文气息的只剩建筑系,我总觉得信息系也勉强可以算上。带艺术气息的软件创作行为(我一直是这么认为的)将在 Application Framework 出现后逐渐成为工匠技术,而我们都将只是软件 IC 装配厂里的男工女工。其实也没什么好顾影自怜,功成名就的冠冕从来也不曾落在程序员头上;我们可能像纽约街头的普普(POP)工作者,自认为艺术家,可别人怎么看呢?不得而知!话说回来,把开发软件这件事情从艺术降格到工技,对人类只有好处没有坏处。不是亨利福特,我们又如何能够享受大众化的汽车?或许以后会出现「纯手工精制」的软件,谁感兴趣不得而知,我自己嘛...唔...倒是从来不嫌机器馒头难吃。

如果要三言两语点出 Application Framework 的特质,我会这么说:我们挖出别人早写好的一整套模块(MFC 或 OWL 或 OpenClass)之中的一部分,给个引子(application object)使它们一一具象化动起来,并被允许修改其中某些零件使这程序更符合私人需求,如是而已。

我怎么说

侯捷的这一段话实在已经点出Application Framework的精神。凝聚性强、组织化强的类库就是 Application Framework。一组合作无间的对象,彼此藉消息的流动而沟通,并且互相调用对方的函数以求完成任务,这就是 Application Framework。对象存在哪里?

在MFC中?! 这样的说法不是十分完善,因为MFC的各个类只是「对象属性(行为)的定义」而已,我们不能够说 MFC 中有实际的对象存在。唯有当程序被application object(这是一个派生自 MFC CWinApp 的全局对象)引爆了,才将我们选用的类一一具象化起来,产生实体并开始动作。图 5-1 是一个说明。

这样子说吧,静态情况下MFC 是一组类库,但在程序运行时期它就生出了一群有活动力的对象组。最重要的一点是,这些对象之间的关系早已建立好,不必我们(程序员)操心。好比说当用户按下菜单的【File/Open】项,开文件对话框就会打开;用户选好文件名后,Application Framework 就开始对着你的数据类,唤起一个名为Serialize 的特殊函数。这整个机制都埋好了,你只要把心力放在那个叫作 Serialize 的函数上即可。

选用标准的类,做出来的产品当然就没有什么特色,因为别人的零件和你的相同,兜起来的成品也就一样。我指的是用户接口(UI)对象。但你要知道,软件工业发展到现阶段这个世代,着重的已不再是UI 的争奇斗艳,取巧哗众;UI 已经渐渐走上标准化了。软件一决胜负的关键在数据的处理。事实上,在「真正做事」这一点,整个application framework 是无能为力的,也就是说对于数据结构的安排,数据的处理,数据的显示,Application Framework 所能提供的,无一不是单单一个空壳而已——在 C++ 语言来讲就是个虚函数。软件开发人员必须想办法改造(override)这些虚函数,才能符合个人所需。基于 C++ 语言的特性,我们很容易继承既有之类并加上自己的特色,这就是面向对象程序设计的主要精神。也因此,C++ 语言中有关于「继承」性质的份量,在MFC程序设计里头占有很重的比例,在学习使用MFC的同时,你应该对C++的继承性质和虚函数有相当的认识。第2章有我个人对C++这两个性质的心得。

图5-1 MFC 是一个零组件超级市场,所贩卖的零组件功能以及零组件

彼此之间的关系都已定义好;我们选择自己喜欢的零件,兜出一个应用程序

Application Framework 究竟能提供我们多么实用的类呢?或者我们这么问:哪些工作已经被处理掉了,哪些工作必须由程序员扛下来?比方说一个标准的 MFC 程序应该有一个可以读写文件数据的功能,然而应用程序本身有它独特的数据结构,从 MFC 得来的零件可能与我的个人需求搭配吗?

我以另一个比喻做回答。

假设你买了一个整厂整线计划,包括仓贮、物料、MIS、生管,各个部门之间的搭配也都建立起来了,包含在整厂整线计划内。这个厂原先是为了生产葡萄酒,现在改变主意要生产白兰地,难道整个厂都不能用了吗?不!只要把进货原料改一改、发酵程序改一改、瓶装程序改一改、整个厂的其它设备以及设备与设备之间的联机配合(这是顶顶重要的)都可以再利用。物料之后到生管,装瓶之后到仓贮,仓贮之后到出货,再加上 MIS监控全厂,这些程序都是不必改变的。

一个整厂整线计划,每一单元之间的联机沟通,合作关系,都已经建立起来,是一种构造好的运作模式。抽换某个单元的性质或部分性质,并不影响整体作业。「整厂整线」最重要最有价值的是各单元之间的流程与控制。

反映到面向对象程序设计里头,Application Framework 就是「整厂整线规划」,Application Framework 提供的类就是上述工厂中一个一个的单元。最具价值的就是各类之间的交互运作模式。

虽然文件读写(此动作在MFC 称为Serialize)单元必须改写以符合个人所需,但是单元与单元之间的关系依然存在而且极富价值。当用户在程序中选按【File/Open】或【File/Save】,框架窗口自动通知Document 对象(内存数据),引发Serialization 动作;而你为了个人的需求,改写了这个 Serialize 虚函数。你对 MFC 的改写范围与程度,视你的程序有多么特异而定。

可是如果酿酒厂想改装为炼钢厂?那可就无能为力了。这种情况不会出现在软件开发上,因为软件的必备功能使它们具有相当的相似性(尤其 Windows 又强调界面一致性)。好比说程序想支持 MDI 接口,想支持 OLE,这基本上是超越程序之专业应用领域之外的一种大格局,一种大架构,最适合 Application Framework 发挥。

很明显,Application Framework是一组超级的类库。能够被称为 Framework 者必须其中的类性质紧密咬合,互相呼应。因此你也就可以想象,Framework 所提供的类是一伙的,不是单片包装的。你把这伙东西放入程序里,你也就得乖乖遵循一种特定的(Application Framework 所规定的)程序风格来进进程序设计工作。但是侯捷也告诉我们,这是福利不是约束。

别人怎么说

其它人又怎么看 Application Framework?我将胪列数篇文章中的相关定义。若将原文译为中文,我恐怕力有未逮辞不达意,所以列出原文供你参考。

1985 年,Apple公司的MacApp严格而系统化地定义出做为一个商业化 Application Framework 所需要的关键理念:

cohesive 的意思是强而有力、有凝聚力的。

steroid 是类固醇。自从加拿大 100 公尺名将班琼森在汉城奥运吃了这药物而夺得金牌并打破世界记录,相信世人对这个名称不会陌生(当然琼森的这块金牌和他的世界记录后来是被取消的)。

类固醇俗称美国仙丹,是一种以胆固醇结构为基础,派生而来的荷尔蒙,对于发炎红肿等症状有极速疗效。然而因为它是透过抑制人类免疫系统而得到疗效,如果使用不当,会带来极不良的副作用。

运动员用于短时间内增强身体机能的雄性激素就是类固醇的一种,会影响脂肪代谢,服用过量会导至极大的副作用。

基本上MacApp以类固醇来比拟Application Framework虽是妙喻,但类固醇会对人体产生不好的副作用而 Application Framework 不会对软件开发产生副作用-- 除非你认为不能随心所欲写你的代码也算是一种副作用。

Apple 更一步更明确地定义一个Application Framework 是:

这里所指的 support 并不只是视觉性 UI 组件如menu、dialog、listbox...,还包括一个应用程序所需要的其它功能设备,像是 Document, View, Printing, Debugging。

另一个相关定义出现在 Ray Valdes 于1992年10月发表于Dr. Dobb's Journal 的 "Sizing up Application Frameworks and Class Libraries" 一文之中:

Donald G. Firesmith 在一篇名为 "Frameworks : The Golden path of the object Nirvana" 的文章中对 Application Framework 有如下定义:

* Nirvana 是涅盘、最高境界的意思。

Bjarne Stroustrup(C++ 原创者)在他的 The C++ Programming Language 一书中对于Application Framework 也有如下叙述:

Kaare Christian 在1994/02/08的PC Magazine 中有一篇"C++ Application Frameworks" 文章,其中有下列叙述(节录):

两年前我在纽约北边的乡村盖了一栋 post-and-beam 房子。在我到达之前我的木匠已经把每一根梁的外形设计好并制作好,把一根根的粗糙木材变成一块块锯得漂漂亮亮的零件,一切准备就线程只待安装。(注:所谓post-and-beam 应是指那种梁柱都已规格化,可以邮购回来自己动手盖的DIY——Do It Yourself——房子)。

使用Application Framework建造一个Windows 应用程序也有类似的过程。你使用一组早已做好的零件,它使你行进快速。由于这些零件坚强耐用而且稳固,后面的工作就简单多了。但最重要的是,不论你使用规格化的梁柱框架来盖一栋房子,或是使用Application Framework 来建立一个 Windows 程序,工作类型已然改变,出现了一种完全崭新的做事方法。在我的 post-and-beam 房子中,工作类型的改变并不总是带来帮助;贸易商在预制梁柱的技巧上可能会遭遇适应上的困扰。同样的事情最初也发生在Windows 身上,因为你原已具备的某些以 C 语言写 Windows 程序的能力,现在在以C++ 和Application Framework 开发程序的过程中无用武之地。时间过去之后,Windows 程序设计的类型移转终于带来了伟大的利益与方便。Application Framework本身把message loops 和其它Windows的苦役都做掉了,它促进一个比较秩序井然的程序结构。

Application Framework——建立Windows应用软件所用的C++类库——如今已行之有年,因为面向对象程序设计已经快速地获得了接受度。Windows API 是程序性的,Application Framework 则让你写面向对象式的Windows程序。它们提供预先写好的机能(以 C++ 类型式呈现出来),可以加速应用软件的开发。

Application Framework 提供数种优点。或许最重要的,是它们在面向对象程序设计模式下对Windows 程序设计过程的影响。你可以使用 Framework 来减轻例行但繁复的琐事,目前的 Application Framework 可以在图形、对话框、打印、求助、OCX 控制组件、剪贴板、OLE 等各方面帮助我们,它也可以产生漂亮的UI接口如工具栏和状态栏。

借着Application Framework的帮助写出来的代码往往比较容易组织化,因为 Framework改变了Windows管理消息的方法。也许有一天Framework还可以帮你维护单一一套代码以应付不同的执行平台。

你必须对Application Framework有很好的知识,才能够修改由它附带的软件开发工具制作出来的骨干程序。它们并不像Visual Basic那么容易使用。但是对Application Framework 专家而言,这些程序代码产生器可以省下大量时间。

使用Application Framework的主要缺点是,没有单一一套产品广被所有的C++编译器支持。所以当你选定一套 Framework,在某个范围来说,你也等于是选择了一个编译器。

为什么使用 Application Framework

虽然Application Framework 并不是新观念,它们却在最近数年才成为 PC 平台上软件开发的主流工具。面向对象语言是具体实现Application Framework 的理想载具,而C++ 编译器在PC平台上的出现与普及终于允许主流PC程序员能够享受Application Framework 带来的利益。

从八十年代早期到九十年代初始,C++大都存在于UNIX 系统和研究人员的工作站中,不在PC 以及商业产品上。C++ 以及其它的面向对象语言(例如 Smalltalk-80)使一些大学和研究计划生产出现今商业化Application Framework 的鼻祖。但是这些早期产品并没有明显区隔出应用程序与 Application Framework 之间的界线。

今天应用软件的功能愈来愈复杂,建造它们的工具亦复如此。Application Framework、Class Library 和GUI toolkits 是三大类型的软件开发工具(请见方块说明),这三类工具虽然以不同的技术方式逼近目标,它们却一致追求相同而基本的软件开发关键利益:降低写程序代码所花的精力、加速开发效率、加强可维护性、增加强固性(robustness)、为组合式的软件机能提供杠杆支点(有了这个支点,再大的软件我也举得起来)。

当我们面临软件工业革命,我们的第一个考虑点是:我的软件开发技术要从哪一个技术面切入?从raw API 还是从高阶一点的工具?如果答案是后者,第二个考虑点是我使用哪一层级的工具?GUI toolkits 还是Class Library 还是 Application Framework?如果答案又是后者,第三个考虑点是我使用哪一套产品?MFC 或 OWL 或 Open Class Library?

(目前 PC 上还没有第四套随编译器附赠的 Application Framework 产品)

别认为这是领导者的事情不是我(工程师)的事情,有这种想法你就永远当不成领导者。也别认为这是工程师的事情不是我(学生)的事情,学生的下一步就是工程师;及早想点工业界的激烈竞争,对你在学生阶段规划人生将有莫大帮助。

我相信,Application Framework 是最好的杠杆支点。

Application Framework,Class Library,GUI toolkit

一般而言,Class Library 和 GUI toolkit 比 Application Framework 的规模小,定位也没那么高阶宏观。Class Library 可以定义为「一组具备面向对象性质的类,它们使应用程序的某些功能实现起来容易一些,这些功能包括数值运算与数据结构、绘图、内存管理等;这些类可以一片一片毫无瓜葛地并入应用程序内」。

请特别注意这个定义中所强调的「一片一片毫无瓜葛」,而不像 Application Framework 是大伙儿一并加入。因此,你尽可以随意使用 Class Library,它并不会强迫你遵循任何特定的程序架构。Class Library 通常提供的不只是 UI 功能、也包括一般性质的机能,像数据结构的处理、日期与时间的转换等等。

GUI toolkit 提供的服务类似Class Library,但它的程序接口是程序导向而非面向对象。而且它的功能大都集中在图形与 UI 接口上。GUI toolkit 的发展历史早在面向对象语言之前,某些极为成功的产品甚至是以汇编语言(assembly)写成。不要必然地把GUI 联想到Windows,GUI toolkit也有DOS 版本。我用过的Chatter Box 就是 DOS 环境下的 GUI 工具(是一个函数库)。

使用 Application Framework 的最直接原因是,我们受够了日益暴增的 Windows API。把MFC 想象为第四代语言,单单一个类就帮我们做掉原先要以一大堆 APIs 才能完成的事情。

但更深入地想,Application Framework绝不只是为了降低我们花在浩瀚无涯的Windows API 的时间而已;它所带来的面向对象程序设计观念与方法,使我们能够站在一群优秀工程师(MFC 或 OWL 的创造者)的努力心血上,继承其成果而开发自己之所需。同时,因为 Application Framework 特殊的工作类型,整体开发工具更容易制作,也制作的更完美。在我们决定使用 Application Framework 的同时,我们也获得了这些整合性软件开发环境的支持。在软件开发过程中,这些开发工具角色之重要性不亚于 Application Framework 本身。

Application Framework 将成为软件技术中最重要的一环。如果你不知道它是什么,赶快学习它;如果你还没有使用它,赶快开始用。机会之窗不会永远为你打开,在你的竞争者把它关闭之前赶快进入!如果你认为改朝换代还早得很,请注意两件事情。第一,江山什么时候变色可谁也料不准,当你埋首工作时,外面的世界进步尤其飞快;第二,面向对象和Application Framework可不是那么容易学的,花多少时间才能登堂入室可还得凭各人资质和基础呢。

浩瀚无涯的 Windows API

Microsoft Foundation Classes (MFC)

PC 世界里出了三套C++ Application Frameworks,并且有越来越多的趋势。这三套是Microsoft的MFC ( Microsoft Foundation Classes ) , Borland 的 OWL( Object WindowLibrary),以及IBM VisualAge C++ 的Open Class Library。至于其它 C++ 编译器厂商如 Watcom、Symantec、Metaware,只是供应集成开发环境(Integraded Development Environment,IDE),其 Application Framework 都是采用微软公司的 MFC。

Delphi(Pascal 语言),依我之见,也称得上是一套 Application Framework。Java 语言本身内建一套标准类库,依我之见,也够得上资格被称为 Application Framework。

Delphi 和Visual Basic,又被称为是一种应用程序快速开发工具(RAD,Rapid Application Development)。它们采用PME(Properties-Method-Event)架构,写程序的过程像是在一张画布上拼凑一个个现成的组件(components):设定它们的属性(properties)、指定它们应该「有所感」的外来刺激(events),并决定它们面对此刺激时在预设行为之外的行为(methods)。所有动作都以拖拉、设定数值的方式完成,非常简单。只有在设定组件与组件之间的互动关系时才牵涉到程序代码的写作(这一小段代码也因此成为顺利成功的关键)。

Borland 公司于1997 年三月推出的C++ Builder也属于PME架构,提供一套Visual Component Library(VCL),内有许许多多的组件。因此 C++ Builder 也算得上是一套RAD(应用程序快速开发工具)。

早初,开发Windows应用程序必须使用微软的SDK(Software Development Kit),直接调用Windows API 函数,向Windows 操作系统提出各种要求,例如配置内存、开启窗口、输出图形...。

所谓API(Application Programming Interface),就是开放给应用程序调用的系统功能。

数以千计的Windows APIs,每个看起来都好像比重相若(至少你从手册上看不出来孰轻孰重)。有些APIs 彼此虽有群组关系,却没有相近或组织化的函数名称。星罗棋布,雾列星驰;又似雪球一般愈滚愈多,愈滚愈大。撰写 Windows 应用程序需要大量的耐力与毅力,以及大量的小心谨慎!

MFC帮助我们把这些浩繁的APIs,利用面向对象的原理,逻辑地组织起来,使它们具备抽象化、封装化、继承性、多态性、模块化的性质。

1989 年微软公司成立Application Framework 技术团队,名为AFX小组,用以开发C++ 面向对象工具给 Windows 应用程序开发人员使用。AFX 的 "X" 其实没有什么意义,只是为了凑成一个响亮好念的名字。

这个小组最初的「宪章」,根据记载,是要 "utilize the latest in object oriented technology to provide tools and libraries for developers writing the most advanced GUI applications on the market",其中并未画地自限与Windows 操作系统有关。果然,其第一个原型产品,有自己的窗口系统、自己的绘图系统、自己的对象数据库、乃至于自己的内存管理系统。

当小组成员以此产品开发应用程序,他们发现实在是太复杂,又悖离公司的主流系统——Windows——太遥远。于是他们修改宪章变成 "deliver the power of object-oriented solutions to programmers to enable them to build world-class Windows based applications in C++." 这差不多正是Windows 3.0 异军崛起的时候。

C++ 是一个复杂的语言,AFX小组预期MFC的用户不可能人人皆为C++ 专家,所以他们并没有采用所有的C++ 高阶性质(例如多重继承)。许多「麻烦」但「几乎一成不变」的Windows程序动作都被隐藏在MFC类之中,例如WinMain 、 RegisterClass、Window Procedure 等等等。

虽说这些被隐藏的Windows 程序动作几乎是一成不变的,但它们透露了 Windows 程序的原型奥秘,这也是为什么我要在本书之中锲而不舍地挖出它们的原因。

为了让MFC尽可能地小,尽可能地快,AFX 小组不得不舍弃高度的抽象(导至过多的虚函数),而引进他们自己发明的机制,尝试在面向对象领域中解决 Windows 消息的处理问题。这也就是本书第9章深入探讨的Message Mapping 和Message routing 机制。注意,他们并没有改变C++语言本身,也没有扩大语言的功能。他们只是设计了一些令人拍案叫绝的宏,而这些宏背后隐藏着巨大的机制。

了解这些宏(以及它们背后所代表的机制)的意义,以及隐藏在 MFC 类之中的那些足以曝露原型机密的「麻烦事儿」,正是我认为掌握 MFC 这套 Application Framework的重要手段。

就如同前面那些形而上的定义,MFC 是一组凝聚性强、组织性强的类库。如果你要利用MFC发展你的应用程序,必须同时引用数个必要类,互相搭配支持。图5-3是一个标准的MFC程序外貌。隐藏在精致画面背后更重要的是,就如我在前面说过,对象与对象之间的关系已经存在,消息的流动程序也都已设定。当你要为这个程序设计真正的应用功能,不必在意诸如「我如何得知用户按左键?左键按下后我如何启动某一个函数?参数如何传递过去...」等琐事,只要专注在左键之后真正要做的功能动作就好。

图 5-3 标准MFC程序的风貌

白头宫女话天宝:Visual C++ 与 MFC

微软公司于1992/04 推出C/C++ 7.0 产品时初次向世人介绍了MFC 1.0,这个初试啼声的产品包含了20,000行C++原始代码,60个以上的Windows相关类,以及其它的一般类如时间、数据处理、文件、内存、诊断、字符串等等等。它所提供的,其实是一个 "thin and efficient C++ transformation of the Windows API"。其32 位版亦在1992/07 随着 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/12 又推出了 16 位的 Visual C++ 1.5,搭配 MFC 2.5。这个版本最大的进步是多了 OLE2 和 ODBC 两组类。整合环境也为了支持这两组类而做了些微改变。

1994/09,微软推出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/01,微软又加上了MAPI(Messaging API)和 WinSock 支持,推出 MFC 3.1(32 位元版),并供应13个通用控制组件,也就是 Windows 95 所提供的tree、tooltip、spin、slider、progress、RTF edit 等等控制组件。

1995/07,MFC 有了3.2 版,那是不值一提的小改版。

然后就是1995/09 的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++ 程序代码。对于绝大多数的技术人员而言,Application Framework 的抉择之道无它,「MFC是微软公司钦定产品」,这个理由就很呛人了。

纵览 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),是发生在你的程序运行时期的不正常情况,像是文件打不开、内存不足、写入失败等等等。我曾经在第2章最后面介绍过异常处理的观念及相关的MFC类,并在第4章「Exception Handling」一节介绍过一个简单的例子。与「异常处理」有关的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 需要在运行时期产生它们(第8章有更详细的说明)。

此外,OLE 常常需要在运行时期做对象的动态生成动作。例如一个OLE server 程序必须能够动态产生 OLE items,用以反应 OLE client 的需求。

MFC 针对上述这些机能,准备了一些宏,让程序能够很方便地继承并实现上述四大机能。这些宏包括:

宏名称                 提供机能                   出现章节

我也已经在第3章提过MFC的消息映射(Message Mapping)与命令循环(Command Routing)两个特性。这两个性质系由以下这些 MFC 宏完成:

宏名称                      提供机能              出现章节

事实上,与其它MFC Programming书籍相比较,本书最大的一个特色就是,要把上述这些MFC宏的来龙去脉交待得非常清楚。我认为这对于撰写MFC程序是非常重要的一件事。

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;