MFC的默认文件菜单,提供了“新建”、“打开”、“保存”“另存为”等功能。序列化(串行化)是微软提供的用于对对象进行文件I/O的一种机制,该机制在框架(Frame)/文档(Document)/视图(View) 模式中得到了很好的应用。“打开”、“保存”、“另存为”都涉及到对象的串行化。
一、 串行化条件
一个类若需要支持串行化,则需要:
Ø 这个类从CObject派生。串行化要求对象从CObject派生,或者从一个CObject的派生类派生。因为这是MFC中实现RTTI、动态创建的基础。
Ø 该类实现了Serialize函数。Serialize函数是对象真正保存数据的函数,是整个串行化的核心。实现方法和CWuyaDoc::Serialize()一样,利用CArchive::IsStoring()和CArchive::IsLoading() 判断当前的操作,并选择<<和>>来保存和读取对象。
Ø 该类在定义时使用了DECLARE_SERIAL宏。
Ø 在类的实现文件中使用了IMPLEMENT_SERIAL宏 。
Ø 这个类有一个不带参数的构造函数,或者某一个带参数的构造函数所有的参数都提供了缺省参数。
可串行化对象条件中没有包括简单类型,对于简单类型,CArchive基本都实现了运算符<<和>>的重载,所以可以直接使用串行化方式进行读写。
简单的可串行化的类如下:
头文件(Wuya.h)
#pragma once class CWuya : public CObject { DECLARE_SERIAL(CWuya) public: CWuya(); virtual ~CWuya(); virtual void Serialize(CArchive& ar); private: CString m_strText; int m_nXML; };
实现文件(wuya.cpp)
#include "stdafx.h" #include "Wuya.h" IMPLEMENT_SERIAL(CWuya,CObject,1 | VERSIONABLE_SCHEMA) CWuya::CWuya() : m_nXML(0) { } CWuya::~CWuya() { } void CWuya::Serialize(CArchive& ar) { // 调用基类的串行化函数 CObject::Serialize(ar); if(ar.IsStoring()) // 判断是否在进行存储操作 { ar << m_strText << m_nXML; } else { ar >> m_strText >> m_nXML; } }
二、 串行化的版本控制
IMPLEMENT_SERIAL宏的第三个参数可以传入类的当前版本。倘若类wuya增加了一个成员变量CString m_strLine,同时修改Serialize函数:
void CWuya::Serialize(CArchive& ar) { // 调用基类的串行化函数 CObject::Serialize(ar); if(ar.IsStoring()) { ar << m_strText << m_nXML << m_strLine; } else { ar >> m_strText >> m_nXML >> m_strLine; } }
这时,将IMPLEMENT_SERIAL的第三个参数由1改为2。此时这个版本参数是没有任何作用的,CArchive按照一个对象一个对象读取,读到文件末尾,然后需要读入m_strLine对象,导致抛出CArchiveException异常,提示“意外的文件格式”。
1、 为了了解MFC对于串行化的整个流程,可以查看MFC的源代码,当点击菜单“打开”时:
1> CDemoApp 中的消息映射ON_COMMAND(ID_FILE_OPEN, &CWinApp::OnFileOpen),导致调用CWinApp::OnFileOpen函数;
2> 调用m_pDocManager->OnFileOpen(),导致调用CDocManager的OnFileOpen();
3> 调用函数DoPromptFileName(),弹出选择文件对话框;
4> 同时调用AfxGetApp()->OpenDocumentFile(newName);
5> 调用CWinApp中的m_pDocManager->OpenDocumentFile(lpszFileName);
6> 调用CDocument* CDocManager::OpenDocumentFile(LPCTSTR lpszFileName)。
7> 函数之后调用pBestTemplate->OpenDocumentFile(szPath),其中pBestTemplate为CDocTemplate的指针,将根据单、多文档,分别调用CSingleDocTemplate或CMultiDocTemplate中的OpenDocumentFile()函数,将创建视图、文档、子框架对象。而后调用pDocument->OnOpenDocument(lpszPathName);
8> BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName)中调用Serialize()函数,并进行了异常处理。由于CDocument从COjbect派生,CDemoDoc从CDocument,而CDemoDoc中覆写了Serialize ()函数,调用CDocument中的Serialize函数。
9> 若CDocument中的Serialize()函数调用发生异常,当前函数未进行处理,将栈展开到函数CDocument::OnOpenDocument中,从而提示“意外的文件格式”。
10> 文件打开成功后,将继续执行CSingleDocTemplate::OpenDocumentFile()中函数InitialUpdateFrame(pFrame, pDocument, bMakeVisible),从而调用函数框架pFrame->InitialUpdateFrame(pDoc, bMakeVisible),而该函数中,对视图发送了WM_INITIALUPDATE消息,对视图进行初始化。
2、 若要是存储的内容可以读出,并没有“意外的文件格式”的提示,需要修改类的Serialize函数。修改如下
void CWuya::Serialize(CArchive& ar) { // 调用基类的串行化函数 CObject::Serialize(ar); if(ar.IsStoring()) { ar << m_strText << m_nXML << m_strLine; } else { UINT uSchema = ar.GetObjectSchema(); ///< 读取存储的类版本 switch(uSchema) { case 1: ar >> m_strText >> m_nXML; m_strLine.Empty(); break; case 2: ar >> m_strText >> m_nXML >> m_strLine; break; default: AfxThrowArchiveException(CArchiveException::badSchema); break; } } }
GetObjectSchema()函数将读取文件中该类的版本号,可以根据该版本号进行判断读取的时候应该采取什么方法。以上的Serialize函数可以保证当前版本2打开版本1存储的文件。
3、 解释
1> IMPLEMENT_SERIAL宏展开之后,可以看到只针对操作符 >> 进行了重载。这是由于ReadObject()函数需要CRunTimeClass信息,而WriteObject()函数不需要。这两个函数分别在 >> 和 << 时调用。
2> 由于MFC的串行化涉及到动态创建,而动态创建的过程是使用new关键字创建的对象,在CDemoDoc的Serialize()函数中,串行化的对象都应该是指针对象,或是存放到CArray、CTypedPtrArray的指针数组。对于指针对象,应该在OnNewDocument()函数中进行初始化,并在DeleteContents()函数中进行内存释放,该函数在新建、打开、关闭时都将被调用。
3> 只通过Serialize函数对对象读写,而不使用ReadObject/WriteObject和运算符重载时,前面的可串行化条件不需要,只要实现Serialize 函数即可。或是对于现存的类,如果它没有提供串行化功能,可以通过使用重载友元operator <<和operator >>来实现。这两种方法也可以实现串行化,但如果这么做,将无法使用GetObjectSchema()获取版本号的功能。实现版本兼容将比较困难。
三、 MFC中默认文件菜单
看完了Serialize()函数中的文件读取,接着看看其他文件菜单的内部操作。在doccore.cpp中,可以看到CDocument类的消息映射中,存在如下代码:
BEGIN_MESSAGE_MAP(CDocument, CCmdTarget) //{{AFX_MSG_MAP(CDocument) ON_COMMAND(ID_FILE_CLOSE, &CDocument::OnFileClose) ON_COMMAND(ID_FILE_SAVE, &CDocument::OnFileSave) ON_COMMAND(ID_FILE_SAVE_AS, &CDocument::OnFileSaveAs) //}}AFX_MSG_MAP END_MESSAGE_MAP()
分别相应了“关闭”、“保存”和“另存为”菜单的操作。
1、 “关闭”菜单
1> void CDocument::OnFileClose()函数调用OnCloseDocument()函数,此函数为虚函数,可以再CDemoDoc中进行覆写。默认的操作关闭所有的视图,并调用虚函数DeleteContents()做清除工作。DeleteContents()函数在打开文档、新建文档时都将先被调用。
2> DeleteContents()函数为虚函数,可以再CDemoDoc中进行覆写。
2、 “保存”菜单
1> void CDocument::OnFileSave()调用DoFileSave()函数。
2> DoFileSave()函数调用DoSave()函数。
3> DoSave()函数调用OnSaveDocument()函数,该函数为虚函数。
4> 默认的OnSaveDocument()函数中调用Serialize()函数进行串行化保存。
“另存为”操作与“保存”类似,直接调用了DoSave()函数进行保存。
3、 “新建”菜单
1> CDemoApp的消息映射中:
ON_COMMAND(ID_FILE_NEW, &CWinApp::OnFileNew)
将新建菜单映射为函数CWinApp的OnFileNew函数。
2> 经过类似“打开”操作的步骤,到调用BOOL CDocument::OnNewDocument()函数。
3> 此函数也为虚函数,可以对该函数进行覆写。