第17章 数据库和XML
本章将讲解数据库和XML的相关内容。在学习数据库相关内容前,建议读者掌握一些基本的SQL知识,应该可以看懂基本的SELECT、INSERT、UPDATE和DELETE等语句,但这并不是必须的,因为Qt中提供了不需要SQL知识就可以浏览和编辑数据库的接口。在学习XML部分前,也建议读者先对XML有一个大概的了解。
QT中的QtSql模块提供了对数据库的支持,该模块的众多类基本上可以分为3层,如表17-1所示。
表17-1 QtSql模块的类分层
用户接口层 | QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel |
SQL接口层 | QSqlDatabase、QSqlQuery、QSqlError、QSqlField、QSqlIndex和QSqlRecord |
驱动层 | QSqlDriver、QSqlDriverCreator |
其中,驱动层为具体的数据库和SQL接口层之间提供了底层的桥梁;SQL接口层提供了对数据库的访问,其中的QSqlDatabase类用来创建连接,QSqlQuery类可以使用SQL语句来实现与数据库交互,其他几个类对该层提供了支持;用户接口层的几个类实现了将数据库中的数据链接到窗口部件上,这些类是使用前一章的模型/视图框架实现的,它们是更高层次的抽象,即便不熟悉SQL也可以操作数据库。如果要使用QtSql模块中的这些类,需要在项目文件(.pro文件)中添加“QT += sql”这一行代码。对应数据库部分的内容,可以在帮助中查看SQL Programming关键字。
1. SQL数据库驱动
QtSql模块使用数据库驱动来和不同的数据库接口进行通信。Qt的SQL模型接口是独立于数据库的,所以所有数据库特定的代码都包含在了这些驱动中。Qt默认支持一些驱动,也可以添加其他驱动,Qt中包含的驱动如表17-2所示。
表17-2 Qt中包含的数据库驱动
驱动名称 | 数据库 |
---|---|
QDB2 | IBM DB2(7.1版本或者以上版本) |
QIBASE | Borland InterBase |
QMYSQL | MySql |
QOCI | Oracle Call Interface Driver |
QODBC | Open Database Connectivity(ODBC)—微软SQL Server和其他ODBC兼容数据库 |
QPSQL | PostgreSQL(7.3版本或者更高) |
QSQLITE2 | SQLite版本2 |
QSQLITE3 | SQLite版本3 |
QTDS | Sybase Adaptive Server 注:从Qt4.7开始已经过时 |
需要说明的是,由于GPL许可证的兼容问题,并不是这里列出的所有插件都提供给了Qt的开源版本。下面通过程序来查看Qt中可用的数据库插件。(项目源码路径:src\17\17-1\databaseDriver)新建空的Qt项目,项目名称为databaseDriver,完成后往项目中添加新的main.cpp文件。下面现在databaseDriver.pro文件中添加如下一行代码:
QT += sql
#include
#include
#include
#include
int main( int argc, char *argv[] )
{
QApplication a( argc, argv );
qDebug() << "Available drivers: ";
QStringList drivers = QSqlDatabase::drivers();
foreach( QString driver, drivers )
qDebug() << driver;
return a.exec();
}
2. 创建数据库连接
要想使用QSqlQuery或者QSqlQueryModel来访问数据库,那么先要创建并打开一个或者多个数据库连接。数据库连接使用连接名来定义,而不是使用数据库名,可以向相同的数据库创建多个连接。QSqlDatabase也支持默认连接的概念,默认连接就是一个没有命名的连接。在使用QSqlQuery或者QSqlQueryModel的成员函数时需要指定一个连接名作为参数,如果没有指定,那么就会使用默认连接。如果在应用程序中只需要有一个数据库连接,那么使用默认连接是很方便的。
创建一个连接就是创建了一个QSqlDatabase类的实例,而直到该连接被打开之前,它都是没有被使用的。下面的代码片段显示了怎样创建一个默认的连接,然后打开它:
QSqlDatabase db = QSqlDatabase::addDatabase( "QSQLITE" );
db.setHostName( "bigblue" );
db.setDatabaseName( "flightdb" );
db.setUserName( "acarlson" );
db.setPassword( "1uTbSbAs" );
bool ok = db.open();
下面通过一个例子来具体看一下数据库连接的建立过程。(项目源码路径:src\17\17-2\databaseDriver)在前面的项目中添加新的C++头文件,名称为connection.h,完成后将其内容更改为:
#ifndef CONNECTION_H
#define CONNECTION_H
#include
#include
#include
static bool createConnection()
{
QSqlDatabase db = QSqlDatabase::addDatabase( "QSQLITE" );
db.setDatabaseName( ":memory" );
if( !db.open() )
{
QMessageBox::critical( 0, "Cannot open database",
"Unable to establish a database connection.", QMessageBox::Cancel );
return false;
}
QSqlQuery query;
query.exec( "create table student(id int primary key,"
"name varchar(20))" );
query.exec( "insert into student values(0, LiMing)" );
query.exec( "insert into student values(0, LiuTao)" );
query.exec( "insert into student values(2, WangHong)" );
return true;
}
#endif // CONNECTION_H
这个头文件添加了一个建立连接的函数,使用这个头文件的目的就是要简化主函数中的内容。这里先创建了一个SQLite数据库的默认连接,设置数据库名称时使用了“:memory:”,表明这个是建立在内存中的数据库,也就是说给数据只在程序运行期间有效,等程序运行结束时就会将其销毁。当然,也可以将其改为一个具体的数据库名称,比如“my.db”,这样就会在项目目录中创建该数据库文件了。下面使用open()函数将数据库打开,如果打开失败,则弹出提示对话框。最后使用QSqlQuery创建了一个student表,并插入了包含id和name两个字段的3条记录,如表17-3所列。其中,id字段是int类型的,“primary key”表明该字段是主键,它不能为空,而且不能有重复的值;而name字段是varchar类型的,并且不大于20个字符。这里使用的SQL语句都要包含在双引号中,如果一行写不完,那么分行写,每一行都要使用两个双引号引起来。关于QSqlQuery的用法,将会在下一节讲到。
表17-3 创建的student表
id | name |
---|---|
0 | LiMing |
1 | LiuTao |
2 | WangHong |
下面到main.cpp文件中,先添加头文件包含:
#include "connection.h"
#include
int main( int argc, char *argv[] )
{
QApplication a( argc, argv );
// 创建数据库连接
if( !createConnection() )
return 1;
// 使用QSqlQuery查询整张表
QSqlQuery query;
query.exec( "select * from student" );
while( query.next() )
{
qDebug() << query.value(0).toInt() << query.value(1).toString();
}
return a.exec();
}
(项目源码路径:src\17\17-3\databaseDriver)首先将connection.h文件中的创建连接的createConnection()函数的内容更改如下:
static bool createConnection()
{
// 创建一个数据库连接,使用“connection1”为连接名
QSqlDatabase db1 = QSqlDatabase::addDatabase( "QSQLITE", "connection1" );
db1.setDatabaseName( "my1.db" );
if( !db1.open() )
{
QMessageBox::critical( 0, "Cannot open database1",
"Unable to establish a database connection.", QMessageBox::Cancel );
return false;
}
// 这里要指定连接
QSqlQuery query1( db1 );
query1.exec( "create table student(id int primary key,"
"name varchar(20)" );
query1.exec( "insert into student values(0, LiMing)" );
query1.exec( "insert into student values(1, LiuTao)" );
query1.exec( "insert into student values(2, WangHong)" );
// 创建另一个数据库连接,要使用不同的连接名,这里是“connection2”
QSqlDatabase db2 = QSqlDatabase::addDatabase( "QSQLITE", "connection2" );
db2.setDatabaseName( "my2.db" );
if( !db2.open() )
{
QMessageBox::critical( 0, "Cannot open database2",
"Unable to establish a database connection.", QMessageBox::Cancel );
return false;
}
// 这里要指定连接
QSqlQuery query2( db2 );
query2.exec( "create table student(id int primary key,"
"name varchar(20)" );
query2.exec( "insert into student values(10, LiQiang)" );
query2.exec( "insert into student values(11, MaLiang)" );
query2.exec( "insert into student values(12, ZhangBin)" );
return true;
}
int main( int argc, char *argv[] )
{
QApplication a( argc, argv );
// 创建数据库连接
if( !createConnection() )
return 1;
// 使用QSqlQuery查询连接1的整张表,先要使用连接名获取该链接
QSqlDatabase db1 = QSqlDatabase::database( "connection1" );
QSqlQuery query1( db1 );
qDebug() << "connection1: "
query1.exec( "select * from student" );
while( query1.next() )
{
qDebug() << query1.value(0).toInt() << query1.value(1).toString();
}
// 使用QSqlQuery查询连接1的整张表
QSqlDatabase db2 = QSqlDatabase::database( "connection2" );
QSqlQuery query2( db2 );
qDebug() << "connection2: "
query2.exec( "select * from student" );
while( query2.next() )
{
qDebug() << query2.value(0).toInt() << query2.value(1).toString();
}
return a.exec();
}
1. 执行一个查询
QSqlQuery类提供了一个接口,用于执行SQL语句和浏览查询的结果集。要执行一个SQL语句,只需要简单地创建一个QSqlQuery对象,然后调用QSqlQuery::exec()函数即可,例如:
QSqlQuery query;
query.exec( "select * from student" );
2. 浏览结果集
QSqlQuery提供了对结果集的访问,可以一次访问一条记录。当执行完exec()函数后,QSqlQuery的内部指针会位于第一条记录前面的位置。必须调用一次QSqlQuery::next()函数来使其前进到第一条记录,然后可以重复使用next()函数来访问其他的记录,直到该函数的返回值为false,例如可以使用以下代码来遍历一个结果集:
while( query.next() )
{
qDebug() << query.value(0).toInt() << query.value(1).toString();
}
在QSqlQuery类中提供了多个函数来实现在结果集中进行定位,比如next()定位到下一条记录、previous()定位到前一条记录、first()定位到第一条记录、last()定位到最后一条记录、seek(n)定位到第n条记录。如果只需要使用next()和seek()来遍历结果集,那么可以在调用exec()函数以前调用setForwardOnly(true),这样可以显著加快结果集上的查询速度。当前行的索引可以使用at()返回;record()函数可以返回当前指向的记录;如果数据库支持,那么可以使用size()来返回结果集中的总行数。要判断是否一个数据库驱动支持一个给定的特性,可以使用QSqlQuery::hasFeature()函数。下面通过例子来看一下这些函数的使用。
(项目源码路径:src\17\17-4\databaseDriver)在源码路径为17-3的历程的基础上进行更改,先在main.cpp文件中添加如下头文件包含:
#include
#include
#include
int numRows;
// 先判断该数据库驱动是否支持QuerySize特性,如果支持,则可以使用size()函数
// 如果不支持,那么就使用其他方法来获取总行数
if( db2.driver()->hasFeature(QSqlDriver::QuerySize) )
{
qDebug() << "has feature: query size";
numRows = query2.size();
}
else
{
qDebug() << "no feature: query size";
query2.last();
numRows = query2.at() + 1;
}
qDebug() << "row number: " << numRows;
// 指向索引为1的记录,即第二条记录
query2.seek( 1 );
// 返回当前索引值
qDebug() << "current index: " << query2.at();
// 获取当前行的记录
QSqlRecord record = query2.record();
// 获取记录中“id”和“name”两个字段的值
int id = record.value("id").toInt();
QString name = record.value("name").toString();
qDebug() << "id: " << id << "name: " << name;
// 获取索引为1的字段,即第二个字段
QSqlField field = record.field( 1 );
// 输出字段名和字段值,结果为“name”和“MaLing”
qDebug() << "second field: " << field.name()
<< "field value: " << field.value().toString();
3. 插入、更新和删除记录
使用QSqlQuery可以执行任意的SQL语句,下面在前面的程序中再添加代码来看一下怎样插入、更新和删除记录。这里还会涉及数值绑定的内容,使用它就可以在SQL语句中使用变量了。
(项目源码路径:src\17\17-5\databaseDriver)在主函数中继续添加代码:
query2.exec( "insert into student(id, name) values(100, ChenYun)" );
query2.prepare( "insert into student(id, name) values(100, ChenYun)" );
int idValue = 100;
QString nameValue = "ChenYun";
query2.bindValue( ":id", idValue );
query2.bindValue( ":name", nameValue );
query2.exec();
query2.prepare( "insert into student(id, name) values(?, ?)" );
int idValue = 100;
QString nameValue = "ChenYun";
query2.addBindValue( idValue );
query2.addBindValue( nameValue );
query2.exec();)
query2.prepare( "insert into student (id, name) values(?, ?)" );
QVariantList ids;
ids << 20 << 21 << 22;
query2.addBindValue( ids );
QVariantList names;
names << "xiaoming" << "xiaoliang" << "xiaogang";
query2.addBindValue( names );
if( !query.execBatch() )
qDebug() << query2.lastError();
这里先使用了占位符,不过每一个字段值都绑定了一个列表,最后只要调用execBatch()函数即可,如果出现错误,可以使用lastError()函数返回错误信息。注意这里还要添加头文件包含:
#include
// 更新
query2.exec( "update student set name=xiaohong where id=20" );
// 删除
query2.exec( "delete from student where id=21" );
事物可以保证一个复杂操作的原子性,就是对于一个数据库操作序列,这些操作要么全部做完,要么一条也不做,它是一个不可分割的工作单位。在Qt中,如果底层的数据库引擎支持事物,那么QSqlDriver::hasFeature(QSqlDriver::Transactions)会返回true。可以使用QSqlDatabase::Transactions()来启动一个事物,然后编写一些希望在事物中执行的SQL语句,最后调用QSqlDatabase::commit()或者QSqlDatabase::rollback()。当使用事物时必须在创建查询以前就开始事物,例如:
QSqlDatabase::database().transaction();
QSqlQuery query;
query.exec( "SELECT id FROM employee WHERE name=Torild Halvorsen" );
if( query.next() )
{
int employeeId = query.value(0).toInt();
query.exe( "INSERT INTO project(id, name, owneride)"
"VALUES(201, Manhattan Project, "
+ QString::number(employeeId) + ')' );
}
QSqlDatabase::database().commit();
除了QSqlQuery,Qt还提供了3个更高级的类来访问数据库,分别是QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel。这3个类都是QAbstractTableModel派生来的,可以很容易地实现将数据库中的数据在QListView和QTableView等项视图类中显示。使用这些类的另一个好处是,这样可以使编写的代码很容易地使用其他数据源。例如,如果开始使用了QSqlTableModel,而后来要改为使用XML文件来存储数据,这样需要做的仅是更换一个数据模型。
1. SQL查询模型
QSqlQueryModel提供了一个基于SQL查询的只读模型。下面来看一下例子。(项目源码路径:src\17\17-6\sqlModel)新建Qt Gui应用,项目名称为sqlModel,类名为MainWindow,基类选择QMainWindow。完成后,在sqlModel.pro文件中添加一行代码QT += sql,然后保存该文件。下面再往项目中添加新的C++头文件,名称为“connection.h”,完成后在其中添加输入库连接函数的定义:
#ifndef CONNECTION_H
#define CONNECTION_H
#include
#include
#include
static bool createConnection()
{
QSqlDatabase db = QSqlDatabase::addDatabase( "QSQLITE" );
db.setDatabaseName( "my.db" );
if( !db.open() )
{
QMessageBox::critical( 0, "Cannot open database1",
"Unable to establish a database connection.", QMessageBox::Cancel );
return false;
}
QSqlQuery query;
// 创建student表
query.exec( QString("create table student(id int primary key,"
"name varchar, course int)") );
query.exec( QString("insert into student values(1, '李强', 11)") );
query.exec( QString("insert into student values(2, '马亮', 11)") );
query.exec( QString("insert into student values(3, '孙红', 12)") );
// 创建course表
query.exec( QString("create table course(id int primary key,"
"name varchar, teacher varchar)") );
query.exec( QString("insert into student values(10, '数学', '王老师')") );
query.exec( QString("insert into student values(11, '英语', '张老师')") );
query.exec( QString("insert into student values(12, '计算机', '白老师')") );
return true;
}
#endif // CONNECTION_H
#include "connection.h"
#include
QTextCodec::setCodecForTr( QTextCodec::codecForLocale() );
QTextCodec::setCodecForCStrings( QTextCodec::codecForLocale() );
if( !createConnection() )
return 1;
#include
#include
#include
#include
#include
#include
#include
QSqlQueryModel *model = new QSqlQueryModel( this );
model->setQuery( "select * from student" );
model->setHeaderData( 0, Qt::Horizontal, tr("学号") );
model->setHeaderData( 1, Qt::Horizontal, tr("姓名") );
model->setHeaderData( 2, Qt::Horizontal, tr("课程") );
QTableView *view = new QTableView( this );
view->setModel( model );
setCentralWidget( view );
model->setQuery( QString("insert into student values(5, 薛静, 10)") );
model->setQuery( "select * from student" );
图17-1 SQL查询模型运行效果
2. SQL表格模型
QSqlTableModel提供了一个一次只能操作一个SQL表的读/写模型,它是QSqlQuery的更高层次的替代品,可以浏览和修改独立的SQL表,并且只需编写很少的代码,而且不需要了解SQL语法。该模型默认是可读可写的,如果想让其成为只读模型,那么可以从视图进行设置,例如:
view->setEditTriggers( QAbstractItemView::NoEditTriggers );
class QSqlTableModel;
图17-2 SQL表格模型设计效果
然后再定义一个私有对象:
QSqlTableModel *model;
model = new QSqlTableModel( this );
model->setTable( "student" );
model->select();
// 设置编辑策略
model->setEditStrategy( QSqlTableModel::OnManualSubmit );
ui->tableView->setModel( model );
表17-4 SQL表格模型的编辑策略
常量 | 描述 |
---|---|
QSqlTableModel::OnFieldChange | 所有对模型的改变都会立即应用到数据库 |
QSqlTableModel::OnRowChange | 对一条记录的改变会在用户选择另一条记录时被应用 |
QSqlTableModel::OnManualSubmit | 所有的改变都会在模型中进行缓存,直到调用submitAll()或者revertAll()函数 |
下面逐个实现那些按钮的功能,每当实现一个按钮的功能,每当实现一个按钮的功能,读者都可以运行一下程序,测试该按钮的效果。下面先进入“提交修改”按钮的单击信号槽,添加如下代码:
// 提交修改按钮
void MainWindow::on_pushButton_clicked()
{
// 开始事物操作
model->database().transaction();
if( model->submitAll() )
{
model->database().commit(); // 提交
}
else
{
model->database().rollback(); // 回滚
QMessageBox::warning( this, tr("tableModel"),
tr("数据库错误:%1").arg(model->lastError().text()) );
}
}
// 查询按钮,进行筛选
void MainWindow::on_pushButton_2_clicked()
{
model.reverAll();
}
void MainWindow::on_pushButton_7_clicked()
{
QString name = ui->lineEdit->text();
// 根据姓名进行筛选,一定要使用单引号
model->setFilter( QString("name='%1'").arg(name) );
model->select();
}
// 显示全表按钮
void MainWindow::on_pushButton_8_clicked()
{
model->setTable( "student" );
model->select();
}
// 按id升序排序按钮
void MainWindow::on_pushButton_5_clicked()
{
// id字段,即第0列,升序排序
model->setSort( 0, Qt::AscendingOrder );
model->select();
}
// 按id降序排列按钮
void MainWindow::on_pushButton_6_clicked()
{
model->setSort( 0, Qt::DescendingOrder );
model->select();
}
// 删除选定行按钮
void MainWindow::on_pushButton_4_clicked()
{
// 获取选中行
int curRow = ui->tableView->currentIndex().row();
// 删除改行
model->removeRow( curRow );
int ok = QMessageBox::warning( this, tr("删除当前行!"),
tr("你确定删除当前行吗?"), QMessageBox::Yes, QMessageBox::No );
if( ok == QMessageBox::No )
{
// 如果不删除,则撤销
model->revertAll();
}
else
{
// 否则提交,在数据库中删除改行
model->submitAll();
}
}
// 添加记录按钮
void MainWindow::on_pushButton_3_clicked()
{
// 获取表的行数
int rowNum = model->rowCount();
int id = 10;
// 添加一行
model->insertRow( rowNum );
model->setData( model->index(rowNum, 0), id );
// 可以直接提交
// model.submitAll();
}
3. SQL关系表格模型
QSqlRelationalTableModel继承自QSqlTableModel,并且对其进行了扩展,提供了对外键的支持。一个外键就是表中的一个字段和其他表中的主键字段之间的一对一的映射。例如,student表中的course字段对应的是course表中的id字段,那么就称字段course是一个外键。因为这里的course字段的值是一些数字,这样显示很不友好,使用关系表格模型,就可以将它显示为course表中的name字段的值。下面来看一个例子。
(项目源码路径:src\17\17-8\sqlModel)在源码路径为17-6建立的例程的基础上修改。在mainwindow.cpp文件中,先删除在其中添加到构造函数中的代码,然后在添加如下代码:
QSqlRelationalTableModel *model = new QSqlRelationalTableModel( this );
model->setTable( "student" );
model->setRelation( 2, QSqlRelation("course", "id", "name") );
model->select();
QTableView *view = new QTableView( this );
view->setModel( model );
setCentralWidget( view );
图17-3 SQL关系表格模型运行效果
Qt中还提供了一个QSqlRelationalDelegate委托类,它可以为QSqlRelationalTableModel显示和编辑数据。这个委托为一个外键提供了一个QComboBox部件来显示所有可选的数据,这样就显得更加人性化了。使用这个委托很简单,先在mainwindow.cpp文件中添加头文件#include
view->setItemDelegate( new QSqlRelationalDelegate(view) );
图17-4 使用关系委托运行效果
可以根据自己的需要来选择使用哪个模型。如果熟悉SQL语法,又不需要将所有的数据都显示出来,那么只需要使用QSqlQuery就可以了。对于QSqlTableModel,它主要是用来显示一个单独表格的,而QSqlQueryModel可以用来显示任意一个结果集,如果想显示任意一个结果集,而且想使其可读/写,那么建议子类化QSqlQueryModel,然后重新实现flags()和setData()函数。这部分内容可以查看Presenting Data in a Table View关键字对应的帮助文档,也可以参考Query Model示例程序。因为这3个模型都是基于模型/视图框架的,所以前一章将的内容在这里都可以使用,例如可以使用QDataWidgetMapper等。关于数据库部分的应用,还可以参考一下SQL分类中的几个示例程序。
XML(Extensible Markup Language,可扩展标记语言)是一种类似于HTML的标记语言,设计目的是传输数据,而不是显示数据。XML的标签没有被预定义,用户需要在使用时自定义。XML是W3C(万维网联盟)的推荐标准。相对于数据库表格的二维表示,XML 使用的树形结构更能表现出数据的包含关系,作为一种文本文件格式,XML简单明了的特性使得它在信息存储和描述领域非常流行。
Qt中提供了QtXml模块来进行XML文档的处理,这里主要提供了3种解析方法:DOM方法,可以进行读/写;SAX方法,可以进行读取;基于流的方法,分别使用QXmlStreamReader和QXmlStreamWriter进行读取和写入。要在项目中使用QtXml模块,还需要在项目文件(.pro文件)中添加“QT+=xml”一行代码。
另外,Qt中还提供了更高级的QtXmlPatterns来进行XML数据的查询和操作,它支持使用XQuery 1.0和XPath 2.0,关于这个模块的使用可以再帮助中参考A Short Path to XQuery和XQuery关键字,还可以查看一下XML Patterns分类下的几个示例程序。Qt中的QtSvg模块提供了QSvgRenderer和QSvgGenerator类来对SVG(一种基于XML的文件格式)进行读/写,关于这些类的使用,可以参考其帮助文档,还可以查看一下SVG Generator Example和SVG Viewer Example示例程序。对应本节内容,可以在帮助中查看XML Processing关键字。
1. 使用DOM读取XML文档
DOM(Document Object Model,文档对象模型),是W3C的推荐标准,提供了接口来访问和改变一个XML文件的内容和结构,可以将XML文档表示为一个存储在内存中具有层次的树视图。文档本身由QDomDocument对象来表示,而文档树中所有的DOM节点都是QDomNode类的子类。
先来看一个标准的XML文档:
Qt
shiming
Linux
yafei
每个XML文档都由XML说明(或者称为XML序言)开始,它是对XML文档处理的环境和要求的说明,比如这里的 xml version="1.0" encoding="UTF-8"?>,其中xml version="1.0",表明使用的XML版本号,这里字母是区分大小写的;encoding="UTF-8"是使用的编码,指出文档是使用何种字符建立的,默认值为Unicode编码。Qt中使用QDomProcessingInstruction类来表示XML说明。XML文档内容由多个元素组成,一个元素由起始标签<标签名>、终止标签标签名>以及两个标签之间的内容组成,而文档中第一个元素被称为根元素,比如这里的
这里对XML文档格式进行了一个简单的介绍,只是为了让没有XML知识的读者可以快速学习本节的内容。如果要应用XML,还是有必要了解一下它的基本语法内容的,这个可以参考其他的书籍或者网络内容(例如:http://www.w3school.com.cn/x.asp)。下面就来使用Qt中的DOM类读取一个XML文档。
(项目源码路径:src\17\17-9\myDOM1)新建Qt4控制台应用,名称为myDOM1。完成后在myDOM1.pro文件中添加如下一行代码:
QT += xml
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 新建QDomDocument类对象,它代表一个XML文档
QDomDocument doc;
QFile file( "../myDom1/my.xml" );
if (!file.open(QIODevice::ReadOnly))
return 0;
// 将文件内容读到doc中
if (!doc.setContent(&file))
{
file.close();
return 0;
}
// 关闭文件
file.close();
// 获得doc的第一个节点,即XML说明
QDomNode firstNode = doc.firstChild();
// 输出XML说明,nodeName()为“xml”,nodeValue()为版本和编码信息
qDebug() << qPrintable(firstNode.nodeName())
<< qPrintable(firstNode.nodeValue());
// 返回根元素
QDomElement docElem = doc.documentElement();
// 返回根节点的第一个子节点
QDomNode n = docElem.firstChild();
// 如果节点不为空,则转到下一个节点
while (!n.isNull())
{
// 如果节点是元素
if (n.isElement())
{
// 将其转换为元素
QDomElement e = n.toElement();
// 返回元素标记和id属性的值
qDebug() << qPrintable(e.tagName())
<< qPrintable(e.attribute("id"));
// 获取元素e的所有子节点的列表
QDomNodeList list = e.childNodes();
// 遍历该列表
for (int i = 0; i < list.count(); i++ )
{
QDomNode node = list.at(i);
if (node.isElement())
qDebug() << " " << qPrintable(node.toElement().tagName())
<< qPrintable(node.toElement().text());
}
}
// 转到下一个兄弟节点
n = n.nextSibling();
}
return a.exec();
}
17-5 使用DOM读取XML文件运行效果
2. 使用DOM创建和操作XML文档
(项目源码路径:src\17\17-10\myDOM2)新建Qt Gui应用,名称为myDOM2,类名为MainWindow,基类为QMainWindow。完成后还是现在项目文件myDOM2.pro中添加“QT += xml”来导入QtXml模板。然后打开mainwindow.ui文件,往其中拖入Push Button、List Widget和Line Edit等部件来设计界面,最终效果如图17-6所示。下面进入main.cpp文件,添加头文件#include
QTextCodec::setCodecForCStrings( QTextCodec::codecForLocale() );
图17-6 使用DOM操作XML文档设计界面
这样就可以在后面的代码中使用QString()来为中文进行编码了。下面到mainwindow.cpp文件中先添加头文件:
#include
#include
QDomDocument doc;
// 添加处理指令即XML说明
QDomProcessingInstruction instruction;
instruction = doc.createProcessingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" );
doc.appendChild( instruction );
// 添加根元素
QDomElement root = doc.createElement( QString("书库") );
doc.appendChild( root );
// 添加第一个图书元素及其子元素
QDomElement book = doc.createElement( QString("图书") );
QDomAttr id = doc.createAttribute( QString("编号") );
QDomElement title = doc.createElement( QString("书名") );
QDomElement author = doc.createElement( QString("作者") );
QDomText text;
id.setValue( QString("1") );
book.setAttributeNode( id );
text = doc.createTextNode( QString("Qt") );
title.appendChild( text );
text =doc.createTextNode( QString("shiming") );
author.appendChild( text );
book.appendChild( title );
book.appendChild( author );
root.appendChild( book );
// 添加第二个图书元素及其子元素
book = doc.createElement( QString("图书") );
id = doc.createAttribute( QString("编号") );
title = doc.createElement( QString("书名") );
author = doc.createElement( QString("作者") );
id.setValue( QString("2") );
book.setAttributeNode( id );
text = doc.createTextNode( QString("Linux") );
title.appendChild( text );
text =doc.createTextNode( QString("yafei") );
author.appendChild( text );
book.appendChild( title );
book.appendChild( author );
root.appendChild( book );
QFile file( "my.xml" );
if( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) )
return;
QTextStream out( &file );
// 将文档保存到文件,4为子元素缩进字符数
doc.save( out, 4 );
file.close();
在设计模式,转到“显示全部”按钮的单击信号的槽中,更改代码如下:
void MainWindow::on_pushButton_5_clicked()
{
// 先清空显示
ui->listWidget->clear();
QFile file( "my.xml" );
if( !file.open(QIODevice::ReadOnly) )
return;
QDomDocument doc;
if( !doc.setContent(&file) )
{
file.close();
return;
}
file.close();
QDomElement docElem = doc.documentElement();
QDomNode n = docElem.firstChild();
while( !n.isNull() )
{
if( n.isElement() )
{
QDomElement e = n.toElement();
ui->listWidget->addItem( e.tagName() + e.attribute(QString("编号")) );
QDomNodeList list = e.childNodes();
for( int i = 0; i < list.count(); i++ )
{
QDomNode node = list.at( i );
if( node.isElement() )
{
ui->listWidget->addItem( " " + node.toElement().tagName()
+ ": " + node.toElement().text() );
}
}
}
n = n.nextSibling();
}
}
void MainWindow::on_pushButton_4_clicked()
{
// 先清空显示,然后显示“无法添加!”,这样如果添加失败则会显示“无法添加!”
ui->listWidget->clear();
ui->listWidget->addItem( QString("无法添加!") );
QFile file( "my.xml" );
if( !file.open(QIODevice::ReadOnly) )
return;
QDomDocument doc;
if( !doc.setContent(&file) )
{
file.close();
return;
}
file.close();
QDomElement root = doc.documentElement();
QDomElement book = doc.createElement( QString("图书") );
QDomAttr id = doc.createElement( QString("编号") );
QDomElement title = doc.createElement( QString("书名") );
QDomElement author = doc.createElement( QString("作者") );
QDomText text;
// 我们获得了最后一个孩子节点编号,然后加1,便是新的编号
QString num = root.lastChild().toElement().attribute( QString("编号") );
int count = num.toInt() + 1;
id.setValue( QString::number(count) );
book.setAttributeNode( id );
text = doc.createTextNode( ui->lineEdit_2->text() );
title.appendChild( text );
text = doc.createTextNode( ui->lineEdit_3->text() );
author.appendChild( text );
book.appendChild( title );
book.appendChild( author );
root.appendChild( book );
if( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) )
return;
QTextStream out( &file );
doc.save( out, 4 );
file.close();
// 最后更改显示为“添加成功!”
ui->listWidget->clear();
ui->listWidget->addItem( QString("添加成功!") );
}
因为查找、删除和更新内容都是对指定元素进行的,所以它们可以在一个函数总实现。下面现在mainwindow.h文件中添加一个public函数声明:
void doXml( const QString operate );
void MainWindow::doXml( const QString operate )
{
ui->listWidget->clear();
ui->listWidget->addItem( QString("没有找到相关内容!") );
QFile file( "my.xml" );
if( !file.open(QIODevice::ReadOnly) )
return;
QDomDocument doc;
if( !doc.setContent(&file) )
{
file.close();
return;
}
file.close();
// 以标签名进行查找
QDomNodeList list = doc.elementsByTagName( QString("图书") );
for( int i = 0; i < list.count(); i++ )
{
QDomElement e = list.at(i).toElement();
if( e.attribute(QString("编号")) == ui->lineEdit->text() )
{
// 如果元素的”编号“属性值与我们所查的相同
if( operate == "delete" )
{
// 如果是删除操作
QDomElement root = doc.documentElement();
// 从根节点上删除该节点
root.removeChild( list.at(i) );
QFile file( "my.xml" );
if( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) )
return;
QTextStream out( &file );
doc.save( out, 4 );
file.close();
ui->listWidget->clear();
ui->listWidget->addItem( QString("删除成功!") );
}
else if( operate == "update" )
{
// 如果是更新操作
QDomNodeList child = list.at(i).childNodes();
// 将它子节点的首个子节点(就是文本节点)的内容更新
child.at(0).toElement().firstChild()
.setNodeValue( ui->lineEdit_2->text() );
child.at(1).toElement().firstChild()
.setNodeValue( ui->lineEdit_3->text() );
QFile file( "my.xml" );
if( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) )
return;
QTextStream out( &file );
doc.save( out, 4 );
file.close();
ui->listWidget->clear();
ui->listWidget->addItem( QString("更新成功!") );
}
else if( operate == "find" )
{
// 如果是查找操作
ui->listWidget->clear();
ui->listWidget->addItem( e.tagName() + e.attribute(QString("编号")) );
QDomNodeList list = e.childNodes();
for( int i = 0; i < list.count(); i++ )
{
QDomNode node = list.at( i );
if( node.isElement() )
ui->listWidget->addItem( " " + node.toElement().tagName()
+ ": " + node.toElement().text() );
}
}
}
}
}
// 查找按钮
void MainWindow::on_pushButton_clicked()
{
doXml( "find" );
}
// 删除按钮
void MainWindow::on_pushButton_2_clicked()
{
doXml( "delete" );
}
// 更新按钮
void MainWindow::on_pushButton_3_clicked()
{
doXml( "update" );
}
SAX(Simple API for XML)为XML解析器提供了一个基于事件的标准接口。在Qt中支持SAX2,但是并不支持Java接口的SAX1。在前面讲解的DOM方法需要在一个树结构中读入和存储整个XML文档,这样会耗费大量内存,不过使用它来操作文档结构是很容易的。如果不需要对文档进行操作,而只需要读取整个XML文档,那么使用SAX方法就很高效。SAX2接口是一个事件驱动机制,用来为用户提供文档信息。这里的事件是由解析器发出的,例如它遇到了一个开始标签或者一个结束标签等。下面来看一个例子:
A quotation.
① 遇到开始标签(),这时会调用startElement()事件处理函数;
② 发现字符数据“A quotation.”,这时会调用characters()事件处理函数;
③ 一个结束标签被解析(),这时会调用endElement()事件处理函数。
每当发生一个事件时,都可以在响应的事件处理函数中进行操作来完成对文档自定义解析。比如可以在startElement()中获取元素名和属性,在characters()中获取元素中的文本,在endElement()中进行一些结束读取该元素时想要进行的操作。这些事件处理函数udoukeyi通过继承QXmlDefaultHandler类来重新实现,Qt中提供的事件处理类如表17-5所列,它们都继承自QXmlDefaultHandler类。
表17-5 QtXml模块中的事件处理类
处理类 | 描述 |
---|---|
QXmlContentHandler | 报告与文档内容相关的事件(例如起始标签或字符) |
QXmlDTDHandler | 报告与DTD相关的事件 |
QXmlErrorHandler | 报告在解析过程中发生的错误或警告 |
QXmlEntityResolver | 报告解析过程中的外部实体,允许用户解析外部实体 |
QXmlDeclHandler | 报告XML数据的声明内容 |
QXmlLexicalHandler | 报告与文档的词法结构相关的事件 |
(项目源码路径:src\17\17-11\mySAX)新建空的Qt项目,项目名称为mySAX,完成后向项目中添加新的C++类,类名为MySAX,基类为QXmlDefaultHandler,类型信息选择“无”。完成后,现在mySAX.pro文件中添加“QT += xml”,然后保存该文件。下面到mysax.h文件中,将MySAX类的定义修改为:
#ifndef MYSAX_H
#define MYSAX_H
#include
class QListWidget;
class MySAX : public QXmlDefaultHandler
{
public:
MySAX();
~MySAX();
bool readFile( const QString &filename );
protected:
bool startElement( const QString &namespaceURI,
const QString &localName,
const QString &qName,
const QXmlAttributes &atts );
bool endElement( const QString &namespaceURI,
const QString &localName,
const QString &qName )
bool characters( const QString &ch );
bool fatalError( const QXmlParseException &exception );
private:
QListWidget *list;
QString currentText;
};
#endif // MYSAX_H
#include "mysax.h"
#include
#include
MySAX::MySAX()
{
list = new QListWidget;
list->show();
}
MySAX::~MySAX()
{
delete list;
}
bool MySAX::readFile(const QString &filename)
{
QFile file( filename );
// 读取文件内容
QXmlInputSource inputSource( &file );
// 建立QXmlSimpleReader对象
QXmlSimpleReader reader;
// 设置内容处理器
reader.setContentHandler( this );
// 设置错误处理器
reader.setErrorHandler( this );
// 解析文件
return reader.parse( inputSource );
}
// 已经解析完一个元素的其实标签
bool MySAX::startElement(const QString &namespaceURI, const QString &localName,
const QString &qName, const QXmlAttributes &atts)
{
if( qName == "library" )
list->addItem( qName );
else if( qName == "book" )
list->addItem( " " + qName + atts.value("id") );
return true;
}
// 已经解析完一块字符数据
bool MySAX::characters(const QString &ch)
{
currentText = ch;
return true;
}
// 已经解析完一个元素的结束标签
bool MySAX::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
{
if( qName == "title" || qName == "author" )
list->addItem( " " + qName + ": " + currentText );
return true;
}
// 错误处理
bool MySAX::fatalError(const QXmlParseException &exception)
{
qDebug() << exception.message();
return false;
}
下面再向项目中添加main.cpp文件,并更改其内容如下:
#include "mysax.h"
#include
int main( int argc, char *argv[] )
{
QApplication app( argc, argv );
MySAX sax;
sax.readFile( "../mySAX/my.xml" );
return app.exec();
}
从Qt 4.3开始引入了两个新的类来读取和写入XML文档:QXmlStreamReader和QXmlStreamWriter。QXmlStreamReader类提供了一个快速的解析器通过一个简单的流API来读取格式良好的XML文档,它是作为Qt的SAX解析器的替代品的身份出现的,因为它比SAX解析器更快更方便。QXmlStreamReader可以从QIODevice或者QByteArray中读取数据。流读取器的基本原理是将XML文档报告为一个记号(tokens)流,这一点与SAX相似,不同之处在于XML记号被报告的方式。在SAX中,应用程序必须提供处理器(回调函数)来从解析器获得所谓的XML事件;而对于QXmlStreamReader,是应用程序代码自身来驱动循环,需要的时候可以从读取器中一个接一个地拉出记号。这个事通过调用readNext()函数实现的,它可以读取下一个记号,然后返回一个记号类型,它由枚举类型QXmlStreamReader::TokenType定义,其所有取值如下表所列。然后可以使用isStartElement()和text()等函数来判断这个记号是否包含需要的信息。使用这种主动拉取记号方式的最大好处就是可以构建递归解析器,也就是可以在不同的函数或者类中来处理XML文档中的不同记号。
表17-6 在QXmlStreamReader中的记号类型
常量 | 描述 |
---|---|
QXmlStreamReader::NoToken | 没有读到任何内容 |
QXmlStreamReader::Invalid |
发生了一个错误,在error()和errorString()中报告 |
QXmlStreamReader::StartDocument |
在documentVersion()中报告XML版本号,在documentEncoding()中指定文档的编码 |
QXmlStreamReader::EndDocument |
报告文档结束 |
QXmlStreamReader::StartElement |
使用namespaceUri()和name()来报告元素开始,可以使用attributes()来获取属性 |
QXmlStreamReader::EndElement |
使用namespaceUri()和name()来报告元素结束 |
QXmlStreamReader::Characters |
使用text()来报告字符,如果字符是空白,那么isWhitespace()返回true,如果字符源于CDATA部分,那么isCDATA()返回true |
QXmlStreamReader::Comment |
使用text()报告一个注释 |
QXmlStreamReader::DTD |
使用text()来报告一个DTD,符号声明在notationDeclarations()中,实体声明在entityDeclarations()中,具体的DTD声明通过dtdName()、dtdPublicId()和dtdSystemId()来报告 |
QXmlStreamReader::EntityReference |
报告一个无法解析的实体引用,引用的名字由name()获取,text()可以获取替换文本 |
QXmlStreamReader::ProcessingInstruction |
使用processingInstructionTarget()和processingInstructionData()来报告一个处理指令 |
下面来看一个使用QXmlStreamReader解析XML文档的例子。(项目源码路径:src\17\17-12\myXmlStream)新建Qt4控制台应用,名称为myXmlStream,完成后向myXmlStream.pro文件中添加“QT += xml”这一行代码,然后保存该文件,下面到main.cpp文件中,将其代码更改为:
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QFile file("../myXmlStream/my.xml");
if (!file.open(QFile::ReadOnly | QFile::Text))
{
qDebug() << "Error: cannot open file!";
return 1;
}
QXmlStreamReader reader;
// 设置文件,这时会将流设置为初始状态
reader.setDevice(&file);
// 若果没有读到文档结尾,而且没有出现错误
while (!reader.atEnd())
{
// 读取下一个记号,它返回记号的类型
QXmlStreamReader::TokenType type = reader.readNext();
// 下面便根据记号的类型来进行不同的输出
if (type == QXmlStreamReader::StartDocument)
{
qDebug() << reader.documentEncoding() << reader.documentVersion();
}
if (type == QXmlStreamReader::StartElement)
{
qDebug() << "<" << reader.name() << ">";
if (reader.attributes().hasAttribute("id"))
qDebug() << reader.attributes().value("id");
}
if (type == QXmlStreamReader::EndElement)
qDebug() << "" << reader.name() << ">";
if (type == QXmlStreamReader::Characters && !reader.isWhitespace())
qDebug() << reader.text();
}
// 如果读取过程中出现错误,那么输出错误信息
if (reader.hasError())
{
qDebug() << "error: " << reader.errorString();
}
file.close();
return a.exec();
}
与QXmlStreamReader对应的是QXmlStreamWriter,它通过一个简单的流API提供了一个XML写入器。QXmlStreamWriter的使用是十分简单的,只需要调用相应的记号的写入函数来写入相关数据即可。下面通过一个例子来进行详解。(项目源码路径:src\17\17-13\myXmlStream)将前面主函数的内容更改如下:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QFile file( "../myXmlStream/my2.xml" );
if( !file.open(QFile::WriteOnly | QFile::Truncate) )
{
qDebug() << "Error: cannot open file!";
return 1;
}
QXmlStreamWriter stream( &file );
stream.setAutoFormatting( true );
stream.writeStartDocument();
stream.writeStartElement( "bookmark" );
stream.writeAttribute( "href", "http://qt.nokia.com" );
stream.writeTextElement( "title", "Qt Home" );
stream.writeEndElement();
stream.writeEndDocument();
file.close();
qDebug() << "write finished!";
return a.exec();
}