baizy77 发表于 2019-8-30 14:43:35

KS04-20 用流方式读写XML

本帖最后由 baizy77 于 2019-9-2 16:19 编辑

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

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

----------------------------------------------------------------引言----------------------------------------------------------------    前面的章节中我们介绍了使用DOM方式读写XML文件。但是使用DOM方式有一个明显的缺陷,它在读取XML文件过程中需要在内存中建立DOM树结构。也就是要把DOM节点在内存中创建出来,这导致内存占用非常大,如果解析一个稍微大些的XML文件,内存占用可能非常惊人,而且性能要差些。本节我们为朋友们介绍使用流方式访问XML文件的方法。
学习建议----------------------------------------------------------------    对照本节配套的源代码进行学习。
正文----------------------------------------------------------------    使用流方式读取XML需要用到QXmlStreamReader;保存XML文件用QXmlStreamWriter。    QXmlStreamReader解析XML文件的特点是,只能从前向后解析XML文件,不能像前面章节用DOM方式那样,得到整个节点树或者某个节点的属性列表进行任意顺序访问。    我们仍然用两个例子来展示读写XML文件,为了进行对比,我们写的XML文件内容同前面用DOM方式保存的文件基本一致。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--软件特攻队-->
<doc>
    <courses count="4" institution="软件特攻队" teacher="女儿叫老白">
      <lesson url="https://study.163.com/instructor/1143174933.htm" id="1" fee="免费">C++老鸟日记</lesson>
      <lesson url="https://study.163.com/instructor/1143174933.htm" id="2" fee="免费">C++跨平台开发中的编译错误</lesson>
      <lesson url="https://study.163.com/instructor/1143174933.htm" id="3">Qt入门与提高-GUI产品开发</lesson>
      <lesson url="sorry, not ready" id="4">C++跨平台服务模块开发</lesson>
    </courses>
</doc>
    现在进入开发环节。    同样的,首先要在pro中增加对xml模块的包含:
QT                        += xml保存XML文件-QXmlStreamWriter    先设置XML文件的保存路径,并自动添加路径最后的斜杠。代码清单04-19-01main.cpp
void example01() {
QString strPath =
               ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
    if (!strPath.endsWith("/")) {
            strPath += "/";
}
//......
}
    下列保存xml的代码均位于example01接口内。    然后,用如下代码自动创建该目录。
QDir dir(strPath);
dir.mkpath(strPath); // mkpath会自动创建strPath的整个目录层次。
   然后,组织文件名,并创建文件对象:
QString strFileName = strPath + "test04_20.xml"; // 程序运行目录下的xml文件
QFile file(strFileName);
if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate))      {
// QFile::Truncate,需要清空原来的内容
                return ;
}
下面开始创建XML流对象并设置编码、版本:
QXmlStreamWriter writer(&file);
writer.setCodec("UTF-8");       // XML 编码设置为UTF-8
writer.setAutoFormatting(true); // 自动格式化
writer.writeStartDocument("1.0", true);// 开始文档(XML 声明)
    读者可以将自动格式化设置为false,然后看看保存的XML文件有何不同。    然后我们添加一行注释:
writer.writeComment(QString::fromLocal8Bit("软件特攻队"));// 注释开始写入根元素:
writer.writeStartElement("doc");// 开始根元素 <doc>开始写入doc的第一个子元素<courses>:
writer.writeStartElement("courses");// 开始子元素 <courses>为courses元素设置属性:
writer.writeAttribute("count", "4");
writer.writeAttribute("institution", QString::fromLocal8Bit("软件特攻队"));
writer.writeAttribute("teacher", QString::fromLocal8Bit("女儿叫老白"));
开始写入courses元素的第一个子元素:
// lesson
writer.writeStartElement("lesson");// 开始子元素 <lesson>
writer.writeAttribute("url", "https://study.163.com/instructor/1143174933.htm");
writer.writeAttribute("id", "1");
writer.writeAttribute("fee", QString::fromLocal8Bit("免费"));
然后写入lesson的文本子元素:
writer.writeCharacters(QString::fromLocal8Bit("C++老鸟日记"));    接着,完成第一个lesson子元素:
writer.writeEndElement();// 结束子元素 </lesson>    从上面的代码可以看出,writeEndElement()接口并未提供任何参数。因此,该接口内部实现是根据之前的代码产生的element堆栈(队列)自动添加结束元素。这就像括号嵌套一样,这里相当于添加一个右括号,它对应的是跟他配对的左括号。比如,之前最后一个元素为lesson,那么此时调用writeEndElement()接口就表示结束lesson元素(即</lesson>标签)。然后,我们循环写入另外几个lesson。在此不再赘述。
    写入courses元素的结束标签,需要使用如下语句:
writer.writeEndElement();// 结束子元素 </courses>    这里也是调用writeEndElement()接口。如前所述,它结束的元素是跟他配对的startElement,也就是courses。    最后,我们写入doc的结束元素:
writer.writeEndElement();// 结束子元素 </doc>    当所有元素结束后,我们写入文档的结束标志并关闭文档:writer.writeEndDocument();// 结束文档
file.close();

读取XML文件-QXmlStreamReader    读取XML文件时,也需要构造文件对象:代码清单04-19-02mian.cppvoid example02() {
QString strPath =
            ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
    if (!strPath.endsWith("/")) {
            strPath += "/";
    }
    // 程序运行目录下的xml文件
    QString strFileName = strPath + "test04_20.xml";
    QFile file(strFileName);
    if (!file.open(QFile::ReadOnly | QFile::Text))      {
            return;
}
//......
}
    代码清单04-19-02的代码跟保存XML文件时的代码类似,不再详述。    然后构建读取XML文件用的流对象:QXmlStreamReader reader(&file);    接着,进入主循环体进行遍历:代码清单04-19-03main.cpp
QXmlStreamReader::TokenType nType = reader.readNext();
while (!reader.atEnd()) {
       // 读取下一个元素
      nType = reader.tokenType();
      switch (nType) {
      //......
      default:
            break;
      }
      reader.readNext();
    }
}
    代码清单04-19-03中:    在第4行,我们先得到元素的类型,reader.tokenType()返回的类型为QXmlStreamReader::TokenType。    当reader对象打开XML文件后,默认指向第一行,此时,reader指向文档开头,其类型为QXmlStreamReader::StartDocument。这时可以读取XML的版本号、XML编码以及Standalone信息。其中Standalone表示本XML是否为自包含的,为false则表示需要引用外部的信息。这就像C++中包含头文件一样。    然后,我们调用reader.readNext(),将reader移动到下一个对象。下一个对象是XML文件的第一个元素:一个注释:代码清单04-19-04main.cpp
case QXmlStreamReader::Comment: {// 注释
                QString strComment = reader.text().toString();
                break;
}
    代码清单04-19-04中:    第2行的reader.text()得到的是一个QStringRef对象,它并不是一个真正的QString对象,因此需要继续转换,方法是调用toString()接口。在switch语句之后,我们调用reader.readNext()。    当继续读取到<doc>时,这是一个QXmlStreamReader::StartElement。与之对应的是</doc>,称之为EndElement.当读取到StartElement时我们需要验证是否读取到的是doc元素:代码清单04-19-05main.cpp
case QXmlStreamReader::StartElement: {// 开始元素
      QString strElementName = reader.name().toString();
      if (QString::compare(strElementName, "doc") == 0) {// 根元素
                parseDoc(reader);
      }
      break;
}
    代码清单04-19-05中:    第3行,用来判断是否为doc元素,如果是则调用parseDoc()接口继续解析。    现在我们来看一下parseDoc()接口。parseDoc()内部很简单,因为我们的XML文件里doc元素很简单,没有并列的兄弟元素,只有子元素。该接口内部也是遍历:代码清单04-19-06main.cpp:parseDoc()
void parseDoc(QXmlStreamReader& reader) {
    QXmlStreamReader::TokenType nType = reader.readNext();
    while (!reader.atEnd()) {
      nType = reader.tokenType();
      switch (nType) {
      case QXmlStreamReader::StartElement: { // 开始元素
            QString strElementName = reader.name().toString();
            if (QString::compare(strElementName, "courses") == 0) {
                qDebug() << QString::fromLocal8Bit("============== 开始元素<courses> ==============");
                // 处理courses
                parseCourses(reader);
            }
            break;
      }
      case QXmlStreamReader::EndElement: { // 结束元素
            QString strElementName = reader.name().toString();
            qDebug() << QString::fromLocal8Bit("============= 结束元素<%1> =============").arg(strElementName);
            return;
      }
      default:
            break;
      }
      nType = reader.readNext();
    }
}
    代码清单04-19-06中:    第2行,首先将reader指向下一个元素。然后,先判断XML文档是否已结束,否则遍历。    第3~24行,在遍历循环内部,此时reader指向doc的下一个元素:courses,其实它是doc元素的子元素。这时读取到的courses是一个StartElement。我们判断读取到的元素为"courses"之后,调用parseCourse(reader)继续对reader进行解析。    第25行,解析完毕后,在switch语句之后,循环体继续执行nType = reader.readNext(),此时将读取到doc的结束元素</doc>,请注意,我们这些元素的判断跟代码的执行过程有直接关系。读者需要根据设计来编写代码,比如,您希望在哪里处理对应的结束元素,就需要把代码写在哪里。我们这里是希望在parseCourse()接口内部处理完courses的结束元素,因此当调用parseCourse()接口结束后,进入上面代码的循环时,reader.readNext()将reader对象指向的就是doc的结束元素</doc>。    下面,我们看一下parseCourse()的实现。代码清单04-19-07main.cpp:parseCourses
void    parseCourses(QXmlStreamReader& reader) {
    // 将reader指向lesson子元素.
    QXmlStreamReader::TokenType nType = reader.readNext();
    while (!reader.atEnd()) {
      nType = reader.tokenType();
      switch (nType) {
      case QXmlStreamReader::StartElement: { // 开始元素
            QString strElementName = reader.name().toString();
            if (QString::compare(strElementName, "lesson") == 0) {
                // lesson元素
                qDebug() << QString::fromLocal8Bit("== 开始元素<lesson> ==");
                // 解析lesson
                parseLesson(reader);
                continue;
            }
            break;
      }
      case QXmlStreamReader::EndElement: { // 结束元素
            QString strElementName = reader.name().toString();
            qDebug() << QString::fromLocal8Bit("== 结束元素<%1>==")
                     .arg(strElementName);
            if (QString::compare(strElementName, "courses") == 0) {
                // 结束元素
                return;
            }
            break;
      }
      default:
            break;
      }
      nType = reader.readNext();
    }
}
    在代码清单04-19-07中,    该接口内部也是一个循环体。当首次进入该接口时,reader指向courses元素。因此,第3行处的代码得到的是courses下一个元素的类型,下一个元素就是courses的第一个子元素:lesson,这里读取到的是一个开始元素StartElement。    第9行,我们对该元素进行判断,如果是lesson则调用parseLesson()进行解析。    在18~27行,在switch内部,是处理结束元素的的代码。如果读取到的结束元素不是"courses"(,那么就是</doc>),就应该返回。    第31行,在switch语句之后,继续调用reader.readNext()指向下一个元素。
    接着,我们看一下parseLesson()接口:代码清单04-19-08main.cpp:parseLessonvoid parseLesson(QXmlStreamReader& reader) {
    QXmlStreamReader::TokenType nType;
    QString strElementName;
    QXmlStreamAttributes attributes;
    QXmlStreamAttributes::iterator iteAttribute;
    QString strText;
    while (!reader.atEnd()) {
      nType = reader.tokenType();
      switch (nType)
      {
      case QXmlStreamReader::StartElement:
            strElementName = reader.name().toString();
            attributes = reader.attributes();
            for (iteAttribute= attributes.begin();
               iteAttribute!= attributes.end();
               iteAttribute++) {
                qDebug() << (*iteAttribute).name()
                         << "=" << (*iteAttribute).value();
            }
            break;
      case QXmlStreamReader::EndElement:
            strElementName = reader.name().toString();   
            if (strElementName != "lesson") {
                return;
            }
            qDebug() << QString::fromLocal8Bit("== 结束元素:%1 ==")
                     .arg(strElementName);
            break;
      case QXmlStreamReader::Characters:
            strText = reader.text().toString();
            qDebug() << QString("Characters:%1").arg(strText);
            break;
      default:
            break;
      }
      nType = reader.readNext();
    }
}
    代码清单04-19-08中:    parseLesson()接口内部也是循环体,通过第36行的reader.readNext()调整reader的游标。    当刚进入parseLesson()接口时,reader的游标指向的是lesson元素,因此,循环体内部直接取元素类型:reader.tokenType()。此时读取到的应该是lesson的开始元素StartElement,也就是<lesson>。    因此,第13行,可以通过reader.attributes()接口获取该元素的属性值的集合,得到的是一个QXmlStreamAttributes类型的对象。    第14~19行,使用迭代器可以遍历该集合:    通过迭代器对象访问属性名:(*iteAttribute).name()。    通过迭代器对象访问属性的值:(*iteAttribute).value()。       第36行,循环体继续执行:reader.readNext(),此时reader对象指向lesson的文本子元素,它的类型为:QXmlStreamReader::Characters。此时可以通过下列方法得到文本的内容:QString strText = reader.text().toString();    循环体继续执行reader.readNext(),则将reader指向lesson的结束元素</lesson>。当读取到最后一组lesson元素的</lesson>结束元素后,再次执行将读取到</courses>,此时退出parseLesson()接口,回到parseCourse()接口。而parseCourse()接口将读取到</courses>,退出parseCourse()接口,回到parseDoc()接口。    在parseDoc()接口中,继续循环将读取到作为EndElement对象的</doc>并退出parseDoc()接口。在最外层的解析接口中,读取到XML文档结尾,至此整个XML文档解析完毕。结语----------------------------------------------------------------   回顾一下本节的内容:1.首先要在pro中增加对xml模块的包含:
QT                        += xml1.流方式保存XML可以使用QXmlStreamWriter。l使用writeStartDocument()编写XML文档的头部。要编写配套的writeEndDocument()。l使用setCodec()设置XML文件的编码。l使用setAutoFormatting()设置自动缩进。l使用writeComment()添加注释。l使用writeStartElement()添加一个子元素,而且要编写配套的writeEndElement()。l使用writeAttribute()为元素设置属性,属性值的类型为字符串。 2.流方式读取XML可以使用QXmlStreamReader。l使用readNext()将游标指向下一个对象。l使用while(!reader.atEnd())编写循环体。l使用tokenType()判断对象的类型。l使用name().toString()得到元素的名称。l使用text().toString()得到字符串类的对象对应的文本。     我们用流方式实现了XML文档的读写访问,使用这种方式将提高XML文档访问速度,并有效减少内存占用。XML文档中,还有很多其他对象我们没有涉及:比如DTD、命名空间等。随着朋友们对XML用法的深入,这些内容必然会接触到。但是我们要遵循循序渐进的原则,先把基本的理解了,再去研究更广泛的内容。



课程目录: 《Qt入门与提高-GUI产品开发》目录
上一节:KS04-19 用单体模式实现全局配置下一节:KS05_01 对话框-走起





页: [1]
查看完整版本: KS04-20 用流方式读写XML