1. 程式人生 > >QT中讀取XML檔案三種方式 的例項

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 的自定義資料。

每次QXmlStreamReaderreadNext()函式呼叫,解析器都會讀取下一個元素,按照下表中展示的型別進行處理。我們通過表中所列的有關函式即可獲得相應的資料值:

型別 例項 有關函式
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()

 

 

 

 

 

 

 

 

 

 

 

 

 

 考慮如下 XML 片段:

<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僅僅是眾多處理介面中的一個,我們還有QXmlEntityResolverQXmlDTDHandlerQXmlErrorHandlerQXmlDeclHandler以及QXmlLexicalHandler等。這些介面都是純虛類,分別定義了不同型別的處理事件。對於大多數應用程式,QXmlContentHandlerQXmlErrorHandler是最常用的兩個。

為簡化處理,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,將ContentHandlerErrorHandler設定為自身。因為我們僅重寫了ContentHandlerErrorHandler的函式。如果我們還需要另外的處理,還需要繼續設定其它的 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 停止處理。此時,我們需要覆蓋QXmlDefaultHandlererrorString()函式來返回一個恰當的錯誤資訊。

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 文件修改為不合法的形式。

我們程式的執行結果同前面還是一樣的,這裡也不再贅述了

原始碼路徑,請點選這裡