1、1第 12 章 文档与视图MFC 提供了将应用程序的数据与显示分开的文档-视图结构,它为应用程序提供了统一的框架,参见图 12-1 和图 12-2。MFC 的文档-视图结构是 MVC 模式的一种部分实现。创建创建创建应用程序对象指向:文档模板指向:创建文档对象指向:框架窗口指向:视图对象指向:图 12-1 SDI 应用程序中的对象 图 12-2 文档-视图结构中各种对象及其创建与关系12.1 MVCMVC 是一种软件架构模式,通过分解程序的不同功能,达到降低程序设计的复杂度、利于程序员专业分工、简化程序的维护和扩展的目的。MVC 是 1979 年挪威计算机科学家Trygve Reenskaug
2、 在 Xerox(施乐公司)PARC(Palo Alto Research Center,帕洛阿尔托研究中心)工作时,为(历史上第二个面向对象程序设计语言和第一个真正的集成开发环境)SmallTalk 提出的。12.1.1 概念MVC(Model-View-Controller,模型视图控制器)是一种软件架构模式,它把软件系统分为如下三个基本部分(参见图 12-3):模型(Model) 数据(库) 。视图(View) 图形界面(表示) 。2控制器(Controller) 程序功能(算法) 。图 12-3 MVC 模式的关系图MVC 模式的目的是实现一种动态的程序设计,简化对程序的后续修改和扩展
3、,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部份分离的同时,也赋予了各个基本部分应有的功能,专业人员可以通过自身的专长进行分组合作:数据库专家利用“模型”进行数据管理和数据库设计、界面设计人员利用“视图”进行图形界面设计、而程序员则利用“控制器”编写程序应有的功能(实现算法等等) 。12.1.2 层次模型(Model,数据模型)用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。 “模型”有对数据直接访问的权利,例如对数据库的访问。 “模型”不依赖“视图”和“控制器” ,也就是说,模型不关心它会被如何显示或是如何
4、被操作。但是模型中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此模型的视图必须事先在此模型上注册,从而,视图可以了解在数据模型上发生的改变。 (比较:观察者模式(软件设计模式) )视图(View) 视图层能够实现数据有目的的显示(理论上,这不是必需的) 。在视图中一般没有程序上的逻辑。为了实现视图上的刷新功能,视图需要访问它监视的数据模型(Model ) ,因此应该事先在被它监视的数据那里注册。控制器(Controller) 控制器起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。 “事件”包括用户的行为和数据模型上的改变。312.1.3 实现S
5、mallTalk1980 年 MVC 最早被应用于 Xerox PARC 的面向对象、动态类型、自反射的编程语言SmallTalk-80 环境中,运行在首个具有 GUI 的原型计算机 Alto(男高音)上。MacApp1985 年 Apple 将 MVC 用于其推出的 Mac OS 系统的面向对象的应用程序框架 MacApp中,这是 MVC 首次在商用产品中的实际应用。MFC1993 年 2 月微软在其随 Visual C+ 1.0 推出的 MFC 2.0 中,引入了文档-视图(Document / View)架构,它也是一种对于 MVC 的早期部分实现。MFC 将程序分成视图(View)和文
6、档(Document )两大类,其中的 CDocument 对应 MVC 中的数据模型(Model ) ,CView 相当于 MVC 中的视图+ 控制(View Controller) ,再加上应用程序类CWinApp,合成三大项。但是 MFC 基本上是一个失败的 MVC 作品。由于 MFC 对 Document/View 的定义过于模糊,未将 Controller(消息映射)部份取出,因此 Controller 既可置入 View 也可置入Document,但不管置入哪一方面,都会与 View 或 Document 绑死而缺乏弹性。Java EEStruts1999 年 12 月 Sun 推
7、出的 Java EE(Java Enterprise Edition,Java 企业版,原来叫J2EE)和其他的各种框架不一样,它为模型对象(Model Objects)定义了一个规范。典型例子是由 Craig McClanahan 于 2000 年 5 月所开发的开源 Java EE 轻型 Web 应用框架 Apache Struts。 视图(View) 在 JAVA EE 应用程序中,视图(View)可能由 JSP(Java Server Page,爪哇服务器网页)承担。生成视图的代码则可能是 Servlet 的一部分,特别是在客户端服务端交互的时候。 控制器(Controller) JA
8、VA EE 应用中,控制器可能是一个 Servlet。 模型(Model) 模型则是由一个实体 Bean 来实现。 .NETWindows Forms2002 年 2 月微软所推出的.NET 框架中还包含有 WinForms(视窗窗体) ,这个针对视图(View)和控制器(Controller)的模式已被很好地定义,而模型( Model)则留给开发4者去设计。 视图(View) 由 Form 或者 Control 类继承来的一个类处理视图的职责。在 WinForm 这个例子中视图和控制器被编译在同一个类中,这个和ASP.NET 不同。 控制器(Controller) 控制器的职责被分割成三部分
9、。事件( Event)的产生和传输是操作系统的一部分。在.Net 框架中 Form 和 Control 类将不同的事件转发给相应的事件处理器。而事件的处理则在分离的代码中实现。 模型(Model) 就像 ASP.NET 一样,WinForm 不严格需要一个模型。开发者可以自行选择创建一个模型类,但是很多人选择放弃这一步,直接把事件处理放在控制器里处理任何计算、数据保存等等。也就是说用模型来包含商业逻辑和数据访问。.NETASP.NET2002 年 2 月微软随.NET 推出了 ASP.NET,针对视图(View)和控制器(Controller)的模式并没有被很好地定义,模型(Model)也留给
10、开发者去设计。 视图(View)ASPX 和 ASCX 文件被用来处理视图的职责。在这个设计中视图实际上是从控制器继承而来。这个和 Smalltalk 的实施有所不同,在Smalltalk 中不同的类都有指针互相指向对方。 控制器(Controller) 控制器的职责被分割成两部分。事件( Event)的产生和传输是框架的一部分,更明确的说是 Page 和 Control 两个类。而事件的处理则在分离的代码中实现。 模型(Model) ASP.NET 不严格需要一个模型。开发者可以自行选择创建一个模型类,但是很多人选择放弃这一步,直接把事件处理放在控制器里处理任何计算、数据保存等等。但用模型来
11、包含商业逻辑和数据访问是可实现的。 2009 年 4 月 9 日微软推出了 ASP.NET MVC 1.0,它在 ASP.NET 3.5 运行库之上提供了一个新的 MVC 架构。此架构为 Web 应用程序文件夹( folder)结构定义了一个特别模式,并提供了一个控制器基类来处理“动作(actions) ”请求。2010 年 3 月 10 日微软推出了 ASP.NET MVC 2.0,可用于.NET 3.5 SP1 和 Visual Studio 2008 及其 SP1,并将集成进.NET 4.0 和 Visual Studio 2010 中。 512.2 文档-视图体系文档(document
12、)对应于用户的数据(可以是文本、数值、图像、声音、视频等) ,它可以从磁盘文件中读入,也可写入磁盘文件,用户还可以创建、修改和管理这些数据。文档对应的 MFC 类为 CDocument。视图(view)是一种窗口对象,对应于框架窗口的客户区,它负责在屏幕和打印机上显示和输出数据,为用户提供观察、选择、编辑文档数据的交互界面(参见图 12-4) 。视图对应的MFC 类为 CView。文档-视图结构有两种主要的方式:SDI(Single Document Interface,单文档界面)和 MDI(Multiple Document Interface,多文档界面) 。从 MFC 7.0 起新增加
13、了一种 MTDI(Multiple Top-level Document Interface,多顶级文档界面) ,参见图 11-5。SDI 传统 MDI 选项卡式 MDI MTDI图 11-5 不同的文档界面SDI 应用程序只有一个框架窗口(类)和一个视图窗口(对应于框架窗口的客户区) ,且只有一个文档类,每次只能打开一个文档。这里的文档和视图一般是一一对应的。例如 Windows 中的记事本、写字板和画图等软件,是典型的 SDI 应用程序。MDI 应用程序有一个主框架窗口(类) ,可有任意多个子框架窗口和对应的视图客户区窗口,也可有多个文档类,可以同时打开多个文档/窗口。这里,每个视图对应于
14、一个文档,而每个文档则可对应于多个视图,参见图 12-6。例如 Word 2000 和 IE 8,分别是传统和选卡式 MDI 应用程序。MTDI 类似于 MDI,只是 MDI 中的每个文档视图窗口都是主框架窗口的子窗口(只能图 12-4 文档与视图图 12-6 一个文档可对应多个视图6位于主框架窗口的客户区内) 。而 MTDI 的文档视图窗口都是顶层窗口,位于主框架窗口之外。例如新版 Word 和老版 IE,就是典型的 MTDI 应用程序。应用程序的文档-视图结构种类,可以在创建 MFC 应用程序时,在“MFC 应用程序向导”对话框的“应用程序类型”页中设置(默认为 MDI) ,在该对话框页中
15、还可以选择是否具有“文档/视图结构支持 ”(默认是选中的) ,参见图 12-7。图 12-7 MFC 应用程序向导中的“文档/ 视图结构支持 ”选项12.2.1 文档模板类文档、框架窗口与视图通过文档模板联系在一起,MFC 的文档模板类为CDocTemplate。对 SDI 与 MDI,它有两个对应的派生类 CSingleDocTemplate 与CMultiDocTemplate,在 MFC 功能包中又增加了多文档模板的扩展类CMultiDocTemplateEx,参见图 12-8。图 12-8 文档模板类的层次结构它们的构造函数的参数都一样:CSingle|MultiDocTemplate
16、Ex ( / 文档模板构造函数7UINT nIDResource, / 文档类型的资源 IDCRuntimeClass* pDocClass, / 派生文档类对象的指针CRuntimeClass* pFrameClass, / 派生 框架窗口类对象的指针CRuntimeClass* pViewClass / 派生视图类对象的指针);CWinApp 类创建文档模板的操作分两步进行,首先用文档模板类的构造函数创建一个SDI 或 MDI 文档模板的实例,然后调用 CWinApp 类的成员函数 AddDocument 将该模板添加到应用程序的模板列表中。创建文档模板的操作,一般在派生应用程序类的 In
17、itInstance成员函数中完成。例如 (SDI) :BOOL CDrawApp:InitInstance() / 注册应用程序的文档模板。文档模板/ 将用作文档、框架窗口和视图之间的连接CSingleDocTemplate* pDocTemplate;pDocTemplate = new CSingleDocTemplate(IDR_MAINFRAME,RUNTIME_CLASS(CDrawDoc),RUNTIME_CLASS(CMainFrame), / 自定义 MDI 子框架RUNTIME_CLASS(CDrawView);if (!pDocTemplate) return FALSE
18、;AddDocTemplate(pDocTemplate);其中,RUNTIME_CLASS 宏返回一个指向 CRunTimeClass 类的指针:CRuntimeClass* RUNTIME_CLASS( class_name )又例如 (MDI) :BOOL CImageApp:InitInstance() / 注册应用程序的文档模板。文档模板8/ 将用作文档、框架窗口和视图之间的连接CMultiDocTemplate* pDocTemplate;pDocTemplate = new CMultiDocTemplate(IDR_BMPTYPE,RUNTIME_CLASS(CImageDoc
19、),RUNTIME_CLASS(CChildFrame), / 自定义 MDI 子框架RUNTIME_CLASS(CImageView);if (!pDocTemplate) return FALSE;AddDocTemplate(pDocTemplate);pDocTemplate = new CMultiDocTemplate(IDR_GIFTYPE,RUNTIME_CLASS(CImageDoc),RUNTIME_CLASS(CChildFrame), / 自定义 MDI 子框架RUNTIME_CLASS(CImageView);if (!pDocTemplate) return FAL
20、SE;AddDocTemplate(pDocTemplate);可见一个 MDI 应用程序可有多个 MDI 模板,每个 MDI 模板在运行时又可有多个实例,对应于同一模板中同一文档类型/视图类型的多个文档对象 /视图窗口,参见图 12-9。应用程序对象CMyApp文档模板 A 文档模板 BCMultiDocTemplate CMultiDocTemplate文档 1 文档 2 文档 3 文档 1CMyDocA CMyDocA CMyDocA CMyDocB打开文档一个类的实例 另一个类的实例图 12-9 具有两个文档类型的 MDI 应用程序912.2.2 文档类所有用户的文档类都是从文档基类
21、CDocument 派生的,参见图 12-10。图 12-10 文档类的层次结构CDocument 类的常用成员函数有(其中粗体表示最常用的): GetFirstViewPosition获得视图列表中与本文档关联的第一个视图的位置,该位置可用于 GetNextView 函数,原型为:virtual POSITION GetFirstViewPosition( ) const; GetNextView返回 rPosition 所指的视图的指针,获得下一个本文档关联的视图的位置到 rPosition 中,原型为:virtual CView* GetNextView( POSITION使用 GetF
22、irstViewPosition 与 GetNextView 可遍历文档的所有视图。 GetTitle返回文档(窗口)的标题,一般为相关联的文件名,原型为:const CString SetTitle设置文档(窗口 )的标题,原型为:virtual void SetTitle( LPCTSTR lpszTitle ); GetPathName返回与文档相关联的文件路径串,无关联文件时返回 NULL,原型为:const CString SetPathName设置存取文档的默认路径(与文档窗口的标题) ,若bAddToMRU = TRUE,则将该路径添加到最近使用 (most recently u
23、sed,MRU) 文件的列表,原型为:virtual void SetPathName( LPCTSTR lpszPathName, BOOL bAddToMRU = TRUE ); IsModified判断文档在最后一次存储后是否被修改过。若被修改过,则在用户关闭文档窗口或应用程序时,会提示保存文件,原型为:BOOL IsModified( ); 10 SetModifiedFlag设置文档在最后一次存储后是否被修改过,原型为:void SetModifiedFlag( BOOL bModified = TRUE ); UpdateAllViews在用户通过视图 pSender 修改了文档数
24、据后,应调用该函数通知所有与文档相关联的其他视图窗口。若 pSender = NULL,则通知与文档相关联的所有视图窗口,该函数会调用每个视图类的 OnUpdate 成员函数,一般是在调用 SetModifiedFlag 后调用该函数。原型为:void UpdateAllViews( CView* pSender, LPARAM lHint = 0L, CObject* pHint = NULL ); Serialize默认时,派生的文档类会覆盖根类 CObject 的序列化成员函数Serialize 以支持文档的读写,原型为:virtual void Serialize( CArchive例
25、如:void CTestDoc:Serialize(CArchive LoadStdProfileSettings在派生应用程序类 C*App 的 InitInstance 成员函数中,默认会调用该成员函数来支持 MRU 文件列表功能,原型为: void LoadStdProfileSettings( UINT nMaxMRU = _AFX_MRU_COUNT );其中,_AFX_MRU_COUNT = 4,nMaxMRU 可取的最大值为_AFX_MRU_MAX_ COUNT = 16,若 nMaxMRU =0,则不支持 MRU。例如:LoadStdProfileSettings(4); / 加载标准 INI 文件选项 (包括 MRU)LoadStdProfileSettings(10); / 自己设置也可调用应用程序类的成员函数virtual void AddToRecentFileList( LPCTSTR lpszPathName );