第17章 数据库和XML

第17章 数据库和XML

本章将讲解数据库和XML的相关内容。在学习数据库相关内容前,建议读者掌握一些基本的SQL知识,应该可以看懂基本的SELECT、INSERT、UPDATE和DELETE等语句,但这并不是必须的,因为Qt中提供了不需要SQL知识就可以浏览和编辑数据库的接口。在学习XML部分前,也建议读者先对XML有一个大概的了解。


17.1 数据库

QT中的QtSql模块提供了对数据库的支持,该模块的众多类基本上可以分为3层,如表17-1所示。


表17-1 QtSql模块的类分层

用户接口层 QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel
SQL接口层 QSqlDatabase、QSqlQuery、QSqlError、QSqlField、QSqlIndex和QSqlRecord
驱动层 QSqlDriver、QSqlDriverCreator、QSqlDriverCreatorBase、QSqlDriverPlugin和QSqlResult  

其中,驱动层为具体的数据库和SQL接口层之间提供了底层的桥梁;SQL接口层提供了对数据库的访问,其中的QSqlDatabase类用来创建连接,QSqlQuery类可以使用SQL语句来实现与数据库交互,其他几个类对该层提供了支持;用户接口层的几个类实现了将数据库中的数据链接到窗口部件上,这些类是使用前一章的模型/视图框架实现的,它们是更高层次的抽象,即便不熟悉SQL也可以操作数据库。如果要使用QtSql模块中的这些类,需要在项目文件(.pro文件)中添加“QT += sql”这一行代码。对应数据库部分的内容,可以在帮助中查看SQL Programming关键字。


17.1.1 连接到数据库

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

完成后按下Ctrl+S快捷键保存该文件,然后将main.cpp文件的内容更改如下:

#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();
}

这里使用了QSqlDatabase类的静态函数drivers()获取了可用的驱动列表,然后将它们遍历输出。运行程序可以看到输出的结果为:“QSQLITE”、“QODBC3”和“QODBC”。表明现在仅支持3个驱动。其实,也可以在Qt安装目录下的plugins/sqldrivers文件中看到所有的驱动插件文件。这里要重点提一下SQLite数据库,它是一款轻型的文件型数据库,主要应用于嵌入式领域,支持跨平台,而且Qt对它提供了很好地默认支持,所以在文章后面的内容将使用该数据库为例子来讲解。关于数据库驱动的更多内容,可以参考SQL Database Drivers关键字对应的帮助文档,这里还列出了编译驱动器插件和编写自定义的数据库驱动的方法。


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();

第一行创建了一个连接对象,最后一行打开该连接以便使用。当创建了连接后,还初始化了一些连接信息,包括数据库名、主机名、用户名和密码等。这里连接到主机bigblue上的名称为flightdb的SQLite数据库。在addDatabase()函数中的“QSQLITE”参数指定了该连接使用的数据库驱动。因为这里并没有指定addDatabase()函数的第二个参数即连接名,所以这样建立的是默认连接。

下面通过一个例子来具体看一下数据库连接的建立过程。(项目源码路径: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();
}

这里调用了createConnection()函数来创建数据库连接,然后使用QSqlQuery查询整张表并将所有内容进行了输出。现在运行程序,可以在应用程序输出栏中看到student表格中的内容。这个例子中使用了默认连接,下面再更改程序,看一下同时建立多个连接的情况。

(项目源码路径: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;
}

这里分别使用了connection1和connection2为连接名创建了两个连接,这两个连接分别设置了数据库名为“my1.db”和“my2.db”,他们是两个数据库文件。当存在多个连接时,使用QSqlQuery就要指定使用的哪个连接,这样才能在正确的数据库上进行操作。使用两个连接分别创立了两个student表,但是其中的记录的内容是不同的。下面到main.cpp文件中分别输出这两个表中的内容,将主函数的内容进行更改如下:

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();
}

这里主要使用了QSqlDatabase的database()静态函数,通过指定连接名来获取相应的数据库连接,然后在QSqlQuery中使用该连接进行数据库的查询操作。现在运行程序,就可以输出两个表中的内容了,而在项目目录中也可以看到生成的“my1.db”和“my2.db”两个数据库文件。


17.1.2 执行SQL语句

1. 执行一个查询

QSqlQuery类提供了一个接口,用于执行SQL语句和浏览查询的结果集。要执行一个SQL语句,只需要简单地创建一个QSqlQuery对象,然后调用QSqlQuery::exec()函数即可,例如:

QSqlQuery query;
query.exec( "select * from student" );

在QSqlQuery的构造函数中可以接收一个可选的QSqlDatabase对象来指定使用的是哪一个数据库连接,当没有指定连接时,就是使用默认连接。如果发生了错误,那么exec()函数返回false,可以使用QSqlQuery::lastError()来获取错误信息。


2. 浏览结果集

QSqlQuery提供了对结果集的访问,可以一次访问一条记录。当执行完exec()函数后,QSqlQuery的内部指针会位于第一条记录前面的位置。必须调用一次QSqlQuery::next()函数来使其前进到第一条记录,然后可以重复使用next()函数来访问其他的记录,直到该函数的返回值为false,例如可以使用以下代码来遍历一个结果集:

while( query.next() )
{
    qDebug() << query.value(0).toInt() << query.value(1).toString();
}

其中,QSqlQuery::Value()函数可以返回当前记录的一个字段值。比如value(0)就是第一个字段的值,各个字段从0开始编号。该函数返回一个QVariant,不同的数据库类型会自动映射为Qt中最接近的相应类型,这里的toInt()和toString()就是将QVariant转换为int和QString类型。在Data Types for Qt-supported Database Systems关键字对应的帮助文档中列出了所有的数据库数据类型在Qt中对应类型,需要时可以参考一下。

在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();

使用QSqlQuery中record()函数可以返回当前指向的记录,一条记录由QSqlRecord来表示,可以使用QSqlRecord中提供的相关函数对一条记录进行操作。而QSqlRecord中的field()函数可以返回当前记录的一个字段,它由QSqlField来表示,可以使用QSqlField中提供的相关函数来对一个字段进行操作。现在可以运行程序,查看输出结果。


3. 插入、更新和删除记录

使用QSqlQuery可以执行任意的SQL语句,下面在前面的程序中再添加代码来看一下怎样插入、更新和删除记录。这里还会涉及数值绑定的内容,使用它就可以在SQL语句中使用变量了。

(项目源码路径:src\17\17-5\databaseDriver)在主函数中继续添加代码:

query2.exec( "insert into student(id, name) values(100, ChenYun)" );

这样就在连接2的student表中重新插入了一条记录。如果想在同一时间插入多条记录,一个有效的方法就是将查询语句和真实的值分离,这可以使用占位符来完成。Qt支持两种占位符:名称绑定和位置绑定。例如,使用名称绑定,上面这条代码就等价于下面的代码片段:

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();)

可以看到,使用这两种方法来绑定值都是很方便的,只需要主要使用的格式即可。当要插入多条记录时,只需要调用QSqlQuery::prepare()一次,然后使用多次bindValue()或者addBindValue()函数来绑定需要的数据,最后调用一次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" );

4. 事物

事物可以保证一个复杂操作的原子性,就是对于一个数据库操作序列,这些操作要么全部做完,要么一条也不做,它是一个不可分割的工作单位。在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();


17.1.3 使用SQL模型类

除了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

这里使用默认数据库连接创建了student和course两张表,这里数据中的内容用了中文,所以要使用QString()将SQL语句包含起来转换编码。下面我们再到main.cpp文件中,先添加头文件包含:

#include "connection.h"
#include 

然后在主函数中第一行创建QApplication对象的代码下面添加如下代码:

QTextCodec::setCodecForTr( QTextCodec::codecForLocale() );
QTextCodec::setCodecForCStrings( QTextCodec::codecForLocale() );
if( !createConnection() )
    return 1;

注意设置编码的两行代码要写在前面,不然数据库中的中文会出现乱码。下面到mainwindow.cpp,先添加头文件包含:

#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 );

这里先创建了QSqlQueryModel对象,然后使用setQuery()来执行SQL语句进行查询整张student表,并使用setHeaderData()来设置显示的表头。后面创建了视图,并将QSqlQueryModel对象作为其要显示的模型。运行程序,效果如图17-1所示。这里要注意,其实QSqlQueryModel中存储的是执行完setQuery()函数后的结果集,所以视图中显示的是结果集的内容。QSqlQueryModel中还提供了columnCount()返回一条记录中字段的个数;rowCount()返回结果集中记录的条数;record()返回第n条记录;index()返回指定记录的指定字段的索引;clear()可以清空模型中的结果集。也可以使用它提供query()函数来获取QSqlQuery对象,这样就可以使用上一节讲到的QSqlQuery的相关内容来操作数据库了。还要注意一点就是,如果又使用setQuery()进行了新的查询,比如进行了插入操作,这时要想视图中可以显示操作后的结果,那么就必须再次查询整张表,也就是要同时执行下面两行代码:

model->setQuery( QString("insert into student values(5, 薛静, 10)") );
model->setQuery( "select * from student" );

第17章 数据库和XML_第1张图片

图17-1 SQL查询模型运行效果


2. SQL表格模型

QSqlTableModel提供了一个一次只能操作一个SQL表的读/写模型,它是QSqlQuery的更高层次的替代品,可以浏览和修改独立的SQL表,并且只需编写很少的代码,而且不需要了解SQL语法。该模型默认是可读可写的,如果想让其成为只读模型,那么可以从视图进行设置,例如:

view->setEditTriggers( QAbstractItemView::NoEditTriggers );

下面通过一个例子来使用该模型对数据库表进行各种操作。 (项目源码路径:src\17\17-7\sqlModel)还在前面程序的基础上进行更改。先打开mainwindow.ui文件,向窗口上拖入Label、Push Button、Line Edit和Table View等部件,最终效果如图17-2所示。下面到mainwindow.h文件中,添加类的前置声明:

class QSqlTableModel;

第17章 数据库和XML_第2张图片

图17-2 SQL表格模型设计效果


然后再定义一个私有对象:

QSqlTableModel *model;

下面到mianwindow.cpp文件中,先将构造函数中在17-6中添加的代码删掉,然后再添加如下代码:

model = new QSqlTableModel( this );
model->setTable( "student" );
model->select();

// 设置编辑策略
model->setEditStrategy( QSqlTableModel::OnManualSubmit );
ui->tableView->setModel( model );

这里创建一个QSqlTableModel后,只须使用setTable()来为其制定数据库表,然后使用select()函数进行查询,调用这两个函数就等价于执行了“select * from student”这个SQL语句。这里还可以使用setFilter()来指定查询时的条件,在后面会看到这个函数的使用。在使用该模型以前,一般还要设置其编辑策略,它由QSqlTableModel::EditStrategy枚举变量定义,一共有3个值,如表17-4所列。用来说明当数据库中的值被编辑后,什么情况下被提交修改。现在可以运行程序,在窗口中会显示student表的内容。

表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()) );
    }
}

这里使用了事物操作,如果可以使用submitAll()将模型中的修改向数据库提交成功,那么执行commit(),否则进行回滚rollback(),并提示错误信息。下面进入“撤销修改”按钮的单击信号槽,添加代码:

// 查询按钮,进行筛选
void MainWindow::on_pushButton_2_clicked()
{
    model.reverAll();
}


这里只是简单调用了reverAll()函数将模型中的修改进行恢复。现在可以运行程序,然后修改表格中的内容,如果单击“撤销修改”,那么所有的修改都会被恢复。但是如果先按下了“提交按钮”,那么数据已经提交到了数据库,再单击“撤销按钮”也无法恢复了。下面再进入“查询”按钮的单击信号槽中,添加如下代码:

void MainWindow::on_pushButton_7_clicked()
{
    QString name = ui->lineEdit->text();

    // 根据姓名进行筛选,一定要使用单引号
    model->setFilter( QString("name='%1'").arg(name) );
    model->select();
}

这里使用setFilter()函数来进行数据筛选,注意,因为姓名是中文的,所以筛选的字符串必须使用tr()包含,而且“%1”必须用单引号括起来。现在运行程序可以在行编辑器中输入一个姓名,然后单击“查询”按钮进行查找操作了。下面进入“显示全表”按钮的单击信号槽:

// 显示全表按钮
void MainWindow::on_pushButton_8_clicked()
{
    model->setTable( "student" );
    model->select();
}

这里再次对整张表进行了查询。下面分别进入“按id升序排序”和“按id降序排序”按钮的单击信号槽,更改如下:

// 按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();
}

这里使用了setSort()函数来对指定的字段列进行排序。下面再进入“删除选中行”按钮的单击信号槽,更改如下:

// 删除选定行按钮
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();
    }
}

这里先获取了当前行的行号,然后调用removeRow()来删除该行,这时该行的最前面会显示“!”号。删除行时会弹出一个对话框,提示是否确定删除该行,如果确定删除,那么就执行submitAll()函数进行提交修改,否则执行reverAll()函数进行恢复。最后进入“添加记录”按钮单击信号的槽中,进行插入操作:

// 添加记录按钮
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();
}

这里实现了在表的最后添加一条新的记录,因为id为主键,所以必须为其提供一个id值。使用insertRow()可以插入一行,使用setData()可以为一个字段设置值。这里可以调用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 );

这里的setRelation()函数用来在两个表之间创建一个关系,其中参数“2”表示student表中的编号为2的列,即第三个字段course是一个外键,它映射到了course表中的id字段,而视图需要向用户显示course表中的name字段的值。运行程序,效果如图17-3所示。

第17章 数据库和XML_第3张图片

图17-3 SQL关系表格模型运行效果


Qt中还提供了一个QSqlRelationalDelegate委托类,它可以为QSqlRelationalTableModel显示和编辑数据。这个委托为一个外键提供了一个QComboBox部件来显示所有可选的数据,这样就显得更加人性化了。使用这个委托很简单,先在mainwindow.cpp文件中添加头文件#include ,然后继续在构造函数中添加如下一行代码:

view->setItemDelegate( new QSqlRelationalDelegate(view) );

下面运行程序,效果如图17-4所示。

第17章 数据库和XML_第4张图片

图17-4 使用关系委托运行效果


可以根据自己的需要来选择使用哪个模型。如果熟悉SQL语法,又不需要将所有的数据都显示出来,那么只需要使用QSqlQuery就可以了。对于QSqlTableModel,它主要是用来显示一个单独表格的,而QSqlQueryModel可以用来显示任意一个结果集,如果想显示任意一个结果集,而且想使其可读/写,那么建议子类化QSqlQueryModel,然后重新实现flags()和setData()函数。这部分内容可以查看Presenting Data in a Table View关键字对应的帮助文档,也可以参考Query Model示例程序。因为这3个模型都是基于模型/视图框架的,所以前一章将的内容在这里都可以使用,例如可以使用QDataWidgetMapper等。关于数据库部分的应用,还可以参考一下SQL分类中的几个示例程序。



17.2 XML

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关键字。


17.2.1 DOM

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",表明使用的XML版本号,这里字母是区分大小写的;encoding="UTF-8"是使用的编码,指出文档是使用何种字符建立的,默认值为Unicode编码。Qt中使用QDomProcessingInstruction类来表示XML说明。XML文档内容由多个元素组成,一个元素由起始标签<标签名>、终止标签以及两个标签之间的内容组成,而文档中第一个元素被称为根元素,比如这里的,XML文档必须有且只有一个根元素。元素的名称是区分大小写的,元素还可以嵌套,比如这里的library、book、title和author等都是元素。元素对应QDomElement类。元素可以包含属性,用来描述元素的相关信息,属性名和属性值在元素的起始标签中给出,格式为<元素名 属性名="属性值">,如,属性值必须在单引号或者双引号中。属性队形QDomAttr类。在元素中可以包含子元素,也可以只包含文本内容,比如这里的Qt中的Qt就是文本内容,文本内容由QDomText类表示。在Qt中,所有的DOM节点,比如这里的说明、元素、属性和文本等,都使用QDomNode来表示,然后使用对应的isProcessingInstruction()、isElement()、isAttr()和isText()等函数来判断是否是该类型的元素,如果是,那么就可以使用toProcessingInstruction()、toElement()、toAttr()和toText()等函数转换为具体的节点类型。

这里对XML文档格式进行了一个简单的介绍,只是为了让没有XML知识的读者可以快速学习本节的内容。如果要应用XML,还是有必要了解一下它的基本语法内容的,这个可以参考其他的书籍或者网络内容(例如:http://www.w3school.com.cn/x.asp)。下面就来使用Qt中的DOM类读取一个XML文档。

(项目源码路径:src\17\17-9\myDOM1)新建Qt4控制台应用,名称为myDOM1。完成后在myDOM1.pro文件中添加如下一行代码:

QT += xml

保存该文件。然后再到新建的项目目录中,新建记事本文本文档,然后将前面介绍的标准XML文档编辑进来,最后以“my.xml”为文件名保存,注意后缀要更改为“.xml”。西面到main.cpp文件中,将其内容更改为:

#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();
}

这里先创建了一个QDomDocument类对象,用来代表整个XML文档。QDomDocument类提供了对文档数据最基本的访问。然后使用QFile类打开了指定的XML文件,使用QDomDocument类的setContent()函数来设置整个文档的内容,它会将XML文档的内容解析为一个DOM树,并保存在内存中,所以完成后就可以使用close()函数把文件关闭了。QDomDocument类也是QDomNode的子类,使用firstChild()函数可以获取它的第一个子节点,这里就是XML说明。使用documentElement()函数可以获得根节点,这也是访问XML文档的入口,它返回的是一个QDomElement类对象,因为这个对象也是QDomNode的子类,所以后面就可以使用QDomNode类提供的一些函数来遍历整个文档,比如firstChild()获得第一个子节点,lastChild()获取最后一个节点,childNodes()获取该节点的所有孩子节点的一个列表,nextSibling()获取下一个兄弟节点,previousSibling()获取前一个兄弟节点。对于一个元素节点,可以使用tagName()来获取标签名,使用attribute()来获取指定的属性的值,使用text()来获取其中的文本内容。现在可以运行程序,查看输出结果,如图17-5所示。

第17章 数据库和XML_第5张图片

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 ,然后在main()函数中添加如下一行代码:

QTextCodec::setCodecForCStrings( QTextCodec::codecForLocale() );

第17章 数据库和XML_第6张图片

图17-6 使用DOM操作XML文档设计界面


这样就可以在后面的代码中使用QString()来为中文进行编码了。下面到mainwindow.cpp文件中先添加头文件:

#include 
#include 

然后在构造函数中添加代码来生成XML文件:

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();

这里先使用QDomDocument类在内存中生成了一颗DOM树,然后调用save()函数利用QTextStream文件流将DOM树保存在了文件中。生成DOM树时主要使用了createElement()等函数来生成各种节点,然后使用appendChild()将各个节点依次追加进去。现在运行程序就可以在项目生成的目录中查看到创建的my.xml文件了,可以直接打开,这样默认会在浏览器中打开,也可以使用记事本等编辑器将其打开,当然,还可以将其拖入Qt Creator中,使用Qt Creator将其打开。下面来输出整个文档的内容。

在设计模式,转到“显示全部”按钮的单击信号的槽中,更改代码如下:

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();
    }
}

这里的代码就是前面读取XML文档时的代码。现在运行呈,然后单击“显示全部”按钮,则会在列表中显示出文档中的所有内容。下面来实现向文档中添加一个元素。转到“添加”按钮的单击信号的槽中,添加如下代码:

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("添加成功!") );
}

向文档中添加元素的过程:先使用只读方式打开xml文件,然后将其解析为内存中的DOM树并关闭文件,再向DOM书中添加元素,最后使用只写方式打开xml文件,将DOM树写入到文件并关闭文件。现在运行程序,在书名和作者行编辑器中输入内容,然后按下“添加”按钮,最后按下“显示全部”按钮,就可以看到添加后的内容了。

因为查找、删除和更新内容都是对指定元素进行的,所以它们可以在一个函数总实现。下面现在mainwindow.h文件中添加一个public函数声明:

void doXml( const QString operate );

然后到mainwindow.cpp文件添加该函数的定义:

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() );
                }
            }
        }
    }
}

这里先使用elementsByTagName()来获取了所有图书元素的列表,然后使用指定的id编号来获取要操作的图书元素,后面分为3种情况来处理。如果是删除操作,那么就是用removeChild()函数来删除该元素并保存到文件;如果是更新操作,那么就使用setNodeValue()来为其设置新的值并保存到文件;如果是查找操作,就将该元素的内容显示出来。下面分别进入“查找”按钮、“删除”按钮和“更新”按钮的单击信号槽中,更改如下:

// 查找按钮
void MainWindow::on_pushButton_clicked()
{
    doXml( "find" );
}

// 删除按钮
void MainWindow::on_pushButton_2_clicked()
{
    doXml( "delete" );
}

// 更新按钮
void MainWindow::on_pushButton_3_clicked()
{
    doXml( "update" );
}

下面运行程序,在图书编号行编辑器中输入图书的编号,然后进行查找、删除和更新等操作,查看一下效果。通过这个例子可以看到使用DOM可以很方便地进行XML文档的随机访问,这也是它最大的有点。关于DOM的使用,还可以参考一下Qt提供DOM Bookmarks example示例程序。



17.2.2 SAX

SAX(Simple API for XML)为XML解析器提供了一个基于事件的标准接口。在Qt中支持SAX2,但是并不支持Java接口的SAX1。在前面讲解的DOM方法需要在一个树结构中读入和存储整个XML文档,这样会耗费大量内存,不过使用它来操作文档结构是很容易的。如果不需要对文档进行操作,而只需要读取整个XML文档,那么使用SAX方法就很高效。SAX2接口是一个事件驱动机制,用来为用户提供文档信息。这里的事件是由解析器发出的,例如它遇到了一个开始标签或者一个结束标签等。下面来看一个例子:

A quotation.

当解析上面这行文档时会触发3个事件:

① 遇到开始标签(),这时会调用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

这里主要是重新声明了QXmlDefaultHandler类的startElement()、endElement()、characters()和fatalError()几个函数,readFile()函数用来读入XML文件,QListWidget部件用来显示解析后的XML文档内容,currentText字符串变量用于暂存字符数据。下面到mysax.cpp文件中,将其内容修改为:

#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;
}

这里添加了几个函数的定义。readFile()函数中设置了文件的解析过程。Qt中提供了一个简单的XML解析器QXmlSimpleReader,它是基于SAX的。该解析器需要QXmlInputSource为其提供数据,QXmlInputSource会使用相应的编码来读取XML文档的数据。解析之前还需要使用setContentHandler()来设置事件处理器,使用setErrorHandler()来设置错误处理器,它们的参数使用了this,表明使用本类作为处理器,也就是在解析过程中出现的各种事件都会使用本类的startElement()等事件处理函数来进行处理,而出现错误时使用本类的fatalError()函数来处理。最后,调用了parse()函数来进行解析,该函数会在解析成功时返回true,否则返回false。在后面的几个事件处理函数中,就是简单地讲数据显示在QListWidget中。

下面再向项目中添加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();
}

现在,把在前面源码路径为17-9的例程中创建的my.xml文件复制到现在的项目目录中,然后运行程序,就可以显示出该文档的内容了。可以看到使用SAX方法来解析XML文档比使用DOM方法要清晰很多,更重要的是它的效率要高很多,不过SAX方法只适用于读取XML文档。对于SAX的使用,也可以参考一下Qt自带的SAX Bookmarks example示例程序。



17.2.3 XML流

从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() << "";
        if (type == QXmlStreamReader::Characters && !reader.isWhitespace())
            qDebug() << reader.text();
    }
    
    // 如果读取过程中出现错误,那么输出错误信息
    if (reader.hasError())
    {
        qDebug() << "error: " << reader.errorString();
    }
    
    file.close();
    
    return a.exec();
} 

可以看到流读取器就是在一个循环中通过使用readNext()来不断读取记号,这里可以对不同的记号和不同的内容进行不同的处理,既可以在本函数中进行,也可以在其他函数或者其他类中进行。下面将前面源码路径为17-9的例程中创建的my.xml文件复制到该项目目录中,然后运行程序查看效果。

与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();
}

这里使用了setAutoFormatting(true)函数来自动设置格式,这样会自动换行和添加缩进。然后使用writeStartDocument(),该函数会自动添加首行的XML说明(即),添加元素可以使用writeStartElement(),不过,这里要注意,一定要在元素的属性、文本等添加完成后,使用writeTextElement()来关闭前一个打开的元素。在最后使用writeEndDocument()来完成文档的写入。现在可以运行程序了,这时会在项目目录中生成一个XML文档。对于QXmlStreamReader和QXmlStreamWriter的使用,还可以参考QXmlStream Bookmarks Example示例程序。


17.3 小结

数据库和XML在很多程序中经常用到,它们的使用也总是和数据的现实联系起来,所以学习好前面一章的知识是很重要的,这两章密不可分。这一章只是讲解了数据库和XML最简单的应用,要深入研究,还需要去学习相关的专业知识。在《Qt及Qt Quick开发实践精解》中的数据管理系统实例综合应用了数据库和XML的知识,学习完本章可以接着学习该实例。

你可能感兴趣的:(Qt,Create快速入门)