第七课 文档/视结构
这一讲介绍文档/视结构的基本概念,并结合一个简单的文本编辑器的例子说明文档视结构的内部运行机制和使用。
文档/视图概念
文档视结构程序实例
让文档视结构程序支持卷滚
定制串行化
不使用串行化的文档视结构程序
小 结
7.1文档/视图概念
7.1.1概念
在文档视结构里,文档是一个应用程序数据基本元素的集合,它构成应用程序所使用的数据单元;另外它还提供了管理和维护数据的手段。
文档是一种数据源,数据源有很多种,最常见的是磁盘文件,但它不必是一个磁盘文件,文档的数据源也可以来自串行口、网络或摄像机输入信号等。在第十二章“多线程和串行通信编程”中,我们展示了如何使用串行口作为数据输入的文档/视结构程序。文档对象负责来自所有数据源的数据的管理。
视图是数据的用户窗口,为用户提供了文档的可视的数据显示,它把文档的部分或全部内容在窗口中显示出来。视图还给用户提供了一个与文档中的数据交互的界面,它把用户的输入转化为对文档中数据的操作。每个文档都会有一个或多个视图显示,一个文档可以有多个不同的视图。比如,在Excel电子表格中,我们可以将数据以表格方式显示,也可以将数据以图表方式显示。一个视图既可以输出到窗口中,也可以输出到打印机上。
图7-1说明了文档及其视图之间的关系。
图 7-1 文档和视图
MFC的文档/视结构机制把数据同它的显示以及用户对数据的操作分离开来。所有对数据的修改由文档对象来完成。视图调用这个对象的方法来访问和更新数据。
7.1.2两类文档视结构程序
有两种类型的文档视结构程序:单文档界面(SDI)应用程序和多文档界面(MDI)应用程序。
在单文档界面程序中,用户在同一时刻只能操作一个文档。象Windows95下的NotePad记事本程序(如图7-2所示)就是这样的例子。在这些应用程序中,打开文档时会自动关闭当前打开的活动文档,若文档修改后尚未保存,会提示是否保存所做的修改。因为一次只开一个窗口,因此不象WORD那样需要一个窗口菜单。单文档应用程序一般都提供一个File菜单,在该菜单下有一组命令,用于新建文档(New)、打开已有文档(Open)、保存或换名存盘文档等。这类程序相对比较简单,常见的应用程序有终端仿真程序和一些工具程序。
图7-2 单文档程序(记事本)
一个多文档界面应用程序也能操作文档,但它允许同时操作多个文档。如图7-2,Microsoft Word就是这样的例子。你可以打开多个文件(同时也就为每个文件打开一个窗口),可以通过切换活动窗口激活相应的文档进行编辑。多文档应用程序也提供一个File菜单,用于新建、打开、保存文档。与单文档应用程序不同的是,它往往还提供提供一个Close(关闭)菜单项,用于关闭当前打开的文档。多文档应用程序还提供一个窗口菜单,管理所有打开的子窗口,包括对子窗口的新建、关闭、层叠、平铺等。关闭一个窗口时,窗口内的文档也被自动关闭。在这一章里,我们只讨论单文档界面应用程序的编制,有关多文档技术在下一章里再做讨论。
图7-3 多文档程序(Microsoft Word)
7.1.3使用文档/视结构的意义
文档视结构的提出对于广大程序员来说是一个福音,它大大简化了多数应用程序的设计开发过程。文档视结构带来的好处主要有:
a. 首先是将数据操作和数据显示、用户界面分离开。这是一种“分而治之”的思想,这种思想使得模块划分更加合理、模块独立性更强,同时也简化了数据操作和数据显示、用户界面工作。文档只负责数据管理,不涉及用户界面;视图只负责数据输出与用户界面的交互,可以不考虑应用程序的数据是如何组织的,甚至当文档中的数据结构发生变化时也不必改动视图的代码。
b.MFC在文档/视结构上提供了许多标准操作界面,包括新建文件、打开文件、保存文件、打印等,减轻了用户的工作量。用户不必再书写这些重复的代码,从而可以把更多的精力放到完成应用程序特定功能的代码上:主要是从数据源中读取数据和显示。
c. 支持打印预览和电子邮件发送功能。用户无需编写代码或只需要编写很少的代码,就可以为应用程序提供打印预览功能。同样的功能如果需要自己写的话,需要数千行的代码。另外,MFC支持在文档视结构中以电子邮件形式直接发送当前文档的功能,当然本地要有支持MAPI(微软电子邮件接口)的应用程序,如Microsoft Exchange。可以这样理解:MFC已经把微软开发人员的智慧和技术溶入到了你自己的应用程序中。
由于文档视结构功能如此强大,因此一般我们都首先使用AppWizard生成基于文档/视结构的单文档或多文档框架程序,然后在其中添加自己的特殊代码,完成应用程序的特定功能。但是,并非所有基于窗口的应用程序都要使用文档/视结构。象Visual C++随带的例子Hello、MDI都没有使用文档/视结构。有两种情况不宜采用文档、视结构:
(1)不是面向数据的应用或数据量很少的应用程序,不宜采用文档/视结构。如一些工具程序包括磁盘扫描程序、时钟程序,还有一些过程控制程序等。
(2)不使用标准的窗口用户界面的程序,象一些游戏等。
7.2文档视结构程序实例
下面,我们以一个简单的文本编辑器为例,说明文档/视结构的原理及应用。由于我们重在讨论文档/视结构而不是编辑器的实现,因此这个编辑器设计的非常简单:用户只能逐行输入字符,以回车结束一行并换行,不支持字符的删除和插入,也没有光标指示当前编辑位置。另外,用户可以选择编辑器显示文本时所使用的字体。
图7-4
首先,使用AppWizard生成编辑器程序的框架:在New对话框的Project Name编辑框中输入项目名为Editor。在AppWizard的第一步选择Single document ,这将创建一个SDI应用程序。AppWizard第二和第三步选项使用缺省值。在AppWizard Step 4 of 6对话框中,如图7-4所示,细心的读者或许会注意到在这一页里,有一个Advanced按钮,以前没有提到过。现在揿击该按钮,弹出Advanced Option对话框,如图7-5所示。Advanced Option对话框是用来设置文档视结构和主框架窗口的一些属性的。
图7-5
该对话框提供两个标签页,一页是Document Template String(文档模板字符串,有关文档模板字符串,我们还将在后面作详细介绍),用于设置文档视结构的一些属性。它包括以下几个编辑框:
File Extension:指定应用程序创建的文档所用的文件名后缀。输入后缀名txt(不需要·号。),表明Editor使用文本文件的后缀名TXT。
File ID:用于在Windows95的注册数据库中标识应用程序的文档类型。
MainFrame Caption:主框架窗口使用得标题,缺省情况下与项目名相一致,你当然可以将它改为任何你喜欢的名字,如Editor for Windows等。
Doc Type name:文档类型名,指定与一个从CDocument派生的文档类相关的文档类型名。
Filter Name:用作“打开文件”、“保存文件”对话框中的过滤器。当你在File Extension中输入后缀名是,Visual Studio会自动给你生成一个过滤器:Editor Files(*.txt)。这样,当你在Open File对话框中选择Editor Files(*.txt)时,只有以txt为后缀名的文件名显示在文件名列表中。
File new name(short name):用于指定在new对话框中使用的文档名。当应用程序支持多种文档类型时,选择File-New菜单项会弹出一个对话框,列出应用程序所支持的所有文档类型,供用户选择。选择一种文档类型后,自动创建相应类型的文档。这里我们只支持编辑器这一种文档类型,故使用缺省值。
File Type name(long name):用于指定当应用程序作为OLE Automation服务器时使用的文档类型名。使用缺省值。
另一页是Window Styles,用于设置主框架窗口的一些属性,包括框架窗口是否使用最大化按钮、最小化按钮,窗口启动时是否最大化或最小化等。这里我们使用缺省值,不需要作任何修改。
按OK按钮,关闭Advanced Option对话框。
AppWizard后面的几页对话框都使用缺省值。创建完Editor框架程序后,Visual Studio自动打开Editor工程。现在要修改Editor框架程序,往程序中添加代码,实现编辑器功能。
7.2.1 文档/视结构中的主要类
在Editor框架程序中,与文档视结构相关的类有CEditorApp、CMainFrame、CEditorView和CEditorDoc,它们分别是应用程序类CWinApp、框架窗口类CFrameWnd、视图类CView和文档类CDocument的派生类。
应用程序对象
其中,应用程序类负责一个且唯一的一个应用程序对象的创建、初始化、运行和退出清理过程。如果在AppWizard生成框架时指定使用单文档或多文档,AppWizard会自动将File菜单下的New、Open和Printer Setup(打印机设置)自动映射到CWinApp的OnFileNew、OnFileOpen、OnFilePrintSetup成员函数,让CWinApp来处理以上这些消息。如清单7.1,浏览CEditorApp类的定义文件有关消息映射的代码。
清单7.1 CEditorApp的消息映射
BEGIN_MESSAGE_MAP(CEditorApp, CWinApp)
//{{AFX_MSG_MAP(CEditorApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
// NOTE - the ClassWizard will add and remove mapping macros here.
// DO NOT EDIT what you see in these blocks of generated code!
//}}AFX_MSG_MAP
// Standard file based document commands
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
// Standard print setup command
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()
这表明,框架已经给我们生成了有关新建文档、打开文档以及打印设置的标准代码,我们不必再去做这些重复的工作了。那么,当我们新建或打开一个文档时,应用程序怎么知道要创建什么样的文档以及创建什么样的视图、框架窗口来显示该文档的呢?在文档/视结构中,应用程序通过为应用程序所支持的每一种文档创建一个文档模板,来创建和管理所有的文档类型并为它们生成相应的视图和框架窗口。
文档模板
文档模板负责创建文档、视图和框架窗口。一个应用程序对象可以管理一个或多个文档模板,每个文档模板用于创建和管理一个或多个同种类型的文档(这取决于应用程序是单文档SDI程序还是多文档MDI程序)。那些支持多种文档类型(如电子表格和文本)的应用程序,有多种文档模板对象。应用程序中的每一种文档,都必需有一种文档模板和它相对应。比如,如果应用程序既支持绘图又支持文本编辑,就需要一种一种绘图文档模板和文本编辑模板。在下一章里,我们举了一个这样的例子,来说明多种文档模板的实现技术。
MFC提供了一个文档模板类CDocTemplate支持文档模板。文档模板类是一个抽象的基类,它定义了文档模板的基本处理函数接口。由于它是一个抽象基类,因此不能直接用它来定义对象而必需用它的派生类。对一个单文档界面程序,使用CSingleDocTemplate(单文档模板类),而对于一个多文档界面程序,使用CMultipleDocTemplate。
文档模板定义了文档、视图和框架窗口这三个类的关系。通过文档模板,我们可以知道在创建或打开一个文档时,需要用什么样的视图、框架窗口来显示它。这是因为文档模板保存了文档和对应的视图和框架窗口的CRuntimeClass对象的指针。此外,文档模板还保存了所支持的全部文档类的信息,包括这些文档的文件扩展名信息、文档在框架窗口中的名字、代表文档的图标等信息。
提示:每个从CObject派生的类都与一个CRuntimeClass结构相关联。通过这个结构,你可以在程序运行时刻获得关于一个对象和它的基类的信息。在函数参数需要作附加类型检查时,这种运行时刻判别对象类型的能力是非常重要的。C++本身并不支持运行时刻类信息。CRuntimeClass结构包含一个以/0结尾的字符串类名、整型的该类对象大小、基类的运行时刻信息等。 一般在应用程序的InitInstance成员函数实现中创建一个或多个文档模板,如清单7.2。
清单7.2 CEditorApp的InitInstance成员函数定义
BOOL CEditorApp::InitInstance()
{
//标准的初始化代码
//......
// Register the application's document templates. Document templates
// serve as the connection between documents, frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CEditorDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CEditorView));
AddDocTemplate(pDocTemplate);
//其他的初始化代码和主框架窗口显示过程
//......
// Enable DDE Execute open
EnableShellOpen();
RegisterShellFileTypes(TRUE);
// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized, so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
// Enable drag/drop open
m_pMainWnd->DragAcceptFiles();
}
在InitInstance中,首先声明一个CSingleDocTemplate*类型的单文档模板对象指针(因为这里的文本编辑器使用单文档界面)。然后创建该类型的模板对象。如果要使用多文档界面,只需要将这里的CSingleDocTemplate改为CMultiDocTemplate,当然CMainFrame也要改为从CFrameWnd改为CMDIChildWnd或其派生类。
在CSingleDocTemplate构造函数中,还包含一个IDR_MAINFRAME参数。它指向一个字符串资源,这个字符串给出了文档所使用及显示时所要求的几个选项,包括文档名字、文档的文件扩展名、在框架窗口上显示的名字等等,我们称之为文档模板字符串。有关文档模板字符串还将在下一章使用多个文档模板这一节作详细阐述,因此这里就不展开讲了。
然后InitInstance调用AddDocTemplate将创建好的文档模板加入到应用程序可用的文档模板链表中去。这样,如果用户选择了File-New或File-Open菜单要求创建或打开一个文档时,应用程序类的OnNewDocument成员函数和OnOpenDocument()成员函数就可以从文档模板链表中检索出文档模板提示用户选择适当的文档类型并创建文档及相关的视图、框架窗口。
文档
Editor的文档类CEditorDoc从CDocument派生下来,它规定了应用程序所用的数据。如果需要在应用程序中提供OLE功能,则需要从COleDocument或其派生类派生出自己的文档类。
视图
Editor的视图类从CView派生,它是数据的用户窗口。视图规定了用户查看文档数据以及同数据交互的方式。有时一个文档可能需要多个视图。
如果文档需要卷滚,需要从CScrollView派生出视图类。如果希望视图按一个对话框模板资源来布置用户界面,可以从CFormView派生。由于CFormView经常同数据库打交道,因此我们把它放在第十章“数据库技术”中结合数据库技术讲解。感兴趣的读者可以先看看Visual C++ MFC例子CHKBOOK(在SAMPLES/MFC/GENERAL/CHKBOOK目录下)。
框架窗口
视图在文档框架窗口中显示,它是框架窗口的子窗口。框架窗口作用有二:一是为视图提供可视的边框,还包括标题条、一些标准的窗口组件(最大、最小化按钮、关闭按钮),象一个容器一样把视图装起来。二是响应标准的窗口消息,包括最大化、最小化、调整尺寸等。当框架窗口关闭时,在其中的视图也被自动删除。视图和框架窗口关系如图7-6所示:
图7-6 视图和框架窗口的关系
对于SDI程序,文档框架窗口也就是应用程序的主框架窗口。在MDI应用程序中,文档框架窗口是显示在主框架窗口中的子窗口(通常是CMDIChildWnd或其派生类)。
可以从主框架窗口类派生出新类来包含你的视图,并指定框架的风格和其他特征。如果是SDI程序,则从CFrameWnd派生出文档框架窗口:
class CMainFrame:public CFrameWnd
{
...
};
如果是MDI窗口,则需要从CMDIFrameWnd派生出主框架窗口,同时在从CMDIChildWnd或其派生类派生出一个新类,来定制特定文档窗口的属性和功能。
在应用程序运行过程中,以上几种类型的对象相互协作,来处理命令和消息。一个且唯一的一个应用程序对象管理一个或多个文档模板,每个文档模板创建和管理一个(SDI)或多个文档(MDI)。用户通过包含在框架窗口中的视图来浏览和操作文档中的数据。在SDI应用程序中,以上对象关系如图7-7所示。
图7-7 在SDI程序中各对象的关系
7.2.2 设计文本编辑器的文档类
弄清这些对象的关系以后,就可以着手往框架里填写代码,实现我们的文本编辑器程序了。从以上分析可以看出,文档视结构程序的主要工作在于文档和视图的设计。
首先设计文档。程序=数据+算法,在MFC文档/视结构中,最关键的就是文档的设计。怎样保存用户输入的文本行?方法之一是保存一组指针,每个指针指向一个文本行。如果使用C语言来写这个程序的话,需要分配内存来存放这些指针,还要自己编写文本行的动态分配、增加、删除等例程。但是MFC简化这些工作,它提供了集合类(collection classes)。
集合类是用来容纳和处理一组对象或标准数据类型变量的C++类。每个集合类对象可以看作一个单独的对象。类成员函数可作用于集合的所有元素。MFC提供两种类型的集合类:
基于模板的集合类
非基于模板的集合类
这两种集合类 对用户来说非常相似。基于模板的集合所包含的元素是用户自定义的数据结构或者说是抽象的数据结构,它以数组、链表和映射表三种方式组织用户自定义的数据结构。使用基于模板的集合类需要用户作一些类型转换工作。非基于 模板的集合类提供的是一组现成的、用于某种预定义的数据类型(如CObject、WORD、BYTE、DWORD、字符串等)的集合。在设计程序时,如果所用的数据类型是预定义的,如下面的编辑要用到的字符串,则使用非基于模板的集合类;如果所用得数据类型是用户自定义的数据结构类型,那就要用到基于模板的集合类。
根据对象在集合中的组织合存储方式,集合类又可分为三种类型:链表、数组、映射(或字典)。应当根据特定的编程问题,选择适当的类型。
链表:链表类用双向链表实现有序的、非索引的元素链表。链表有一个头或尾。很容易从头或尾增加或删除元素、遍历所有元素,在中间插入或删除元素。链表在需要增加、删除元素的场合效率很高。非基于模板的链表有三种:CObList、CPtrList、CStringList,分别用于管理对象指针、无类型指针和字符串。可以使用链表创建堆栈和队列。
要访问链表的成员,可以使用GetNext和GetHeadPosition()。
要删除链表的成员,可以用GetHeadPosition()和GetNext()来遍历链表,然后用delete删除其中的对象,最后调用RemoveAll删除链表所包含的指针。
数组类提供一个可动态调整数组大小的、有序的、按整数索引的对象数组。数组在内存中连续的存放固定长度的数组元素。数组的最大优点是可以随时存取任一元素。数组类包括基于模板的CArray,它可以存放任何类型的数据;MFC还为字节、字、双字、CString对象、CObject指针和无类型指针提供了预定义的类。数组的元素可以通过一个以零为基础的整数下标直接进行访问。下标操作符([])可用于设置或检取数组元素。如果要设置一个超过数组当前范围的元素,可以指定该数组是否自动增大。但是如果要调整数组大小时,则数组占用的内存块需要重新移动,效率很低。如果不要求调整数组大小,则对数组集合的访问和对标准C数组的访问一样快。在使用数组之前,应使用SetSize建立其大小,并分配内存。若不用SetSize,象数组添加元素时会导致频繁的再分配内存和拷贝数据。数组类适用于那些需要快速检索、很少需要增加或删除元素的集合。
数组通过GetAt(索引值)来访问数组中的成员。
要删除数组中的成员,可以用GetSize()取得大小,然后遍历数组中成员,用delete删除,然后调用RemoveAll()清除其中的指针数据。
下面是使用数组模板类的例子:
CArray
myArray; CMyClass myClass;
myArray->Add(myClass);
映射类以一种字典的方式组织数据。每个元素由一个关键字和一个数值项组成,关键字用作数值项的标识符,在集合中不允许重复,必须是唯一的。如果给出一个关键字,映射类会很快找到对应的数值项。映射查找是以哈希表的方式进行的,因此在映射中查找数值项的速度很快。除了映射类模板外,预定义的映射类能支持CString对象、字、CObject指针和无类型指针。比如,CMapWordToOb类创建一个映射表对象后,就可以用WORD类型的变量作为关键字来寻找对应的CObject指针。映射类最适用于需要根据关键字进行快速检索的场合。
要访问映射中的数据,可以用GetStartPosition()定位到开始处,再用GetNextAssoc访问映射表中的成员。
要删除映射中的数据,可以用GetStartPosition和GetNextAssoc遍历并用delete删除对象,然后调用RemoveAll。
下面是使用CMap模板类的例子:
CMap
myMap; CPerson person;
LPCSTR lpstrName=“Tom”;
myMap->SetAt(lpstrName,person);
有关集合类的使用可以参见MFC的例子COLLECT。
对于文本编辑器,由于需要动态增加和删除每一行字符串,因此使用CStringList来保存文本编辑器的数据,CStringList中的每一个元素是CString类型的,它代表一行字符。可以把CString看作一个字符数组,但它提供了丰富的成员函数,比字符数组功能强大的多。
另外,还需要增加一个数据成员nLineNum,用于指示当前编辑行行号。如清单7.3,在文档类的头文件EditorDoc.h中,加入以下代码:
清单7.3 CEditorDoc.h
class CEditorDoc : public CDocument
{
protected: // create from serialization only
CEditorDoc();
DECLARE_DYNCREATE(CEditorDoc)
// Attributes
public:
CStringList lines;
int nLineNum;
...
};
在定义了文档数据成员后,还要对文档数据成员进行初始化。
初始化文档类的数据成员
当用户启动应用程序,或从应用程序的File菜单种选择New选项时,都需要对文档类的数据成员进行初始化。一般的,类的数据成员的初始化都是在构造函数中完成的,在构造函数调用结束时对象才真正存在。但对于文档来说却不同,文档类的数据成员初始化工作是在OnNewDocument成员函数中完成的,此时文档对象已经存在。为什么呢?这是因为:在单文档界面(SDI)应用程序中,在应用程序启动时,文档对象就已经被创建。文档对象直到主框架窗口被关闭时才被销毁。在用户选择File-New菜单时,应用程序对象并不是销毁原来的文档对象然后重建新的文档对象,而只是重新初始化(Re-Initialization)文档对象的数据成员,这个初始化工作就是应用程序对象的OnFileNew()消息处理成员函数通过调用OnNewDocument()函数来完成的。试想,如果把初始化数据成员的工作放在构造函数中的话,由于对象已经存在,构造函数就无法被调用,也就无法完成初始化数据成员的工作。为了避免代码的重复,在应用程序启动时,应用程序对象也是通过调用OnNewDocument成员函数来初始化文档对象的数据成员的。如果是多文档界面(MDI)程序,则数据成员的初始化也可以放到构造函数中完成。因为在MDI中,选择File->New菜单时,应用程序对象就让文档模板创建一个新文档并创建对应的框架窗口和视图。但是,为了保证应用程序在单文档和多文档界面之间的可移植性,我们还是建议将文档数据成员的初始化工作放在OnNewDocument()中完成,因为在MDI的应用程序对象的OnFileNew成员函数中,同样会调用文档对象的OnNewDocument成员函数。
在OnNewDocument成员函数中手工加入代码,如清单7.4。
清单7.4 OnNewDocument成员函数
BOOL CEditorDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
nLineNum=0;
POSITION pos;
pos=lines.GetHeadPosition();
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
}
lines.RemoveAll();
return TRUE;
}
其中pos类型为POSITION,相当于链表的指针,指向链表当前元素。CStringList的成员函数GetHeadPosition()返回链表头指针。链表的GetNext()函数以当前指针为参数,返回下一个元素指针,同时修改pos,使它指向下一个元素。使用强制类型转换将GetNext()函数返回的元素指针转化为CString类型,然后调用CString::Empty()方法清除该行中的所有字符。通过一个while循环,清除所有文本行的数据。最后调用CStringList的RemoveAll()成员函数,清除链表中的所有指针(注意:此时这些指针指向的元素已经被清除)。
提示:应用程序对象的成员函数CWinApp::OnFileNew()在选择File菜单的New命令时被调用,缺省时在InitInstance()中也会被调用。原理是在InitInstance()中有一个命令行参数的执行过程,当命令行上没有参数时,函数ParseCommandLine(cmdInfo)会调用CCommandLineInfo :: 把m_nShellCommand成员置为CCommandLineInfo::FileNew,这导致ProcessShellCommand成员函数调用OnFileNew。用户可在InitInstance()中显式的调用OnFileNew()。
应用程序对象的OnFileNew消息处理流程如下:首先判断应用程序是否有多个文档模板,若是,则显示一个对话框让用户选择创建哪种类型的文档(模板)。对话框中显示的字符串是与文档模板对象的构造函数的第一个参数相对应的字符串(若资源中无相应字符串则不显示)。然后该函数调用CDocManager::OpenDocumentFile(NULL)成员函数,打开一个新文件。CDocManager::OpenDocumentFile函数调用了CSingleDocTemplate的OpenDocumentFile,后者完成实际的创建文档、框架、视图工作。文档模板的OpenDocumentFile首先判断文档是否已经被创建,若未创建,则创建一个新文档。然后根据文件名参数是否为空,分别调用CDocument的OnNewDocument( )和CDocument的OnOpenDocument()函数。CDocument的OnNewDocument首先调用DeleteContents(),并将文档修改标志该为FALSE(关闭窗口时将根据文档修改标志决定是否提示用户保存文档)。清理文档类的数据成员
在关闭应用程序删除文档对象时,或用File->Open菜单打开一个文档时,需要清理文档中的数据。同文档的初始化一样,文档的清理也不是在文档的析构函数中完成,而是在文档的CDocument::DeleteContents()成员函数中完成的(想想为什么?)。析构函数只用于清除那些在对象生存期都将存在的数据项。DeleteContents()成员函数的调用有两个作用:
1.删除文档的数据;
2确信一个文档在使用前为空。
前面已经说到,OnNewDocument函数会调用DeleteContents()函数。在用户选择File->Open菜单时,应用程序对象调用应用程序类的OnFileOpen成员函数,CWinApp::OnFileOpen调用内部的文档管理类CDocManager::OnFileOpen()成员函数,提示用户输入文件名。然后调用CWinApp::OpenDocumentFile打开一个文件。OpenDocumentFile在打开文件后首先调用DeleteContents成员函数清理文档中的数据,确保消除以前打开的文档的数据被清理掉。
缺省的DeleteContents函数什么也不做。你需要重载DeleteContents函数,并编写自己的文档清理代码。要重载DeleteContents成员函数:
从View菜单下选择ClassWizard,启动ClassWizard,选择Message Maps页。在ClassName下拉列表框中选择CEditorDoc,从ObjectIDs列表框选择CEditorDoc,在Message列表框双击DeleteContents。此时DeleteContents出现在Member functions列表框中,并被选中。点Edit Code按钮,开始编辑DeleteContents函数定义。在DeleteContents函数体中加入代码后,如清单7.5所示:
清单7.5 CEditorDoc的DeleteContents成员函数
void CEditorDoc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
nLineNum=0;
/*删除集合类的数据:
用GetHeadPosition和GetNext遍历并用delete删除其中的数据,然后调 用RemoveAll()删除链表所包含的指针
*/
POSITION pos;
pos=lines.GetHeadPosition();
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
//调用CString的Empty()方法清除文本行的数据,对于其它类型的对 //象,应当调用delete 删除该对象
}
lines.RemoveAll();
CDocument::DeleteContents();
}
编辑器的DeleteContents()实现与OnNewDocument()基本相同,别的程序则可能会有所不同。
CDocument::OnOpenDocument成员函数在调用DeleteContents()函数后,将文档修改标记设置为FALSE(未修改),然后调用Serialize进行文档的串行化工作。
读写文档——串行化
文档对象的串行化是指对象的持续性,即对象可以将其当前状态,由其成员变量的值表示,写入到永久性存储体(通常是指磁盘)中。下次则可以从永久性存储体中读取对象的状态,从而重建对象。这种对象的保存和恢复的过程称为串行化。对象的可持续性允许你将一个复杂的对象网络保存到永久性存储体中,从而在对象从内存中删去后仍保持它们的状态。以后,可以从永久性存储器中载入对象并在内存中重载。保存和载入可持续化、串行化的数据通过CArchive对象作为中介来完成。
文档的串行化在Serialize成员函数中进行。当用户选择File Save、Save As或Open命令时,都会自动执行这一成员函数。AppWizard只给出了一个Serialze()函数的框架,读者要做的时定制这个Serialize函数。Serialize()函数由一个简单的if-else语句组成:
void CEditorDoc::Serialze(CArchive& ar)
{
if(ar.IsStoring())
{
//TODO: add storing code here.
}
else
{
//TODO: add loading code here.
}
}
在框架中,Serialize函数的参数ar是一个CArchive类型对象,它包含一个CFile类型的文件指针(类似于C语言的文件指针),执行一个文件。CArchive对象为读写CFile(文件类)对象中的可串行化数据提供了一种类型安全的缓冲机制。通常CFile代表一个磁盘文件;但它也可以是一个内存文件(CMemFile对象)或剪贴板。一个给定的CArchive对象只能读数据或写数据,而不能同时读写数据。当保存数据到archive对象中时,archive把它放在一个缓冲区中。直到缓冲区满,才把数据写入它所包含的文件指针指向的CFile对象中。同样的,当从archive对象读数据时,archive对象从文件中读取内容到缓冲区,然后再从缓冲区读入到可串行化的对象中。这种缓冲机制减少了访问物理磁盘的次数,从而提高了应用程序的性能。
在创建和使用一个CArchive对象之前,必须先创建一个CFile文件类对象。而且还必须确保archive的载入和保存状态同文件打开模式相兼容。幸运的是,应用程序框架已经为我们做好了这些工作。
当应用程序响应File->Open、File-Save和File-Save As命令时,应用程序框架都会通过调用CDocument成员函数(对于File->Open调用OnOpenDocument,对于File->Save和File->Save As调用OnSaveDocument)创建CFile对象,并以适当的方式打开文件,对于File->Open是打开文件并读,对于Save和SaveAs是打开文件并写。然后框架会自动把文件对象连接到一个CArchive对象上,并设置CArchive的读写方式。
在Editor的Serialize()函数体内,我们看到CArchive对象有一个IsStoring()成员函数。该成员函数告诉串行化函数是需要写入还是读取串行数据。如果数据要写入(Save或Save As),IsStoring()返回布尔值TRUE;如果数据是被读取,则返回FALSE。
现在添加串行化操作代码,实现编辑器文档的读写功能。修改后的Serialize()函数形式如清单7.6。
清单7.6 CEditorDoc的串行化方法
/
// CEditorDoc serialization
void CEditorDoc::Serialize(CArchive& ar)
{
CString s("");
int nCount=0;
CString item("");
if (ar.IsStoring())
{
POSITION pos;
pos=lines.GetHeadPosition();
if(pos==NULL)
{
return;
}
while(pos!=NULL)
{
item=lines.GetNext(pos);
ar<
item.Empty();//clear the line buffer
}
}
else
{
// TODO: add loading code here
while(1)
{
try{
ar>>item;
lines.AddTail(item);
nCount++;
}
catch(CArchiveException *e)
{
if(e->m_cause!=CArchiveException::endOfFile)
{
TRACE0("Unknown exception loading file!/n");
throw;
}else
{
TRACE0("End of file reached.../n");
e->Delete();
}
break;
}
}
nLineNum=nCount;
}
}
在If子句中,从字符串链表中逐行读取字符串,然后通过调用CArchive对象的<<操作符,将文本行写入ar对象中。在else子句中,从CArchive对象逐一读入字符串对象,然后加入到链表中。由于在Serialize()函数的载入文档调用之前,框架已经调用CDocument的DeleteContents()成员函数作好了清理工作,这里不必再重复清理字符串链表。在载入字符串对象的同时,统计了字符串的个数即文本行数。由于这里使用CString的串行化,因此获得的文件不同于普通的文本文件。
文档串行化与一般文件处理方式最大的不同在于:在串行化中,对象本身对读和写负责。在上面的例子中,CArchive并不知道也不需要知道CString类的文本行内部数据结构,它只是调用CString类的串行化方法实现对象到文件的读写操作,也就是说,实际完成读写操作的是CString类,CArchive只是对象到CFile类的对象的一个中介。而文档的串行化正是通过调用文档中需要保存的各个对象的串行化方法来完成的。这几个对象的关系如图7-8所示。这里的对象必须是MFC对象,如果想让自己设计的对象也具有串行化能力,就必须定制该对象的串行化方法。有关定制串行化对象的技术在后面再作详细介绍。
图7-8 文档对象和文件对象
CArchive对象使用重载的插入(<<)和提取(>>)操作符执行读和写操作。有人会说,这种方式很象C++的输入输出流。其实,一个archive对象就是可以理解成一种二进制流。象输入/输出流一样,一个archive对象与一个文件相关联,并提供缓冲读写机制。但是,一个输入/输出流处理的是ASCII字符,而一个archive对象处理的是二进制对象。
如果不是使用框架创建和希望自己创建CArchive的话,可以这么做:
CFile file;//声明一个CFile类对象
file.Open(“c://readme.txt”,CFile::modeCreate|CFile::modeWrite);//打开文件
CArchive ar(&file,CArchive::store);//用指向file的指针创建CArchive类对
//象,指定模式为store即存储,如果需要从CArchive //中载入,可设为load
...//一些串行化工作
ar.Close();//首先关闭CArchive,然后关闭file
file.Close();
在文档中引用视图类
有时要在文档对象中访问视图对象,而一个文档可能会对应多个视图,此时可以采用如下方法:
POSITION pos=GetFirstViewPosition();//获取视图链表的头指针
CEditorView *MyView=(CMyView*)GetNextView(pos);
7.2.3 文本编辑器的视图类
视图类数据成员设计
现在设计文本编辑器的视图类。由于编辑器需要提供显示字体选择功能,因此在编辑器内增加一个数据成员代表当前所用的字体。另外,还需要两个变量lHeight和cWidth分别代表所用字体的高度和宽度,以便控制输出,因为Windows以图形方式输出,输出文本也需要程序员自己计算坐标。修改后的视图类如下面的片段所示:
class CEditorView : public CView
{
protected: // create from serialization only
CEditorView();
DECLARE_DYNCREATE(CEditorView)
CFont* pFont;
int lHeight;
int cWidth;
...
}
也许有人会问:既然文档类包含应用程序的数据,而视图只负责输出,为什么不把数据全部放在文档类之中呢?从应用程序角度来看,视图是不包含数据的,显示文档的所有数据都是从文档对象中读取的。但这并不意味着视图不能包含数据成员。视图是从CView派生出来的类,作为类,它当然可以包含数据成员。而且,为了显示输出的需要,它经常包含一些与显示相关的数据成员。设计文档视结构的关键就是确切的定义用户文档应当包含哪些信息。那么,如何合理分配文档和视图的数据成员呢?一条简单的原则是:如何使用更方便,就如何分配数据成员。另外,还要看该数据成员是否需要保存到文档中,如果要保存到文档中,就必须放在文档中。因为文档可以对应多个视图,如果放在视图中,由于不同的视图的数据成员可以有不同的数值,这样文档保存时就不知道该使用哪一个数值了。一般的,与显示相关的数据成员都可以放在视图类中。在上面的文本编辑器中,我们并不需要保存编辑器使用何种字体这一信息,而这一信息又与文档显示密切相关,因此把它放在视图类中是很恰当的。这样的话,还可以用多个使用不同字体的视图观察同一文档。但是,如果编辑器是一个类似于Microsoft WORD之类的字处理器,在显示中支持多种字体的同一屏幕输出,这时需要保存字体信息,就要把字体信息放在文档类中了。
视图数据成员的初始化
在文档类中,通过成员函数OnNewDocument()来完成文档类数据成员的初始化工作。视图类也提供了一个CView::OnInitialUpdate()成员函数来初始化视图类的数据成员。
在以下情况下,应用程序将自动执行视图类的OnInitialUpdate()来初始化视图类数据成员:
用户启动应用程序
从File菜单选择New菜单项,CWinApp::OnFileNew在调用CDocument::OnNewDocument后即调用OnInitialUpdate准备绘图输出;
用File->Open命令打开一个文件,此时希望清除视图原有的显示内容
在编辑器中要做的主要工作是对编辑器使用的字体的初始化,见清单7.7。
清单7.7 视图的OnInitialUpdate方法
void CEditorView::OnInitialUpdate()
{
// TODO: Add your specialized code here and/or call the base class
CDC *pDC=GetDC();
pFont=new CFont();
if(!(pFont->CreateFont(0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE,
ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS,
DEFAULT_QUALITY,DEFAULT_PITCH,"Courier New")))
{
pFont->CreateStockObject(SYSTEM_FONT);
}
CFont* oldFont=pDC->SelectObject(pFont);
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
pDC->SelectObject(oldFont);
CView::OnInitialUpdate();
}
OnInitialUpdate()首先调用GetDC()取得当前窗口的设备上下文指针并存放在pDC中。设备上下文(简称DC,英文全称是device context)Windows数据结构,它描述了在一个窗口中绘图输出时所需的信息,包括使用的画笔、画刷、当前选用的字体及颜色(前景色和背景色)、绘图模式,以及其它所需要的绘图信息。MFC提供一个CDC类封装设备上下文,以简化存取DC的操作。
然后OnInitialUpdate()创建视图显示时所用的字体。同前面提到的其他MFC对象如框架窗口一样,字体对象的创建也分为两步:第一步,创建一个C++对象,初始化CFont的实例;第二步,调用CreateFont()创建字体。除了CreateFont之外,还有两个创建字体的函数:CreateFontIndirect和FromHandle(),前者要求一个指向所需字体的LOGFONT(逻辑字体)的指针作参数,后者需要一个字体句柄作参数。如果CreateFont()因为某种原因失败,那么就调用CreateStockObject()从预定义的GDI对象中创建字体。
注意:在Windows的GDI中,包含一些预定义的GDI对象,无需用户去创建,马上就可以拿来使用。这些对象称作库存(Stock)对象。库存对象包括BLACK_BRUSH(黑色画刷)、DKGRAY_BRUSH(灰色画刷)、HOLLOW_BRUSH(空心画刷)、WHITE_BRUSH(白色画刷)、空画刷、黑色画笔、白色画笔以及一些字体和调色板等。CGdiObject:: CreateStockObject()并不真正创建对象,而只是取得库存对象的句柄,并将该句柄连到调用该函数的GDI对象上。
然后调用CDC的SelectObject()方法,将字体选入到设备上下文中。SelectObject()函数原型如下:CPen* SelectObject( CPen* pPen );
CBrush* SelectObject( CBrush* pBrush );
virtual CFont* SelectObject( CFont* pFont );
CBitmap* SelectObject( CBitmap* pBitmap );
int SelectObject( CRgn* pRgn );
SelectObject的参数可以是一个画笔、画刷、字体、位图或区域,它们统称为GDI(图形设备接口)对象。SelectObject将一个GDI对象选入到一个设备上下文中,新选中的对象将替换原有的同类型对象,然后返回指向被替换的对象的指针。SelectObject()知道它所选中的对象的类型,且总是返回同类的旧对象的指针。还要存储返回的CFont指针,在退出OnInitialUpdate之前调用pDC->SelectObject(oldFont),将CDC重新设置成原来的初始状态。
读者以后编程也应当养成这样一个习惯:在用SelectObject选择新的GDI对象时,应当保存指向原先使用的GDI对象的指针,在绘图结束后,再用SelectObject选择原来的对象,设置CDC为其初始状态。否则的话,会有非法句柄留在设备上下文对象中,积累下去将导致无法预见的错误。但是,如果该设备上下文是自己创建而不是用参数传递过来的,则不必恢复画笔或刷子。象上面的例子,其实用户不必在退出时恢复原来的字体。而在下面要讲的OnDraw函数中,由于pDC是框架传给OnDraw的,因此在退出时必须恢复设备上下文中原来的字体设置。总之,如果用户能肯定画笔或刷子等GDI对象废弃以前设备对象会被销毁,则不必恢复设备上下文中GDI对象的设置。不过,为概念上的明确,还是建议调用恢复过程。
TEXTMETRIC是一个数据结构,它包含字体的宽度、高度、字的前后空白等字段。调用CDC::GetTextMetrics()获取字体的TEXTMETRIC,从而取得字体的宽度和高度等信息。最后调用CView类的OnInitialUpdate()函数来画视图。
由于在堆栈上创建了视图所用的字体对象pFont,在关闭视图时就需要删除该字体对象。这部分工作在视图的析构函数中完成。修改视图的析构函数:
CEditorView::~CEditorView()
{
if(pFont!=NULL)
delete pFont;
}
视图的绘制
现在要让视图显示编辑器中的文本。AppWizard为视图类CEditorView生成了一个OnDraw()方法,当需要重画视图时,该函数就会被调用。清单7.8是编辑器的OnDraw函数定义:
清单7.8 视图的OnDraw方法
/
// CEditorView drawing
void CEditorView::OnDraw(CDC* pDC)
{
CEditorDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CFont *oldFont;
//选择新字体
oldFont=pDC->SelectObject(pFont);
//纵向yval坐标为0
int yval=0;
POSITION pos;
CString line;
//取得文本行链表的头指针
if(!(pos=pDoc->lines.GetHeadPosition()))
{
return;
}
//循环输出各文本行
while(pos!=NULL)
{
line=pDoc->lines.GetNext(pos);
pDC->TextOut(0,
yval,
line,
line.GetLength());
//更新y坐标值,让它加上文本行所用字体的高度
yval+=lHeight;
}
//恢复原来DC所用的字体
pDC->SelectObject(pFont);
}
框架调用视图的CView::OnDraw(CDC* pDC)方法完成屏幕显示、打印、打印预览功能,对于不同的输出功能它会传递不同的DC指针给OnDraw()函数。
在OnDraw()函数中,首先调用GetDocument()函数,取得指向当前视图所对应的文档的指针。通过这个指针,来访问文档中的数据。以后在视图中修改文档中的数据,也是通过GetDocument()来取得文档指针,再通过该文档指针修改文档中的数据。
在绘图时,可以通过传给OnDraw函数的一个设备上下文DC的指针pDC进行GDI调用。开始绘图之前,往往需要选择GDI资源(或GDI对象,包括画笔、刷子、字体等),将它选入到设备上下文中。在文本编辑器中,我们选择一种字体pFont到设备上下文中,以后在窗口客户区的文本输出就都会使用该字体绘制。在绘制过程中,绘图代码是设备无关的,也就是说它并不需要知道目前使用的是什么设备(屏幕、打印机或其他绘图设备)。
读者以前如果用Borland C++或SDK编写过Windows程序的话,都会知道:当窗口或窗口的一部分变成无效的话(比如其他窗口从本窗口上拖过、窗口调整大小等),操作系统就会向窗口发送一条WM_PAINT消息。窗口接收到该消息之后,调用Borland C++的EvPaint()或Visual C++的OnPaint()完成窗口绘制工作。这里OnDraw()函数也同样完成窗口绘图输出,这两者有什么关系呢?
我们先看一下OnPaint()函数:
void CMyWindow::OnPaint()
{
CPaintDC dc(this); //用于窗口绘制的设备上下文
CString str(“Hello,world!”);
...
//绘图输出代码
dc.TextOut(10,10,str,str.GetLength());
}
在OnPaint()函数中,首先创建一个CPaintDC类的对象dc。CPaintDC必需也只能用在WM_PAINT消息处理中。在CPaintDC类对象dc的构造函数中,调用了在SDK下需要显式调用的BeginPaint函数,取得处理WM_PAINT消息时所需的设备上下文。然后OnPaint()函数使用该设备上下文完成各种输出。在OnPaint()函数退出时,dc对象被删除。在dc对象的析构函数中,包含了对EndPaint函数的调用。EndPaint一方面释放设备上下文,另一方面还从应用消息队列中删除WM_PAINT消息。如果在处理WM_PAINT时不使用CPaintDC,则WM_PAINT不被消除,会产生不断重画的现象。
视图是一个子窗口,它自然也从窗口类继承了OnPaint()成员函数,用以响应WM_PAINT消息。类似于上面的例子,视图OnPaint处理函数首先创建一个与显示器相匹配的CPaintDC类的设备上下文对象dc,但是OnPaint不再直接完成窗口输出,而是将设备上下文传给OnDraw()成员函数,由OnDraw()函数去完成窗口输出。当打印输出时,框架会调用视图的DoPreparePrinting创建一个与打印机相匹配的设备上下文并将该DC传递给OnDraw()函数,由OnDraw函数完成打印输出。这样,OnDraw()函数就把用于屏幕显示和打印机输出的工作统一起来,真正体现了设备无关的思想。如果想知道当前OnDraw函数是在用于屏幕显示还是打印输出,可以调用CView::IsPrinting()函数。当处于打印状态时,IsPrinting()返回TRUE;在用于屏幕显示时,返回FALSE。
文档修改时通知视图的更新
当文档以某种方式变化时,必须通知视图作相应的更新即重绘,以反应文档的变化。这种情况通常发生在用户通过视图修改文档时。此时,视图将调用文档的UpdateAllViews成员函数通知同一文档的所有视图对自己进行更新。UpdateAllViews将调用每个视图的OnUpdate成员函数,使视图的客户区无效。
5 视图的消息处理
视图作为一个子窗口,当然可以处理消息。但是应用程序运行时,除了视图外,还有应用程序对象、主框架窗口、文档等,它们都是可以处理消息的。那么消息传递过程是什么样的呢?
MFC的命令消息按以下方式传递:
图7-9 文档视结构中的消息传递
键盘消息处理
前面的视图绘制就是完成窗口消息WM_PAINT的处理。编辑器要接收用户的键盘输入,就必须处理键盘消息;另外,在用户输入字符时,还必须马上就把用户输入的内容在屏幕上显示出来。
用ClassWizard生成处理WM_CHAR消息的函数OnChar(),然后打开该函数进行编辑。修改后的OnChar函数如清单7.9:
清单7.9 CEditorView的OnChar()成员函数
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
CFont *oldFont;
//选择新字体
oldFont=dc.SelectObject(pFont);
CString line("");//存放编辑器当前行字符串
POSITION pos=NULL;//字符串链表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行号返回字符串链表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//没有找到该行号对应的行,因此它是一个空行,
//我们把它加到字符串链表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//当前文本行还没有换行结束,因此将文本加入到行末
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
dc.SelectObject(oldFont);
CView::OnChar(nChar,nRepCnt,nFlags);
}
因为编辑器要将用户输入内容加入到文本行缓冲区中,因此首先调用GetDocument()获取指向文档的指针,以便对文档中的数据进行修改。
为了在收到键盘输入消息后在窗口中输入字符,需要定义一个CClientDC类的对象dc。CClientDC是用于管理窗口客户区的设备上下文对象,它在构造函数中调用GetDC()取得窗口客户区设备上下文,在析构函数中调用ReleaseDC()释放该设备上下文。CClientDC同样用于在窗口客户区的输出,它与CPaintDC不同之处在于:
CPaintDC专门用于在窗口OnPaint()中的输出,而不能用于其它非窗口重画消息的处理。如果不是在OnDraw或OnPaint()中绘图,则需要创建一个CClientDC对象,然后调用CClientDC的方法来完成绘图输出。
OnChar()接下去处理用户输入。如果输入是一个回车,则将总行数nLineNum加一,否则将输入字符加到当前行行末。最后调用TextOut函数输出当前编辑中的文本行。
最后调用文档的SetModifiedFlag()方法设置文档的修改标志。SetModifiedFlag()函数原型如下:
void SetModifiedFlag( BOOL bModified = TRUE );
从函数原型可以看出,函数缺省参数为TRUE。当调用SetModifiedFlag时,将文档内的修改标志置为真。如果用户执行了Save或Save As操作,则将文档的修改标志置为假。这样,当用户关闭文档的最后一个视图时,框架根据该修改标记决定是否提示用户保存文档中的数据到文件。如果用户上次作了修改还没有存盘,则弹出一个消息框,提示是否保存文件。这些都是框架程序来完成的。
用户如果在视图的其它任何地方修改了文档,也必须调用SetModifiedFlag来设置文档修改标记,以便关闭窗口时让框架提示保存文档。
菜单消息处理
现在还要增加一个菜单,用户选择菜单时会弹出一个字体选择对话框,让用户选择视图输出文档时所用的字体。用菜单编辑器在View菜单下增加一个菜单项“Select Font”,菜单项相关参数如下:
菜单名:Select &Font
菜单ID:ID_SELECT_FONT
提示文字:Select a font for current view
然后用ClassWizard为该菜单项生成消息处理函数SelectFont。在选择消息响应的类时,用户可以选择文档、视图、框架或应用程序类,这根据具体情况而定。如果操作是针对某一视图(比如象本例中改变字体操作),则消息处理放在视图中比较合适。如果操作是针对文档的(比如要显示文档中对象的属性等),则放在文档中处理比较合适。如果选项对应用程序中的所有文档和视图都有效(即是全局的选项),那么可以把它放在框架窗口中。
修改OnSelectFont()函数,使它能显示字体选择对话框,修改后的OnSelect函数见清单7.10:
清单7.10 OnSelectFont()函数
void CEditorView::OnSelectFont()
{
CFontDialog dlg;
if(dlg.DoModal()==IDOK)
{
LOGFONT LF;
//获取所选字体的信息
dlg.GetCurrentFont(&LF);
//建立新的字体
pFont->DeleteObject();
pFont->CreateFontIndirect(&LF);
Invalidate();
UpdateWindow();
}
}
在OnSelectFont()消息处理函数中,首先定义一个选择字体公用对话框,然后显示该对话框,返回所选的字体。有关选择字体公用对话框的知识参见第五章对话框技术。字体对话框通过GetCurrentFont()返回逻辑字体信息。所谓逻辑字体是一种结构,它包含了字体的各种属性的描述,包括字体的名字、宽度、高度和是否斜体、加粗等信息。字体对象首先通过DeleteObject删除原来的字体对象,然后通过CreateFontIndirect、利用逻辑字体的属性来创建字体。由于我们选择了一种新的字体,所以要用新的字体来重绘视图。为此,调用Invalidate()函数向视图发送WM_PAINT消息。由于WM_PAINT消息级别比较低,不会立即被处理。因此,再调用UpdateWindow()强制窗口更新。这也是一种常用的技巧。
现在已经完成了编辑器文档类和视图类的设计,对主框架窗口类不需要修改。编译、链接并运行程序,弹出文本编辑器窗口。试着输入几行文本,存盘。然后再载入刚才保存的文件,如图7-10。在File-Exit菜单项上面,有一个文件名列表,列出最近打开过的文件,这个表称作MRU表(MRU是英文Most Recently Used的缩写)。可以从MRU中选择一个文件名,打开该文件。
图7-10 一个简单的文本编辑器
7.3 让文档视结构程序支持卷滚
但是,编辑器现在还不支持卷滚。当文本行超过窗口大小时,窗口并不自动向上滚动以显示输入的字符。当打开一个文件时,如果文件大小超过窗口大小,也无法通过卷滚视图来看文档的全部内容。现在我们要让编辑器增加卷滚功能。
7.3.1逻辑坐标和设备坐标
在引入文档卷滚功能之前,首先要介绍以下逻辑坐标和设备坐标这两个重要概念。
在Windows中,文档坐标系称作逻辑坐标系,视图坐标系称为设备坐标系。它们之间的关系如下图所示:
图7-11文档坐标和视图坐标
逻辑坐标按照坐标设置方式(又成为映射模式)可分为8种,它们在坐标上的特性如下表所示:
表7-1 各种映射模式下的坐标转换方式
映射模式 逻辑单位 x递增方向
y递增方向
MM_TEXT 像素 向右
向下
MM_LOMETRIC 0.1mm 向右
向上
MM_HIMETRIC 0.01mm 向右
向上
MM_LOENGLISH 0.01inch 向右
向上
MM_HIENGLISH 0.001inch 向右
向上
MM_TWIPS 1/1440inch 向右
向上
MM_ISOTROPIC 可调整 (x=y) 可选择
可选择
MM_ANISOTROPIC 可调整(x!=y) 可选择
可选择
我们一般使用的映射模式是MM_TEXT,它也是缺省设置。在该模式下,坐标原点在工作区左上角,而x坐标值是向右递增,y坐标值是向下递增,单位值1代表一个像素。
要设置映射模式,可以调用CDC::SetMapMode()函数。
CClientDC dc;
nPreMapMode=dc.SetMapMode(nMapMode);
它将映射模式设置为nMapMode,并返回前一次的映射模式nPreMapMode,GetMapMode可取得当前的映射模式:
CClientDC dc;
nMapMode=dc.GetMapMode();
MFC绘图函数都使用逻辑坐标作为位置参数。比如
CString str(“Hello,world!”);
dc.TextOut(10,10,str,str.GetLength());
这里的(10,10)是逻辑坐标而不是像素点数(只是在缺省映射模式MM_TEXT下,正好与像素点相对应),在输出时GDI函数会将逻辑坐标(10,10)依据当前映射模式转化为“设备坐标”,然后将文字输出在屏幕上。
设备坐标以像素点为单位,且x轴坐标值向右递增,y轴坐标值向下递增,但原点(0,0)位置却不限定在工作区的左上角。依据设备坐标的原点和用途,可以将Windows下使用的设备坐标系统分为三种:工作区坐标系统,窗口坐标系统和屏幕坐标系统。
(1)工作区坐标系统:
工作区坐标系统是最常见的坐标系统,它以窗口客户区左上角为原点(0,0),主要用于窗口客户区绘图输出以及处理窗口的一些消息。鼠标消息WM_LBUTTONDOWN、WM_MOUSEMOVE传给框架的消息参数以及CDC一些用于绘图的成员都是使用工作区坐标。
(2)屏幕坐标系统:
屏幕坐标系统是另一类常用的坐标系统,以屏幕左上角为原点(0,0)。以CreateDC(“DISPLAY” , ...)或GetDC(NULL)取得设备上下文时,该上下文使用的坐标系就是屏幕坐标系。
一些与窗口的工作区不相关的函数都是以屏幕坐标为单位,例如设置和取得光标位置的函数SetCursorPos()和GetCursorPos();由于光标可以在任何一个窗口之间移动,它不属于任何一个单一的窗口,因此使用屏幕坐标。弹出式菜单使用的也是屏幕坐标。另外,CreateWindow、MoveWindow、SetWindowPlacement()等函数用于设置窗口相对于屏幕的位置,使用的也是屏幕坐标系统。
(3)窗口坐标系统:
窗口坐标系统以窗口左上角为坐标原点,它包含了窗口控制菜单、标题栏等内容。一般情况下很少在窗口标题栏上绘图,因此这种坐标系统很少使用。
三类设备坐标系统关系如下图所示:
图7-12. 三类设备坐标
MFC提供ClientToScreen()、ScreenToClient()两个函数用于完成工作区坐标和屏幕坐标之间的转换工作。
void ScreenToClient( LPPOINT lpPoint ) const;
void ScreenToClient( LPRECT lpRect ) const;
void ClientToScreen( LPPOINT lpPoint ) const;
void ClientToScreen( LPRECT lpRect ) const;
其实,我们在前面介绍弹出式菜单时已经使用了ClientToScreen函数。在那里,由于弹出式菜单使用的是屏幕坐标,因此当处理弹出式菜单快捷键shift+F10时,如果要在窗口左上角(5,5)处显示快捷菜单,就必须先调用ClientToScreen函数将客户区坐标(5,5)转化为屏幕坐标。
CRect rect;
GetClientRect(rect);
ClientToScreen(rect);
point = rect.TopLeft();
point.Offset(5, 5);
在视图滚动后,如果用户在视图中单击鼠标,那么会得到鼠标位置的设备(视图)坐标。在使用这个数据处理文档(比如画点或画线)时,需要把它转化为文档坐标。这是因为利用MFC绘图时,所有传递给MFC作图的坐标都是逻辑坐标。当调用MFC绘图函数绘图时,Windows自动将逻辑坐标转换成设备坐标,然后再绘图。设备上下文类CDC提供了两个成员函数LPToDP和DPToLP完成逻辑坐标和设备坐标之间的转换工作。如其名字所示那样,LPToDP将逻辑坐标转换为设备坐标,DPToLP将设备坐标转换为逻辑坐标。
void LPtoDP( LPPOINT lpPoints, int nCount = 1 ) const;
void LPtoDP( LPRECT lpRect ) const;
void LPtoDP( LPSIZE lpSize ) const;
void DPtoLP( LPPOINT lpPoints, int nCount = 1 ) const;
void DPtoLP( LPRECT lpRect ) const;
void DPtoLP( LPSIZE lpSize ) const;
7.3.2 滚动文档
由于MFC绘图函数使用的是逻辑坐标,因此用户可以在一个假想的通常是比视图要大的“文档窗口”中绘图;Windows自动在幕后完成坐标转换工作,并将落在视图范围内的那一部分“文档窗口”显示出来,其余的部分被裁剪。
但是光这样还不能卷滚文档。要卷滚显示文档,还必须知道文档卷滚到了什么位置;一旦用户拖动滚动条时要告诉视图改变在文档中的相应位置。所有这些,由MFC的CScrollView来完成。
MFC提供了CScrollView类,简化了滚动需要处理的大量工作。除了管理文档中的滚动操作外,MFC还通过调用Windows API函数画出滚动条、箭头和滚动光标。它还负责处理:
用户初始化滚动条范围(通过滚动视图的SetScrollRange()方法)
处理滚动条消息,并滚动文档到相应位置
管理窗口和视图的尺寸大小
调整滚动条上滑块(或称拇指框)的位置,使之与文档当前位置相匹配
程序员要做的工作是:
从CScrollView类中派生出自己的视图类,以支持卷滚
提供文档大小,确定滚动范围和设置初始位置
协调文档位置和屏幕坐标
要让应用程序支持卷滚,可以在用AppWizard生成框架程序时就指定视图的基类为CSrollView。可以在AppWizard的MFC AppWizard-Step 6 of 6对话框中,在对话框上方应用程序所包含的类中选择CEditorView,然后在Base Class下拉列表框中选择应用程序视图类的基类为CScrollView,如图7-11所示:
图7-13 为应用程序的视图类指定基类
现在我们要手工修改CEditorView,使它的基类为CScrollView。
1. 修改视图类所对应的头文件,将所有用到CView的地方改为CScrollView。通常,首先修改视图类赖以派生的父类,形式如下:
class CEditorView:public CScrollView
2. 修改视图类实现的头文件,把所有用到CView的地方改为CScrollView。首先修改IMPLEMENT_DYNACREATE一行:
IMPLEMENT_DYNACREATE(CEditorView,CScrollView)
然后修改BEGIN_MESSAGE_MAP宏
BEGIN_MESSAGE_MAP(CEditorView,CScrollView)
然后将其他所有用到CView的地方改为CScrollView。
一个更简单的方法是:使用Edit-Replace功能,进行全局替换。
到现在为止,已经将编辑器视图类CEditorView的基类由CView转化为CScrollView。
现在,要设置文档大小,以便让CScrollView知道该如何处理文档。视图必需知道文档的卷滚范围,这样才能确定何时卷滚到文档的头部和尾部,以及当拖动卷滚条的滑块时按适当比例调整文档当前显示位置。
为此,我们首先在文档类CEditorDoc的头文件editordoc.h中增加一个CSize类型的数据成员m_sizeDoc用以表示文档的大小。CSize对象包含cx和cy两个数据成员,分别用于存放文档的x方向坐标范围和y方向坐标范围。另外,还要提供一个成员函数GetDocSize()来访问该文档大小范围数据成员。修改后的editordoc.h如清单7.11。
清单7.11 CEditorDoc头文件
class CEditorDoc : public CDocument
{
protected: // create from serialization only
CEditorDoc();
DECLARE_DYNCREATE(CEditorDoc)
//保存文档大小
CSize m_sizeDoc;
// Attributes
public:
CSize GetDocSize(){return m_sizeDoc;}
// Operations
public:
CStringList lines;
int nLineNum;
......
};
既然增加了m_sizeDoc这一数据成员,就需要在CEditorDoc构造函数中进行初始化,给m_sizeDoc设置一合理的数值,比如说x=700,y=800。构造函数如清单7.12。
清单7.12 CEditorDoc的构造函数
CEditorDoc::CEditorDoc()
{
// TODO: add one-time construction code here
nLineNum=0;
m_sizeDoc=CSize(700,800);
}
一个设计优秀的应用程序应当能够动态调整文档的卷滚范围。比如,在WORD中新建一个文件时,在“页面模式”下将可卷滚范围设为一页大小。随着用户输入,逐渐增加文档的卷滚范围。但是这里为简明起见,将文档卷滚范围设为固定大小700X800点像素大小。设置文档大小通过由视图类的CEditorView::OnInitialUpdate()调用SetScrollSizes()成员函数来完成。
SetScrollSizes()用于设置文档卷滚范围。一般在重载OnInitialUpdate()成员函数或OnUpdate()时调用该函数,用以调整文档卷滚特性。比如,在文档初始显示或文档大小作了调整之后。
清单7.13 在OnInitialUpdate()中设置卷滚范围
void CEditorView::OnInitialUpdate()
{
// TODO: Add your specialized code here and/or call the base class
CDC *pDC=GetDC();
pFont=new CFont();
if(!(pFont->CreateFont(0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE,
ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS,
DEFAULT_QUALITY,DEFAULT_PITCH,"Courier New")))
{
pFont->CreateStockObject(SYSTEM_FONT);
}
CFont* oldFont=pDC->SelectObject(pFont);
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnInitialUpdate();
}
SetScrollSizes()第一个参数为映射模式。SetScrollSizes()可以使用除MM_ISOTROPIC和MM_ANISOTROPIC之外的其他任何映射模式。SetScrollSizes()第二个参数为文档大小,用一个CSize类型的数值表示。
另外,我们还要检查两个包含绘图输出功能的函数:CEditorView::OnChar()和CEditorView::OnDraw()函数。
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
CString line("");//存放编辑器当前行字符串
POSITION pos=NULL;//字符串链表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行号返回字符串链表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//没有找到该行号对应的行,因此它是一个空行,
//我们把它加到字符串链表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//there is a line,so add the incoming char to the end of
//the line
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnChar(nChar,nRepCnt,nFlags);
}
在程序运行开始的时侯,视图坐标原点和文档坐标原点是重合的。但是,当用户拖动滚动条时,视图原点就与文档原点不一致了,如图7-14。由于GDI是按照文档坐标(逻辑坐标)来输出图形的,这样自然就无法正确显示文档内容。
图7-14 文档滚动前后文档坐标原点和视图坐标原点的变化
这时,要想获得正确输出,就必须调整视图坐标,让视图坐标原点和文档坐标原点重合,如图7-15所示。
图7-15 调整视图设备上下文原点后
CScrollView视图类提供了一个CScrollView::OnPrepareDC()成员函数,完成视图设备上下文坐标原点的调整工作。
现在修改OnChar(),加入OnPrepareDC()函数,见清单7.15。
清单7.15 修改后的OnChar成员函数
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
OnPrepareDC(&dc);
CFont* pOldFont=dc.SelectObject(pFont);
CString line("");//存放编辑器当前行字符串
POSITION pos=NULL;//字符串链表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行号返回字符串链表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//没有找到该行号对应的行,因此它是一个空行,
//我们把它加到字符串链表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//there is a line,so add the incoming char to the end of
//the line
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
dc.SelectObject(pOldFont);
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnChar(nChar,nRepCnt,nFlags);
}
但是,对于视图OnDraw()函数,则不需要作这样的调整。这是因为,框架在调用OnDraw()之前,已经自动调用了OnPrepareDC()成员函数完成设备上下文坐标调整工作了。
提示:对于框架传过来的设备上下文,不需要调用OnPrepareDC(),因为框架知道它是用于绘图的,因此事先调用了OnPrepareDC()作好了坐标调整工作。如果是自己构造或用GetDC()取得得设备上下文,则需要调用OnPrepareDC()完成设备上下文坐标调整工作。
现在编辑器已经能够支持文档滚动了,如图7-16。
图7-16支持滚动的文本编辑器
7.4 定制串行化
前面编辑器的例子使用CString类的字符串来保存文本行,由于它是MFC类,因此可以串行化自己,将自己写入磁盘或从磁盘文件中读取二进制数据来建立对象。那么,如果不是标准的MFC类,比如用户自己定义的类,如何让它支持串行化呢?下面,我们结合前面第五章提到的就业调查表的例子来演示如何让用户定义的类支持串行化功能。
要让用户定义的类支持串行化,一般分为五步:
1.从CObject或其派生类派生出用户的类
2.重载Serialize()成员函数,加入必要的代码,用以保存对象的数据成员到CArchive对象以及从CArchive对象载入对象的数据成员状态。
3.在类声明文件中,加入DECLARE_SERIAL宏。编译时,编译器将扩充该宏,这是串行化对象所必需的。
4.定义一个不带参数的构造函数。
5.在实现文件中加入IMPLEMENT_SERIAL宏。
class CRegister:public CObject
{
public:
DECLARE_SERIAL( CRegister)
//必需提供一个不带任何参数的空的构造函数
CRegister(){};
public:
CString strIncome;
CString strKind;
BOOL bMarried;
CString strName;
int nSex;
CString strUnit;
int nWork;
UINT nAge;
void Serialize(CArchive&);
};
MFC在从磁盘文件载入对象状态并重建对象时,需要有一个缺省的不带任何参数的构造函数。串行化对象将用该构造函数生成一个对象,然后调用Serialize()函数,用重建对象所需的值来填充对象的所有数据成员变量。
构造函数可以声明为public、protected或private。如果使它成为protect或private,则可以确保它只被串行化过程所使用。
在类定义文件中给出Serialize()的定义。它包括对象的保存和载入两部分。前面已经提到,CArchive类提供一个IsStoring()成员函数指示是保存数据到磁盘文件还是从磁盘文件载入对象。
void CRegister::Serialize(CArchive& ar)
{
//首先调用基类的Serialize()方法。
CObject::Serialize( ar);
if(ar.IsStoring())
{
ar<
ar<
ar<<(int)bMarried;
ar<
ar<
ar<
ar<
ar<<(WORD)nAge;
}
else
{
ar>>strIncome;
ar>>strKind;
ar>>(int)bMarried;
ar>>strName;
ar>>nSex;
ar>>strUnit;
ar>>nWork;
ar>>(WORD)nAge;
}
}
我们看到,对象的串行化实际上是通过调用对象中的数据成员的串行化来完成的。
注意:CArchive类的>>和<<操作符并不支持所有的标准数据类型。它支持的数据类型有:CObject、BYTE、WORD、int、LONG、DWORD、float和double。其他的类型的数据要进行串行化输入输出时,需要将该类型的数据转化为上述几种类型之一方可。
另外,在类的实现(类定义)文件开始处,还要加入IMPLEMENT_SERIAL宏。IMPLEMENT_SERIAL( CRegister, CObject, 1 )
IMPLEMENT_SERIAL宏用于定义一个从CObject派生的可串行化类的各种函数。宏的第一和第二个参数分别代表可串行化的类名和该类的直接基类。
第三个参数是对象的版本号,它是一个大于或等于零的整数。MFC串行化代码在将对象读入内存时检查版本号。如果磁盘文件上的对象的版本号和内存中的对象的版本号不一致,MFC将抛出一个CArchiveException异常,阻止程序读入一个不匹配版本的对象。
现在,我们就可以象使用标准MFC类一样使用CRegister的串行化功能了。
CArchive ar;
CRegister reg1,reg2;
ar<
读者请试着在第五章职工调查表程序基础上,增加保存调查信息到文件以及从文件中读入调查表信息功能。对于多个调查表,可考虑采用CObjList链表保存多个对象的指针。
串行化简化了对象的保存和载入,为对象提供了持续性。但是,串行化本身还是具有一定的局限性的。串行化一次从文件中载入所有对象,这不适合于大文件编辑器和数据库。对于数据库和大文件编辑器,它们每次只是从文件中读入一部分。此时,就要避开文档的串行化机制来直接读取和保存文件了。另外,使用外部文件格式(预先定义的文件格式而不是本应用程序定义的文件格式)的程序一般也不使用文档的串行化。下面我们就给出这样一个例子,说明在不使用串行化情况下如何读取和保存文件。
7.5不使用串行化的文档视结构程序
在MFC例子中有一个DIBLOOK(见SAMPLES/MFC/GENERAL/DIBLOOK目录),它是一个位图显示程序,演示了在不使用串行化的情况下实现文档的输入输出功能。有关位图、调色板的使用在第十一章有详细介绍,这里只讨论与文档视结构相关的内容。我们先看DIBLOOK的文档声明和定义。
清单7-16 CDibDoc的类声明文件
// dibdoc.h : interface of the CDibDoc class
#include "dibapi.h"
class CDibDoc : public CDocument
{
protected: // create from serialization only
CDibDoc();
DECLARE_DYNCREATE(CDibDoc)
// Attributes
public:
HDIB GetHDIB() const
{ return m_hDIB; }
CPalette* GetDocPalette() const
{ return m_palDIB; }
CSize GetDocSize() const
{ return m_sizeDoc; }
// Operations
public:
void ReplaceHDIB(HDIB hDIB);
void InitDIBData();
// Implementation
protected:
virtual ~CDibDoc();
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
protected:
HDIB m_hDIB;
CPalette* m_palDIB;
CSize m_sizeDoc;
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
virtual BOOL OnNewDocument();
// Generated message map functions
protected:
//{{AFX_MSG(CDibDoc)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
/
清单7-17 CDibDoc类的实现文件
// dibdoc.cpp : implementation of the CDibDoc class
#include "stdafx.h"
#include "diblook.h"
#include
#include "dibdoc.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/
// CDibDoc
IMPLEMENT_DYNCREATE(CDibDoc, CDocument)
BEGIN_MESSAGE_MAP(CDibDoc, CDocument)
//{{AFX_MSG_MAP(CDibDoc)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/
// CDibDoc construction/destruction
CDibDoc::CDibDoc()
{
//初始化文档的DIB句柄和调色板
m_hDIB = NULL;
m_palDIB = NULL;
m_sizeDoc = CSize(1,1); // dummy value to make CScrollView happy
}
CDibDoc::~CDibDoc()
{
if (m_hDIB != NULL)
{
::GlobalFree((HGLOBAL) m_hDIB);
}
if (m_palDIB != NULL)
{
delete m_palDIB;
}
}
BOOL CDibDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
return TRUE;
}
void CDibDoc::InitDIBData()
{
if (m_palDIB != NULL)
{
delete m_palDIB;
m_palDIB = NULL;
}
if (m_hDIB == NULL)
{
return;
}
// Set up document size
LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);
if (::DIBWidth(lpDIB) > INT_MAX ||::DIBHeight(lpDIB) > INT_MAX)
{
::GlobalUnlock((HGLOBAL) m_hDIB);
::GlobalFree((HGLOBAL) m_hDIB);
m_hDIB = NULL;
CString strMsg;
strMsg.LoadString(IDS_DIB_TOO_BIG);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
return;
}
m_sizeDoc = CSize((int) ::DIBWidth(lpDIB), (int) ::DIBHeight(lpDIB));
::GlobalUnlock((HGLOBAL) m_hDIB);
// Create copy of palette
m_palDIB = new CPalette;
if (m_palDIB == NULL)
{
// we must be really low on memory
::GlobalFree((HGLOBAL) m_hDIB);
m_hDIB = NULL;
return;
}
if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)
{
// DIB may not have a palette
delete m_palDIB;
m_palDIB = NULL;
return;
}
}
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
return FALSE;
}
DeleteContents();
BeginWaitCursor();
// replace calls to Serialize with ReadDIBFile function
TRY
{
m_hDIB = ::ReadDIBFile(file);
}
CATCH (CFileException, eLoad)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eLoad,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
m_hDIB = NULL;
return FALSE;
}
END_CATCH
InitDIBData();
EndWaitCursor();
if (m_hDIB == NULL)
{
// may not be DIB format
CString strMsg;
strMsg.LoadString(IDS_CANNOT_LOAD_DIB);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
return FALSE;
}
SetPathName(lpszPathName);
SetModifiedFlag(FALSE); // start off with unmodified
return TRUE;
}
BOOL CDibDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
TRUE, AFX_IDP_INVALID_FILENAME);
return FALSE;
}
// replace calls to Serialize with SaveDIB function
BOOL bSuccess = FALSE;
TRY
{
BeginWaitCursor();
bSuccess = ::SaveDIB(m_hDIB, file);
file.Close();
}
CATCH (CException, eSave)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eSave,
TRUE, AFX_IDP_FAILED_TO_SAVE_DOC);
return FALSE;
}
END_CATCH
EndWaitCursor();
SetModifiedFlag(FALSE); // back to unmodified
if (!bSuccess)
{
// may be other-style DIB (load supported but not save)
// or other problem in SaveDIB
CString strMsg;
strMsg.LoadString(IDS_CANNOT_SAVE_DIB);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
}
return bSuccess;
}
void CDibDoc::ReplaceHDIB(HDIB hDIB)
{
if (m_hDIB != NULL)
{
::GlobalFree((HGLOBAL) m_hDIB);
}
m_hDIB = hDIB;
}
/
// CDibDoc diagnostics
#ifdef _DEBUG
void CDibDoc::AssertValid() const
{
CDocument::AssertValid();
}
void CDibDoc::Dump(CDumpContext& dc) const
{
CDocument::Dump(dc);
}
#endif //_DEBUG
/
// CDibDoc commands
DIBLOOK读入和保存标准的Windows设备无关位图。在内存中,位图以一个HDIB句柄表示。DIBLOOK没有重 载CDocument::Serialize()函数,而是重载了CDocument::OnOpenDocument和CDocument::OnSaveDocument函数。这两个函数使用框架传过来得文件路径名pszPathName,打开一个文件对象,读入或保存DIB数据。这就是说,DIBLOOK把本来在Serialize()中完成的对象保存和载入两个任务分别交与OnSaveDocument()函数和OnOpenDocument()函数去完成。如果读者希望绕过文档的串行化提供文档数据的保存和载入,也只需要重载这两个成员函数:OnOpenDocument()和OnSaveDocument(),通过文件路径参数打开文件,从中读取应用程序数据或向文件里写入应用程序数据。
在OnOpenDocument()中,还必需自己调用DeleteContents()清除原来文档的数据,并调用SetModifiedFlag(FALSE)。在OnSaveDocument()中也要调用SetModifiedFlag(FALSE)将文档修改标志改为FALSE。
在OnOpenDocument()函数开始处(见清单7.18),有一些地方需要解释一下。
清单7.18 OnOpenDocument()函数
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
return FALSE;
}
......
}
7.5.1 文件操作
文件读写
OnOpenDocument首先声明一个CFile类的对象。CFile是MFC提供的一个类,它提供了访问二进制文件的接口。可以使用带参数的CFile构造函数创建对象,在构造函数中指定了文件名和打开文件的模式,这样在对象创建的同时也就打开了这个文件;也可以象本例那样使用不带参数的CFile构造函数构造一个CFile对象,然后调用CFile::Open()打开一个文件。
BOOL CFile::Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL );
CFile::Open成员函数带三个参数,第一个参数指定了要打开的文件的完整路径名,如“c:/hello/hello.cpp”;第二个参数指定打开文件的模式。
常见的文件打开模式有以下几种:
CFile::modeCreate:创建一个新文件,如果该文件已经存在,则把该文件长度置为零
CFile::modeNoTruncate:与modeCreate一起使用。告诉CFile,如果要创建的文件已经存在,则不再将文件长度设置为零。这对于系统设置文件、日志文件等特别有用,因为第一次启动系统时,这些文件通常不存在,需要创建,而下次则只需要修改文件。
CFile::modeRead:打开文件用于读
CFile::modeWrite:打开文件用于写
CFile::modeReadWrite:打开文件且对文件可读可写
可以使用比特位或“|”对上述操作进行组合。比如,要打开文件写,可以用以下方式打开:
CFile file;
file.Open(“c://readme.txt”,CFile::modeCreate|CFile::modeWrite);
读文件
既然已经打开了文件,就可以对文件进行读写操作了。要读取文件内容到内存,可以调用CFile::Read()。CFile::Read()函数原型如下:
UINT Read( void* lpBuf, UINT nCount );
Read函数包含两个参数,第一个参数是一个缓冲区指针,该缓冲区用于存放从文件读进来的内容。第二个参数是要读取的字节数。Read函数返回实际读入的字节数。例如:CFile file;
char buf[100];
int nBytesRead;
nBytesRead=file.Read(buf,100);
写文件
写文件与读文件操作方式类似,通过调用CFile::Write函数来完成。
void Write( const void* lpBuf, UINT nCount );
Write函数第一个参数是指向要写入到文件中的缓冲区的指针,第二个参数是要写入到文件中的字节数。例如:
CFile file;
CString str(“This is a string.”);
file.Write(str,str.GetLength());
关闭文件在完成文件读写操作后,要调用CFile::Close成员函数及时将文件关闭。
CFile file;
//一些读写操作.....
file.Close();
7.5.2异常处理
在打开和保存文件时,我们并未作传统的错误检查,而是采用一种异常机制来处理错误。
异常处理为异常事件提供了结构化、规范化的服务。它一般是指处理错误状态。
我们先回顾一下传统的错误处理方式。传统的错误处理方式通常有两种:
1.返回错误值
2.使用goto,setjmp/longjmp协助报告错误
对于第一种技术,要求程序员记住各种错误代码,并且加入大量的检查情况。由于大多数错误是很少会发生,这样处理的结果是代码冗余性很大,效率不高。
第二种技术不但使程序可读性降低,更严重的是,使得函数里的对象不能释放、删除。比如:
void SomeOperation()
{
CMyClass obj1;
if(error)goto errHandler;
...
}
...
errHandler:
//handler error
在上面的程序片断中,由于goto跳转,无法调用obj的析构函数在退出SomeOperation()函数时释放其所占的内存,造成内存泄漏。
而且,以上两种错误处理方法都无法考虑到不可预见的错误。C++引入异常处理这一重要概念很好的解决了上述问题。异常处理在处理异常事件时会自动调用已经超出范围的局部对象的析构函数,这样就可以防止内存泄漏。
下面是OnSaveDocument()函数中的异常处理代码:
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
TRUE, AFX_IDP_INVALID_FILENAME);
return FALSE;
}
// replace calls to Serialize with SaveDIB function
BOOL bSuccess = FALSE;
TRY
{
BeginWaitCursor();
bSuccess = ::SaveDIB(m_hDIB, file);
file.Close();
}
CATCH (CException, eSave)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eSave,
TRUE, AFX_IDP_FAILED_TO_SAVE_DOC);
return FALSE;
}
END_CATCH
异常处理由一个TRY-CATCH-END_CATCH结构组成。TRY{ }语句块中包含可能发生错误的代码,可以理解为“试运行”这一语句块。CATCH{} END_CATCH子块包含了错误处理代码。如果发生错误,就转入CATCH{} END_CATCH子块执行。该子块可以根据CATCH中的参数分析产生错误的原因,报告错误或做出相应处理。
CATCH()包含两个参数,第一个参数是异常类。MFC的异常有下列几种:
MFC异常类
处理的异常
CMemoryException 内存异常 CNotSupportedException 设备不支持 CArchiveException 档案(archive)异常 CFileException 文件异常 OsErrorException 把DOS错误转换为异常 ErrnoToException 把错误号转换为异常 CResourceException 资源异常 COleException OLE异常
用户还可以从CException类派生出自己的异常类,用以处理特定类型的错误。CATCH的第二个参数是产生的异常的名字。
引起异常的原因存放在异常的数据成员m_cause中。OnSaveDocument()只是简单的处理文件保存错误,并没有指出引起错误的原因。我们可以对它进行一些修改,使它能够报告引起错误的原因。
...
TRY
{
...
}
CATCH(CFileException,e)
{
switch(e->m_cause)
{
case CFileException::accessDenied:
AfxMessageBox(“Access denied!”);
break;
case CFileException::badPath:
AfxMessageBox(“Invalid path name”);
break;
case CFileException::diskFull:
AfxMessageBox(“Disk is full”);
break;
case CFileException::hardIO:
AfxMessageBox(“Hardware error”);
break;
}
}
END_CATCH
...
}
用户也可以不必直接处理异常,而通过调用THROW_LAST(),把异常交给上一级TRY-CATCH结构来处理。其实,在DIBLOOK中,就是这么做的,请看OnSaveDocument()函数调用的SaveDIB函数的片段:
BOOL WINAPI SaveDIB(HDIB hDib, CFile& file)
{
//...
TRY
{
//...
}
CATCH (CFileException, e)
{
//...
::GlobalUnlock((HGLOBAL) hDib);
THROW_LAST();
}
END_CATCH
//...
}
在SaveDIB中,并没有直接处理异常,而是通过调用THROW_LAST(),把异常交由调用它的上一级函数OnSaveDocument()去处理。
异常并不仅仅用于错误处理。比如,在文本编辑器的CEditorDoc::Serialize()成员函数中,我们就利用读取文件引起的异常判断是否已经到了文件尾部。读者请回顾一下该函数。
异常处理给程序的错误处理带来许多便利。但是,必需意识到异常处理并不是万能的。在加入异常处理后,程序员仍然有许多工作要做。更不可以滥用异常,因为异常会带来一些开销。应用程序应当尽可能排除可能出现的错误。