1、大多数应用程序都为用户提供了数据的保存功能,这些数据可能是电子表格、字处理文档、一组数据或图形等等。从磁盘存储器上存取这些数据的工作往往是通过文件操作或者数据库操作来完成的。关于数据库操作的内容,我们将在后面的章节中进行详细的介绍,在本章的内容中,我们主要讨论如何通过文件操作实现一般意义上的数据存取工作。
通过文件操作来实现数据的存取工作通常有两种工作方式:一种是使对象具有序列化;另一种方法就是直接使用CFile对象处理文件。本章就将对这两部分内容分别做出详解。
2、序列化:在MFC当中,对象的序列化功能主要是通过文档/视图结构中特有的文档对象的序列化机制来实现的。本节,我们将详细介绍如何使用序列化机制来实现对象的序列化。
序列化,简单地说就是向一个持久性的存储媒体——如磁盘文件保存对象或读取对象的过程。序列化分为两部分,当把应用程序数据以文件形式存储在系统磁盘中时,叫做序列化;当从磁盘文件中恢复应用程序数据的状态时,叫做反序列化,这两个部分的组合构成了Visual C++中的应用程序对象的序列化。
3、 CArchive类和Serialize函数
Visual C++应用程序中的序列化是通过CArchive类来实现的。CArchive类总是与一个CFile对象相关联,CArchive类是作为CFile对象的输入输出流而设计的,如图8-00所示,它使用经过重载的C++流入(<<)和流出(>>)操作符从存储应用程序数据的文件中实现读取和写入数据,而将这些数据保存到磁盘文件中的工作由CArchive对象指示CFile对象来完成。
可以实现序列化的类——即从CObject继承而来的类,有一个叫做Serialize的成员函数,序列化工作主要是在这个函数当中进行的。当应用程序读取或写入文件时,文档对象的Serialize函数被调用,并传递用于从文件读取或向文件写入数据的CArchive对象。在Serialize函数中,要遵循的典型逻辑是通过调用CArchive类的IsStoring或IsLoading函数来判定当前行为是正在对文件写入还是读取。根据这两个函数中任何一个的返回值即可判定应用程序需要从CArchive类的I/O流中读取还是向其写入。
当用户在打开或保存拥有文档对象数据的文件或者使用文档对象的Open、Save、Save As菜单命令时,MFC便会自动调用Serialize函数,一个典型的Serialize函数如下所示:
void CAge::Serialize( CArchive& ar )
{
CObject::Serialize( ar );
if( ar.IsStoring() )
ar << m_years;
else
ar >> m_years;
}
其中,ar是一个指明应用程序序列化对象的CArchive引用参数。CArchive::Serialize成员函数可以告诉用户序列化对象当前是否用来存储或加载。可以将Serialize函数放置在所创建的任何类中,以便文档的Serialize函数中调用这些类的Serialize函数。
4、使自己的类支持序列化
在前几章讲过的例子中使用CString类的字符串来保存文本行,由于它是MFC类,因此可以串行化自己,将自己写入磁盘或从磁盘文件中读取二进制数据来建立对象。那么,如果不是标准的MFC类,比如用户自己定义的类,如何让它支持序列化呢?
要让用户定义的类支持序列化,一般分为五步:
1.从CObject或其派生类派生出用户的类
2.在类声明文件中,加入DECLARE_SERIAL宏。编译时,编译器将扩充该宏,这是串行化对象所必需的。
3.重载Serialize()成员函数,加入必要的代码,用以保存对象的数据成员到CArchive对象以及从CArchive对象载入对象的数据成员状态。
4.定义一个不带参数的构造函数。
5.在实现文件中加入IMPLEMENT_SERIAL宏。
下面将通过一个实例来演示如何让用户定义的类支持序列化功能。
5、实例:保存和显示图形
还记得第6章的绘图程序吗,用户画好的图形不仅不能保存下来,而且当窗口发生重绘时,图形也就不见了,本实例就将解决这两个问题,不仅使所画的图形在窗口重绘时依然保留,而且还给它添加了保存及再显示功能。我们在第6章绘图程序上加的内容够多了,这里为了更清晰的讲述本章的重点——序列化,将新建一个工程,当然,这个工程所要实现的功能还是和第6章绘图程序一样,只不过给它加个序列化,完整例程请参见光盘中的例子代码EX08_00,具体操作步骤如下:
l 步骤1:新建一个MFC单文档应用程序,工程名为EX08_00或用户自定义。
l 步骤2:为新建的工程先实现第6章的简单绘图功能(详细步骤请参见第6章)。
1. 在资源面板中修改原来的标准菜单,新插入一个菜单名为“绘图”,下面有四个菜单项“点”、“线”、“矩形”、“椭圆”,修改它们的ID分别为:ID_GRAPH_DOT、ID_GRAPH_LINE、ID_GRAPH_RECTANGLE、ID_GRAPH_ELLIPSE。
2. 在CEX08_00View类中添加两个成员变量CPoint m_ptOrigin和int m_nType,分别表示绘图的起点和绘图的类型,并在构造函数中初始化为0和-1。
3. 在CEX08_00View中加入四个菜单项“点”、“线”、“矩形”和“椭圆”的WM_COMMAND消息的响应函数OnGraphDot、OnGraphLine、OnGraphRectangle 、OnGraphEllipse,在消息响应函数中设置变量m_nType的值。m_nType为0,表示画点;m_nType为1,表示画线;m_nType为2,表示画矩形;m_nType为3,表示画椭圆。
4. 在CEX08_00View类中加入WM_LBUTTONDOWN和WM_LBUTTONUP的消息响应函数OnLButtonDown和OnLButtonUp 。在OnLButtonDown中保存鼠标按下的点;在OnLButtonUp中,根据m_nType的值画相应的图形。
l 步骤3:给工程添加一个可序列化的类CGraph。
1.新建一个类CGraph,从CObject派生。
打开工作台ClassView页面,鼠标右击最顶层的EX08_00 classes,在弹出的快捷方式菜单中选择New Class,在弹出的New Class对话框上,在Class type中要选Generic Class,在类名Name中输入CGraph,单击Base class下面列表框中Derived From下面高亮显示的第一栏,输入将要派生的基类CObject,后面类型为publice,如图8-01所示,然后单击OK。
图8-01 添加新类CGraph
当单击OK按钮添加该类时,会弹出一个如图8-02所示的对话框,该对话框提示Class Wizard无法为从CObject派生出的CGraph类找到合适的头文件。我们不用理会它,在此消息框中单击确定按钮即可,因为合适的头文件已经包含在CGraph类中。
2.在CGraph类中重载Serialize()成员函数。
我们要实现序列化,先对其进行改造,在工作台的ClassView页面中选择CGraph类,单击鼠标右键,选择AddMember Function增加一个成员函数,在弹出的对话框中Functiong Type中输入void,在FunctionDeclaretion编辑框中输入Serialize(CArchive& ar),然后选择Public,按OK即可。然后在ClassView中可以看到这个函数。
3. 在类CGraph的头文件中,加入DECLARE_SERIAL宏,代码如下:
class CGraph : public CObject
{
public:
voidSerialize(CArchive& ar);
CGraph();
virtual ~CGraph();
DECLARE_SERIAL(CGraph)
};
要对CGraph类实现序列化,需要在类的.h文件中加入宏DECLARE_SERIAL的调用,这个宏不需要加分号,并且后面有一个参数表示添加序列化特性的类名。
4.定义一个不带参数的构造函数。
打开工作台的CGraph类,可以看到,不带参数的构造函数已经存在于类中了,这是我们最开始创建CGraph这个新类时自动添加的。
MFC在从磁盘文件载入对象状态并重建对象时,需要有一个缺省的不带任何参数的构造函数。序列化对象将用该构造函数生成一个对象,然后调用Serialize()函数,用重建对象所需的值来填充对象的所有数据成员变量。
5.在类CGraph实现文件中加入IMPLEMENT_SERIAL宏。
打开类CGraph的实现文件,在该类的构造函数前添加MPLEMENT_SERIAL宏,代码如下:
// Graph.cpp: implementation of the CGraph class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "EX08_00.h"
#include "Graph.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
IMPLEMENT_SERIAL(CGraph,CObject,1)
CGraph::CGraph()
{
}
可见,将该宏的调用添加在构造函数前,也不需要分号。
IMPLEMENT_SERIAL宏用于定义一个从CObject派生的可序列化类的各种函数。宏的第一和第二个参数分别代表可序列化的类名和该类的直接基类。
第三个参数是对象的版本号,它是一个大于或等于零的整数。MFC序列化代码在将对象读入内存时检查版本号。如果磁盘文件上的对象的版本号和内存中的对象的版本号不一致,MFC将抛出一个CArchiveException异常,阻止程序读入一个不匹配版本的对象。
现在,我们就可以象使用标准MFC类一样使用CGraph的序列化功能了。
l 步骤4:构造CGraph类,做准备工作。
1. 在类CGraph的头文件中添加三个成员变量m_ptOrigin、m_ptEnd、m_nType。
我们既然想保存下来所画的图形,那么至少要保留住关于这些图形的一些信息,不管用户画的是线、矩形还是椭圆,它们都有一个共同点:就是由两点决定这个图形,那么从起点到终点画的到底是什么图形,就要看m_nType的值了,因此这里定义了两个CPoint型的变量,用于保存用户所画的一组图形的各个起点和终点;另一个为int型变量用来指定所画的每个图形的类型。
class CGraph : public CObject
{
public:
CPointm_ptOrigin;//记录起始点
CPointm_ptEnd;//记录终点
intm_nType;//记录画图类型
voidSerialize(CArchive& ar);
CGraph();
virtual~CGraph();
DECLARE_SERIAL(CGraph)
};
2. 给类CGraph添加一个带参数的构造函数
鼠标右击CGraph类,在弹出的快捷方式菜单中选择Add Member Function,函数类型(Function Type)编辑框中什么都不填,因为构造函数没有返回值,函数声明(FunctionDeclaration)为CGraph(intm_drawType,CPoint m_ptFrom, CPointm_ptTo),编辑这个带参数的构造函数,添加如下代码:
CGraph::CGraph(int m_drawType, CPoint m_ptFrom, CPointm_ptTo)
{
m_nType=m_drawType;
m_ptOrigin=m_ptFrom;
m_ptEnd=m_ptTo;
}
在这个对象构造函数中,用传递到构造函数的画图类型、起点和终点三个参数来初始化类CGraph中相应意义的三个变量。
l 步骤5:在文档类CEX08_00Doc中定义一个成员变量,用于保存每一个图形对象。
现在已经有了一个可以用来表示用户所绘图形的对象,那么接下来的重点是如何将这些对象保存下来。当用户画一个图形,就产生一个这样的图形对象,因此,这个对象是动态的不断增长的。
CObArray类是一个对象数组类,它可以动态调整自己的大小以适应放在它里面的元素的个数。它可以存放任何从CObject类派生出的对象(如前面的CGraph对象),它的大小只受系统的内存空间的限制。MFC中其他动态数组类包括CStringArray、CByteArray、CWordArray、CDWordArray、CPtrArray,它们的不同之处在于存放的对象的类型。
这里,我们鼠标右击CEX08_00Doc类,在弹出的快捷方式菜单中选择Add Member Variable,变量类型为CObArray,变量名为m_obArray。
l 步骤6:将用户画的每一个图形对象保存到m_obArray中。
要将图形对象保存到对象数组m_obArray中,首先就要得到对象的绘图类型和起点、终点,然后创建一个新的图形对象,并把它加入到对象数组m_obArray中。
我们知道,当用户按下鼠标左键,然后随之拖动出一个图形,最后当鼠标抬起的时候,那么就是这个图形对象生成的时候,因此,应该在CEX08_00View::OnLButtonUp函数中保存图形对象。在OnLButtonUp函数中的原来代码的尾部添加如下代码:
void CEX08_00View::OnLButtonUp(UINT nFlags, CPoint point)
{……
CGraph *pGraph=newCGraph(m_nType,m_ptOrigin, point);
GetDocument()->m_obArray.Add(pGraph);
CView::OnLButtonUp(nFlags,point);
}
这段代码中,首先声明一个CGraph类的一个指针对象,并且用该类的带参数的构造函数来构建这个对象,构造函数中的参数就是OnLButtonUp函数中用于画图的类型和起点、终点。
用户再回头看一看CGraph类中带参数的构造函数,就会明白,实际上构造函数传递过来的参数是为了初始化CGraph类的三个成员变量m_ptOrigin、m_ptEnd、m_nType,这三个成员变量构成了一个CGraph类对象,然后将该对象通过m_obArray.Add保存到对象数组中。由于对象数组是在文档类中定义的,在视图类中不能直接引用,因此,前边需要调用GetDocument函数来取得访问文档类的权利。
最后别忘了在视图类的实现文件中添加#include"Graph.h",将类CGraph的头文件包含进来。
l 步骤7:在CGraph类中完成绘图功能。
CGraph类对象中包含着三个重要的画图参数,因此,这个对象是可以绘制自身的,当视图类需要重绘图形时,它只需要向该类发送一条消息,告诉它要绘制自己就可以了。
鼠标右击CGraph类,选择Add Member Function,函数类型(Function Type)为void,函数声明(Function Declaration)为Draw(CDC* pDC),在该函数中添加如下代码:
void CGraph::Draw(CDC *pDC)
{
switch(m_nType)
{
case 0:
pDC->SetPixel(m_ptEnd.x,m_ptEnd.y,RGB(255,0,0));
break;
case 1:
pDC->MoveTo(m_ptOrigin);
pDC->LineTo(m_ptEnd);
break;
case 2:
pDC->Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptEnd.x,m_ptEnd.y);
break;
case 3:
pDC->Ellipse(m_ptOrigin.x,m_ptOrigin.y,m_ptEnd.x,m_ptEnd.y);
break;
default:
break;
}
}
在CGraph类中绘图,Draw函数中用到三个绘图参数就是CGraph类的三个成员变量。
l 步骤8:在CEX08_00View::OnDraw函数中绘图,在OnDraw函数中添入如下代码:
void CEX08_00View::OnDraw(CDC* pDC)
{
CEX08_00Doc* pDoc =GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw codefor native data here
if(pDoc->m_obArray.GetSize())
{
for(inti=0;i<pDoc->m_obArray.GetSize();i++)
{
((CGraph*)pDoc->m_obArray.GetAt(i))->Draw(pDC);
}
}
}
在这个函数中,首先判断数组对象中是否有元素,即是否有图形需要绘制,如果没有就什么都不做,如果有元素,那么就通过for循环从文档类的数组对象中依次取出图形对象(包括三个重要绘图参数),然后调用图形对象的Draw函数,即第7步骤中的Draw函数。
到现在,这个绘图程序就具备了重绘的功能,无论窗口怎样改变,用户所绘制的图形依然显示在窗口上。下面我们继续给它添加保存和再显示的功能。
l 步骤9:保存和再显示图形。
1. 打开CEX08_00Doc::Serialize函数,将原来的代码删除,添加如下代码:
void CEX08_00Doc::Serialize(CArchive& ar)
{
m_obArray.Serialize(ar);
}
这里利用了CObArray类的功能。在文档类的Serialize函数中调用对象数组的Serialize函数,该对象数组将会把指令向下传递到对象数组中,并调用每个对象的Serialize函数。因此,下面我们将完善CGraph类的Serialize函数。
2. 完善CGraph::Serialize()函数,代码如下:
void CGraph::Serialize(CArchive &ar)
{
if (ar.IsStoring())
ar<<m_nType<<m_ptOrigin<<m_ptEnd;
else
ar>>m_nType>>m_ptOrigin>>m_ptEnd;
}
在上面的代码中我们用到了>>和<<,在这里对它们作一个介绍,>>和<<是一种操作符,用来指示向CArchive对象读取还是保存数据,必要时我们可以重载重定向符。如ar>>m_nType>>m_ptOrigin>>m_ptEnd;这一句,其中>>表示从ar中读出数据m_nType, m_ptOrigin和 m_ptEnd,这个符号及>>可以连用,亦可以分开来用,如ar>> m_nType;ar>> m_ptOrigin;同样ar<<m_nType<<m_ptOrigin<<m_ptEnd;中的<<是把数据存入ar中。需要注意的是,三个变量读取和保存的顺序一定要和在类CGraph中带参数的构造函数中三个参数的声明顺序是一致的。
现在编译运行这个程序,随意画几个图形,然后选择菜单“文件”|“保存”,在弹出的“保存”对话框中输入文件名将所画图形保存到文件中,然后关闭应用程序,再重新运行该应用程序,选择“文件”|“打开”菜单,在弹出的“打开”对话框上双击先前保存有图形的文件,就可以看到先前保存的图形再一次显示在窗口上了。
6、删除文档数据
当用户单击“打开”和“新建”菜单项或按钮时会自动调用DeleteContents虚函数,这个时候是删除文档数据的最好时机。下面将以上一小节的例子EX08_00工程为基础,讲述如何删除文档数据。
打开工作台的Class View页面,鼠标右击CEX08_00Doc类,在弹出的快捷方式菜单中选择Add Virtual Function,在弹出的对话框上左边的列表框中找到DeleteContents函数,双击该函数将其加入到文档类中,用户可以在这个函数中删除文档数据。
通常,用户在删除文档数据时,经常犯以下两种错误:
1.错误方法一:
for (inti=0;i<m_obArray.GetSize();i++)
delete(CGraph*)m_obArray.GetAt(i);
m_obArray.RemoveAll();
原因:每循环一次,m_obArray.GetSize()返回的值都会减小,造成数据的漏删。
2.错误方法二:
int index= m_obArray.GetSize();
for (int i=0;i<index;i++)
{
delete (CGraph *)m_obArray.GetAt(i);
m_obArray.RemoveAt(i);
}
原因:每删除一个数组元素,数组都会重新排序,它的下标会变。
正确的删除方法应按如下方式:
void CEX08_00Doc::DeleteContents()
{
// TODO: Add yourspecialized code here and/or call the base class
intindex=m_obArray.GetSize();
while(index--)
delete(CGraph*)m_obArray.GetAt(index);
m_obArray.RemoveAll();
CDocument::DeleteContents();
}
最后不要忘了在文档类CEX08_00Doc实现文件的前面添加#include"Graph.h"语句,将类CGraph的头文件包含进来。
序列化简化了对象的保存和载入,为对象提供了持久性。但是,序列化本身仍具有一定的局限性。由于序列化一次从文件中载入所有对象,因此,它不适合于大文件编辑器和数据库。对于数据库和大文件编辑器,它们每次只是从文件中读入一部分。此时,就不应该采用文档的序列化机制来直接读取和保存文件了。另外,使用外部文件格式(预先定义的文件格式而不是本应用程序定义的文件格式)的程序一般也不使用文档的序列化。
注册表操作
注册表是Windows建造的一个复杂的信息数据库。在不同系统上,注册表的基本结构相同,如图是注册表在操作系统中的作用。
注册表是Windows的一个内部数据库,是一个巨大的树状分层的数据库,它记录了用户安装在计算机上的软件和每个程序的相互关联关系;它包含了计算机的硬件配置信息,包括自动配置的即插即用的设备和已有的各种设备。注册表中存放着各种参数,直接控制着Windows的启动、硬件驱动程序的装载以及一些Windows应用程序的运行,从而在整个系统中起着核心作用。它包括以下内容:软、硬件的有关配置的初始状态信息。注册表中保存有应用程序和资源管理其外壳的初始条件、首选项和卸载数据。整个计算机系统的设置,该系统获得的各种许可协议,应用程序与文件扩展名的关联、硬件部件的描述、状态和属性。性能记录和其他底层的系统状态信息,以及其他数据。
注册表结构:注册表主要由主键(也称控制键、根键)组成,在注册表编辑器中注册表项是用主键来显示或者编辑的。主键使得找到和编辑信息项更容易。因此,注册表使用这些条目。
各主键的简单介绍:
<1>HKEY_CLASSES_ROOT:在注册表中HKEY_CLASSES_ROOT是系统中控制所有数据文件的项,HKEY_CLASSES_ROOT主键包括了所有文件扩展和所有和执行文件相关的文件。
<2>HKEY_CURRENT_USER:该主键允许程序员和开发者易于存取目前登陆用户的设置,所有当前的操作改变只是针对当前用户而改变,并不影响其他用户。
<3>HEKY_LOCAL_MACHINE:用来控制系统和软件的设置(比如文件的位置,注册和未注册的状态,版本号等)。
<4>HEY_USERS:该主键将默认用户和当前登录用户的信息输入到注册表编辑器。
<5>HKEY_CURRENT_CONFIG:包括了系统中现有的所有配置文件的细节,其允许软件和设备驱动程序员很方便的更新注册表,而不涉及到多个配置文件信息。