QT中讀取XML檔案三種方式 的例項
第一部分:QXmlStreamReader
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(gml(1969)->sgml(1985)->html(1993)->xml(1998))。
我們說過,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 文件,期間不會有反覆的情況,也就是不會讀完第一個標籤,然後讀第二個,讀完第二個又返回去讀第一個,這是不允許的);- DOM(Document Object Model):將整個 XML 文件讀入記憶體,構建成一個樹結構,允許程式在樹結構上向前向後移動導航,這是與另外兩種方式最大的區別,也就是允許實現多次解析器(對應於前面所說的一次解析器)。DOM 方式帶來的問題是需要一次性將整個 XML 文件讀入記憶體,因此會佔用很大記憶體;
- SAX(Simple API for XML):提供大量虛擬函式,以事件的形式處理 XML 文件。這種解析辦法主要是由於歷史原因提出的,為了解決 DOM 的記憶體佔用提出的(在現代計算機上,這個一般已經不是問題了)。
在 Qt4 中,這三種方式都位於 QtXml 模組中。Qt5 則將QXmlStreamReader
/QXmlStreamWriter
移動到 QtCore 中,QtXml 則標記為“不再維護”,這已經充分表明了 Qt 的官方意向。
至於生成 XML 文件,Qt 同樣提供了三種方式:
QXmlStreamWriter
,與QXmlStreamReader
相對應;- DOM 方式,首先在記憶體中生成 DOM 樹,然後將 DOM 樹寫入檔案。不過,除非我們程式的資料結構中本來就維護著一個 DOM 樹,否則,臨時生成樹再寫入肯定比較麻煩;
- 純手工生成 XML 文件,顯然,這是最複雜的一種方式。
使用QXmlStreamReader
是 Qt 中最快最方便的讀取 XML 的方法。因為QXmlStreamReader
使用了遞增式的解析器,適合於在整個 XML 文件中查詢給定的標籤、讀入無法放入記憶體的大檔案以及處理 XML 的自定義資料。
每次QXmlStreamReader
的readNext()
函式呼叫,解析器都會讀取下一個元素,按照下表中展示的型別進行處理。我們通過表中所列的有關函式即可獲得相應的資料值:
型別 | 例項 | 有關函式 |
StartDocument | documentVersion() ,documentEncoding() ,isStandaloneDocument() |
|
EndDocument | ||
StartElement | namespaceUri() ,name() ,attributes() ,namespaceDeclarations() |
|
EndElement | namespaceUri() ,name() |
|
Characters | text() ,isWhitespace() ,isCDATA() |
|
Comment | text() | |
DTD | text() ,notationDeclarations() ,entityDeclarations() ,dtdName() ,dtdPublicId() , |
|
EntityReference | name() ,text() |
|
ProcessingInstruction | processingInstructionTarget() ,processingInstructionData() |
<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 version="1.0" encoding="utf-8"?>
<bookindex> <!--根標籤-->
<entry term="葉節點0">
<page>10</page>
<page>34-35</page>
<page>307-308</page>
</entry>
<entry term="葉節點1">
<entry term="葉節點1.1">
<page>115</page>
<page>244</page>
</entry>
<entry term="葉節點1.2">
<page>9</page>
</entry>
</entry>
<entry term="葉節點2">
<entry term="葉節點2.1">
<page>115</page>
<page>244</page>
</entry>
<entry term="葉節點2.2">
<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;
private:
Ui::MainWindow *ui;
};
MainWindow
顯然就是我們的主視窗,其建構函式也沒有什麼好說的:
setWindowTitle(tr("XML Reader"));
treeWidget = new QTreeWidget(this);
QStringList headers;
headers << "Items" << "Pages";
treeWidget->setHeaderLabels(headers);
setCentralWidget(treeWidget);
上面是建構函式
QFile file(QApplication::applicationDirPath() + "/demo.xml");
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())
{
qDebug()<<"2222222222222222";
if (reader.name() == "bookindex")
{
readBookindexElement();//遞迴下降演算法,層層讀取
}
else
{
reader.raiseError(tr("Not a valid book file"));
}
}
else
{
qDebug()<<"111111111111111";
reader.readNext(); //循壞呼叫首次移動3次,後面移動一次
}
}
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 迴圈判斷。處理完之後就可以關閉檔案,如果有錯誤則顯示錯誤。
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())
{
qDebug()<<"3333333333333333";
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
,都直接跳過。
然後就能看到執行結果:
第二部分: DOM(Document Object Model)
DOM 是由 W3C 提出的一種處理 XML 文件的標準介面。Qt 實現了 DOM Level 2 級別的不驗證讀寫 XML 文件的方法。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:
explicit 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;
private:
Ui::MainWindow *ui;
};
建構函式與上面類似
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle(tr("XML DOM Reader"));
treeWidget = new QTreeWidget(this);
QStringList headers;
headers << "Items" << "Pages";
treeWidget->setHeaderLabels(headers);
setCentralWidget(treeWidget);
}
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;
//填充dom樹
if (!doc.setContent(&file, false, &errorStr, &errorLine,
&errorColumn))//形參2,是否建立名稱空間
{
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();//獲取dom樹的根標籤
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")//qdomnode ————》qdomelement的轉換基類到子類的轉換
{
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);//最開始的一次為空
qDebug()<<"allPages "<<allPages;
if (!allPages.isEmpty())
{
allPages += ", ";
}
allPages += page;
parent->setText(1, allPages);
}
parsePageElement()
則比較簡單,我們還是通過字串拼接設定葉子節點的文字。這與上一章的步驟大致相同。
程式執行結果同上一章一模一樣,這裡不再貼出截圖。
通過這個例子我們可以看到,使用 DOM 當時處理 XML 文件,除了一開始的setContent()
函式,其餘部分已經與原始文件沒有關係了,也就是說,setContent()
函式的呼叫之後,已經在記憶體中構建好了一個完整的 DOM 樹,我們可以在這棵樹上面進行移動,比如取相鄰節點(nextSibling()
)。對比上一章流的方式,雖然我們早早關閉檔案,但是我們始終使用的是readNext()
向下移動,同時也不存在readPrevious()
這樣的函式。
第三部分:SAX(Simple API for XML)
前面兩章我們介紹了使用流和 DOM 的方式處理 XML 的相關內容,本章將介紹處理 XML 的最後一種方式:SAX。SAX 是一種讀取 XML 文件的標準 API,同 DOM 類似,並不以語言為區別。Qt 的 SAX 類基於 SAX2 的 Java 實現,不過具有一些必要的名稱上的轉換。相比 DOM,SAX 的實現更底層因而處理起來通常更快。但是,我們前面介紹的QXmlStreamReader
類更偏向 Qt 風格的 API,並且比 SAX 處理器更快,所以,現在我們之所以使用 SAX API,更主要的是為了把 SAX API 引入 Qt。在我們通常的專案中,並不需要真的使用 SAX。
Qt 提供了QXmlSimpleReader
類,提供基於 SAX 的 XML 處理。同前面所說的 DOM 方式類似,這個類也不會對 XML 文件進行有效性驗證。QXmlSimpleReader
可以識別良格式的 XML 文件,支援 XML 名稱空間。當這個處理器讀取 XML 文件時,每當到達一個特定位置,都會呼叫一個用於處理解析事件的處理類。注意,這裡所說的“事件”,不同於 Qt 提供的滑鼠鍵盤事件,這僅是處理器在到達預定位置時發出的一種通知。例如,當處理器遇到一個標籤的開始時,會發出“新開始一個標籤”這個通知,也就是一個事件。我們可以從下面的例子中來理解這一點:
<doc>
<quote>Gnothi seauton</quote>
</doc>
當讀取這個 XML 文件時,處理器會依次發出下面的事件:
startDocument()
startElement("doc")
startElement("quote")
characters("Gnothi seauton")
endElement("quote")
endElement("doc")
endDocument()
每出現一個事件,都會有一個回撥,這個回撥函式就是在稱為 Handler 的處理類中定義的。上面給出的事件都是在QXmlContentHandler
介面中定義的。為簡單起見,我們省略了一些函式。QXmlContentHandler
僅僅是眾多處理介面中的一個,我們還有QXmlEntityResolver
,QXmlDTDHandler
,QXmlErrorHandler
,QXmlDeclHandler
以及QXmlLexicalHandler
等。這些介面都是純虛類,分別定義了不同型別的處理事件。對於大多數應用程式,QXmlContentHandler
和QXmlErrorHandler
是最常用的兩個。
為簡化處理,Qt 提供了一個QXmlDefaultHandler
。這個類實現了以上所有的介面,每個函式都提供了一個空白實現。也就是說,當我們需要實現一個處理器時,只需要繼承這個類,覆蓋我們所關心的幾個函式即可,無需將所有介面定義的函式都實現一遍。這種設計在 Qt 中並不常見,但是如果你熟悉 Java,就會感覺非常親切。Java 中很多介面都是如此設計的。
使用 SAX API 與QXmlStreamReader
或者 DOM API 之間最大的區別是,使用 SAX API 要求我們必須自己記錄當前解析的狀態。在另外兩種實現中,這並不是必須的,我們可以使用遞迴輕鬆地處理,但是 SAX API 則不允許(回憶下,SAX 僅允許一遍讀取文件,遞迴意味著你可以先深入到底部再回來)。
下面我們使用 SAX 的方式重新解析前面所出現的示例程式。
class MainWindow : public QMainWindow ,public QXmlDefaultHandler
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
bool readFile(const QString &fileName);
protected:
bool startElement(const QString &namespaceURI,
const QString &localName,
const QString &qName,
const QXmlAttributes &attributes);
bool endElement(const QString &namespaceURI,
const QString &localName,
const QString &qName);
bool characters(const QString &str);
bool fatalError(const QXmlParseException &exception);
private:
QTreeWidget *treeWidget;
QTreeWidgetItem *currentItem;
QString currentText;
private:
Ui::MainWindow *ui;
};
注意,我們的MainWindow
不僅繼承了QMainWindow
,還繼承了QXmlDefaultHandler
。也就是說,主視窗自己就是 XML 的解析器。我們重寫了startElement()
,endElement()
,characters()
,fatalError()
幾個函式,其餘函式不關心,所以使用了父類的預設實現。成員變數相比前面的例子也多出兩個,為了記錄當前解析的狀態。
MainWindow
的建構函式和解構函式同前面沒有變化:
下面來看 readFile() 函式:
bool MainWindow::readFile(const QString &fileName)
{
currentItem = 0;
QFile file(fileName);
QXmlInputSource inputSource(&file);
QXmlSimpleReader reader;
reader.setContentHandler(this);
reader.setErrorHandler(this);
return reader.parse(inputSource);//解析
}
這個函式中,首先將成員變數清空,然後讀取 XML 文件。注意我們使用了QXmlSimpleReader
,將ContentHandler
和ErrorHandler
設定為自身。因為我們僅重寫了ContentHandler
和ErrorHandler
的函式。如果我們還需要另外的處理,還需要繼續設定其它的 handler。parse()
函式是QXmlSimpleReader
提供的函式,開始進行 XML 解析。
bool MainWindow::startElement(const QString & /*namespaceURI*/,
const QString & /*localName*/,
const QString &qName,
const QXmlAttributes &attributes)
{
if (qName == "entry")
{
currentItem = new QTreeWidgetItem(currentItem ?
currentItem : treeWidget->invisibleRootItem());
currentItem->setText(0, attributes.value("term"));
}
else if (qName == "page")
{
currentText.clear();
}
//this->errorString();錯誤提示
return true;//最後,我們返回 true,告訴 SAX 繼續處理檔案。如果有任何錯誤,則可以返回 false 告訴 SAX 停止處理。
}
startElement()
在讀取到一個新的開始標籤時被呼叫。這個函式有四個引數,我們這裡主要關心第三和第四個引數:第三個引數是標籤的名字(正式的名字是“限定名”,qualified name,因此形參是 qName);第四個引數是屬性列表。前兩個引數主要用於帶有名稱空間的 XML 文件的處理,現在我們不關心名稱空間。函式開始,如果是 <entry> 標籤,我們建立一個新的QTreeWidgetItem
。如果這個標籤是巢狀在另外的 <entry> 標籤中的,currentItem 被定義為當前標籤的子標籤,否則則是根標籤。我們使用setText()
函式設定第一列的值,同前面的章節類似。如果是 <page> 標籤,我們將 currentText 清空,準備接下來的處理。最後,我們返回 true,告訴 SAX 繼續處理檔案。如果有任何錯誤,則可以返回 false 告訴 SAX 停止處理。此時,我們需要覆蓋QXmlDefaultHandler
的errorString()
函式來返回一個恰當的錯誤資訊。
bool MainWindow::characters(const QString &str)
{
currentText += str;
return true;
}
注意下我們的 XML 文件。characters()
僅在 <page> 標籤中出現。因此我們在characters()
中直接追加 currentText。
bool MainWindow::endElement(const QString & /*namespaceURI*/,
const QString & /*localName*/,
const QString &qName/*標籤名字*/)
{
if (qName == "entry")
{
currentItem = currentItem->parent();//
}
else if (qName == "page")
{
if (currentItem)
{
QString allPages = currentItem->text(1);
if (!allPages.isEmpty())
allPages += ", ";
allPages += currentText;
currentItem->setText(1, allPages);
}
}
return true;
}
endElement()
在遇到結束標籤時呼叫。和startElement()
類似,這個函式的第三個引數也是標籤的名字。我們檢查如果是 </entry>,則將 currentItem 指向其父節點。這保證了 currentItem 恢復到處理 <entry> 標籤之前所指向的節點。如果是 </page>,我們需要把新讀到的 currentText 追加到第二列。
bool MainWindow::fatalError(const QXmlParseException &exception)
{
QMessageBox::critical(this,
tr("SAX Error"),
tr("Parse error at line %1, column %2:\n %3")
.arg(exception.lineNumber())
.arg(exception.columnNumber())
.arg(exception.message()));
return false;
}
當遇到處理失敗的時候,SAX 會回撥fatalError()
函式。我們這裡僅僅向用戶顯示出來哪裡遇到了錯誤。如果你想看這個函式的執行,可以將 XML 文件修改為不合法的形式。
我們程式的執行結果同前面還是一樣的,這裡也不再贅述了
原始碼路徑,請點選這裡