找回密码
 立即注册
收起左侧

KS04-17 类的XML格式序列化

0
回复
4129
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2019-8-19 17:43:44 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
本帖最后由 baizy77 于 2019-8-28 17:13 编辑

版权声明
---------------------------------------------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
---------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------------------------------------------------------
引言
----------------------------------------------------------------
在前面的章节中,我们介绍了类的二进制格式序列化。二进制格式拥有文件尺寸小、访问性能高等特点。但是从兼容性角度来讲,二进制格式就不太方便了,而这点正是XML格式所擅长的。本节,我们为朋友们介绍类的XML格式序列化。

正文
----------------------------------------------------------------
在本节中,我们使用QDomDocument来实现类的XML格式序列化。使用QDomDocument的优点是编程方便,缺点是占用内存较大。如果您的内存空间有限,那么就需要考虑其他的XML解析方式了,比如SAX。
前面章节中我们介绍过,如果需要用QDomDocument,那么在pro中需要使用Qt的xml支持:

  1. QT        += xml
复制代码
而且还需要引入头文件
  1. #include <QDomDocument>
复制代码
我们先来看一下demo保存的XML文件:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <doc>
  3. <content name="中国" continent="">
  4. <province name="山东">
  5. <city name="济南"/>
  6. <city name="青岛"/>
  7. </province>
  8. <province name="河北">
  9. <city name="北戴河"/>
  10. <city name="张家口"/>
  11. </province>
  12. </content>
  13. </doc>
复制代码
我们首先为CCountry类增加了序列化到文件的接口:
代码清单04-17-01
  1. /**
  2. * @brief 用来把类对象进行文本方式序列化的函数。
  3. * @param[in] fileName xml文件名。
  4. * @param[in|out] pError 错误信息。
  5. * @return ESerializeCode枚举值。
  6. */
  7. ESerializeCode serializeXML(const QString& fileName, QString* pError) const;

  8. /**
  9. * @brief 用来把类对象进行文本方式序列化的函数。
  10. * @param[in|out] doc QDomDocument对象,需要外部构建
  11. * @param[in|out] pError 错误信息。
  12. * @return ESerializeCode枚举值。
  13. */
  14. ESerializeCode serializeXML(QDomDocument& doc, QString* pError) const;
复制代码
代码清单04-17-01中,同二进制格式一样,我们也提供了两个接口,一个以文件名为参数,另一个以QDomDocument对象为参数。这样做的目的也是为了使接口更灵活,调用时更方便。
我们先来看第一个接口的实现:

代码清单04-17-02
  1. ESerializeCode CCountry::serializeXML(const QString& fileName, QString* pError) const {

  2. QFile file(fileName);
  3. if (!file.open(QFile::WriteOnly | QFile::Text))
  4. {
  5. return ESERIALIZECODE_FILENOTFOND;
  6. }

  7. QTextStream out(&file);
  8. out.setCodec("UTF-8");
  9. QDomDocument document;
  10. int ret = serializeXML(document, pError);
  11. if (ret == ESERIALIZECODE_OK) {
  12. document.save(out, 4, QDomNode::EncodingFromTextStream);
  13. }
  14. file.close();
  15. return ESERIALIZECODE_OK;
  16. }
复制代码

在代码清单04-17-02中,第10行我们使用了QTextStream类来协助执行序列化,在前面章节我们已经介绍过这种方法,此处不再赘述。在该接口中调用了QDomDocument作为参数的另一个序列化接口。
最终调用document.save()时,我们使用了4个空格的缩进。保存时save()接口第三个参数表明采用了第12行为QTextStream对象设置的编码。
我们来看一下第二个接口:

代码清单04-17-03
  1. ESerializeCode CCountry::serializeXML(QDomDocument& doc, QString* pError) const {
  2. QDomElement rootDoc = doc.createElement(c_tag_doc);

  3. // 文件内容
  4. QDomElement eleContent = doc.createElement(c_tag_content);
  5. eleContent.setAttribute(c_attribute_name, m_strName);
  6. eleContent.setAttribute(c_attribute_continent, m_strContinent);

  7. QList<CProvince*>::ConstIterator iteLst = m_lstProvinces.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
  8. ESerializeCode ret = ESERIALIZECODE_OK;
  9. while (iteLst != m_lstProvinces.end()) {
  10. QDomElement eleProvince = doc.createElement(c_tag_province);
  11. ESerializeCode retcode = (*iteLst)->serializeXML(doc, eleProvince, pError);
  12. if (ESERIALIZECODE_OK != retcode) {
  13. ret = retcode;
  14. }
  15. eleContent.appendChild(eleProvince);
  16. iteLst++;
  17. }
  18. rootDoc.appendChild(eleContent);
  19. doc.appendChild(rootDoc);        
  20. return ESERIALIZECODE_OK;
  21. }
复制代码
因为在之前的章节我们已经介绍过XML文件的存取,因此我们这里重点看一下XML的组织。
我们再来看一下demo保存的XML文件:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <doc>
  3. <content name="中国" continent="">
  4. <province name="山东">
  5. <city name="济南"/>
  6. <city name="青岛"/>
  7. </province>
  8. <province name="河北">
  9. <city name="北戴河"/>
  10. <city name="张家口"/>
  11. </province>
  12. </content>
  13. </doc>
复制代码
从该XML文件可以看出,我们保存时是按照各个对象的从属关系进行保存。这是常见的方式。当然也可以把他们扁平化保存,然后通过保存关联关系从而在读取文件时恢复对象的从属关系。比如我们为每个对象创建一个id。然后在对象的XML节点中保存其父id即可。用这种方案保存的XML如下(省略了部分内容):
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <doc>
  3. <content id="1" name="中国" continent=""/>
  4. <province id="2" parent_id="1" name="山东"/>
  5. <city id="3" parent_id="2" name="济南"/>
  6. <city id="4" parent_id="2" name="青岛"/>
  7. </doc>
复制代码
我们再回到示例中的方案。在代码清单04-17-03中CCountry类的序列化接口中,通过对其子成员m_lstProvinces遍历调用序列化接口,我们将这些子成员成功保存。
下面我们看一下CProvince类的XML格式序列化。先来看一下头文件中的定义,请注意,我们在下列代码中设计第一个参数QDomDocument对象的目的是,在该接口中要用它来通过createElement()接口生成城市节点元素:

代码清单04-17-04
  1. /**
  2. * @brief 用来把类对象进行文本方式序列化的函数。
  3. * @param[in|out] doc QDomDocument对象,需要外部构建
  4. * @param[in|out] eleProvince 省级元素节点,需要外部构建
  5. * @param[in|out] pError 错误信息。
  6. * @return ESerializeCode枚举值。
  7. */
  8. ESerializeCode serializeXML(QDomDocument& doc, QDomElement& eleProvince, QString* pError) const;
复制代码
接着看一下它的实现:

代码清单04-17-05
  1. ESerializeCode CProvince::serializeXML(QDomDocument& doc, QDomElement& eleProvince, QString* pError) const
  2. {
  3. eleProvince.setAttribute(c_attribute_name, m_strName);
  4. ESerializeCode ret = ESERIALIZECODE_OK;
  5. QList<CCity*>::ConstIterator iteList = m_lstCities.constBegin();
  6. while (iteList != m_lstCities.constEnd()) {
  7. QDomElement eleCity = doc.createElement(c_tag_city);
  8. ESerializeCode retcode = (*iteList)->serializeXML(doc, eleCity, pError);
  9. if (ESERIALIZECODE_OK != retcode) {
  10. ret = retcode;
  11. }
  12. eleProvince.appendChild(eleCity);
  13. iteList++;
  14. }
  15. return ret;
  16. }
复制代码
代码清单04-17-05第8行代码中,请注意我们用传入的doc对象调用createElement()来生成城市节点。请务必不要使用临时的QDomDocument变量来完成该调用,否则可能导致程序退出时或者运行时异常。
CCity类的序列化同CCountry类似,因此不再赘述。建议读者自行调试一遍代码以便加深理解。
我们再来看一下CCountry的反序列化接口:

代码清单04-17-06
  1. /**
  2. * @brief 用来把类对象进行文本方式反序列化的函数。
  3. * @param[in] fileName xml文件名。
  4. * @return ESerializeCode枚举值。
  5. */
  6. ESerializeCode deSerializeXML(const QString& fileName, QString* pError);
  7. /**
  8. * @brief 用来把类对象进行文本方式序列化的函数。
  9. * @param[in|out] doc QDomDocument对象,需要外部构建
  10. * @return ESerializeCode枚举值。
  11. */
  12. ESerializeCode deSerializeXML(const QDomDocument& doc, QString* pError = NULL);
复制代码
代码清单04-17-06中,为了方便,反序列化接口也提供了两个。第一个提供文件名作为参数,第二个提供QDomDocument对象作为参数。

代码清单04-17-07
  1. ESerializeCode CCountry::deSerializeXML(const QString& strFileName, QString* /*pError*/) {
  2. if (strFileName.isEmpty())        {
  3. return ESERIALIZECODE_FILENOTFOND;
  4. }

  5. QFile file(strFileName);
  6. if (!file.open(QFile::ReadOnly | QFile::Text))        {        
  7. return ESERIALIZECODE_FILENOTFOND;
  8. }
  9. QDomDocument document;
  10. QString error;
  11. int row = 0, column = 0;
  12. if (!document.setContent(&file, false, &error, &row, &column))        {
  13. return ESERIALIZECODE_SETCONTENT_ERROR;
  14. }
  15. deSerializeXML(document);
  16. file.close();
  17. return ESERIALIZECODE_OK;
  18. }
复制代码
代码清单04-17-07中,deSerializeXML()接口通过调用第二个反序列化接口(代码清单04-17-08中定义)实现功能。我们直接来看第二个接口:

代码清单04-17-08
  1. ESerializeCode CCountry::deSerializeXML(const QDomDocument& doc, QString* pError) {

  2. ESerializeCode ret = ESERIALIZECODE_OK;
  3. ESerializeCode retcode = ESERIALIZECODE_OK;

  4. QDomElement rootDoc = doc.firstChildElement();
  5. if (rootDoc.nodeName() != c_tag_doc)        {
  6. if (NULL != pError)
  7. {
  8. *pError = QObject::tr("Unrecognized graphics files!");
  9. }
  10. return ESERIALIZECODE_DOC_ELEMENT_NOTFOUND;
  11. }
  12. QDomElement eleProvince = rootDoc.firstChildElement();
  13. CProvince* pProvince = NULL;
  14. while (eleProvince.isElement()) {
  15. if (eleProvince.tagName() != c_tag_province) {
  16. eleProvince = eleProvince.nextSiblingElement();
  17. continue;
  18. }
  19. pProvince = new CProvince();
  20. addProvince(pProvince);
  21. retcode = pProvince->deSerializeXML(eleProvince, pError);
  22. if (ESERIALIZECODE_OK != retcode) {
  23. ret = retcode;
  24. }        
  25. eleProvince = eleProvince.nextSiblingElement();
  26. }
  27. return ret;
  28. }
复制代码
代码清单04-17-08中,请注意第24行,在构建完pProvince对象后,请记得调用addProvince()接口,以便将新构建的CProvince对象添加到CCountry的成员列表中。上述代码所展示的反序列化过程同二进制序列化类似,其中的dom节点处理部分在前面的XML解析的章节也进行过介绍,此处不再展开。我们需要关注的是代码中第19行的c_tag_province。这是我们定义的一个字符串常量,用来表示元素的标签。我们专门针对元素标签以及属性的名字定义了一批常量字符串:

代码清单04-17-09
  1. /////////////////////////////////////////////////////////////////////////
  2. // dom元素标签定义区
  3. static const char* c_tag_content = "content";
  4. static const char* c_tag_doc = "doc";
  5. static const char* c_tag_province = "province";

  6. // dom元素属性名定义区
  7. static const char* c_attribute_name = "name";
  8. static const char* c_attribute_continent = "continent";
复制代码
这样做的好处是这些常量尽在一处定义,我们可以直接使用定义好的static const变量即可。每次需要修改时仅在一处代码修改就可以了。
另一个好处是标签、属性统一管理,防止出现命名冲突的情况。因为我们定义这些const字符串常量的时候就按照字母进行排序,新增时按照排序结果插入到相应位置,可以有效避免命名冲突的情况。
    下面我们看一下CProvince类的XML格式反序列化,首先看一下接口定义:

代码清单04-17-10
  1. /**
  2. * @brief 用来把类对象进行文本方式序列化的函数。
  3. * @param[in|out] eleProvince QDomElement对象,表示省节点。
  4. * @param[in|out] pError 错误信息。
  5. * @return ESerializeCode枚举值。
  6. */
  7. ESerializeCode deSerializeXML(const QDomElement& eleProvince, QString* pError = NULL);
复制代码
接着我们看一下该接口的实现:

代码清单04-17-11
  1. ESerializeCode CProvince::deSerializeXML(const QDomElement& eleProvince, QString* pError)
  2. {
  3. m_strName = eleProvince.attribute(c_attribute_name);
  4. QDomElement eleCity = eleProvince.firstChildElement();
  5. CCity* pCity = NULL;
  6. ESerializeCode ret = ESERIALIZECODE_OK;
  7. while (!eleCity.isNull()) {
  8. pCity = new CCity;
  9. addCity(pCity);

  10. ret = pCity->deSerializeXML(eleCity, pError);
  11. if (ESERIALIZECODE_OK != ret) {
  12. return ret;
  13. }
  14. eleCity = eleCity.nextSiblingElement();
  15. }
  16. return ret;
  17. }
复制代码
CCity类的XML反序列化接口的设计、实现跟CCountry类似,我们不再展开。读者可以自行调试代码以便加深理解。


结语
----------------------------------------------------------------

   在本节中,我们介绍了把类通过XML格式进行序列化的方法,在这里我们采用了QDomDocument类进行XML格式序列化。当然还可以通过其他方式,比如用流方式(后面章节会讲到)。XML格式提供了很好的扩展性和兼容性,这点二进制格式有先天缺陷。但是这并不代表二进制格式不具备扩展性或者兼容性,下一节中,我们将介绍一些方法,使得二进制格式也具备一定的扩展性和兼容性。让我们拭目以待吧。

课程目录: 【独家连载】《Qt入门与提高-GUI产品开发》目录

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了