MFC六大关键技术(第四部分)——永久保存(串行化)

MFC 六大关键技术 ( 第四部分 ) ——永久保存(串行化)

先用一句话来说明永久保存的重要:弄懂它以后,你就越来越像个程序员了!

如果我们的程序不需要永久保存,那几乎可以肯定是一个小玩儿。那怕我们的记事本、画图等小程序,也需要保存才有真正的意义。

对于 MFC 的很多地方我不甚满意,总觉得它喜欢拿一组低能而神秘的宏来故弄玄虚,但对于它的连续存储( serialize )机制,却是我十分钟爱的地方。在此,可让大家感受到面向对象的幸福。

MFC 的连续存储( serialize )机制俗称串行化。“在你的程序中尽管有着各种各样的数据, serialize 机制会象流水一样按顺序存储到单一的文件中,而又能按顺序地取出,变成各种不同的对象数据。”不知我在说上面这一句话的时候,大家有什么反应,可能很多朋友直觉是一件很简单的事情,只是说了一个“爽”字就没有下文了。

要 实现象流水一样存储其实是一个很大的难题。试想,在我们的程序里有各式各样的对象数据。如画图程序中,里面设计了点类,矩形类,圆形类等等,它们的绘图方 式及对数据的处理各不相同,用它们实现了成百上千的对象之后,如何存储起来?不想由可,一想头都大了:我们要在程序中设计函数 store() ,在我们单击“文件 / 保存”时能把各对象往里存储。那么这个 store() 函数要神通广大,它能清楚地知道我们设计的是什么样的类,产生什么样的对象。大家可能并不觉得这是一件很困难的事情,程序有能力知道我们的类的样子,对象也不过是一块初始化了存储区域罢了。就把一大堆对象“转换”成磁盘文件就行了。

即使上面的存储能成立,但当我们单击“文件 / 打开”时,程序当然不能预测用户想打开哪个文件,并且当打开文件的时候,要根据你那一大堆垃圾数据 new 出数百个对象,还原为你原来存储时的样子,你又该怎么做呢?

试 想,要是我们有一个能容纳各种不同对象的容器,这样,用户用我们的应用程序打开一个磁盘文件时,就可以把文件的内容读进我们程序的容器中。把磁盘文件读进 内存,然后识别它“是什么对象”是一件很难的事情。首先,保存过程不像电影的胶片,把景物直接映射进去,然后,看一下胶片就知道那是什么内容。可能有朋友 说它象录像磁带,拿着录像带我们看不出里面变化的磁场信号,但经过录像机就能把它还原出来。

其实不是这样的,比如保存一个矩形,程序并不是把矩形本身按点阵存储到磁盘中,因为我们绘制矩形的整个过程只不过是调用一个 GDI 函数罢了。它保存只是坐标值、线宽和某些标记等。程序面对“ 00 FF ”这样的东西,当然不知道它是一个圆或是一个字符!

拿 刚才录像带的例子,我们之所以能最后放映出来,前提我们知道这对象是“录像带”,即确定了它是什么类对象。如果我们事先只知道它“里面保存有东西,但不知 道它是什么类型的东西”,这就导致我们无法把它读出来。拿录像带到录音机去放,对录音机来说,那完全是垃圾数据。即是说,要了解永久保存,要对动态创建有 深刻的认识。

现在大家可以知道困难的根源了吧。我们在写程序的时候,会不断创造新的类,构造新的对象。这些对象,当然是旧的类对象(如 MyDocument )从未见过的。那么,我们如何才能使文档对象可以保存自己新对象呢,又能动态创建自己新的类对象呢?

许多朋友在这个时候想起了 CObject 这个类,也想到了虚函数的概念。于是以为自己“大致了解”串行化的概念。他们设想:“我们设计的 MyClass (我们想用于串行化的对象)全部从 CObject 类派生, CObject 类对象当然是 MyDocument 能认识的。”这样就实现了一个目的:本来 MyDocument 不能识别我们创建的 MyClass 对象,但它能识别 CObject 类对象。由于 MyClass CObject 类派生,我产的新类对象“是一个 CObject ”,所以 MyDocument 能把我们的新对象当作 CObiect 对象读出。或者根据书本上所说的:打开或保存文件的时候, MyDocument 会调用 Serialize (), MyDocument Serialize ()函会呼叫我们创建类的 Serialize 函数 [ 即是在MyDocument Serialize ()中调用:m_pObject -> Serialize() ,注意:在此m_pObjectCObject 类指针,它可以指向我们设计的类对象] 。最终结果是 MyDocument 的读出和保存变成了我们创建的类对象的读出和保存,这种认识是不明朗的。

有意思还有,在网上我遇到几位自以为懂了 Serialize 的朋友,居然不约而同的犯了一个很低级得让人不可思议的错误。他们说: Serialize 太简单了! Serialize ()是一个虚函数,虚函数的作用就是“优先派生类的操作”。所以 MyDocument 不实现 Serialize ()函数,留给我们自己的 MyClass 对象去调用 Serialize ()……真是哭笑不得,我们创建的类 MyClass 并不是由 MyDocument 类派生, Serialize ()函数为虚在 MyDocument MyClass 之间没有任何意义。 MyClass 产生的 MyObject 对象仅仅是 MyDocument 的一个成员变量罢了。

话说回来,由于 MyClass CObject 派生,所以CObject 类型指针能指向 MyClass 对象,并且能够让 MyClass 对象执行某些函数(特指重载的 CObject 虚函数),但前提必须在 MyClass 对象实例化了,即在内存中占领了一块存储区域之后。不过,我们的问题恰恰就是在应用程序随便打开一个文件,面对的是它不认识的 MyClass 类,当然实例化不了对象。

幸好我们在上一节课中懂得了动态创建。即想要从CObject 派生的MyClass 成为可以动态创建的对象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC 宏就可以了(注意:最终可以Serialize 的对象仅仅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL 宏,这是因为DECLARE_SERIAL/IMPLEMENT_SERIAL 包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC 宏)。

从解决上面的问题中,我们可以分步理解了:

1、   Serialize 的目的:让 MyDocument 对象在执行打开 / 保存操作时,能读出(构造)和保存它不认的 MyClass 类对象。

2、   MyDocument 对象在执行打开 / 保存操作时会调用它本身的 Serialize ()函数。但不要指望它会自动保存和读出我们的 MyClass 类对象。这个问题很容易解决,就直接在 MyDocument:: Serialize (){

// 在此函数调用MyClass 类的Serialize ()就行了!即

MyObject. Serialize ();       

}

3、   我们希望 MyClass 对象为可以动态创建的对象,所以要求在MyClass 类中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC 宏。

但目前的Serialize 机制还很抽象。我们仅仅知道了表面上的东西,实际又是如何的呢?下面作一个简单深刻的详解。

先看一下我们文档类的Serialize ()

void CMyDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())

    {

        // TODO: add storing code here

    }

    else

    {

        // TODO: add loading code here

    }

}

目前这个子数什么也没做(没有数据的读出和写入),CMyDoc 类正等待着我们去改写这个函数。现在假设CMyDoc 有一个MFC 可识别的成员变量m_MyVar, 那么函数就可改写成如下形式:

void CMyDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())     // 读写判断

    {

        ar<<m_MyVar;        //

    }

    else

    {

        ar>>m_MyVar;        //

    }

}

许多网友问:自己写的类(即 MFC 未包含的类)为什么不行?我们在 CMyDoc 里包含自写类的头文件MyClass.h ,这样CMyDoc 就认识MyDoc 类对象了。这是一般常识性的错误,MyDoc 类认识MyClass 类对象与否并没有用,关键是CArchive 类,即对象ar 不认识MyClass (当然你梦想重写CArchive 类当别论)。“>> ”、“<< ”都是CArchive 重载的操作符。上面ar>>m_MyVar 说白即是在执行一个以arm_MyVar 为参数的函数,类似于function(ar,m_MyVar) 罢了。我们当然不能传递一个它不认识的参数类型,也因此不会执行function(ar,m_MyObject) 了。

[ 注:这里我们可以用指针。让MyClassCobject 派生,一切又起了质的变化,假设我们定义了:MyClass *pMyClass = new MyClass; 因为MyClassCObject 派生,根据虚函数原理,pMyClass 也是一个CObject* ,即pMyClass 指针是CArchive 类可认识的。所以执行上述function(ar, pMyClass) ,即ar << pMyClass 是没有太多的问题(在保证了MyClass 对象可以动态创建的前提下)。]

 

回过头来,如果想让 MyClass 类对象能 Serialize ,就得让MyClassCObject 派生,Serialize ()函数在CObject 里为虚,MyClassCObject 派生之后就可以根据自己的要求去改写它,象上面改写CMyDoc::Serialize ()方法一样。这样MyClass 就得到了属于MyClass 自己特有的Serialize ()函数。

现在,程序就可以这样写:

……

#include “MyClass.h”

……

void CMyDoc::Serialize(CArchive& ar)

{

    // 在此调用 MyClass 重写过的 Serialize()

    m_MyObject. Serialize(ar);      // m_MyObject MyClass 实例

}

至此,串行化工作就算完成了,一即简单直观:从 CObject 派生自己的类,重写 Serialize () 。在此过程中,我刻意安排:在没有用到 DECLARE_SERIAL/IMPLEMENT_SERIAL 宏,也没有用到CArray 等模板类的前提下就完成了串行化的工作。我看过某些书,总是一开始就讲DECLARE_SERIAL/IMPLEMENT_SERIAL 宏或马上用CArray 模板,让读者觉得串行化就是这两个东西,导致许多朋友因此找不着北。

大家看到了,没有DECLARE_SERIAL/IMPLEMENT_SERIAL 宏和CArray 等数据结构模板也依然可以完成串行化工作。

 

现在可以腾出时间讲一下大家觉得十分抽象的 CArchive 。我们先看以下程序(注:以下程序包含动态创建等,请包含DECLARE_SERIAL/IMPLEMENT_SERIAL 宏)

void MyClass ::Serialize(CArchive& ar)

{

    if (ar.IsStoring())     // 读写判断

    {

        ar<< m_pMyVar;      // 问题:ar 如何把m_pMyVar 所指的对象变量保存到磁盘?

    }

    else

    {

        pMyClass = new MyClass; // 准备存储空间

        ar>> m_pMyVar;     

    }

}

要回答上面的问题,即“ ar<<XXX ”的问题。和 我们得看一下模拟 CArchive 的代码。

ar<<XXX ”是执行CArchive 对运算符“<< ”的重载动作。arXXX 都是该重载函数中的一参数而已。函数大致如下:

CArchive& operator<<( CArchive& ar, const CObject* pOb)

{

    …………

        // 以下为CRuntimeClass 链表中找到、识别pOb 资料。

        CRuntimeClass* pClassRef = pOb->GetRuntimeClass();

        // 保存pClassRef 即类信息(略)

       

        ((CObject*)pOb)->Serialize();// 保存MyClass 数据

    …………

}

从上面可以看出,因为 Serialize() 为虚函数,即“ar<<XXX ”的结果是执行了XXX 所指向对象本身的Serialize() 。对于“ar>>XXX ”,虽然不是“ar<<XXX ”逆过程,大家可能根据动态创建和虚函数的原理料想到它。

至此,永久保存算是写完了。在此过程中,我一直努力用最少的代码,详尽的解释来说明问题。以前我为本课题写过一个版本,并在几个论坛上发表过,但不知怎么在网上遗失(可能被删除)。所以这篇文章是我重写的版本。记得第一个版本中,我是对DECLARE_SERIAL/IMPLEMENT_SERIAL 和可串行化的数组及链表对象说了许多。这个版本中我对DECLARE_SERIAL/IMPLEMENT_SERIAL 其中奥秘几乎一句不提,目的是让大家能找到中心,有更简洁的永久保存的概念,我觉得这种感觉很好!

摘自:http://blog.csdn.net/liyi268/archive/2006/03/13/623367.aspx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ MFC中的文件打开和保存对话框可以通过CFileDialog类来实现。CFileDialog类是MFC提供的一个对话框类,用于选择文件的打开和保存操作。 要使用CFileDialog类,首先需要包含头文件<afxdlgs.h>。然后可以创建一个CFileDialog对象,并使用DoModal()函数来显示对话框。 下面是一个简单的示例代码,演示如何使用CFileDialog类来实现文件的打开和保存对话框: ```cpp #include <afxdlgs.h> // 文件打开对话框示例 void OpenFile() { CFileDialog dlg(TRUE); // 创建一个打开文件对话框 if (dlg.DoModal() == IDOK) // 显示对话框并检查用户是否点击了“打开”按钮 { CString filePath = dlg.GetPathName(); // 获取用户选择的文件路径 // 在这里可以对文件进行处理,比如读取文件内容等 } } // 文件保存对话框示例 void SaveFile() { CFileDialog dlg(FALSE); // 创建一个保存文件对话框 if (dlg.DoModal() == IDOK) // 显示对话框并检查用户是否点击了“保存”按钮 { CString filePath = dlg.GetPathName(); // 获取用户选择的文件路径 // 在这里可以将数据保存到文件中 } } ``` 在上面的示例代码中,OpenFile()函数演示了如何使用CFileDialog类来实现文件的打开对话框。首先创建一个CFileDialog对象,参数TRUE表示打开文件对话框。然后调用DoModal()函数显示对话框,并检查用户是否点击了“打开”按钮。如果用户点击了“打开”按钮,可以通过GetPathName()函数获取用户选择的文件路径。 SaveFile()函数演示了如何使用CFileDialog类来实现文件的保存对话框。与打开对话框类似,首先创建一个CFileDialog对象,参数FALSE表示保存文件对话框。然后调用DoModal()函数显示对话框,并检查用户是否点击了“保存”按钮。如果用户点击了“保存”按钮,可以通过GetPathName()函数获取用户选择的文件路径。 需要注意的是,CFileDialog类还提供了其他一些函数来设置对话框的标题、过滤器、默认文件名等属性,可以根据实际需求进行设置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值