baizy77 发表于 2019-8-19 17:43:44

KS04-17 类的XML格式序列化

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

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

[*]【独家连载】《Qt入门与提高-GUI产品开发》目录
[*]网页版课程源码 提取码:1uy7
[*]免费版视频教程

----------------------------------------------------------------------------------------------------------------------
引言----------------------------------------------------------------在前面的章节中,我们介绍了类的二进制格式序列化。二进制格式拥有文件尺寸小、访问性能高等特点。但是从兼容性角度来讲,二进制格式就不太方便了,而这点正是XML格式所擅长的。本节,我们为朋友们介绍类的XML格式序列化。
正文----------------------------------------------------------------在本节中,我们使用QDomDocument来实现类的XML格式序列化。使用QDomDocument的优点是编程方便,缺点是占用内存较大。如果您的内存空间有限,那么就需要考虑其他的XML解析方式了,比如SAX。前面章节中我们介绍过,如果需要用QDomDocument,那么在pro中需要使用Qt的xml支持:
QT      += xml而且还需要引入头文件#include <QDomDocument>我们先来看一下demo保存的XML文件:<?xml version="1.0" encoding="UTF-8"?>
<doc>
<content name="中国" continent="">
<province name="山东">
<city name="济南"/>
<city name="青岛"/>
</province>
<province name="河北">
<city name="北戴河"/>
<city name="张家口"/>
</province>
</content>
</doc>
我们首先为CCountry类增加了序列化到文件的接口:代码清单04-17-01/**
* @brief 用来把类对象进行文本方式序列化的函数。
* @param fileName xml文件名。
* @param pError 错误信息。
* @return ESerializeCode枚举值。
*/
ESerializeCode serializeXML(const QString& fileName, QString* pError) const;

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

QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text))
{
return ESERIALIZECODE_FILENOTFOND;
}

QTextStream out(&file);
out.setCodec("UTF-8");
QDomDocument document;
int ret = serializeXML(document, pError);
if (ret == ESERIALIZECODE_OK) {
document.save(out, 4, QDomNode::EncodingFromTextStream);
}
file.close();
return ESERIALIZECODE_OK;
}

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

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

QList<CProvince*>::ConstIterator iteLst = m_lstProvinces.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
ESerializeCode ret = ESERIALIZECODE_OK;
while (iteLst != m_lstProvinces.end()) {
QDomElement eleProvince = doc.createElement(c_tag_province);
ESerializeCode retcode = (*iteLst)->serializeXML(doc, eleProvince, pError);
if (ESERIALIZECODE_OK != retcode) {
ret = retcode;
}
eleContent.appendChild(eleProvince);
iteLst++;
}
rootDoc.appendChild(eleContent);
doc.appendChild(rootDoc);      
return ESERIALIZECODE_OK;
}
因为在之前的章节我们已经介绍过XML文件的存取,因此我们这里重点看一下XML的组织。我们再来看一下demo保存的XML文件:<?xml version="1.0" encoding="UTF-8"?>
<doc>
<content name="中国" continent="">
<province name="山东">
<city name="济南"/>
<city name="青岛"/>
</province>
<province name="河北">
<city name="北戴河"/>
<city name="张家口"/>
</province>
</content>
</doc>
从该XML文件可以看出,我们保存时是按照各个对象的从属关系进行保存。这是常见的方式。当然也可以把他们扁平化保存,然后通过保存关联关系从而在读取文件时恢复对象的从属关系。比如我们为每个对象创建一个id。然后在对象的XML节点中保存其父id即可。用这种方案保存的XML如下(省略了部分内容):<?xml version="1.0" encoding="UTF-8"?>
<doc>
<content id="1" name="中国" continent=""/>
<province id="2" parent_id="1" name="山东"/>
<city id="3" parent_id="2" name="济南"/>
<city id="4" parent_id="2" name="青岛"/>
</doc>
我们再回到示例中的方案。在代码清单04-17-03中CCountry类的序列化接口中,通过对其子成员m_lstProvinces遍历调用序列化接口,我们将这些子成员成功保存。下面我们看一下CProvince类的XML格式序列化。先来看一下头文件中的定义,请注意,我们在下列代码中设计第一个参数QDomDocument对象的目的是,在该接口中要用它来通过createElement()接口生成城市节点元素:
代码清单04-17-04/**
* @brief 用来把类对象进行文本方式序列化的函数。
* @param doc QDomDocument对象,需要外部构建
* @param eleProvince 省级元素节点,需要外部构建
* @param pError 错误信息。
* @return ESerializeCode枚举值。
*/
ESerializeCode serializeXML(QDomDocument& doc, QDomElement& eleProvince, QString* pError) const;
接着看一下它的实现:
代码清单04-17-05ESerializeCode CProvince::serializeXML(QDomDocument& doc, QDomElement& eleProvince, QString* pError) const
{
eleProvince.setAttribute(c_attribute_name, m_strName);
ESerializeCode ret = ESERIALIZECODE_OK;
QList<CCity*>::ConstIterator iteList = m_lstCities.constBegin();
while (iteList != m_lstCities.constEnd()) {
QDomElement eleCity = doc.createElement(c_tag_city);
ESerializeCode retcode = (*iteList)->serializeXML(doc, eleCity, pError);
if (ESERIALIZECODE_OK != retcode) {
ret = retcode;
}
eleProvince.appendChild(eleCity);
iteList++;
}
return ret;
}
代码清单04-17-05第8行代码中,请注意我们用传入的doc对象调用createElement()来生成城市节点。请务必不要使用临时的QDomDocument变量来完成该调用,否则可能导致程序退出时或者运行时异常。CCity类的序列化同CCountry类似,因此不再赘述。建议读者自行调试一遍代码以便加深理解。我们再来看一下CCountry的反序列化接口:
代码清单04-17-06/**
* @brief 用来把类对象进行文本方式反序列化的函数。
* @param fileName xml文件名。
* @return ESerializeCode枚举值。
*/
ESerializeCode deSerializeXML(const QString& fileName, QString* pError);
/**
* @brief 用来把类对象进行文本方式序列化的函数。
* @param doc QDomDocument对象,需要外部构建
* @return ESerializeCode枚举值。
*/
ESerializeCode deSerializeXML(const QDomDocument& doc, QString* pError = NULL);
代码清单04-17-06中,为了方便,反序列化接口也提供了两个。第一个提供文件名作为参数,第二个提供QDomDocument对象作为参数。
代码清单04-17-07ESerializeCode CCountry::deSerializeXML(const QString& strFileName, QString* /*pError*/) {
if (strFileName.isEmpty())      {
return ESERIALIZECODE_FILENOTFOND;
}

QFile file(strFileName);
if (!file.open(QFile::ReadOnly | QFile::Text))      {      
return ESERIALIZECODE_FILENOTFOND;
}
QDomDocument document;
QString error;
int row = 0, column = 0;
if (!document.setContent(&file, false, &error, &row, &column))      {
return ESERIALIZECODE_SETCONTENT_ERROR;
}
deSerializeXML(document);
file.close();
return ESERIALIZECODE_OK;
}
代码清单04-17-07中,deSerializeXML()接口通过调用第二个反序列化接口(代码清单04-17-08中定义)实现功能。我们直接来看第二个接口:
代码清单04-17-08ESerializeCode CCountry::deSerializeXML(const QDomDocument& doc, QString* pError) {

ESerializeCode ret = ESERIALIZECODE_OK;
ESerializeCode retcode = ESERIALIZECODE_OK;

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

// dom元素属性名定义区
static const char* c_attribute_name = "name";
static const char* c_attribute_continent = "continent";
这样做的好处是这些常量尽在一处定义,我们可以直接使用定义好的static const变量即可。每次需要修改时仅在一处代码修改就可以了。另一个好处是标签、属性统一管理,防止出现命名冲突的情况。因为我们定义这些const字符串常量的时候就按照字母进行排序,新增时按照排序结果插入到相应位置,可以有效避免命名冲突的情况。
    下面我们看一下CProvince类的XML格式反序列化,首先看一下接口定义:
代码清单04-17-10/**
* @brief 用来把类对象进行文本方式序列化的函数。
* @param eleProvince QDomElement对象,表示省节点。
* @param pError 错误信息。
* @return ESerializeCode枚举值。
*/
ESerializeCode deSerializeXML(const QDomElement& eleProvince, QString* pError = NULL);
接着我们看一下该接口的实现:
代码清单04-17-11ESerializeCode CProvince::deSerializeXML(const QDomElement& eleProvince, QString* pError)
{
m_strName = eleProvince.attribute(c_attribute_name);
QDomElement eleCity = eleProvince.firstChildElement();
CCity* pCity = NULL;
ESerializeCode ret = ESERIALIZECODE_OK;
while (!eleCity.isNull()) {
pCity = new CCity;
addCity(pCity);

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

结语----------------------------------------------------------------
   在本节中,我们介绍了把类通过XML格式进行序列化的方法,在这里我们采用了QDomDocument类进行XML格式序列化。当然还可以通过其他方式,比如用流方式(后面章节会讲到)。XML格式提供了很好的扩展性和兼容性,这点二进制格式有先天缺陷。但是这并不代表二进制格式不具备扩展性或者兼容性,下一节中,我们将介绍一些方法,使得二进制格式也具备一定的扩展性和兼容性。让我们拭目以待吧。
课程目录: 【独家连载】《Qt入门与提高-GUI产品开发》目录
上一节:KS04-16   类的二进制格式序列化-读取下一节:KS04-18 类的二进制格式序列化-向前兼容
页: [1]
查看完整版本: KS04-17 类的XML格式序列化