- <?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[in] fileName xml文件名。
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode serializeXML(const QString& fileName, QString* pError) const;
- /**
- * @brief 用来把类对象进行文本方式序列化的函数。
- * @param[in|out] doc QDomDocument对象,需要外部构建
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode serializeXML(QDomDocument& doc, QString* pError) const;
复制代码代码清单04-17-01中,同二进制格式一样,我们也提供了两个接口,一个以文件名为参数,另一个以QDomDocument对象为参数。这样做的目的也是为了使接口更灵活,调用时更方便。
我们先来看第一个接口的实现:
代码清单04-17-02
- ESerializeCode 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-03
- ESerializeCode 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[in|out] doc QDomDocument对象,需要外部构建
- * @param[in|out] eleProvince 省级元素节点,需要外部构建
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode serializeXML(QDomDocument& doc, QDomElement& eleProvince, QString* pError) const;
复制代码接着看一下它的实现:
代码清单04-17-05
- ESerializeCode 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[in] fileName xml文件名。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode deSerializeXML(const QString& fileName, QString* pError);
- /**
- * @brief 用来把类对象进行文本方式序列化的函数。
- * @param[in|out] doc QDomDocument对象,需要外部构建
- * @return ESerializeCode枚举值。
- */
- ESerializeCode deSerializeXML(const QDomDocument& doc, QString* pError = NULL);
复制代码代码清单04-17-06中,为了方便,反序列化接口也提供了两个。第一个提供文件名作为参数,第二个提供QDomDocument对象作为参数。
代码清单04-17-07
- ESerializeCode 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-08
- ESerializeCode 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[in|out] eleProvince QDomElement对象,表示省节点。
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode deSerializeXML(const QDomElement& eleProvince, QString* pError = NULL);
复制代码接着我们看一下该接口的实现:
代码清单04-17-11
- ESerializeCode 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格式提供了很好的扩展性和兼容性,这点二进制格式有先天缺陷。但是这并不代表二进制格式不具备扩展性或者兼容性,下一节中,我们将介绍一些方法,使得二进制格式也具备一定的扩展性和兼容性。让我们拭目以待吧。