在第七讲中,我们大致了解了一下AppWizard生成的单文档程序框架中各个类的功能与特点,并且已经知道文档类、视类和主框架类之间存在着紧密的联系,在本讲之中,心铃准备详细讨论一下这种联系,以便让大家对MFC程序的文档/视图结构形成有一个初步的概念,不过大家先要有点思想准备,比较难哦。
l
文档模板
我们在上一讲中已经知道,在APP类的InitInstance()成员函数中有如下一段代码,其作用是为程序定义一种文档模板类型,而
文档模板把文档类、主框架类和视类联系在一起。
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CScheduleDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CScheduleView));
AddDocTemplate(pDocTemplate);
这段代码首先创建了CSingleDocTemplate的一个对象实例(请牢记类与对象之间的关系),该对象是适用于单文档程序的文档模板。它的构造函数的第一个参数指定了在打开此类文档时使用的缺省资源(包括菜单、图标、加速键和字串等)的ID值,这就是为什么我们会在ResourceView中看到很多ID值都是“IDR_MAINFRAME”但类型不同的资源,因为它们是一“套”的。构造函数的后三个参数都使用了RUNTIME_CLASS宏,我们在VC6的文本编辑器中对着RUNTIME_CLASS按鼠标右键,从关联菜单中选择“Go to Definition of RUNTIME_CLASS”命令,编辑器就会为我们打开定义了这个宏的MFC头文件Afx.h,并自动定位到该宏的定义处,仔细阅读后可以看出RUNTIME_CLASS接受一个类名作为参数,返回指向一个CRuntimeClass结构(大家也许注意到了,MFC类的名字都以字母C开头,但CRuntimeClass却只是一个结构,好在C++中结构与类有很多相似的地方,结构也可以拥有成员函数)的指针,从MSDN库中了解到,该结构可以用来在运行过程中获取一个对象的类及其基类的信息,并可以动态创建类的对象实例。CRuntimeClass结构实际上是RUNTIME_CLASS的参数中指定的类的一个静态成员,那么这个静态成员是在何处定义的呢?让我们看看CScheduleDoc的定义,可以发现下面这条语句:
DECLARE_DYNCREATE(CScheduleDoc)
再次查看Afx.h后可以发现正是DECLARE_DYNCREATE宏为CScheduleDoc定义了一个CRuntimeClass结构。需要指出的是,DECLARE_DYNCREATE和RUNTIME_CLASS两种宏都只能在CObject的派生类中使用。
讲到这儿,连心铃自己都觉得上面的叙述有点太深奥了,毕竟我们已经开始深入到MFC类库的内部了!如果有的朋友对很多细节暂时无法理解,那么就请先记住本讲中用粗体显示出来的内容:
当一个从
CObject
派生的类在定义中使用了DECLARE_DYNCREATE
宏(实际上还用了IMPLEMENT_DYNCREATE
宏)后,程序中其它部分的代码就可以在运行过程中利用RUNTIME_CLASS
宏返回的指针来动态地生成该类的实例,CSingleDocTemplate对象保存的三个指针就可以分别用来创建CScheduleDoc、CMainFrame和CScheduleView三个类的实例。
CSingleDocTemplate对象被AddDocTemplate()函数加入到APP对象内部的一个文档模板链表(该链表实际上是由一个CDocManager对象来管理,但我们不妨将该对象看做是与APP对象一体的)中,多数单文档程序都只支持一种文档类型,因此链表中一般只存有一个文档模板对象。写字板程序是一个能处理多种文档类型的单文档程序,在这种情况下链表中就不止一个文档模板对象了。
如果没有在命令行中指定要打开的文件名,APP类的ProcessShellCommand()函数就会自动生成一个新文档。在生成新文档时,APP类将检查文档模板链表,如果链表中只有一个文档模板对象,那么就直接调用该对象的OpenDocumentFile()函数,如果链表中有多个文档模板对象,那么它会弹出一个对话框,让用户选择一种类型,然后调用所选文档模板对象的OpenDocumentFile()函数,这个函数将负责创建文档对象和主框架对象。
l
文档、主框架和视图三种对象的创建过程
OpenDocumentFile()的内部进行了一些什么工作呢?这个函数的源代码所在的文件名为Docsingl.cpp,路径为“Program Files\Microsoft Visual Studio\Vc98\Mfc\Src”,大家不妨打开它(在第十三讲中我们要讨论如何通过调试来找到该文件),并找到OpenDocumentFile()的实现代码
。
OpenDocumentFile()首先使用保存的文档类的CRuntimeClass结构指针创建了一个新的文档对象,然后如法泡制创建一个主框架对象,并调用CFrameWnd::LoadFrame()来创建主框架窗口。心铃在这里要明确一个概念:在VC中使用MFC类来创建一个窗口一般要经过两个步骤,
第一步是创建一个对象实例,第二步才是调用对象的
Create()
函数来真正创建一个窗口。LoadFrame()把文档对象的指针和视类的CRuntimeClass结构指针传递给主框架对象,由主框架对象来创建工具栏、状态栏及视图对象。
让我们来总结一下,文档、主框架和视图三种对象的创建过程可以简述如下:
文档模板对象首先创建一个文档对象,接下来又创建一个主框架对象,最后由主框架对象创建视图对象。在这个过程中,文档模板对象把文档对象和视类信息告诉了主框架对象,后者在创建视图对象时会告诉视图对象对应的文档对象,同时告诉文档对象新创建了一个视图对象,这样文档和视图就联系起来了。
我们现在讨论的文档对象都只对应着一个视图对象,其实,
一个文档对象可以同时对应多个视图对象,例如使用了分割窗口的程序,这些视图对象甚至可以属于不同的视类,但是,
一个视图对象只能对应一个文档对象。
还需要指出一点,在缺省情况下,不支持多种文档类型的单文档程序只在程序启动时创建一个文档对象(视图对象也一样),用户在程序运行过程中选择新建文档或打开文档命令时,程序不会将原来的文档对象删除,而是进行“废物再利用”,重新将原来的对象初始化。支持多种文档类型的单文档程序在程序启动时也只创建一个文档对象,当用户新建一个类型相同的文档时,程序会重复使用原来的对象,当用户要新建一个类型不同的文档时,程序就创建一个相应类型的新文档对象,这样内存中就同时存在两个可以重复利用的文档对象,它们在程序退出时才会被删除。多文档程序又不一样,它们在每次新建或打开文档时都会创建一个新的文档对象,每次关闭一个文档时又会将相应的文档对象删除。
l
文档与视图的相互作用
当文档对象和视图对象之间建立起联系后,它们是如何相互作用的呢?首先,我们要记住
文档对象内部保存有一个链表(由CPtrList来管理,大家是否还记得在第四讲中提到的这个类?),
其中存放了与文档对象对应的所有视图对象的指针,这样文档对象就能访问它所对应的所有视图对象。其次,
每个视图对象的内部又保存了它所对应的文档对象的指针,这样视图对象也可以访问它所对应的文档对象。
当用户选择新建或打开文档时,文档对象将在OnNewDocument()函数中进行必要的初始化工作或从文件中读取数据,然后主框架对象将调用视图对象的OnInitialUpdate()函数,该函数的缺省实现是调用视图对象的OnUpdate()函数,后者的用途是从文档对象中取得数据,并按照预定的方式显示出来。在上述过程中,OnUpdate()是通过CScheduleView::GetDocument()函数来获得文档对象的,为了让视图对象能够取得数据,我们自己必须为CScheduleDoc类添加一些公共成员函数或成员变量,OnUpdate()如何实现显示输出功能也要由我们自己编程实现,AppWizard生成的程序框架中没有(也不可能会有,为什么呢?请大家自己思考一下)这些代码。
多数程序都把大部分编辑工作放在视类中完成,这样当用户进行了编辑之后,视图对象将调用文档对象的成员函数,或者直接访问文档对象的成员变量,以便把更新后的数据保存回文档对象中,接下来视图对象将调用CDocument::UpdateAllViews()函数,该函数的用途是更新与文档对象关联的所有其它视图对象,UpdateAllViews()将调用所有其它视图对象的OnUpdate()函数。
从上面的讨论中我们可以看出,视类的OnUpdate()是一个很重要的函数,通常在需要更新显示时它就会被调用,因此我们可以把完成输出工作的代码放到这里,但更常用的方法是把输出代码放在视类的OnDraw()函数,而在OnUpdate()中让视图的一部分区域无效,这样会向视图窗口发送一条WM_PAINT消息,最终导致OnDraw()函数被调用。
l
总结
前面的讨论比较琐碎,现在让我们用一个表格来总结一下在程序启动、运行和退出过程中,文档对象和视图对象之间的相互作用及关系。
该表格比较清楚地表示了文档/视图结构的运作机制,希望大家能够在理解的基础上记住表中的内容,这对以后编程有好处。
对MFC程序的文档/视图结构的讨论就到这里了,与上一讲相比,本讲涉及的内容已从AppWizard生成的源程序转移到了MFC类库的头文件和源代码中,对C/C++语言还不够熟练的朋友可能会感到有些困难,其实,我们分析MFC的源代码除了要了解它们的具体功能外,关键还是要从整体上来把握。现在让我们抛开细节部分,而从比较系统的角度来观察和体会一下文档/视图结构,是不是有一种很优美的感觉?在APP类和文档模板类的帮助下,文档类、视类和主框架类三者近乎完美般地结合起来了,数据在文档类和视类之间顺畅地流动,主框架类配合视类与用户进行交互,三个类各司其职,形成了一幅和谐的画面。
心铃讲这番话想表达的意思是希望大家在学习编程时不要被一大堆的源代码吓住,要多思考,多联想,多找找感觉,现在阅读现成的程序要有感觉,将来编写自己的程序时也要有感觉,当你找对感觉的时候,工作起来势必会事半功倍。在下一讲中,我们将暂停一下对源代码的讨论,转而看看与VC工程有关的各种设置,了解它们对工程有些什么影响。
名词释疑:
类与对象:这是面向对象程序设计的两个基本术语。对象(Object)是一个实体,在C++中,不妨把它理解成一块已分配好了的内存区域,其中存放的数据描述了对象的状态,或者说它们有机地构成了对象,这些数据可以由对象支持的方法来改变。类(Class)则是用来创建一个对象的样板,它包含所要创建的对象的状态描述和方法的定义。从类创建一个对象也叫做对象实例化,在C++中,这是通过调用类的构造函数来完成的。
|