Visual C++ 2012 入门经典(第6版)

存储和打印文档

 

本章要点
● 序列化的工作方式
● 如何使类的对象可序列化
● CArchive对象在序列化中的作用
● 如何在自己的类中实现序列化
● 如何在Sketcher应用程序中实现序列化
● 打印如何使用MFC
● 支持打印的视图类函数
● CPrintInfo对象包含的内容及其在打印过程中的应用
● 如何在Sketcher应用程序中实现多页打印


利用目前在Sketcher程序中所完成的工作,可以创建一个相当全面、具有各种比例视图的文档,但是因为现在没有办法保存文档,所以信息是临时的。本章将解决这个问题,分析如何在磁盘上存储文档,以及如何把文档输出到打印机上。
17.1  了解序列化
在基于MFC的程序中,文档并非一个简单的实体——它可以是非常复杂的类对象。它通常包含各种对象,而这些对象又都可能包含其他对象,这些对象仍然又都可能包含其他对象……这种结构可能延续很多层次。
虽然希望能够把文档保存在文件中,但是将类对象写入文件意味着多少会有一些问题,因为类对象不同于整数或字符串这样的基本数据项。基本数据项由已知数目的字节组成,所以将它们写入文件只要求写入适当数目的字节。因此,如果已知一个int型的值写入了文件,那么在恢复它时,只需要读取适当数目的字节即可。
将对象写入文件则是另外一回事。即使是连续地写入一个对象的所有数据成员,这也不足以恢复原始对象。类对象包含函数成员以及数据成员,所有成员,包括数据成员和函数成员,都有访问说明符;因此,要在外部文件中记录对象,写入文件的信息必须包含所有类结构的完整规范。读取过程也必须非常智能,能够根据文件中的数据完整地组合成原始对象。MFC支持一种称为序列化的机制,它能够以最少的时间和精力,帮助实现类对象的输入和输出操作。
序列化的基本思想是任何可序列化的类都必须负责存储和检索自己。这意味着,要使类成为可序列化的——就Sketcher应用程序而言,这包括CElement类和派生于它的形状类——它们就必须能够将自己写入文件。这意味着要使一个类成为可序列化的,用于声明该类数据成员的所有类类型也必须是可序列化的。
17.2  序列化文档
所有这些听起来虽然相当棘手,但是序列化文档的基本功能已经完全由Application Wizard内置到应用程序中。File | Save、File | Save As和File | Open菜单项的处理程序都假定您想对文档实现序列化,并且已经包含了支持序列化的代码。下面将分析CSketcherDoc类的定义和实现中有关使用序列化创建文档的部分内容。
17.2.1  文档类定义中的序列化
CSketcherDoc类定义中支持文档对象序列化的代码在下列代码段中以粗体显示:
class CSketcherDoc : public CDocument
{
protected: // create from serialization only
CSketcherDoc();
DECLARE_DYNCREATE(CSketcherDoc)
// Rest of the class...
// Overrides
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
// Rest of the class...
};
其中有3个部分与序列化文档对象有关:
(1) DECLARE_DYNCREATE()宏
(2) Serialize()成员函数
(3) 默认的类构造函数
DECLARE_DYNCREATE()宏在序列化输入过程中,支持应用程序框架动态地创建CSketcherDoc类的对象。在类实现中,有一个互补宏IMPLEMENT_DYNCREATE()与它配合使用。这些宏只应用于CObject派生的类,但是我们很快将看到,它们并非唯一一对可以在这种上下文中使用的宏。对于所有要序列化的类来说,CObject都必须是直接或间接基类,因为它将添加支持序列化操作的功能。这就是CElement类派生于CObject的原因。几乎所有MFC类都是派生于CObject,因此,它们都是可序列化的。

 


在CSketcherDoc类定义中还包括虚函数Serialize()的声明。每个可序列化的类都必须包括这个函数。调用它时将对CSketcherDoc类的数据成员执行输入和输出序列化操作。作为参数传递给这个函数的CArchive类对象确定将要发生的操作是输入还是输出。在讨论对文档类实现序列化时,将详细地探讨这个函数。
注意这个类显式地定义了一个默认的构造函数。这对于序列化操作来说也是必要的,因为从一个文件读取数据时,框架将使用这个默认的构造函数组合一个对象,然后利用来自这个文件的数据填充无参数构造函数生成的组合对象,以设置该对象数据成员的值。
17.2.2  文档类实现中的序列化
在SketcherDoc.cpp文件中,有两个部分与序列化有关。第一个部分是与DECLARE_ DYNCREATE()宏互补的IMPLEMENT_DYNCREATE()宏:
// SketcherDoc.cpp : implementation of the CSketcherDoc class
//
#include "stdafx.h"
// SHARED_HANDLERS can be defined in an ATL project implementing preview, thumbnail
// and search filter handlers and allows sharing of document code with that project.
#ifndef SHARED_HANDLERS
#include "Sketcher.h"
#include "PenDialog.h"
#endif
#include "SketcherDoc.h"
#include <propkey.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CSketcherDoc
IMPLEMENT_DYNCREATE(CSketcherDoc, CDocument)
// Message maps and the rest of the file...
这个宏把CSketcherDoc类的基类定义为CDocument。为了正确地动态创建CSketcherDoc对象,包括创建继承这个基类的成员,必须有这个宏。
1. Serialize()函数
在CSketcherDoc类实现中还包括Serialize()函数的定义:
void CSketcherDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
该函数序列化这个类的数据成员。传递给这个函数的参数ar是对CArchive类对象的引用。如果操作是在文件中存储数据成员,那么这个类对象的IsStoring()成员将返回TRUE,如果操作是从以前存储的文档中读回数据成员,则返回FALSE。
因为Application Wizard不知道您的文档中包含什么数据,所以就像注释所表明的那样,读写这些信息的过程全依赖于您。为了了解这个过程,下面比较详细地分析一下CArchive类。
2. CArchive类
CArchive类是发动序列化机制的引擎。C++中的流操作在控制台程序中从键盘读取数据,然后写入屏幕,CArchive类则提供了基于MFC的流操作。CArchive对象提供了一种机制,将您的对象流出后放到文件中,或者重新把它们恢复为输入流,在这个过程中自动地重新构造类的对象。
CArchive对象有一个与其相关联的CFile对象,它为二进制文件提供了磁盘输入/输出功能,并且提供到物理文件的实际连接。在序列化过程中,CFile对象处理文件输入和输出操作的所有具体问题,CArchive对象处理组织写入的对象数据或者根据读取的信息重新构造对象的逻辑问题。只有在构造自己的CArchive对象时,才需要考虑关联对象CFile的细节问题。对于Sketcher程序中的文档,框架已经进行了处理,并且把它构造的CArchive对象ar传递给CSketcherDoc中的Serialize()函数。在实现Serialize()函数的序列化时,可以在添加到形状类中的所有Serialize()函数中使用相同的对象。
CArchive类重载了析取和插入运算符(<<和>>),它们对派生于CObject的类的对象以及大量基本数据类型分别进行输入和输出操作。这些重载运算符处理下列对象类型和简单类型,见表17-1。
表  17-1

对于对象中的基本数据类型,使用插入和析取运算符序列化数据。在读写派生于CObject的可序列化类的对象时,可以针对对象调用Serialize()函数,也可以使用插入或析取运算符。无论选择使用哪种方法,对于输入和输出都必须一致,不应当在输出对象时使用插入运算符,而在读回时使用Serialize()函数,反之亦然。
如果在读取一个对象的类型但对它一无所知,如读取文档内形状列表中的指针时,那么只能使用Serialize()函数。因为这将使虚函数机制登场亮相,所以适合于所指对象类型的Serialize()函数将在运行时确定。
构造CArchive对象的目的是用于存储对象或者检索对象。如果对象用于输出,那么CArchive函数IsStoring()将返回TRUE,如果对象用于输入,则返回FALSE。前面定义CSketcherDoc类的Serialize()成员时,已经使用过这个函数。
CArchive类还有许多其他成员函数,它们涉及序列化过程的详细技术,不过在您的程序中使用序列化时,实际上不需要了解它们。
17.2.3  基于CObject的类的功能
对于从MFC类CObject派生的类来说,它们有3个层次的功能。在一个类中获得的功能层次取决于在类定义中使用的3种不同的宏,见表17-2。
表  17-2

其中每个宏都需要一个前缀为IMPLEMENT_而非DECLARE_的互补宏,这些互补宏存放在包含类实现的文件中。由表17-2可知,这些宏提供的功能逐渐增多,由于第三个宏DECLARE_SERIAL()不仅包含前两个宏的功能,而且提供了另外的功能,所以本书将主要讨论它。这就是您在自己的类中支持序列化时应当使用的宏。它要求在包含类实现的文件中添加宏IMPLEMENT_SERIAL()。
您也许想知道,为什么文档类CSketcherDoc要使用DECLARE_DYNCREATE()宏,而不使用DECLARE_SERIAL()宏。DECLARE_DYNCREATE()宏在它出现的类中动态地创建该类的对象。DECLARE_SERIAL()宏能够对类进行序列化,并且能够动态地创建类对象,所以它包含了DECLARE_DYNCREATE()宏的功能。文档类CSketcherDoc不需要序列化,因为框架只需要组合这个类对象,然后还原它的数据成员的值;但是,一个文档的数据成员必须是可序列化的,因为这是用于存储和检索它们的过程。
将序列化添加到类中的宏
如果在基于CObject的类的定义中有DECLARE_SERIAL()宏,那么可以访问CObject提供的序列化支持。这包括特殊的new和delete操作符,它们把内存泄漏检测加入到调试模式中。在使用这个宏时不需要做任何事情,因为它将自动完成操作。DECLARE_SERIAL()宏要求把类名指定为参数,所以对CElement类进行序列化时,需要在类定义中添加下列行:
DECLARE_SERIAL(CElement)

这个宏在类定义中的位置无关紧要,但是如果能够始终把它放在第一行,那么即使类定义包括许多行代码,也能够知道它是否存在。
IMPLEMENT_SERIAL()宏存储在类的实现文件中,它要求指定3个参数。第一个参数是类的名称,第二个参数是直接基类的名称,第三个参数是一个标识模式号的无符号32位整数,对于Sketcher程序来说就是版本号。如果写入对象和读取对象时使用的程序版本不同,这时类也可能不同,那么可能出现一些问题,而这个模式号能够防止序列化过程出现的这些问题。
例如,可以添加下列行到CElement类的实现中:
IMPLEMENT_SERIAL(CElement, CObject, 1001)
如果以后修改了类定义,那么需要把这个模式号修改成另外一个不同的模式号,如1002。如果这个程序试图从当前活动程序中读取利用不同模式号编写的数据,那么将抛出一个异常。这个宏最好是放在.cpp文件中#include指令和初始注释之后的第一行。
当CObject是类的间接基类时,例如,在CLine类的情况中,那么要使序列化能够在顶级类中操作,层次结构中的每个类都必须添加序列化宏。要使序列化能够在CLine类中操作,也必须在CElement中添加这些宏。
17.2.4  序列化的工作方式
图17-1以一种简化形式描述了对文档进行序列化的整个过程。
文档对象中的Serialize()函数将为它的每个数据成员调用Serialize()函数(或者使用重载的插入运算符)。如果一个成员是类对象,那么这个对象的Serialize()函数将对它的所有数据成员进行序列化,直至最后将基本数据类型写入文件。由于MFC中的大部分类最终都派生于CObject,因此它们都包含序列化支持,因而对MFC类的对象几乎始终可以进行序列化处理。

 

在类的Serialize()成员函数以及应用程序文档对象中将要处理的数据在任何情况下都是数据成员。有关的类和重新构造原始对象时需要的其他任何数据的结构都将由CArchive对象自动处理。
如果从CObject派生了多个层次的类,那么一个类中的Serialize()函数都必须调用其直接基类的Serialize()成员,以确保能够对直接基类数据成员进行序列化处理。注意序列化不支持多重继承,所以在一个层次结构中定义的每个类只能有一个基类。
17.2.5  如何实现类的序列化
根据以前的讨论,下面总结了在一个类中添加序列化时需要采取的步骤:
(1) 确保这个类是直接或间接派生于CObject。
(2) 添加DECLARE_SERIAL()宏到类定义中(如果直接基类不是CObject或另一个标准MFC类,还要在直接基类中添加这个宏)。
(3) 把Serialize()函数声明为这个类的成员函数。
(4) 在包含类实现的文件中添加IMPLEMENT_SERIAL()宏。
(5) 实现这个类的Serialize()函数。
下面讨论如何针对Sketcher程序中的文档实现序列化。
17.3  应用序列化
要在Sketcher应用程序中实现序列化,必须在CSketcherDoc类中实现Serialize()函数,以便这个函数可以处理该类的所有数据成员。对于指定可能要包括在文档中的对象的每个类,都需要添加序列化。开始在应用程序类中添加序列化之前,应当对Sketcher程序做一些小的修改,以记录用户对草图文档所做的修改。虽然这并非完全必要,但是强烈建议这样做,因为这能够防止程序在没有保存修改的情况下关闭文档。
17.3.1  记录文档修改
已经有一种机制用于记录文档的修改;它使用CSketcherDoc的一个继承成员SetModifiedFlag()。每次修改文档时都调用这个函数,可以把文档已被修改的事实记录在文档类对象的数据成员中。如果试图在没有保存已修改文档的情况下退出应用程序,就会自动显示一个提示消息。SetModifiedFlag()函数的参数是一个BOOL型的值,默认值是TRUE。如果偶尔要说明文档未被修改,那么可以用参数FALSE调用这个函数。
修改文档对象中草图的情况只有4种:
● 调用CSketcherDoc的成员AddElement()添加新元素。
● 调用CSketcherDoc的成员DeleteElement()删除元素。
● 调用文档对象的SendToBack()函数
● 移动元素。
这4种情况都容易处理。需要做的仅仅是针对这些操作中所涉及的每个函数添加对SetModified- Flag()的调用。AddElement()的定义出现在CSketcherDoc类定义中。可以把这个定义扩展为:
void AddElement(std::shared_ptr<CElement>& pElement)  // Add an element to the list
{
m_Sketch.push_back(pElement);
UpdateAllViews(nullptr, 0, pElement.get());        // Tell all the views
SetModifiedFlag();                             // Set the modified flag
}
DeleteElement()函数的定义也在CSketcherDoc定义中。应当在这个定义中添加一行如下所示的代码:
void DeleteElement(std::shared_ptr<CElement>& pElement)
{
m_Sketch.remove(pElement);
UpdateAllViews(nullptr, 0, pElement.get());        // Tell all the views
SetModifiedFlag();                             // Set the modified flag
}
SendToBack()函数也需要添加这行代码:
void SendToBack(std::shared_ptr<CElement>& pElement)
{
if(pElement)
{
m_Sketch.remove(pElement);                // Remove the element from the list
m_Sketch.push_back(pElement);             // Put a copy at the end of the list
SetModifiedFlag();                      // Set the modified flag
}
}
在视图对象中,移动元素的操作出现在由WM_MOUSEMOVE消息处理程序调用的MoveElement()成员中,但是只有在按下鼠标左按钮时才能修改文档。如果右击鼠标,那么元素将返回其原来的位置,所以只需要在OnLButtonDown()函数中添加对文档的SetModifiedFlag()函数的调用,如下所示:
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC aDC(this);                            // Create a device context
OnPrepareDC(&aDC);                             // Get origin adjusted
aDC.DPtoLP(&point);                            // convert point to Logical
CSketcherDoc* pDoc = GetDocument();               // Get a document pointer
if(m_MoveMode)
{ // In moving mode, so drop the element
m_MoveMode = false;                           // Kill move mode
auto pElement(m_pSelected);                    // Store selected address
m_pSelected = nullptr;                        // De-select the element
pDoc->UpdateAllViews(nullptr, 0, pElement.get()); // Redraw all the views
pDoc->SetModifiedFlag();                      // Set the modified flag
}
// Rest of the function as before...
}
调用视图类的继承成员GetDocument()可以访问指向文档对象的指针,然后使用这个指针调用SetModifiedFlag()函数。
在文档中可以进行修改的所有地方现在就介绍完毕。文档对象还存储了元素类型、元素颜色和线宽,所以也需要跟踪它们的修改。下面更新OnColorBlack():
void CSketcherDoc::OnColorBlack()
{
m_Color = ElementColor::BLACK;               // Set the drawing color to black
SetModifiedFlag();                         // Set the modified flag
}
给其他颜色和元素类型的处理程序添加相同的语句。设置线宽的处理程序需要修改:
void CSketcherDoc::OnPenWidth()
{
CPenDialog aDlg;                  // Create a local dialog object
aDlg.m_PenWidth = m_PenWidth;       // Set pen width as that in the document
if(aDlg.DoModal() == IDOK)       // Display the dialog as modal
{
m_PenWidth = aDlg.m_PenWidth;    // When closed with OK, get the pen width
SetModifiedFlag();               // Set the modified flag
}
}
如果在构建和运行Sketcher程序时修改文档或者添加元素,那么在退出这个程序时,将出现保存文档的提示。当然,除了可以清除修改标志以及把空文件保存到磁盘中以外,File | Save菜单选项现在还不能进行其他操作。为了可以正确地把文档连续写入磁盘,必须实现序列化,下面对此进行介绍。
17.3.2  序列化文档
第一步是针对CSketcherDoc类实现Serialize()函数。在这个函数内,为了对CSketcherDoc的数据成员进行序列化,必须添加代码。在这个类中已经声明的数据成员如下所示:
protected:
ElementType m_Element;                       // Current element type
ElementColor m_Color;                        // Current drawing color
std::list<std::shared_ptr<CElement>> m_Sketch;  // A list containing the sketch
int m_PenWidth;                             // Current pen width
CSize m_DocSize;                            // Document size
这些数据成员必须可序列化,以允许CSketcherDoc对象可反序列化。需要做的仅仅是在这个类的Serialize()成员中插入存储和检索这5个数据成员的语句。但这还是有一个问题。对象list<std::shared_ ptr<CElement>>不是可序列化的,因为其模板不是从CObject派生的。事实上,STL容器都不是可序列化的,因此必须自己处理STL容器的序列化。
但问题还有希望解决。如果能够序列化容器中的指针指向的对象,那么当把它读回来时,就能够重新构建容器。

 

如有需要电子版样章试读的请留下邮箱,一有时间就会发给大家的,更多图书更新请持续关注我们。