#(60):使用 DOM 處理 XML
DOM 是由 W3C 提出的一種處理 XML 文檔的標準接口。Qt 實現了 DOM Level 2 級別的不驗證讀寫 XML 文檔的方法。
與[上一章](http://www.devbean.net/2013/07/qt-study-road-2-read-xml-with-stream/)所說的流的方式不同,DOM 一次性讀入整個 XML 文檔,在內存中構造為一棵樹(被稱為 DOM 樹)。我們能夠在這棵樹上進行導航,比如移動到下一節點或者返回上一節點,也可以對這棵樹進行修改,或者是直接將這顆樹保存為硬盤上的一個 XML 文件。考慮下面一個 XML 片段:
~~~
<doc>
<quote>Scio me nihil scire</quote>
<translation>I know that I know nothing</translation>
</doc>
~~~
我們可以認為是如下一棵 DOM 樹:
~~~
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
~~~
上面表格中,帶有 [] 的可以帶有子節點,反之則不能。
下面我們還是以上一章所列出的?books.xml?這個文件來作示例。程序的目的還是一樣的:用`QTreeWidget`?來顯示這個文件的結構。需要注意的是,由于我們選用 DOM?方式處理 XML,無論是 Qt4 還是 Qt5 都需要在 .pro?文件中添加這么一句:
~~~
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()`這樣的函數。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)