本章開始咱們將瞭解到如何使用 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();
而後就能看到運行結果: