上回老师又布置了一个作业,用MFC实现类的序列化功能,于是乎我就滚去摸索mfc的序列化,经过两三周的时间,终于把这个会员管理器搭得差不多了,主要的功能均已实现,算是半成品吧,因为还有一个小问题没解决(虽说不影响使用)。交给老师答辩当天其实还在赶工写这个,虽然没写完,但是还是得了这次答辩的全班最高分(虽然这个程序算不上啥,不过还是请允许我在这里装一下B<-泥垢了)~
好吧闲话不扯那么多了(<-已经扯得够多了啊喂),这个会员管理器的核心功能是类的序列化和反序列化,其中还用到了许多控件和一些消息的事件,对于刚学mfc没多久的同学的学习应该也是挺不错的。运行界面如下:
这个小程序的代码量还是有点多的,在这里我就只挑关键的知识点讲解就行了,源工程会在文章后面放出
1、文件的序列化
序列化,简单地说就是向一个持久性的存储媒体,如磁盘文件保存对象或读取对象的过程。对象的序列化的主要工作就是将自己的成员变量或者当前状态保存起来,而序列化的逆过程——反序列化,其主要工作是将对象的变量、状态等数据读出来。 对某个类使用序列化的前提条件:这个类必须支持序列化。 MFC 将 CArchive类的对象用作将被序列化的对象和存储媒体之间的中介物。该对象始终与CFile 对象相关联,它从CFile 对象获得序列化所需的信息,包括文件名和请求的操作是读取还是写入。执行序列化操作的对象可在不考虑存储媒体本质的情况下使用CArchive 对象。
创建一个支持序列化的类: 从 CObject 派生类(或从 CObject 派生的某个类中派生)、重写 Serialize成员函数、使用 DECLARE_SERIAL宏(在类声明中)、定义不带参数的构造函数、为类在实现文件中使用 IMPLEMENT_SERIAL宏。
比如,在这个程序中,我写了一个叫CMember的类,用来表示一个会员,为了让这个类支持序列化,这个类写成了这样:
Member.h
#pragma once#include "afx.h"///<summary>///会员类,继承自CObject以满足序列化条件///</summary>class CMember :public CObject{public://空类型的构造函数CMember(void);~CMember(void);//使用DECLARE宏DECLARE_SERIAL(CMember)//重写Serialize成员函数void Serialize(CArchive& archive);void setName(CString name);void setMemberId(CString id);void setSex(CString sex);void setIdcardNum(CString idnum);void setJoinTime(CString jointime);void setLove(CString love);void setRemarks(CString remarks);void setImagePath(CString imagePath);CString getName();CString getMemberId();CString getSex();CString getIdcardNum();CString getJoinTime();CString getLove();CString getRemarks();CString getImagePath();//运算符=的重载//CMember& operator = (const CMember& member);private://会员号CString m_memberId;//姓名CString m_name;//身份证号CString m_idcardNum;//性别CString m_sex;//入会时间CString m_joinTime;//爱好CString m_love;//备注CString m_remarks;//用户头像CString m_imagePath;};
Member.cpp
#include "stdafx.h"#include "Member.h"//使用IMPLEMENT宏定义序列化中所需要的各种函数IMPLEMENT_SERIAL( CMember, CObject, 1 )CMember::CMember(void){}CMember::~CMember(void){}void CMember::Serialize(CArchive& archive){//首先调用基类的序列化函数以确保当前对象能被序列化CObject::Serialize(archive);//正在存储if(archive.IsStoring()){//依次序列化会员号、姓名、身份证号、性别、入会时间、爱好、备注、头像路径名称archive<<m_memberId<<m_name<<m_idcardNum<<m_sex<<m_joinTime<<m_love<<m_remarks<<m_imagePath;}//正在读取else{//依次反序列化会员号、姓名、身份证号、性别、入会时间、爱好、备注、头像路径名称archive>>m_memberId>>m_name>>m_idcardNum>>m_sex>>m_joinTime>>m_love>>m_remarks>>m_imagePath;}//也可使用 CArchive::Read及 CArchive::Write成员函数来读写大量未键入的数据}//CMember& CMember::operator = (const CMember& member)//{//if(this != &member)//{//this->m_idcardNum = member.m_idcardNum;//this->m_joinTime = member.m_joinTime;//this->m_love = member.m_love;//this->m_memberId = member.m_memberId;//this->m_name = member.m_name;//this->m_remarks = member.m_remarks;//this->m_sex = member.m_sex;//}////////返回左边的对象而不是右边的对象//return *this;//}void CMember::setName(CString name){m_name = name;}void CMember::setMemberId(CString id){m_memberId = id;}void CMember::setSex(CString sex){m_sex = sex;}void CMember::setIdcardNum(CString idnum){m_idcardNum = idnum;}void CMember::setJoinTime(CString jointime){m_joinTime = jointime;}void CMember::setLove(CString love){m_love = love;}void CMember::setRemarks(CString remarks){m_remarks = remarks;}void CMember::setImagePath(CString imagePath){m_imagePath = imagePath;}CString CMember::getName(){return m_name;}CString CMember::getMemberId(){return m_memberId;}CString CMember::getSex(){return m_sex;}CString CMember::getIdcardNum(){return m_idcardNum;}CString CMember::getJoinTime(){return m_joinTime;}CString CMember::getLove(){return m_love;}CString CMember::getRemarks(){return m_remarks;}CString CMember::getImagePath(){return m_imagePath;}
然后使用这个类进行序列化和反序列化的时候:
序列化这个类(存储数据),这里一次连续序列化了两个类的实例:
CMember member;//注意路径的"/"不要写成"\",否则会读取失败CFile file(_T("D:/MembersMessages.txt"),CFile::modeCreate|CFile::modeReadWrite);CArchive ar(&file, CArchive::store);member.setIdcardNum(_T("23333"));member.setJoinTime(_T(""));member.setLove(_T("miku"));member.setMemberId(_T("001"));member.setName(_T("新名字"));member.setRemarks(_T("没有备注。。。"));member.setSex(_T("男"));CMember member2;member2.setIdcardNum(_T("233333399999"));member2.setJoinTime(_T(""));member2.setLove(_T("miku~~~~~~~"));member2.setMemberId(_T("002"));member2.setName(_T("新名字2"));member2.setRemarks(_T("没有备注。。。。。。"));member2.setSex(_T("女"));ar.WriteObject(&member2);ar.WriteObject(&member);//确保缓存区的数据都保存到文件ar.Flush();//关闭Archive流ar.Close();//file.SeekToBegin();file.Close();
反序列化(从本地文件中读取出数据):
CMember new1;//CMember member;//读取数据的时候不应该用CFile::modeCreate,否则会抛出ExceptionCFile input(_T("D:/MembersMessages.txt"),CFile::modeReadWrite);CArchive arLoad(&input,CArchive::load);CRuntimeClass* runtimeClass = new1.GetRuntimeClass();CMember* p = (CMember*)arLoad.ReadObject(runtimeClass);runtimeClass = new2.GetRuntimeClass();//这时候p就读取出了之前存进去的数据,如果要全部读取出来,需将这整个方法循环执行p = (CMember*)arLoad.ReadObject(runtimeClass);arLoad.Close();input.Close();
另一种序列化的方法是构建一个数组或者链表,然后将需要序列化的类全部存进这些数组或者链表中,然后将这个数组或者链表序列化,这种方法如果执行成功,个人认为会较上面的方法方便很多,而且MFC中的许多集合类都支持序列化,比如CArray、CList、CPtrArray、CTypedPtrArray等,具体用法参照网上和msdn给出的方法如下:
//假设有两个CList的对象分别为dataList和loadList//序列化过程CFile output(_T("data.txt"),CFile::modeCreate|CFile::modeWrite); CArchive ar(&output, CArchive::store); dataList.Serialize(ar); ar.Close(); output.Close(); //反序列化过程,这时候loadList读取出来的内容就是dataList存进去的内容CFile input(_T("data.txt"),CFile::modeRead); CArchive in(&input,CArchive::load); loadList.Serialize(in); in.Close(); input.Close();
MFC其他的集合类的序列化的用法基本类似,不过我个人测试了许多,发现用这种序列化集合的方法问题多多,而且目前测试了多个集合类都不成功,个人不推荐使用。
2、对话框的构建
这个程序中增删改查的对话框都是一个个自定义的类,在使用他们的时候,只需要将这些类实例化,即可使用并且弹出对话框。
新增对话框的一般方法为:打开资源视图,右键->插入dialog,比如插入了一个“新增”的对话框:
然后在对话框的可视编辑窗口右键->添加类
然后输入类的名字,直接点击“完成”就可以了
调用出这个对话框的时候,只需:
//比如刚才添加添加的类的名称就是CAddDialog//创建“新增”对话框CAddDialog AddDlg;//保存DoModal返回的值INT_PTR ptr;//弹出模态对话框ptr = AddDlg.DoModal();//如果按下“取消”按钮if(ptr == IDCANCEL){//做些什么}//如果按下“确定”按钮if(ptr == IDOK){//做些什么}
3、工具栏以及一些控件的使用
由于程序仍然是基于单文档的,因此可以在资源视图编辑工具栏:
由于工具栏是共用菜单的消息事件的,所以想要让某个工具项生效,只需要将它的ID设置为某个菜单项的ID一致,然后只需要在菜单项中添加消息处理函数,比如“新增会员”工具栏选项对应菜单栏上的“会员管理”->“新增会员”,则将菜单栏上的”新增会员“的ID赋给工具栏”新增会员的“ID:
另外“选择图片”这个功能用的不是默认的控件,而是使用了MFC中的向导页和属性页:
新建两个对话框,分别是选择默认图片和从本地资源中任意选取一张图片
然后新建一个MFC类(注意是MFC类而不是普通的C++类),将这两个对话框作为自己的子页添加进去,这里我建的类名为CSelectPages:
SelectPages.h
#pragma once#include "SelectHeadPicPage.h"#include "SelectHeadPicFromLocal.h"// CSelectPagesclass CSelectPages : public CPropertySheet{DECLARE_DYNAMIC(CSelectPages)public:CSelectPages(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);CSelectPages(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);virtual ~CSelectPages();void setImagePath(CString imagePath);CString getImagePath();//选择默认图片的对话框CSelectHeadPicPage page1;//从本地选择图片的对话框CSelectHeadPicFromLocal page2;//图片的路径CString m_imagePath;protected:DECLARE_MESSAGE_MAP()};
SelectPages.cpp
// SelectPages.cpp : 实现文件//#include "stdafx.h"#include "MemberControlTest.h"#include "SelectPages.h"// CSelectPagesIMPLEMENT_DYNAMIC(CSelectPages, CPropertySheet)CSelectPages::CSelectPages(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage):CPropertySheet(nIDCaption, pParentWnd, iSelectPage){//初始化的时候将这两个对话框添加为自己的子页AddPage(&page1);AddPage(&page2);m_imagePath = _T("");}CSelectPages::CSelectPages(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage):CPropertySheet(pszCaption, pParentWnd, iSelectPage){//初始化的时候将这两个对话框添加为自己的子页AddPage(&page1);AddPage(&page2);m_imagePath = _T("");}CSelectPages::~CSelectPages(){}BEGIN_MESSAGE_MAP(CSelectPages, CPropertySheet)END_MESSAGE_MAP()void CSelectPages::setImagePath(CString imagePath){m_imagePath = imagePath;}CString CSelectPages::getImagePath(){return m_imagePath;}// CSelectPages 消息处理程序
使用的时候只需:
//创建属性页对话框INT_PTR ptr;//设置标题CSelectPages pages(_T("选择头像"));ptr = pages.DoModal();//如果按下了“确定”按钮if(ptr == IDOK){//做些什么}
关于Picture Control这个控件,想要自由改变控件的大小,需在控件的属性中将“Center Image”设置为True
之后才能方便地控制图片的自适应,不过还需要在代码中控制,控件的属性中没有控制图片自适应的方法。
这个小程序就介绍到这里,源程序地址:/detail/yueya_shanhua/8404419,有疑问还请提出,多多交流,多多指教~