4.4.1 文档/视图结构
在文档/视图结构里,文档是一个应用程序数据基本元素的集合,它构成应用程序所使用的数据单元;另外它还提供了管理和维护数据的手段。
文档是一种数据源,数据源有很多种,最常见的是磁盘文件,但它不必是一个磁盘文件,文档的数据源也可以来自串行口、网络或摄像机输入信号等。文档对象负责来自所有数据源的数据的管理。
视图是数据的用户窗口,为用户提供了文档的可视的数据显示,它把文档的部分或全部内容在窗口中显示出来。视图还给用户提供了一个与文档中的数据交互的界面,它把用户的输入转化为对文档中数据的操作。每个文档都会有一个或多个视图显示,一个文档可以有多个不同的视图。比如,在Excel电子表格中,我们可以将数据以表格方式显示,也可以将数据以图表方式显示。一个视图既可以输出到窗口中,也可以输出到打印机上。
1.文档与视图的概念和关系
Visual C++(www.cppentry.com) 6.0以其功能强大、用户界面友好而备受程序员们的青睐。但是,在当前的Microsoft 基本类库4.2版本中,大约有将近200个类,数千个函数,加之Microsoft公司隐藏了一些技术细节,使得人们深入学习MFC变得十分困难。
MFC的AppWizard可以生成三种类型的应用程序:基于对话框的应用、单文档应用(SDI)和多文档应用(MDI)。笔者从MFC中的文档/视图结构入手,分析一些函数的流程,并解决编制MDI 应用程序过程中的一些常见问题。
(1)了解文档/视图结构
MFC应用程序模型历经多年已有了相当大的发展。有一个时期,它只是个使用应用程序对象和主窗口对象的简单模型。在这个模型中,应用程序的数据作为成员变量保持在框架窗口类中,在框架窗口的客户区中,该数据被提交给显示器。随着MFC 2.0的问世,一种应用程序结构的新方式——MFC文档/视图结构出现了。在这种结构中,CFrameWnd繁重的任务被委派给几个不同类,实现了数据存储和显示的分离。一般情况下,采用文档/视图结构的应用程序至少应由以下对象组成。
应用程序是一个CWinApp派生对象,它充当全部应用程序的容器。应用程序沿消息映射网络分配消息给它的所有子程序。
框架窗口是一个CFrameWnd派生对象。
文档是一个CDocument派生对象,它存储应用程序的数据,并把这些信息提供给应用程序的其余部分。
视窗是CView派生对象,它与其父框架窗口用户区对齐。视窗接受用户对应用程序的输入并显示相关联的文档数据。
通常,应用程序数据存在于简单模型中的框架窗口中。在文档/视图方式中,该数据移入称为document的独立数据对象。当然,文档不一定是文字,文档是可以表现应用程序使用的数据集的抽象术语。而用户输入处理及图形输出功能从框架窗口转向视图。单独的视窗完全遮蔽框架窗口的客户区,这意味着即使程序员直接绘画至框架窗口的客户区,视图仍遮蔽绘画,在屏幕上不出现任何信息。所以输出必须通过视图。框架窗口仅仅是个视图容器。
CDocument类对文档的建立及归档提供支持,并提供应用程序用于控制其数据的接口。MDI应用程序可以处理多个类型的文档,每个类型的文档拥有一个相关联的文档模板对象。文档对象驻留在场景后面,提供由视图对象显示的信息。文档至少有一个相关联的视图。视图只能与一个文档相关联。
在文档/视图方式中,对象的建立是由文档模板来管理的,它是CDocTemplate派生对象,建立并维护框架窗口,文档及视图。
MFC调用命令处理程序以响应发生在应用程序中的事件。命令发送的优先级是:活动的视图→框架窗口→文档→应用程序→默认窗口过程(DefWindowsProc)。
总之,在文档/视图方式中,文档和视图是分离的,即:文档用于保存数据,而视图是用来显示这些数据。文档模板维护它们之间的关西。这种文档/视图结构在开发大型软件项目时特别有用。
(2)了解与文档/视图结构有关的各种类之间的关系。
在文档/视图应用程序中,CWinApp对象拥有并控制文档模板,后者产生文档、框架窗口及视窗。
从用户的角度来看,“视图”实际上是一个普通的窗口。像其他基于Windows应用的窗口一样,可以改变它的尺寸,对它进行移动,也可以随时关闭它。若从程序员的角度来看,视图实际上是一个从MFC类库中的CView类所派生出的类的对象。文档对象是用来保存数据的,而视图对象是用来显示数据的,并且允许对数据进行编辑。SDI或MDI的文档类是由CDocument类派生出来的,它可以有一个或多个视图类,而这些视图类最终都是由CView类派生出来的。视图对象只有一个与之相联系的文档对象,它所包含的CView::GetDocument函数允许应用在视图中得到与之相联系的文档,据此,应用程序可以对文档类成员函数及公共数据成员进行访问。如果视图对象接受到了一条消息,表示用户在编辑控制中输入了新的数据,此时,视图就必须通知文档对象对其内部数据进行相应的更新。
如果文档数据发生了变化,则所有的视图都必须被通知到,以便它们能够对所显示的数据进行相应的更新。CDocument::UpdateAllViews函数即可完成此功能。当该函数被调用时,派生视类的CView::OnUpdate函数被触发。通常OnUpdate函数要对文档进行访问,读取文档数据,然后再对视图的数据成员或控制进行更新,以便反映出文档的变化。另外,还可以利用OnUpdate函数使视图的部分客户区无效,以便触发CView::OnDraw函数,利用文档数据来重新对窗口进行绘制。
在MDI应用程序中,可以处理多个文档类型,即多个文档模板,每个模板又可以有多个文档,每个文档又可以多视图显示。为管理方便,上一级往往保留了下一级的指针列表。解释如下:
每个应用程序类(CWinApp的派生类)都保留并维护了一份所有文档模板的指针列表,这是一个链表结构。应用程序为所要支持的每个文档类型动态分配一个CMultiDocTemplate 对象。
CmultiDocTemplate(UINT nIDResource, CruntimeClass*pDocClass, CruntimeClass*pFrameClass, CruntimeClass*pViewClass);
|
并在应用程序类的CWinApp::InitInstance成员函数中将每个CMultiDocTemplate对象传递给CWinApp::AddDocTemplate。该函数将一个文档模板加入到应用程序可用文档模板的列表中。函数原形为:
void AddDocTemplate(CdocTemplate*pTemplate);
|
应用程序可以用CWinApp::GetFirstDocTemplatePostion获得应用程序注册的第一个文档模板的位置,利用该值来调用CWinApp::GetNextDocTemplate函数,获得第一个CDocTemplate对象指针。函数原形如下:
POSITION GetFirstDocTemplate()const; CDocTemplate*GetNextDocTemplate(POSITION&pos)const;
|
第二个函数返回由pos 标识的文档模板。POSITION是MFC定义的一个用于迭代或对象指针检索的值。通过这两个函数,应用程序可以遍历整个文档模板列表。如果被检索的文档模板是模板列表中的最后一个,则pos参数被置为NULL。
一个文档模板可以有多个文档,每个文档模板都保留并维护了一个所有对应文档的指针列表。应用程序可以用CDocTemplate::GetFirstDocPosition函数获得与文档模板相关的文档集合中第一个文档的位置,并用POSITION值作为Cdoc Template::GetNextDoc的参数来重复遍历与模板相关的文档列表。函数原形为:
viaual POSITION GetFirstDocPosition()const = 0; visual Cdocument*GetNextDoc(POSITION&rPos)const = 0;
|
如果列表为空,则rPos被置为NULL。
在文档中可以调用CDocument::GetDocTemplate获得指向该文档模板的指针。函数原形如下:
CDocTemplate*GetDocTemplate()const;
|
如果该文档不属于文档模板管理,则返回值为NULL。
一个文档可以有多个视图。每一个文档都保留并维护一个所有相关视图的列表。CDocument::AddView将一个视图连接到文档上,将该视图加入到文档相联系的“视”的列表中,并将“视”的文档指针指向该文档。当有File/New、File/Open、Windows/New或Window/Split的命令而将一个新创建的视图的对象连接到文档上时,MFC会自动调用该函数,框架通过文档/视图的结构将文档和视图联系起来。当然,程序员也可以根据自己的需要调用该函数。
virtual POSITION GetFirstViewPosition()const; virtual CViw*GetNextView(POSITION&rPosition)cosnt;
|
应用程序可以调用CDocument::GetFirstViewPosition返回与调用文档相联系的视图的列表中的第一个视图的位置,并调用CDocument::GetNextView返回指定位置的视图,并将rPositon的值置为列表中下一个视图的POSITION值。如果找到的视图为列表中的最后一个视图,则将rPosition置为NULL。
当在文档上新增一个视图或删除一个视图时,MFC会调用OnChangeViewList函数。如果被删除的视图是该文档的最后一个视图,则删除该文档。
一个视图只能有一个文档。在视图中,调用CView::GetDocument可以获得一个指向视图的文档的指针。函数原形如下:
CDocument*GetDocument()const; |
如果该视图不与任何文档相对应,则返回NULL。
MDI框架窗口通过调用CFrameWnd::GetActiveDocument 可以获得与当前活动的视图相连的CDocument 指针。函数原形如下:
virtual CDocument*GetActiveDocument(); |
通过调用CFrameWnd::GetActiveView 可以获得指向与CFrameWnd框架窗口连接的活动视图的指针,如果是被CMDIFrameWnd框架窗口调用,则返回NULL。MDI框架窗口可以首先调用MDIGetActive找到活动的MDI子窗口,然后找到该子窗口的活动视图。函数原形如下:
virtual Cdocument*GetActiveDocument();
|
MDI框架窗口通过调用CFrameWnd::GetActiveFrame, 可以获得一个指向MDI框架窗口的活动多文档界面子窗口的指针。
CMDIChildWnd调用GetMDIFrame获得MDI框架窗口(CMDIFrameWnd)。
CWinApp 调用AfxGetMainWnd得到指向应用程序的活动主窗口的指针。
看下面一段代码,就是利用CDocTemplate、CDocument和CView之间的存取关系,遍历整个文档模板、文档及视图。
CMyApp*pMyApp = (CMyApp*)AfxGetApp(); POSITION p = pMyApp->GetFirstDocTemplatePosition(); while(p!= NULL) { CDocTemplate*pDocTemplate = pMyApp->GetNextDocTemplate(p); POSITION p1 = pDocTemplate->GetFirstDocPosition(); while(p1!= NULL) { CDocument*pDocument = pDocTemplate->GetNextDoc(p1); POSITION p2 = pDocument->GetFirstViewPosition(); while(p2!= NULL) { CView*pView = pDocument->GetNextView(p2); } } } CMyApp*pMyApp = (CMyApp*)AfxGetApp(); POSITION p = pMyApp->GetFirstDocTemplatePosition(); while(p!= NULL) { CDocTemplate*pDocTemplate = pMyApp->GetNextDocTemplate(p); POSITION p1 = pDocTemplate->GetFirstDocPosition(); while(p1!= NULL) { CDocument*pDocument = pDocTemplate->GetNextDoc(p1); POSITION p2 = pDocument->GetFirstViewPosition(); while(p2!= NULL) { CView*pView = pDocument->GetNextView(p2); } } } |
在应用程序的任何地方,程序员都可以调用AfxGetApp()获得应用程序的对象指针。
2.文档与视图相互作用
文档与视图的相互作用,使程序中对数据的编辑与存储得以实现。虽然文档与视图之间有着复杂的相互作用过程,但我们并不需要去完全了解。文档与视图将数据与显示分离的结构,使得文档程序设计变得简单易行。
虽然MFC中并不强制使用文档/视图结构,大多数的MFC特性也都可以在非文档/视图中得以支持,但文档/视图结构使得程序设计更为方便有效。所有的文档类都派生于CDocument,所有的视图类都派生于CView,CDocument提供了用户自定义文档的基本功能,而CView则提供了用户自定义视图的基本框架。在使用AppWizard创建基于SDI或MDI的程序时,默认情况下,系统将使用CDocument类和CView类派生出程序中的文档与视图类。
3.单文档(SDI)与多文档(MDI)应用程序
在单文档界面程序中,用户在同一时刻只能操作一个文档。像Windows下的记事本(NotePad)程序就是这样的例子,如图4-1所示。在这些应用程序中,打开文档时会自动关闭当前打开的活动文档,若文档修改后尚未保存,会提示是否保存所做的修改。因为一次只开一个窗口,因此不像Visual C++(www.cppentry.com)集成开发环境那样需要一个窗口菜单。单文档应用程序一般都提供一个文件(File)菜单,在该菜单下有一组命令,用于新建文档(New)、打开已有文档(Open)、保存或重命名存盘文档等。这类程序相对比较简单,常见的应用程序有终端仿真程序和一些工具程序。

图4-1 单文档程序(NotePad)
多文档界面应用程序也能操作文档,但它允许同时操作多个文档。Visual C++(www.cppentry.com)集成开发环境就是这样的例子,其界面如图4-2所示。在界面中可以同时打开多个文件(同时也就为每个文件打开一个窗口),可以通过切换活动窗口激活相应的文档进行编辑。多文档应用程序也提供一个File菜单,用于新建、打开、保存文档。与单文档应用程序不同的是,它往往还提供提供一个Close(关闭)菜单项,用于关闭当前打开的文档。多文档应用程序还提供一个窗口菜单,管理所有打开的子窗口,包括对子窗口的新建、关闭、层叠和平铺等。关闭一个窗口时,窗口内的文档也被自动关闭。

图4-2 多文档程序(Visual C++(www.cppentry.com) IDE)
4.使用文档/视图结构的意义
文档/视图结构的提出大大简化了多数应用程序的设计开发过程。文档/视图结构带来的好处如下。
(1)首先是将数据操作和数据显示、用户界面分离开。这是一种“分而治之”的思想,这种思想使得模块划分更加合理、模块独立性更强,同时也简化了数据操作和数据显示、用户界面工作。文档只负责数据管理,不涉及用户界面;视图只负责数据输出与用户界面的交互,可以不考虑应用程序的数据是如何组织的,甚至当文档中的数据结构发生变化时也不必改动视图的代码。
(2)MFC在文档/视图结构上提供了许多标准操作界面,包括新建文件、打开文件、保存文件、打印等,减轻了用户的工作量。用户不必再书写这些重复的代码,从而可以把更多的精力放到完成应用程序特定功能的代码上:主要是从数据源中读取数据和显示。
(3)支持打印预览和电子邮件发送功能。用户无须编写代码或只需要编写很少的代码,就可以为应用程序提供打印预览功能。同样的功能如果需要自己写的话,需要数千行的代码。另外,MFC支持在文档/视图结构中以电子邮件形式直接发送当前文档的功能,当然本地要有支持MAPI(微软电子邮件接口)的应用程序,如Microsoft Exchange。可以这样理解:MFC已经把微软开发人员的智慧和技术溶入到了你自己的应用程序中。
由于文档/视图结构功能如此强大,因此一般我们都首先使用AppWizard生成基于文档/视图结构的单文档或多文档框架程序,然后在其中添加自己的特殊代码,完成应用程序的特定功能。但是,并非所有基于窗口的应用程序都要使用文档/视图结构。像Visual C++(www.cppentry.com)随带的例子Hello、MDI都没有使用文档/视图结构。以下两种情况不宜采用文档/视图结构。
(1)不是面向数据的应用或数据量很少的应用程序,不宜采用文档/视图结构。如一些工具程序包括磁盘扫描程序、时钟程序,还有一些过程控制程序等。
(2)不使用标准的窗口用户界面的程序,像一些游戏等。
【责任编辑:
杨硕 TEL:(010)68476636-8001】