第一部分: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 文档修改为不合法的形式。
我们程序的运行结果同前面还是一样的,这里也不再赘述了
源代码路径,请点击这里