DOM 是由 W3C 提出的一种处理 XML 文档的标准接口。Qt 实现了 DOM Level 2 级别的不验证读写 XML 文档的方法。函数
与以前所说的流的方式不一样,DOM 一次性读入整个 XML 文档,在内存中构造为一棵树(被称为 DOM 树)。咱们可以在这棵树上进行导航,好比移动到下一节点或者返回上一节点,也能够对这棵树进行修改,或者是直接将这颗树保存为硬盘上的一个 XML 文件。考虑下面一个 XML 片断:this
<doc> <quote>Scio me nihil scire</quote> <translation>I know that I know nothing</translation> </doc>
咱们能够认为是以下一棵 DOM 树spa
Document |--Element(doc) |--Element(quote) | |--Text("Scio me nihil scire") |--Element(translation) |--Text("I know that I know nothing")
上面所示的 DOM 树包含了不一样类型的节点。例如,Element 类型的节点有一个开始标签和对应的一个结束标签。在开始标签和结束标签之间的内容做为这个 Element 节点的子节点。在 Qt 中,全部 DOM 节点的类型名字都以 QDom 开头,所以,QDomElement
就是 Element 节点,QDomText
就是 Text 节点。不一样类型的节点则有不一样类型的子节点。例如,Element 节点容许包含其它 Element 节点,也能够是其它类型,好比 EntityReference,Text,CDATASection,ProcessingInstruction 和 Comment。按照 W3C 的规定,咱们有以下的包含规则:指针
[Document] <- [Element] <- DocumentType <- ProcessingInstrument <- Comment [Attr] <- [EntityReference] <- Text [DocumentFragment] | [Element] | [EntityReference] | [Entity] <- [Element] <- [EntityReference] <- Text <- CDATASection <- ProcessingInstrument <- Comment
上面表格中,带有 [] 的能够带有子节点,反之则不能。code
下面咱们仍是以上一章所列出的 books.xml 这个文件来做示例。程序的目的仍是同样的:用QTreeWidget
来显示这个文件的结构。须要注意的是,因为咱们选用 DOM 方式处理 XML,不管是 Qt4 仍是 Qt5 都须要在 .pro 文件中添加这么一句:xml
QT += xml
头文件也是相似的:对象
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); bool readFile(const QString &fileName); private: void parseBookindexElement(const QDomElement &element); void parseEntryElement(const QDomElement &element, QTreeWidgetItem *parent); void parsePageElement(const QDomElement &element, QTreeWidgetItem *parent); QTreeWidget *treeWidget; };
MainWindow
的构造函数和析构函数和上一章是同样的,没有任何区别:递归
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(tr("XML DOM Reader")); treeWidget = new QTreeWidget(this); QStringList headers; headers << "Items" << "Pages"; treeWidget->setHeaderLabels(headers); setCentralWidget(treeWidget); } MainWindow::~MainWindow() { }
readFile()
函数则有了变化:接口
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; } QString errorStr; int errorLine; int errorColumn; QDomDocument doc; if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn)) { QMessageBox::critical(this, tr("Error"), tr("Parse error at line %1, column %2: %3") .arg(errorLine).arg(errorColumn).arg(errorStr)); return false; } QDomElement root = doc.documentElement(); if (root.tagName() != "bookindex") { QMessageBox::critical(this, tr("Error"), tr("Not a bookindex file")); return false; } parseBookindexElement(root); return true; }
readFile()
函数显然更长更复杂。首先须要使用QFile
打开一个文件,这点没有区别。而后咱们建立一个QDomDocument
对象,表明整个文档。注意看咱们上面介绍的结构图,Document 是 DOM 树的根节点,也就是这里的QDomDocument
;使用其setContent()
函数填充 DOM 树。setContent()
有八个重载,咱们使用了其中一个:内存
bool QDomDocument::setContent ( QIODevice * dev, bool namespaceProcessing, QString * errorMsg = 0, int * errorLine = 0, int * errorColumn = 0 )
不过,这几个重载形式都是调用了同一个实现:
bool QDomDocument::setContent ( const QByteArray & data, bool namespaceProcessing, QString * errorMsg = 0, int * errorLine = 0, int * errorColumn = 0 )
两个函数的参数基本相似。第二个函数有五个参数,第一个是QByteArray
,也就是所读取的真实数据,由QIODevice
便可得到这个数据,而QFile
就是QIODevice
的子类;第二个参数肯定是否处理命名空间,若是设置为 true,处理器会自动设置标签的前缀之类,由于咱们的 XML 文档没有命名空间,因此直接设置为 false;剩下的三个参数都是关于错误处理。后三个参数都是输出参数,咱们传入一个指针,函数会设置指针的实际值,以便咱们在外面获取并进行进一步处理。
当QDomDocument::setContent()
函数调用完毕而且没有错误后,咱们调用QDomDocument::documentElement()
函数得到一个 Document 元素。若是这个 Document 元素标签是 bookindex,则继续向下处理,不然则报错。
void MainWindow::parseBookindexElement(const QDomElement &element) { QDomNode child = element.firstChild(); while (!child.isNull()) { if (child.toElement().tagName() == "entry") { parseEntryElement(child.toElement(), treeWidget->invisibleRootItem()); } child = child.nextSibling(); } }
若是根标签正确,咱们取第一个子标签,判断子标签不为空,也就是存在子标签,而后再判断其名字是否是 entry。若是是,说明咱们正在处理 entry 标签,则调用其本身的处理函数;不然则取下一个标签(也就是nextSibling()
的返回值)继续判断。注意咱们使用这个 if 只选择 entry 标签进行处理,其它标签直接忽略掉。另外,firstChild()
和nextSibling()
两个函数的返回值都是QDomNode
。这是全部节点类的基类。当咱们须要对节点进行操做时,咱们必须将其转换成正确的子类。这个例子中咱们使用toElement()
函数将QDomNode
转换成QDomElement
。若是转换失败,返回值将是空的QDomElement
类型,其tagName()
返回空字符串,if 判断失败,其实也是符合咱们的要求的。
void MainWindow::parseEntryElement(const QDomElement &element, QTreeWidgetItem *parent) { QTreeWidgetItem *item = new QTreeWidgetItem(parent); item->setText(0, element.attribute("term")); QDomNode child = element.firstChild(); while (!child.isNull()) { if (child.toElement().tagName() == "entry") { parseEntryElement(child.toElement(), item); } else if (child.toElement().tagName() == "page") { parsePageElement(child.toElement(), item); } child = child.nextSibling(); } }
在parseEntryElement()
函数中,咱们建立了一个树组件的节点,其父节点是根节点或另一个 entry 节点。接着咱们又开始遍历这个 entry 标签的子标签。若是是 entry 标签,则递归调用自身,而且把当前节点做为父节点;不然则调用parsePageElement()
函数。
void MainWindow::parsePageElement(const QDomElement &element, QTreeWidgetItem *parent) { QString page = element.text(); QString allPages = parent->text(1); if (!allPages.isEmpty()) { allPages += ", "; } allPages += page; parent->setText(1, allPages); }
parsePageElement()
则比较简单,咱们仍是经过字符串拼接设置叶子节点的文本。这与上一章的步骤大体相同。
程序运行结果同上一章如出一辙,这里再也不贴出截图。
经过这个例子咱们能够看到,使用 DOM 当时处理 XML 文档,除了一开始的setContent()
函数,其他部分已经与原始文档没有关系了,也就是说,setContent()
函数的调用以后,已经在内存中构建好了一个完整的 DOM 树,咱们能够在这棵树上面进行移动,好比取相邻节点(nextSibling()
)。对比上一章流的方式,虽然咱们早早关闭文件,可是咱们始终使用的是readNext()
向下移动,同时也不存在readPrevious()
这样的函数。