DOM
DOM 是由 W3C 提出的一种处理 XML 文档的标准接口。Qt 实现了 DOM Level 2 级别的不验证读写 XML 文档的方法。DOM 一次性读入整个 XML 文档,在内存中构造为一棵树(被称为 DOM 树)。我们能够在这棵树上进行导航,比如移动到下一节点或者返回上一节点,也可以对这棵树进行修改,或者是直接将这颗树保存为硬盘上的一个 XML 文件。
示例1
头文件
QDomDocument doc;
QStringList PlaySongName;
QString historyName;
QStringList pathList;
QString fileName = "Song.xml";
//写XML
void WriteXml(QStringList &MusicPaths);
//读XML
void readXml(QString& fileName, QDomDocument &XmlDoc);
void addXml(const QString & name, const QString & text, QDomDocument &document);
void AnalysisListData(QDomDocument &xmldoc,QFile &File);
QString DeleteFileNameSpecialCharacters(QString &songName);
void WriteXml(QStringList &MusicPaths);
void MainWindow::WriteXml(QStringList &MusicPaths)
{
doc.clear();
if (doc.toString().compare("")==0){
//创建根节点
QDomElement root = doc.createElement("Song");
//添加根节点
doc.appendChild(root);
}
// QStringList::iterator iter;
int k = 0,i = 0;
for(i = 0;i < MusicPaths.size();++i)
{
//分离出多个MP3文件路径中每个文件的所有信息
QString musicPath = MusicPaths.at(k++);
//从MP3文件信息中获取文件名及文件格式信息
QString FileName = QFileInfo( musicPath ).fileName();
QString songName = DeleteFileNameSpecialCharacters(FileName);
addXml(songName,musicPath,doc);
//qDebug()<<"i="<<i<<" size="<<MusicPaths.size();
}
// fileName = "Song.xml";
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly| QIODevice::Text))
return ;
//将文本流保存到文件,4为子元素缩进的字符数
QTextStream out(&file);
doc.save(out, 4, QDomNode::EncodingFromTextStream);
file.close();
readXml(fileName,doc);
}
void readXml(QString& fileName, QDomDocument &XmlDoc);
void MainWindow::readXml(QString &fileName,
QDomDocument &XmlDoc)
{
QFile file(fileName);
qDebug()<<fileName<<" readonly history list";
if (!file.open(QIODevice::ReadOnly)){
// qDebug()<<file.errorString();
// qDebug()<<file.error();
// qDebug()<<"open file failed";
return ;
}
AnalysisListData(XmlDoc,file);
QDomElement root = XmlDoc.documentElement();//读取根节点
//读取第一个子节点
QDomNode node = root.firstChild();
//遍历节点
QDomNodeList list = root.childNodes();
int count = list.count();
qDebug() << "node count:" << count;
int i = 0;
while (i < count)
{
//获取节点的内容
QString data = node.toElement().text();
// qDebug () << "read == "<<data;
m_playList->addMedia(QUrl::fromLocalFile(data));
// PlaySongName.push_back(QFileInfo( data ).fileName());
historyName = data.split("/").last();
ui->MusicWidget->addItem(historyName);
//读取下一个兄弟节点
node = node.nextSibling();
node.nextSiblingElement();
i++;
}
file.close();
}
void addXml(const QString & name, const QString & text, QDomDocument &document);
void MainWindow::addXml(const QString &name,
const QString &text,
QDomDocument &document)
{
//读取根节点
QDomElement root = document.documentElement();
//创建元素节点
QDomElement newChild = document.createElement(name);
//添加元素节点到根节点
root.appendChild(newChild);
//创建元素文本
QDomText newChildText = document.createTextNode(text);
//添加元素文本到元素节点
newChild.appendChild(newChildText);
}
void MainWindow::AnalysisListData(QDomDocument &xmldoc, QFile &File)
void MainWindow::AnalysisListData(QDomDocument &xmldoc, QFile &File)
{
QString errorStr;
int errorLine = 0,errorCol = 0;
if(!xmldoc.setContent(&File,true,&errorStr,&errorLine,&errorCol))
{
//如果出错,则会进入这里。errorStr得到的是出错说明,errorLine和errorCol则是出错的行和列
qDebug() << errorStr << "line: " << errorLine << "col: " << errorCol;
return;
}
}
QString MainWindow::DeleteFileNameSpecialCharacters(QString &songName)
QString MainWindow::DeleteFileNameSpecialCharacters(QString &songName)
{
//处理特殊字符等(可自定义)
songName = songName.replace(' ',"").replace(':',"");
songName = songName.replace('-',"").replace('(',"");
songName = songName.replace(')',"").replace('.',"");
return songName;
}
示例2
一个汽车销售记录的xml,以时间为顺序进行记录。
xml中用这个作为一个节点 : …
下面给出这个xml的树形结构图:
<?xml version='1.0' encoding='utf-8'?>
<日销售清单>
<日期 Date="2018--06--17">
<时间 time="11--33--22">
<工厂>一汽大众</工厂>
<品牌>奥迪A6</品牌>
<报价>36</报价>
<数量>1</数量>
<金额>36</金额>
</时间>
<时间 time="11--33--25">
<工厂>一汽大众</工厂>
<品牌>奔驰</品牌>
<报价>83</报价>
<数量>2</数量>
<金额>166</金额>
</时间>
<时间 time="11--33--31">
<工厂>二汽神龙</工厂>
<品牌>毕加索</品牌>
<报价>39</报价>
<数量>2</数量>
<金额>78</金额>
</时间>
<时间 time="11--33--35">
<工厂>二汽神龙</工厂>
<品牌>富康</品牌>
<报价>28</报价>
<数量>2</数量>
<金额>56</金额>
</时间>
<时间 time="11--33--40">
<工厂>二汽神龙</工厂>
<品牌>标致307</品牌>
<报价>27</报价>
<数量>3</数量>
<金额>81</金额>
</时间>
</日期>
</日销售清单>
#include "domxml.h"
#include <QFile>
#include <QDomDocument> //文件
#include <QDomProcessingInstruction> //格式头部
#include <QDomElement> //元素
#include <QTextStream> //文件流
#include <QDebug>
#include <QDateTime>
#define cout qDebug() << "[" << __FILE__ << ":" << __LINE__ << "]"
DomXML::DomXML()
{
}
//创建xml空文件
void DomXML::createXML(QString filePath)
{
QFile file(filePath); //关联文件名字
if( true == file.exists() ) //如果存在不创建
{
cout << "文件已经存在";
return;
}
else
{ //不存在才创建
//只写方式打开文件
bool isOk = file.open(QIODevice::WriteOnly);
if(true == isOk)//如果打开成功
{
//创建xml文档对象
QDomDocument doc;
//创建xml头部格式 <?xml version='1.0' encoding='utf-8'?>
QDomProcessingInstruction ins;
ins = doc.createProcessingInstruction("xml", "version=\'1.0\' encoding=\'utf-8\'");
//追加元素
doc.appendChild(ins);
//根节点元素
QDomElement root = doc.createElement("日销售清单");
doc.appendChild(root);
//保存
QTextStream stream(&file); //文本流关联文件
doc.save(stream, 4); //4 缩进字符
file.close(); //关闭文件
}
else
{
cout << "WriteOnly error";
return;
}
}
}
void DomXML::appendXML(QString filePath, QStringList list)
{
QFile file(filePath);
bool isOk = file.open(QIODevice::ReadOnly);
if(true == isOk) //打开成功
{
//file和xml文档对象关联
QDomDocument doc;
isOk = doc.setContent(&file);
if(isOk) //如果关联成功
{
file.close(); //关闭文件
//获取根节点元素
QDomElement root = doc.documentElement();
//获取当前时间
QDateTime date = QDateTime::currentDateTime();
QString dateStr = date.toString("yyyy-MM-dd"); //2016-01-08
//判断根节点下有没有子节点
if( root.hasChildNodes() ) //如果有子节点
{
//查找最后一个子节点
QDomElement lastEmt = root.lastChildElement();
if(lastEmt.attribute("date") == dateStr)
{//有有当天日期
//写有效数据
writeXML(doc, lastEmt, list);
}
else //如果没有
{
//创建日期子节点元素
QDomElement dateEmt = doc.createElement("日期");
//创建date属性
QDomAttr dateAttr = doc.createAttribute("date");
//设置属性的值
dateAttr.setNodeValue(dateStr);
//节点和属性关联
dateEmt.setAttributeNode(dateAttr);
//把日期节点追加到根节点上
root.appendChild(dateEmt);
//写有效数据
writeXML(doc, dateEmt, list);
}
}
else //没有子节点
{
//创建日期子节点元素
QDomElement dateEmt = doc.createElement("日期");
//创建date属性
QDomAttr dateAttr = doc.createAttribute("date");
//设置属性的值
dateAttr.setNodeValue(dateStr);
//节点和属性关联
dateEmt.setAttributeNode(dateAttr);
//把日期节点追加到根节点上
root.appendChild(dateEmt);
//写有效数据
writeXML(doc, dateEmt, list);
}
//保存文件
isOk = file.open(QIODevice::WriteOnly);
if(isOk)
{
QTextStream stream(&file);
doc.save(stream, 4);
file.close();
}
}
else
{
cout << "setContent error";
return;
}
}
else
{
cout << "ReadOnly error";
}
}
void DomXML::writeXML(QDomDocument &doc, QDomElement &root, QStringList &list)
{
//获取当前时间
QDateTime time = QDateTime::currentDateTime();
QString timeStr = time.toString("hh-mm-ss"); //16:05:22
//创建时间节点元素
QDomElement timeEmt = doc.createElement("时间");
//创建属性
QDomAttr timeAttr = doc.createAttribute("time");
//给属性设置值
timeAttr.setNodeValue(timeStr);
//时间节点元素和属性关联
timeEmt.setAttributeNode(timeAttr);
//把时间节点追击到日期节点后面
root.appendChild(timeEmt);
QDomElement factory = doc.createElement("厂家");
QDomElement brand = doc.createElement("品牌");
QDomElement price = doc.createElement("报价");
QDomElement num = doc.createElement("数量");
QDomElement total = doc.createElement("金额");
QDomText text = doc.createTextNode(list.at(0));
factory.appendChild(text);
text = doc.createTextNode(list.at(1));
brand.appendChild(text);
text = doc.createTextNode(list.at(2));
price.appendChild(text);
text = doc.createTextNode(list.at(3));
num.appendChild(text);
text = doc.createTextNode(list.at(4));
total.appendChild(text);
timeEmt.appendChild(factory);
timeEmt.appendChild(brand);
timeEmt.appendChild(price);
timeEmt.appendChild(num);
timeEmt.appendChild(total);
}
void DomXML::readXML(QString filePath, QStringList &fList, QStringList &bList, QStringList &pList, QStringList &nList, QStringList &tList)
{
QFile file(filePath);
bool isOk = file.open(QIODevice::ReadOnly);
if(true == isOk) //打开成功
{
//file和xml文档对象关联
QDomDocument doc;
isOk = doc.setContent(&file);
if(isOk) //如果关联成功
{
//获取根节点
QDomElement root = doc.documentElement();
file.close();
QDateTime date = QDateTime::currentDateTime();
QString dateStr = date.toString("yyyy-MM-dd");
if(root.hasChildNodes()) //有没有子节点
{
//找最后一个节点元素
QDomElement lastEmt = root.lastChildElement();
if(lastEmt.attribute("date") == dateStr)//判断有没有当天日期
{
//找出当前日期下所有时间子节点
QDomNodeList list = lastEmt.childNodes();
for(int i = 0; i < list.size(); i++)
{
//list.at(0).toElement();
//转换为元素,找到时间节点下的所有子节点
QDomNodeList subList = list.at(i).toElement().childNodes();
//厂家
QString factory = subList.at(0).toElement().text();
fList.append(factory);
QString brand = subList.at(1).toElement().text();
bList.append(brand);
QString price = subList.at(2).toElement().text();
pList.append(price);
QString num = subList.at(3).toElement().text();
nList.append(num);
QString total = subList.at(4).toElement().text();
tList.append(total);
}
}
else
{
cout << "没有当天日期";
return;
}
}
else
{
cout << "没有子节点";
return;
}
}
else
{
cout << "setContent error";
return;
}
}
else
{
cout << "ReadOnly error";
return;
}
}
示例3
下面一个 books.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
上面表格中,带有 [] 的可以带有子节点,反之则不能。
程序的目的还是一样的:用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对象,代表整个文档
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()这样的函数。