大家好,我是阿桃,一个想成为被点赞关注的程序员。
工控行业、物联网行业、机器人行业软件开发可联系我,联系方式QQ:845699561
第一章:Win32 基本程序观念
我也赞同书中所讲,应用MFC框架开发Windows程序需要深入到底层,如果只停留在表面应用知其然而不知其所以然,这样会限制你更好的应用MFC框架。
Win32 程序开发流程
下图说明一个32位Windows SDK程序的开发流程:
Windows 程序分为「程序代码」和「 UI( User Interface)资源」两大部份,两部份最后以RC编译器整合为一个完整的EXE 文件。所谓UI 资源是指功能菜单、对话框外貌、程序图标、光标形状等等东西。这些UI 资源的实际内容(二进制代码)系借助各种工具产生,并以各种扩展名存在如.ico、 .bmp、 .cur 等等。程序员必须在一个所谓的资源描述档( .rc)中描述它们。RC 编译器( RC.EXE)读取RC 档的描述后将所有UI资源档集中制作出一个.RES 档,再与程序代码结合在一起,这才是一个完整的Windows可执行档。
需要什么函数库( .LIB)
Windows API是以动态库的方式提供给开发软元调用,并不是延伸档名为.dll 者才是动态联结函数库( DLL, Dynamic Link Library),事实上.exe、 .dll、 .fon、 .mod、 .drv、 .ocx 都是所谓的动态联结函数库。
Windows 程序调用的函数可分为C Runtimes 以及Windows API 两大部份。早期的C Runtimes 并不支持动态联结,但Visual C++ 4.0 之后已支持,并且在32 位操作系统中已不再有small/medium/large 等内存模式之分。
LIBC.LIB - 这是C Runtime 函数库的静态联结版本。
MSVCRT.LIB - 这是C Runtime 函数库动态联结版本( MSVCRT40.DLL)的import 函数库。如果联结此一函数库,你的程序执行时必须有MSVCRT40.DLL在场。
Windows API,由操作系统本身(主要是Windows 三大模块GDI32.DLL 和USER32.DLL 和KERNEL32.DLL)提供。
Windows 发展至今,逐渐加上的一些新的API 函数(例如Common Dialog、 ToolHelp)并不放在GDI 和USER 和KERNEL 三大模块中,而是放在诸如COMMDLG.DLL、TOOLHELP.DLL 之中。如果要使用这些APIs,联结时还得加上这些DLLs 所对应的import 函数库,诸如COMDLG32.LIB 和TH32.LIB。
需要什么头文件( .H)
所有Windows 程序都必须包含WINDOWS.H。早期这是一个巨大的头文件,大约有5000行左右, Visual C++ 4.0 已把它切割为各个较小的文件,但还以WINDOWS.H 总括之。除非你十分清楚什么API 动作需要什么头文件,否则为求便利,单单一个WINDOWS.H也就是了。
不过, WINDOWS.H 只照顾三大模块所提供的API 函数,如果你用到其它system DLLs,例如COMMDLG.DLL 或MAPI.DLL 或TAPI.DLL 等等,就得包含对应的头文件,例如COMMDLG.H 或MAPI.H 或TAPI.H 等等。
以消息为基础, 以事件驱动之
Windows 程序的进行系依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个while 回路),等待任何可能的输入,然后做判断,然后再做适当的处理。
操作系统如何捕捉外围设备(如键盘和鼠标)所发生的事件呢?噢, USER 模块掌管各个外围的驱动程序,它们各有侦测回路。
如果把应用程序获得的各种「输入」分类,可以分为由硬件装置所产生的消息(如鼠标移动或键盘被按下),放在系统队列( system queue)中,以及由Windows 系统或其它Windows 程序传送过来的消息,放在程序队列( application queue)中。
程序调用GetMessage API 就取得一个消息,程序的生命靠它来推动。所有的GUI 系统,包括UNIX的X Window 以及OS/2 的Presentation Manager,都像这样,是以消息为基础的事件驱动系统。
接受并处理消息的主角就是窗口。每一个窗口都应该有一个函数负责处理消息,程序员必须负责设计这个所谓的「窗口函数」( window procedure,或称为window function)。如果窗口获得一个消息,这个窗口函数必须判断消息的类别,决定处理的方式。
下图说明Windows程序的本体与操作系统之间的关系,外界输入的消息会存放到系统或应用程序消息队列中,DispatchMessage经由USER Module模块的协调将消息传送带窗口函数,窗口函数根据消息类型进行相应的处理函数。
一个具体而微的Win32 程序
makefile,就是让你能够设定某个文件和某个文件相比-- 比较其产生日期。由其比较结果来决定要不要做某些你所指定的动作。只要右边任一文件比左边的文件更新,就执行下一行所指定的动作。这动作可以是任何命令列动作。
在Win32 中CALLBACK 被定义为__stdcall,是一种函数调用习惯,关系到参数挤压到堆栈的次序,以及处理堆栈的责任归属。其它的函数调用习惯还有_pascal 和_cdecl。
窗口类别之注册与窗口之诞生
窗口。这没有什么困难,因为API 函数CreateWindow 完全包办了整个巨大的工程。但是窗口产生之前,其属性必须先设定好。所谓属性包括窗口的「外貌」和「行为」,一个窗口的边框、颜色、标题、位置等等就是其外貌,而窗口接收消息后的反应就是其行为(具体地说就是指窗口函数本身)。程序必须在产生窗口之前先利用API 函数RegisterClass设定属性(我们称此动作为注册窗口类别)。 RegisterClass 需要一个大型数据结构WNDCLASS 做为参数, CreateWindow 则另需要11 个参数。
wc.lpfnWndProc 所指定的函数就是窗口的行为中枢,也就是所谓的窗口函数。注意, CreateWindow 只产生窗口,并不显示窗口,所以稍后我们必须再利用ShowWindow 将之显示在屏幕上。
在Windows 3.x 时代,窗口类别只需注册一次,即可供同一程序的后续每一个执行实例( instance)使用(之所以能够如此,是因为所有进程共在一个地址空间中),所以我们把RegisterClass 这个动作安排在「只有第一个执行个体才会进入」的InitApplication 函数中。至于此一进程是否是某个程序的第一个执行实例,可由WinMain 的参数hPrevInstance 判断之;其值由系统传入。
产生窗口, 是每一个执行实例(instance ) 都得做的动作, 所以我们把
CreateWindow 这个动作安排在「任何执行实例都会进入」的InitInstance 函数中。
以上情况在Windows NT 和Windows 95 中略有变化。由于Win32 程序的每一个执行实例( instance)有自己的地址空间,共享同一窗口类别已不可能。但是由于Win32 系统令hPrevInstance 永远为0(这里应该是因为让窗口类只实例化一次,虽然系统支持多实例化占不同内存资源),所以我们仍然得以把RegisterClass 和CreateWindow 按旧习惯安排。既符合了新环境的要求,又兼顾到了旧源代码的兼容。
InitApplication 和InitInstance 只不过是两个自定函数,为什么我要对此振振有词呢?原因是MFC 把这两个函数包装成CWinApp 的两个虚拟成员函数。
在非抢占性多任务操作系统中,程序应该主动释放控制权,在抢占性多任务中不再非靠GetMessage释放CPU控制权不可。
窗口的生命中枢: 窗口函数
窗口函数是call back 函数,虽然由你设计,但是永远不会也不该被你调用,它们是为Windows 系统准备的。
注意,不论什么消息,都必须被处理,所以switch/case 指令中的default: 处必须调用DefWindowProc,这是Windows 内部预设的消息处理函数。
为什么不让程序在抓到消息( GetMessage)之后直接调用它就好了?原因是,除了你需要调用它,有很多时候操作系统也要调用你的窗口函数(例如当为什么不让程序在抓到消息( GetMessage)之后直接调用它就好了?原因是,除了你需要调用它,有很多时候操作系统也要调用你的窗口函数(操作系统为什么要调用窗口函数这里没深入,后面要看一下原因)。
消息映射( Message Map) 的雏形
以下作法是MFC「消息映射表格」(第9章)的雏形,我所采用的结构名称和变量名称,都与MFC 相同,藉此让你先有个暖身。
把方法封装在结构体中其实也有面向对象的概念在里面。
对话框的运作
Windows 的对话框依其与父窗口的关系,分为两类:
1. 「令其父窗口除能,直到对话框结束」,这种称为modal 对话框。
2. 「父窗口与对话框共同运行」,这种称为modeless 对话框。
为了做出一个对话框,程序员必须准备两样东西:
1. 对话框模板( dialog template)。这是在RC 文件中定义的一个对话框外貌,以各种方式决定对话框的大小、字形、内部有哪些控制组件、各在什么位置...等等。
2. 对话框函数( dialog procedure)。其类型非常类似窗口函数,但是它通常只处理WM_INITDIALOG 和WM_COMMAND 两个消息。对话框中的各个控制组件也都是小小窗口,各有自己的窗口函数,它们以消息与其管理者(父窗口,也就
是对话框)沟通。而所有的控制组件传来的消息都是WM_COMMAND,再由其参数分辨哪一种控制组件以及哪一种通告( notification)。
对话框內部自有一个消息回路(由系統维护)。
模块定义文件( .DEF)
Windows 程序需要一个模块定义文件,将模块名称、程序节区和资料节区的内存特性、模块堆积( heap)大小、堆栈( stack)大小、所有callback 函数名称...等等登记下来。
资源描述档( .RC)
RC 文件是一个以文字描述资源的地方。常用的资源有九项之多,分别是ICON、 CURSOR、BITMAP、 FONT、 DIALOG、 MENU、 ACCELERATOR、 STRING、 VERSIONINFO。还可能有新的资源不断加入,例如Visual C++ 4.0 就多了一种名为TOOLBAR 的资源。这些文字描述需经过RC 编译器,才产生可使用的二进制代码。
Windows 程序的生与死
对Windows 消息种类以及发生时机的透彻了解,正是程序设计的关键,这节说明整个窗口的诞生和死亡,说明消息的发生与传递,以及应用程序的兴起与结束。
为什么结束一个程序复杂如斯?因为操作系统与应用程序职司不同,二者是互相合作的关系,所以必需各做各的份内事,并互以消息通知对方。如果不依据这个游戏规则,可能就会有麻烦产生。
空闲时间的处理: OnIdle
背景工作最适宜在空闲时间完成。
PeekMessage与GetMessage性质不同,如果从消息队列抓不到消息,程序的主执行线程会被操作系统悬挂住,当操作系统再次恢复主执行线程时如果扔没抓到消息GetMessage会过门不入,操作系统去照顾其它人。PeekMessage则会取回控制权,使程序得以执行一段时间。于是上述消息循环进入OnIdle 函数中。
Console 程序
console 程序还另有妙用。如果你的程序和使用者之间是以巨量文字来互动,或许你会选择使用edit 控制组件(或MFC 的CEditView)。但是你知道,计算机在一个纯粹的「文字窗口」(也就是console 窗口)中处理文字的显现与卷动比较快,你的程序动作也比较简单。所以,你也可以在Windows 程序中产生console 窗口,独立出来操作。
Console 程序与DOS程序的差别
描述两者之间的各种差别。
什么是C Runtime函数库的多线程版本
当C runtime 函数库于1970s 年代产生出来时, PC 的内存容量还很小,多任务是个新奇观念,更别提什么多执行线程了,所以早期只有静态库,重新开发一套支持多执行线程的runtime 函数库,可能导至程序代码大小和执行效率都遭受不良波及-- 即使你只激活了一个执行线程。
Visual C++ 的折衷方案是提供两种版本的C runtime 函数库。一种版本给单线程程序使用,一种版本给多线程程序使用。
进程与执行线程( Process and Thread)
我们习惯以进程( process)表示一个执行中的程序,并且以为它是CPU 排程单位。事实上执行线程才是排程单位
核心对象
你可以说核心对象是系统的一种资源(噢,这说法对GDI 对象也适用),系统对象一旦产生,任何应用程序都可以开启并使用该对象。系统给予核心对象一个计数值( usagecount)做为管理之用。核心对象包括下列数种:
「 process 对象」究竟做什么用呢?它并不如你想象中用来「执进程序代码」;不,程序代码的执行是执行线程的工作,「 process 对象」只是一个数据结构,系统用它来管理进程。
一个进程的诞生与死亡
进程与子进程之间可以有某些关系存在,但shell 在调用CreateProcess 时已经把母子之间的脐带关系剪断了,因此它们事实上是独立实例。
建立新进程之前,系统必须做出两个核心对象,也就是「进程对象」和「执行线程对象」。
只要你有某个进程的handle,就可以结束它的生命。 TerminateProcess 并不被建议使用,倒不是因为它的权力太大,而是因为一般进程结束时,系统会通知该进程所开启(所使用)的所有DLLs,但如果你以TerminateProcess 结束一个进程,系统不会做这件事,而这恐怕不是你所希望的。
第二章:C++的重要性质
所谓纯对象导向语言,是指不管什么东西,都应该存在于对象之中。 JAVA 和Small Talk都是纯对象导向语言。
类别及其成员- 谈封装( encapsulation)
成员变量可以只在类别内被处理,也可以开放给外界处理。以资料封装的目的而言,自然是前者较为妥当,但有时候也不得不开放。为此, C++ 提供了private、 public 和protected 三种修饰词。一般而言成员变量尽量声明为private,成员函数则通常声明为public。
把资料声明为private,不允许外界随意存取,只能透过特定的接口来操作,这就是对象导向的封装( encapsulation)特性。
基础类别与衍生类别: 谈继承(Inheritance)
C++ 神秘而特有的性质其实在于继承。
注意:衍生类别与基础类别的关系是“IsKindOf” 的关系。
如果语法允许你产生一个不应该有的抽象对象,或如果语法不支持「把所有形状(不管什么形状)都display 出来」的一般化动作作,这就是个失败的语言。 C++ 是成功的,自然有它的整治方式。
this 指针
成员函数有一个隐藏参数,名为this 指针。
虚拟函数与多态(Polymorphism)
如果你以一个「基础类别之指针」指向一个「衍生类别之对象」,那么经由此指针,你就只能够调用基础类别(而不是衍生类别)所定义的函数。
1. 如果你以一个「基础类别之指针」指向「衍生类别之对象」,那么经由该指针你只能够调用基础类别所定义的函数。
2. 如果你以一个「衍生类别之指针」指向一个「基础类别之对象」,你必须先做明显的转型动作( explicit cast)。这种作法很危险,不符合真实生活经验,在程序设计上也会带给程序员困惑。
3. 如果基础类别和衍生类别都定义了「相同名称之成员函数」,那么透过对象指针调用成员函数时,到底调用到哪一个函数,必须视该指针的原始型别而定,
而不是视指针实际所指之对象的型别而定。
只要是拥有纯虚拟函数的类别,就是一种抽象类别,它是不能够被具象化( instantiate)的,也就是说,你不能根据它产生一个对象(你怎能说一种形状为'Shape' 的物体呢)。
虚拟函数做结论:
1. 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函数设为virtual。
2. 以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to assume many forms",也就是多态。
3. 虚拟函数是C++ 语言的Polymorphism 性质以及动态绑定的关键。
4. 既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)。
5. 我们可以说,拥有纯虚拟函数者为抽象类别( abstract Class),以别于所谓的具象类别( concrete class)。
6. 抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
7. 虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。
类别与对象大解剖
每一个「内含虚拟函数的类别」,编译器都会为它做出一个虚拟函数表,表中的每一笔元素都指向一个虚拟函数的地址。此外,编译器当然也会为类别加上一项成员变量,是一个指向该虚拟函数表的指针(常被称为vptr)。
每一个由此类别衍生出来的对象,都有这么一个vptr。当我们透过这个对象调用虚拟函数,事实上是透过vptr 找到虚拟函数表,再找出虚拟函数的真正地址。
衍生类别会继承基础类别的虚拟函数表(以及所有其它可以继承的成员),当我们在衍生类别中改写虚拟函数时,虚拟函数表就受了影响:表中元素所指的函数地址将不再是基础类别的函数地址,而是衍生类别的函数地址。
动态绑定机制,在执行时期,根据虚拟函数表,做出了正确的选择。
Object slicing与虚拟函数
因为衍生对象不但继承其基础类别的成员,又有自己的成员。那么所谓的upcasting(向上强制转型): (CDocument)mydoc,将会造成对象的内容被切割( objectslicing)
由于((CDocument)mydoc).func() 是个传值而非传址动作,编译器以所谓
的拷贝构造式( copy constructor)把CDocument 对象内容复制了一份,使得mydoc 的vtable 内容与CDocument 对象的vtable 相同。
静态成员(变量与函数)
static 成员变量不属于对象的一部份,而是类别的一部份,所以程序可以在还没有诞生任何对象的时候就处理此种成员变量。但首先你必须初始化它。
由于static 成员函数不需要借助任何对象,就可以被调用执行,所以编译器不会为它暗加一个this 指针。也因为如此, static 成员函数无法处理类别之中的non-static 成员变量。
static 成员函数「没有this 参数」的这种性质,正是我们的MFC 应用程序在准备callback 函数时所需要的。
C++ 程序的生与死:兼谈构造式与析构式
C++ 的new 运算子和C 的malloc 函数都是为了配置内存,但前者比之后者的优点是, new 不但配置对象所需的内存空间时,同时会引发构造式的执行。
一个有着阶层架构的类别群组,当衍生类别的对象诞生之时,构造式的执行是由最基础类别( most based)至最尾端衍生类别( most derived);当对象要毁灭之前,析构式的执行则是反其道而行。
四种不同的对象生存方式
静态全域对象的构造式调用动作必须靠startup 码帮忙。 startup 码
是什么?是更早于程序进入点( main 或WinMain)执行起来的码,由C++ 编译器提供,被联结到你的程序中。 startup 码可能做些像函数库初始化、进程信息设立、 I/O stream 产生等等动作,以及对static 对象的初始化动作(也就是调用其构造式)。
所谓 "Unwinding"
C++ 对象依其生存空间,适当地依照一定的顺序被析构( destructed)。但是如果发生异常情况( exception),而程序设计了异常情况处理程序( exception handling),控制权就会截弯取直地「直接跳」到你所设定的处理例程去,这时候堆栈中的C++对象有没有机会被析构?这得视编译器而定。如果编译器有支持unwinding 功能,就会在一个异常情况发生时,将堆栈中的所有对象都析构掉。
执行时期型别信息( RTTI)
我们有可能在程序执行过程中知道某个对象是属于哪一种类别吗?这种在C++ 中称为执行时期型别信息( Runtime Type Information, RTTI)的能力。
虽然编译器支持RTTI但MFC有着自己的一套方法,目前编译器的具体实现还没有解析过,后面有必要再看一下。
动态生成( Dynamic Creation)
你能够以Serialize 函数写档,我能够以Serialize 函数读档,但我就是没办法恢复你原来的状态-- 除非我的程序能够「动态生成」。
异常处理( Exception Handling)
Exception(异常情况)是一个颇为新鲜的C++ 语言特征,可以帮助你管理执行时期的错误,特别是那些发生在深度巢状( nested)函数调用之中的错误。
事实上exception handling 是MFC 和OWL 两个application frameworks 的防弹中心。
MFC 早就支持exception,不过早期它用的是非标准语法。 Visual C++ 4.0 编译器本身支持完整的C++ exceptions, MFC 也因此有了两个exception 版本:你可以使用语言本身提供的性能,也可以沿用MFC 古老的方法(以宏形式出现)。
Template
复制一段既有程序代码的一个最平常的理由就是为了改变数据类型。
说明宏与Template的对比,有了Template你可以具有宏只写一次的优点并且还具有多载函数类型检验的优点。
每一个使用Template 的程序代码的目的档中都存在有template码,联结器负责复制和删除。
联结器会把所有赘余的template 码剔除。 这在Borland 联结器里头称为smart 技术。 其它联结器亦使用类似的技术。
第三章:MFC 六大关键技术之仿真
MFC类别阶层
MFC 数个最重要类别的阶层关系如下:
MFC 程序的初始化过程
CmyWinApp创建线程后进行初始化操作,InitApplication 和InitInstance 是MFC 的CWinApp的两个虚拟函数。InitApplication用于注册窗口类、InitInstance用于产生窗口。
RTTI (执行时期型别辨识)
要达到RTTI 的能力,我们(类别库的设计者)一定要在类别构造起来的时候,记录必要的信息,以建立型录。
看起来整个IMPLEMENT_DYNAMIC 内容好象只是指定初值,不然,其曼妙处在于它所使用的一个struct AFX_CLASSINIT,这个结构体包含构造式,该构造式负责链表的串接工作。
Dynamic Creation (动态生成)
如果我能够把类别的大小记录在类别型录中,把构造函数(注意,这里并非指C++ 构造式,而是指即将出现的CRuntimeClass::CreateObject)也记录在类别型录中,当程序在执行时期获得一个类别名称,它就可以在「类别型录网」中找出对应的元素,然后调用其构造函数(这里并非指C++ 构造式),产生出对象。
Persistence(永续生存)机制
在Document/View 架构中,资料都放在一份document(文件)里头,我们只要把其中的成员变量依续写进文件即可。成员变量很可能是个对象,而面对对象,我们首先应该记载其类别名称,然后才是对象中的资料。
动态生成技术在MFC中应用于程序读取Document到类中。
MFC 有一套Serialize 机制,目的在于把档名的选择、文件的开关、缓冲区的建立、资料的读写、萃取运算子( >>)和嵌入运算子( <<)的多载( overload)、对象的动态生成都包装起来。
Message Mapping(消息映射)
通过派生类与其父类构成消息流动网。
Command Routing(命令绕行)
这一章节并没讲程序如何推送消息的,只是将消息流动的路线描述出来。
MFC 对于消息绕行的规定是:
如果是一般的Windows 消息( WM_xxx),一定是由衍生类别流向基础类别,
没有旁流的可能。
如果是命令消息WM_COMMAND,就有奇特的路线了:
第四章:Visual C++整合开发环境
已经过时,后面需要研究的是Visual Studio。
AppWizard:这是一个程序代码产生器。相同类型(或说风格)的MFC 程序一定具备相同的程序骨干,AppWizard用于生成这样的骨干,但是每一个project 使用AppWizard 的机会只有一次,生成后不能还原。
Resource Editor 做出来的各类资源与你的程序代码之间如何维系关系?譬如说对话框中的一个控制组件被按下后程序该有什么反应? 这就要靠 ClassWizard 搭起鹊桥。
ClassWizard: AppWizard 制作出来的程序骨干是「起手无悔」的,接下来你只能够在程序代码中加油添醋(最重要的工作是加上自己的成员变量并改写虚拟函式),或搭起消息与程序代码之间的鹊桥(建立Message Map),这全得仰仗
ClassWizard。
每一位C 程序员在DOS 环境下都有使用「夹杀法」的除错经验:把可能错误的范围不断缩小,再缩小,最后以printf 印出你心中的嫌疑犯,真象大白。
Windows 程序员就没有方便的printf 可用,唯MessageBox 差可比拟。
在开始你的程序撰写之前,小心做好系统分析的工作,AppWizard是不能够还原的。
第五章: 总观 Application Framework
Application Framework是一组合作无间的「类别」架构起来的大模型。
你动不了Application Framework 的大架构,也不需要动。这是福利不是约束。
如果要三言两语点出Application Framework 的特质,我会这么说:我们挖出别人早写好的一整套模块( MFC 或OWL 或OpenClass)之中的一部份,给个引子( application object)使它们一一具象化动起来,并被允许修改其中某些零件使这程序更符合私人需求,如是而已。
凝聚性强、组织化强的类别库就是Application Framework。一组合作无间的对象,彼此藉消息的流动而沟通,并且互相调用对方的函数以求完成任务,这就是Application Framework。
当我们面临软件工业革命,我们的第一个考量点是:我的软件开发技术要从哪一个技术面切入?从raw API 还是从高阶一点的工具?如果答案是后者,第二个考量点是我使用哪一层级的工具? GUI toolkits 还是Class Library 还是Application Framework?如果答案又是后者,第三个考量点是我使用哪一套产品? MFC 或OWL 或Open Class Library?
Application Framework 绝不只是为了降低我们花在浩瀚无涯的Windows API 的时间而已;它所带来的对象导向程序设计观念与方法,使我们能够站在一群优秀工程师( MFC 或OWL 的创造者)的努力心血上,继承其成果而开发自己之所需。
MFC 类别主要可分为下列数大群组:
1.General Purpose classes - 提供字符串类别、数据处理类别(如数组与串行),异常情况处理类别、文件类别...等等。
2. Windows API classes - 用来封包Windows API,例如窗口类别、对话框类别、DC 类别...等等。
3. Application framework classes - 组成应用程序骨干者,即此组类别, 包括Document/View、消息邦浦、消息映射、消息绕行、动态生成、文件读写等等。
4. high level abstractions - 包括工具栏、状态列、分裂窗口、卷动窗口等等。
5. operation system extensions - 包括OLE、 ODBC、 DAO、 MAPI、 WinSock、 ISAPI等等。
CObject 是万类之首,凡类别衍生自CObject 者,得以继承数个对象导向重要性质,包括RTTI(执行时期型别鉴识)、 Persistence(对象保存)、 Dynamic Creation(动态生成)、 Diagnostic(错误诊断)。
第六章: MFC 程序的生死因果
SDK 程序设计的第一要务是了解最重要的数个API 函数的意义和用法, 像是RegisterClass、 CreateWindow、 GetMessage、 DispatchMessage,以及消息的获得与分配。MFC 程序设计的第一要务则是熟记MFC 的类别阶层架构,并清楚知晓其中几个一定会用到的类别。
一个应用程序在发展过程中常需要不断地编译。 Windows 程序包含的标准.H 文件非常巨大但内容不变,编译器浪费在这上面的时间非常多。 Precompiled header 就是将.H 档第一次编译后的结果贮存起来,第二次再编译时就可以直接从磁盘中取出来用。
MFC 就把有着相当固定行为之WinMain 内部动作包装在CWinApp 中,把有着相当固定行为之WndProc 内部动作包装在CFrameWnd 中。
CWinApp 代表程序本体
CFrameWnd 代表一个主框窗口( Frame Window)
注意:应用程序一定要改写虚拟函数InitInstance,因为它在CWinApp 中只是个空函数,没有任何内建(预设)动作。
第七章: 简单而完整: MFC 骨干程序
MFC 程序设计的第一要务是熟记各类别的阶层架构,并清楚了解其中几个一定会用到的类别。一个MFC 骨干程序(不含ODBC 或OLE 支持)运用到
的类别如图所示:
Document/View 是MFC 进化为Application Framework 的灵魂。这个特征表现于程序设计技术上远多于表现在使用者接口上,因此使用者可能感觉不到什么是Document/View。
Document/View 的价值在于,这些MFC 类别已经把一个应用程序所需的「数据处理与显示」的函数空壳都设计好了,这些函数都是虚拟函数,所以你可以(也应该)在衍生类别中改写它们。有关文件读写的动作在CDocument 的Serialize 函数进行,有关画面显示的动作在CView 的OnDraw 或OnPaint 函数进行。当我为自己衍生两个类别CMyDoc和CMyView,我只要把全付心思花在CMyDoc::Serialize 和CMyView::OnDraw 身上,其它琐事一概不必管,整个程序自动会运作得好好的。
软件界当初发展GUI 系统时,目的也是希望把程序员的心力导引到应用软件的真正目标去,而不必花在使用者接口上。
MFC 的Document/View 架构希望更把程序员的心力导引到真正的数据结构设计以及真正的数据显示动作上,而不要花在模块的沟通或消息的流动传递上。
Document Frame 窗口是View 窗口的一个容器。资料的内容、资料的表象、以及「容纳资料表象之外框窗口」三者是一体的,换言之,程序每打开一份文件(资料),就应该产生三份对象:
1. 一份Document 对象,
2. 一份View 对象,
3. 一份CMDIChildWnd 对象(做为外框窗口)
这三份对象由一个所谓的Document Template 对象来管理。让这三份对象产生关系的关键在于CmultiDocTemplate。
View 本身虽然已经是一个窗口,其外围却必须再包装一个外框窗口做为舞台。这样的切割其实是为了让View 可以非常独立地放置于「 MDI Document Frame 窗口」或「 SDI Document Frame 窗口」或「 OLE Document Frame 窗口」等各种应用之中。
SDK 程序中要做到Drag and Drop,并不算太难,这里简单提一下它的原理以及作法。当使用者从Shell 中拖放一个文件到程序A, Shell 就配置一块全域内存,填入被拖曳的文件名称(包含路径),然后发出WM_DROPFILES 传到程序A的消息队列。程式A取得此消息后,应该把内存的内容取出,再想办法开档读档。
当使用者打开一份文件文件,程序应该把主窗口上的菜单换掉,这个动作在SDK 程序中由程序员负责,在MFC 程序中则由Framework 代劳了。
Scribble 可以激活许多对话框,前一节提了许多。唯一要程序员自己动手(我的意思是出现在我们的程序代码中)的只有About 对话框。
比之于SDK 程序中的对话框,这真是方便太多了。传统SDK 程序要在RC 文件中定义对话框模板( dialog template,也就是其外形),在C 程序中设计对话框函数。现在只需从CDialog 衍生出一个类别,然后产生该类别之对象,并指定RC 文件中的对话框面板资源,再调用对话框对象的DoModal 成员函数即可。
第八章: Document-View 深入探讨
当你开发自己的程序,应该从CDocument 衍生出一个属于自己的Document 类别,并且在类别中声明一些成员变量,用以承载(容纳)数据。然后再(至少)改写专门负责文件读写动作的Serialize 函数。
当你开发自己的程序,应该从CView 衍生出一个属于自己的View 类别,并且在类别中(至少)改写专门负责显示资料的OnDraw 函数(针对屏幕)或OnPrint 函数(针对打印机)。
我想你会惊讶为什么UI 的管理不由View 直接负责,却要交给Frame窗口?你知道,有时候机能与机能之间要有点黏又不太黏才好,把UI 管理机能隔离出来,可以降低彼此之间的依存性,也可以使机能重复使用于各种场合如SDI、 MDI、 OLE in-place editing(即地编辑)之中。如此一来View 的弹性也会大一些。
我们的设计最高原则就是尽量使用MFC 已有的类别,提高软件IC 的重复使用性。
文件的操作常需配合对异常情况( exception)的处理,因为文件的异常情况特别多:档案找不到啦、文件handles 不足啦、读写失败啦...。
文章每周持续更新,原创虽短,确不容易,欢迎大家点赞关注,一起交流技术一起提升成长。