本章开始咱们将了解到如何使用 Qt 处理 XML 格式的文档。html
XML(eXtensible Markup Language)是一种通用的文本格式,被普遍运用于数据交换和数据存储(虽然近年来 JSON 盛行,大有取代 XML 的趋势,可是对于一些已有系统和架构,好比 WebService,因为历史缘由,仍旧会继续使用 XML)。XML 由 World Wide Web Consortium(W3C)发布,做为 SHML(Standard Generalized Markup Language)的一种轻量级方言。XML 语法相似于 HTML,与后者的主要区别在于 XML 的标签不是固定的,而是可扩展的;其语法也比 HTML 更为严格。遵循 XML 规范的 HTML 则被称为 XHTML(不过这一点有待商榷,感兴趣的话能够详见这里)。html5
咱们说过,XML 相似一种元语言,基于 XML 能够定义出不少新语言,好比 SVG(Scalable Vector Graphics)和 MathML(Mathematical Markup Language)。SVG 是一种用于矢量绘图的描述性语言,Qt 专门提供了 QtSVG 对其进行解释;MathML 则是用于描述数学公式的语言,Qt Solutions 里面有一个 QtMmlWidget 模块专门对其进行解释。数据结构
另一面,针对 XML 的通用处理,Qt4 提供了 QtXml 模块;针对 XML 文档的 Schema 验证以及 XPath、XQuery 和 XSLT,Qt4 和 Qt5 则提供了 QtXmlPatterns 模块。Qt 提供了三种读取 XML 文档的方法:架构
QXmlStreamReader
:一种快速的基于流的方式访问良格式 XML 文档,特别适合于实现一次解析器(所谓“一次解析器”,能够理解成咱们只需读取文档一次,而后像一个遍历器从头至尾一次性处理 XML 文档,期间不会有反复的状况,也就是不会读完第一个标签,而后读第二个,读完第二个又返回去读第一个,这是不容许的);在 Qt4 中,这三种方式都位于 QtXml 模块中。Qt5 则将QXmlStreamReader
/QXmlStreamWrite
r 移动到 QtCore 中,QtXml 则标记为“再也不维护”,这已经充分代表了 Qt 的官方意向。ide
至于生成 XML 文档,Qt 一样提供了三种方式:函数
QXmlStreamWriter
,与QXmlStreamReader
相对应;使用QXmlStreamReader
是 Qt 中最快最方便的读取 XML 的方法。由于QXmlStreamReader
使用了递增式的解析器,适合于在整个 XML 文档中查找给定的标签、读入没法放入内存的大文件以及处理 XML 的自定义数据。this
每次QXmlStreamReader
的readNext()
函数调用,解析器都会读取下一个元素,按照下表中展现的类型进行处理。咱们经过表中所列的有关函数便可得到相应的数据值:spa
类型 | 示例 | 有关函数 |
StartDocument |
– | documentVersion() ,documentEncoding() ,isStandaloneDocument() |
EndDocument |
– | |
StartElement |
<item> | namespaceUri() ,name() ,attributes() ,namespaceDeclarations() |
EndElement |
</item> | namespaceUri() ,name() |
Characters |
AT&T | text() ,isWhitespace() ,isCDATA() |
Comment |
<!– fix –> | text() |
DTD |
<!DOCTYPE …> | text() ,notationDeclarations() ,entityDeclarations() ,dtdName() ,dtdPublicId() ,dtdSystemId() |
EntityReference |
™ | name() ,text() |
ProcessingInstruction |
<?alert?> | processingInstructionTarget() ,processingInstructionData() |
Invalid |
>&<! | error() , errorString() |
考虑以下 XML 片断:.net
<doc> <quote>Einmal ist keinmal</quote> </doc>
一次解析事后,咱们经过readNext()
的遍历能够得到以下信息:指针
StartDocument StartElement (name() == "doc") StartElement (name() == "quote") Characters (text() == "Einmal ist keinmal") EndElement (name() == "quote") EndElement (name() == "doc") EndDocument
经过readNext()
函数的循环调用,咱们可使用isStartElement()
、isCharacters()
这样的函数检查当前读取的类型,固然也能够直接使用state()
函数。
下面咱们看一个完整的例子。在这个例子中,咱们读取一个 XML 文档,而后使用一个QTreeWidget
显示出来。咱们的 XML 文档以下:
<bookindex> <entry term="sidebearings"> <page>10</page> <page>34-35</page> <page>307-308</page> </entry> <entry term="subtraction"> <entry term="of pictures"> <page>115</page> <page>244</page> </entry> <entry term="of vectors"> <page>9</page> </entry> </entry> </bookindex>
首先来看头文件:
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); bool readFile(const QString &fileName); private: void readBookindexElement(); void readEntryElement(QTreeWidgetItem *parent); void readPageElement(QTreeWidgetItem *parent); void skipUnknownElement(); QTreeWidget *treeWidget; QXmlStreamReader reader; };
MainWindow
显然就是咱们的主窗口,其构造函数也没有什么好说的:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(tr("XML Reader")); treeWidget = new QTreeWidget(this); QStringList headers; headers << "Items" << "Pages"; treeWidget->setHeaderLabels(headers); setCentralWidget(treeWidget); } MainWindow::~MainWindow() { }
接下来看几个处理 XML 文档的函数,这正是咱们关注的要点:
bool MainWindow::readFile(const QString &fileName) { QFile file(fileName); if (!file.open(QFile::ReadOnly | QFile::Text)) { QMessageBox::critical(this, tr("Error"), tr("Cannot read file %1").arg(fileName)); return false; } reader.setDevice(&file); while (!reader.atEnd()) { if (reader.isStartElement()) { if (reader.name() == "bookindex") { readBookindexElement(); } else { reader.raiseError(tr("Not a valid book file")); } } else { reader.readNext(); } } file.close(); if (reader.hasError()) { QMessageBox::critical(this, tr("Error"), tr("Failed to parse file %1").arg(fileName)); return false; } else if (file.error() != QFile::NoError) { QMessageBox::critical(this, tr("Error"), tr("Cannot read file %1").arg(fileName)); return false; } return true; }
readFile()
函数用于打开给定文件。咱们使用QFile
打开文件,将其设置为QXmlStreamReader
的设备。也就是说,此时QXmlStreamReader
就能够从这个设备(QFile
)中读取内容进行分析了。接下来即是一个 while 循环,只要没读到文件末尾,就要一直循环处理。首先判断是否是StartElement
,若是是的话,再去处理 bookindex 标签。注意,由于咱们的根标签就是 bookindex,若是读到的不是 bookindex,说明标签不对,就要发起一个错误(raiseError()
)。若是不是StartElement
(第一次进入循环的时候,因为没有事先调用readNext()
,因此会进入这个分支),则调用readNext()
。为何这里要用 while 循环,XML 文档不是只有一个根标签吗?直接调用一次readNext()
函数不就行了?这是由于,XML 文档在根标签以前还有别的内容,好比声明,好比 DTD,咱们不能肯定第一个readNext()
以后就是根标签。正如咱们提供的这个 XML 文档,首先是 声明,其次才是根标签。若是你说,第二个不就是根标签吗?可是 XML 文档还容许嵌入 DTD,还能够写注释,这就不肯定数目了,因此为了通用起见,咱们必须用 while 循环判断。处理完以后就能够关闭文件,若是有错误则显示错误。
接下来看readBookindexElement()
函数:
void MainWindow::readBookindexElement() { Q_ASSERT(reader.isStartElement() && reader.name() == "bookindex"); reader.readNext(); while (!reader.atEnd()) { if (reader.isEndElement()) { reader.readNext(); break; } if (reader.isStartElement()) { if (reader.name() == "entry") { readEntryElement(treeWidget->invisibleRootItem()); } else { skipUnknownElement(); } } else { reader.readNext(); } } }
注意第一行咱们加了一个断言。意思是,若是在进入函数的时候,reader 不是StartElement
状态,或者说标签不是 bookindex,就认为出错。而后继续调用readNext()
,获取下面的数据。后面仍是 while 循环。若是是EndElement
,退出,若是又是StartElement
,说明是 entry 标签(注意咱们的 XML 结构,bookindex 的子元素就是 entry),那么开始处理 entry,不然跳过。
那么下面来看readEntryElement()
函数:
void MainWindow::readEntryElement(QTreeWidgetItem *parent) { QTreeWidgetItem *item = new QTreeWidgetItem(parent); item->setText(0, reader.attributes().value("term").toString()); reader.readNext(); while (!reader.atEnd()) { if (reader.isEndElement()) { reader.readNext(); break; } if (reader.isStartElement()) { if (reader.name() == "entry") { readEntryElement(item); } else if (reader.name() == "page") { readPageElement(item); } else { skipUnknownElement(); } } else { reader.readNext(); } } }
这个函数接受一个QTreeWidgetItem
指针,做为根节点。这个节点被当作这个 entry 标签在QTreeWidget
中的根节点。咱们设置其名字是 entry 的 term 属性的值。而后继续读取下一个数据。一样使用 while 循环,若是是EndElement
就继续读取;若是是StartElement
,则按需调用readEntryElement()
或者readPageElement()
。因为 entry 标签是能够嵌套的,因此这里有一个递归调用。若是既不是 entry 也不是 page,则跳过位置标签。
而后是readPageElement()
函数:
void MainWindow::readPageElement(QTreeWidgetItem *parent) { QString page = reader.readElementText(); if (reader.isEndElement()) { reader.readNext(); } QString allPages = parent->text(1); if (!allPages.isEmpty()) { allPages += ", "; } allPages += page; parent->setText(1, allPages); }
因为 page 是叶子节点,没有子节点,因此不须要使用 while 循环读取。咱们只是遍历了 entry 下全部的 page 标签,将其拼接成合适的字符串。
最后skipUnknownElement()
函数:
void MainWindow::skipUnknownElement() { reader.readNext(); while (!reader.atEnd()) { if (reader.isEndElement()) { reader.readNext(); break; } if (reader.isStartElement()) { skipUnknownElement(); } else { reader.readNext(); } } }
咱们没办法肯定到底要跳过多少位置标签,因此仍是得用 while 循环读取,注意位置标签中全部子标签都是未知的,所以只要是StartElement
,都直接跳过。
好了,这是咱们的所有程序。只要在main()
函数中调用一下便可:
MainWindow w; w.readFile("books.xml"); w.show();
而后就能看到运行结果: