serialization是一种思想,这个思想是——对象是连续的,这意味着我们可以在程序退出时把它们保存到磁盘上,程序重新运行是又恢复它们。(摘自VC++6.0技术内幕)
1.CArchive类
CArchive类没有基类,它的作用类似于一个二进制流,用于文件的输入输出;
它也重载了操作符"<<",">>",用法和cout和cin一样,但CArchive不仅可以输出基本类型的数据,还可以输出CObject类型的对象(包括从CObject中派生的类型)
CArchive类使用实例:(结合文件对话框的使用)
void CGraphicView::OnFileRead()
{
// TODO: Add your command handler code here
CFileDialog fileDlg(TRUE); //创建一个文本对话框,TRUE表示用于文本打开
fileDlg.m_ofn.lpstrTitle="我的文件打开对话框";
fileDlg.m_ofn.lpstrFilter="Text Files(*.txt)/0*.txt/0All Files(*.*)/0*.*/0/0";//这里注意过滤器的格式
if(IDOK==fileDlg.DoModal())
{
CFile file(fileDlg.GetFileName(),CFile::modeRead);
CArchive ar(&file,CArchive::load);
int i;
float f;
CString str;
CString fstr;
ar>>i>>f>>str;
fstr.Format("%d,%f,%s",i,f,str);
MessageBox(fstr);
}
}
void CGraphicView::OnFileWrite()
{
// TODO: Add your command handler code here
CFile file("1.txt",CFile::modeWrite|CFile::modeCreate);
CArchive ar(&file,CArchive::store);
int i=1;
float f=11.1f;
CString str="MFC13";
ar<<i<<f<<str;
}
2.修改文档标题
方法1:在CDoc中的OnNewDocument中的return前插入(程序启动或文件新建命令被响应最终都会调用该函数)
SetTitle("Never give up!");
方法2:修改IDR_MAINFRAME字符串:在两个/n之间插入字符串。在这里不仅可以设置文档标题,仔细看一下可以发现共有七处可以插入字符串,不同位置含义不同,含义按顺序分别如下:
- CDocTemplate::windowTitle 主窗口上的标题栏上的字符串
- CDocTemplate::docName 文档名称
- CDocTemplate::fileNewName 文档类型名称,出现在File/New对话框中
- CDocTemplate::filterName 过滤器
- CDocTemplate::filterExt 过滤器格式(扩展名). 这两处的格式见第12课笔记
- CDocTemplate::regFileTypeId 注册表文件类型
- CDocTemplate::regFileTypeName 注册表文件类型名称
ps:以上项目名称都可以作为参数传递给GetDocString(),该函数用来查看这些项目中的的具体内容
3.文档串行化
文件新建和文件保存操作最终要调用下面函数:
void CGraphicDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
int i=1;
float f=11.1f;
CString str="MFC13";
ar<<i<<f<<str;
}
else
{
// TODO: add loading code here
int i;
float f;
CString str;
CString fstr;
ar>>i>>f>>str;
fstr.Format("%d,%f,%s",i,f,str);
AfxMessageBox(fstr);
}
}
如果保存后立即打开刚才存储的文件,MessageBox中并不会出现我们刚才输入的文字,因为MFC会判断出此时和数据相关联的文件对象仍然存在,就不会再调用Serialize函数;
关闭应用程序窗口后再运行一次,这时MessageBox里就可以看到我们刚才输入的文字了。
4.可串行化的类
每个可串行化的类都必须有一个Serialize成员函数,要想使一个类可串行化,可以经过一下五个步骤实现:
1.把要实现串行化的类从CObject(或其派生类)中派生;
2.重写Serialize成员函数
3.在类声明使用DECLARE_SERIAL宏:DECLARE_SERIAL(class_name)//注意没有分号
4.构造不带参数的构造函数;//还不太理解为什么
5.在类的实现文件中(.cpp文件)使用IMPLEMENT_SERIAL宏:IMPLEMENT_SERIAL(class_name, base_class_name,wSchema/*版本号*/)//也没有分号
在这里,孙老师其实一共讲了2个不同的类串行化的实现,下面让我们拆开一个一个来看
一、CGraph类串行化的实现
(1)改变其头文件让其派生于CObject
(2)分别在头文件和实现文件中添加上面说的两个宏
(3)在view中增加一个CObArray类型成员,并用add()把保存着图形的CGraph对象保存到其中;
(4)重写CGraphicDoc::Serialize(CArchive& ar)函数:在文档类中CGraphicDoc中获得view指针(方法在下面详解),并通过该指针把CObArray中的元素进行串行输入或输出
void CGraphicDoc::Serialize(CArchive& ar)
{
POSITION pos=GetFirstViewPosition();
CGraphicView *pView=(CGraphicView*)GetNextView(pos);
if (ar.IsStoring())
{
// TODO: add storing code here
int nCount=pView->m_obArray.GetSize();
ar<<nCount;
for(int i=0;i<nCount;i++)
{
ar<<pView->m_obArray.GetAt(i);
}
}
else
{
ar>>nCount;
CGraph *pGraph;
for(int i=0;i<nCount;i++)
{
ar>>pGraph;
pView->m_obArray.Add(pGraph);
}
}
}
(5)实际上,调用CDoc类的Serialize()运行到ar<<pView->m_obArray.GetAt(i)(或ar>>pGraph)时,会调用CGraph类的Serialize()函数,因此需要实现该函数:
void CGraph::Serialize(CArchive& ar)
{
if(ar.IsStoring())
{
ar<<m_nDrawType<<m_ptOrigin<<m_ptEnd;
}
else
{
ar>>m_nDrawType>>m_ptOrigin>>m_ptEnd;
}
}
二、利用CObArray实现
(1)同上面一样,先完成对CGraph的串行化工作
(2)这次把CObArray的对象作为CDoc的变量,通过在View类中得到CDoc的指针(实现方法下面会具体讲到),完成对该序列的添加;
(3)CObArray本身支持串行化,这时不需要具体实现CDoc中Serialize函数,而只需要在函数末尾调用CObArray的Serialize函数即可
(4)和上面很相似,CObArray的Serialize函数又会调用CGraph的Serialize函数来完成自己的串行化。
5.文档对象数据的销毁
我们知道,通过new在堆上创建的变量,一般都要通过delete来回收,否则会发生内存泄露。
在view类中的LBUTTONUP的响应函数里,我们动态创建了一个CGraph的对象,并把它加入CObArray中:
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
因此,我们不仅要释放CGraph对象的内存,还要把它从CObArray中移除。那么这些操作的代码应该加在哪里呢?
首先,单文档程序只有一个文档对象CDocument,因此在创建新文档或打开其他文档时该文档对象就会被重复使用,在重复使用前要做的第一个工作,当然是清除文档的内容,保证文档时干净的。
我们注意到在文档创建的响应函数OnNewDocmen中会首先调用一个DeleteContents的虚函数(并由此推断打开另一个文档时也是这样的),因此,我们只要在把释放内存的代码写入DeleteContents函数中就可以了。
下面具体说一下这些代码该如何实现(这里孙老师讲的就精髓了)
1.必须保证先delete掉CGraph的指针,再从CObArray中移除。因为一旦先从CObArray移除掉,我们就无法找到CGraph指针,也就无法释放它所指向的内存了
2.关于GetSize(),其实我们可以把CObArray看做时MFC为我们提供的一个链表类(其实也是这样),当一个链表删除某个元素时,它的总数目就会自动-1,最后的两端代码实际上就是解决了这个问题:
void CGraphicDoc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
int nCount;
nCount=m_obArray.GetSize();
/*方法一:在for循环中只delete,不移除,这样就保证链表的总数不会变化 ,for循环结束后一起移除*/
/*for(int i=0;i<nCount;i++)
{
delete m_obArray.GetAt(i);
//m_obArray.RemoveAt(i);
}
m_obArray.RemoveAll();*/
/*方法二:你变我也变,每次都把总数减1,这时一种巧妙的实现方式*/
while(nCount--)
{
delete m_obArray.GetAt(nCount);
m_obArray.RemoveAt(nCount);
}
CDocument::DeleteContents();
}