第11章 数据库
Qt SQL模块提供数据库编程的支持,Qt支持多种常见的数据库,如MySQL、Oracle、MS SQL Server、SQLite等。Qt SQL模块包括多个类,可以实现数据库连接、SQL语句执行、数据获取与界面显示等功能,数据与界面之间使用Model/View架构,从而可以方便地实现数据的界面显示和操作。
11.1 Qt SQL模块概述
要在项目中使用Qt SQL模块,需在项目配置文件中增加下面一条设置语句:
Qt += sql
在头文件或源程序文件中使用Qt SQL模块中的类,可以使用包含语句:
#include <QtSql>
这样会将Qt SQL模块中的所有类都包含进去,如果只使用其中的某些类,为避免冗余可以单独包含某个类。
11.1.1 Qt SQL支持的数据库
Qt SQL提供了一些常见数据库的驱动,包括网络型数据库,如Oracle、MS SQL Server等,也包括简单的单机型数据库,如SQLite。Qt SQL提供的数据库驱动见表11-1。
表11-1 Qt SQL支持的数据库
| 驱动名 | 数据库 |
|---|---|
| QDB2 | IBM DB2(7.1及以上版本)数据库 |
| QIBASE | Borland InterBase数据库 |
| QMYSQL | MySQL数据库 |
| QOCI | Oracle 调用接口驱动(Oracle Call Interface Driver) |
| QODBC | Open Database Connectivity(ODBC),Microsoft的SQL Server数据库,以及其他支持ODBC接口的数据库,如Access |
| QPSQL | PostgreSQL(7.3及以上版本)数据库 |
| QSQLITE2 | SQLite 2数据库 |
| QSQLITE | SQLite 3数据库 |
| QTDS | Sybase Adaptive Server,注意:从Qt 4.7开始已过时 |
11.1.2 SQLite数据库
本书假设读者对数据库的基本概念和SQL语句已经有基础的知识,为了用实例研究Qt SQL的数据库编程功能,本章采用SQLite数据库作为数据库实例。
SQLite是一种无需服务器、无需进行任何配置的数据库,所有的数据表、索引等数据库元素全都存储在一个文件里,在应用程序里使用SQLite数据库就完全可以当作一个文件来使用。SQLite是可以跨平台使用的数据库,在不同平台之间可以随意复制数据库。SQLite的驱动库文件很小,包含完整功能的驱动可以小到只有500 KB。
SQLite是一种开源免费使用的数据库,可以从其官网下载最新版本的数据库驱动安装文件。
SQLite Expert是SQLite数据库可视化管理工具,可以从其官网下载最新的安装文件,SQLite Expert安装文件带有SQLite数据库驱动,所以安装SQLite Expert后无需再下载安装SQLite数据库驱动。
在SQLite Expert软件里建立一个数据库demodb.db3,在此数据库里建立4个表,本章的Qt SQL编程实例都采用这个数据库文件作为数据库实例。
SQLite Expert软件进行数据库字段设计的界面如原书的图11-1所示。
(1)employee数据表是一个员工信息表,用于在实例samp11_1里演示通过QSqlTableModel获取、显示和编辑数据表的内容,employee的字段定义见表11-2。
表11-2 employee数据表的字段定义
| 序号 | 字段名 | 类型 | 描述 | 说明 |
|---|---|---|---|---|
| 1 | EmpNo | INT | 员工编号 | 主键,非空 |
| 2 | Name | VARCHAR(20) | 姓名 | 非空 |
| 3 | Gender | VARCHAR(4) | 性别 | 缺省值=“男” |
| 4 | Height | FLOAT | 身高 | 缺省值=1.71 |
| 5 | Birthday | DATE | 出生日期 | |
| 6 | Mobile | VARCHAR(18) | 手机号 | |
| 7 | Province | VARCHAR(20) | 省份 | |
| 8 | City | VARCHAR(20) | 城市 | |
| 9 | Department | VARCHAR(30) | 工作部门 | |
| 10 | Education | VARCHAR(16) | 教育程度 | |
| 11 | Salary | CURRENCY | 工资 | 缺省值=3500 |
| 12 | Photo | BLOB | 照片 | BLOB字段可存储任何二进制内容 |
| 13 | Memo | MEMO | 备注 | MEMO字段可存储任意长度普通文本 |
(2)departments数据表是一个学院信息表,记录学院编号和学院名称。其字段定义见表11-3。
表11-3 departments数据表的字段定义
| 序号 | 字段名 | 类型 | 描述 | 说明 |
|---|---|---|---|---|
| 1 | departID | INT | 学院编号 | 主键,非空 |
| 2 | departments | VARCHAR(40) | 学院名称 | 非空 |
(3)majors数据表是专业信息表,记录各专业的信息。其字段定义见表11-4。
表11-4 majors数据表的字段定义
| 序号 | 字段名 | 类型 | 描述 | 说明 |
|---|---|---|---|---|
| 1 | majorID | INT | 专业编号 | 主键,非空 |
| 2 | major | VARCHAR(40) | 专业名称 | 非空 |
| 3 | departID | INT | 所属学院编号 | 非空,等于departments表中某个学院的departID |
departments和majors构成一个Master/Detail关系数据表,majors表里的departID字段记录了这个专业属于哪个学院,departments表里的一条记录关联majors表中的多条记录。
(4)studInfo是一个记录学生信息的数据表。其字段定义见表11-5。
表11-5 studInfo数据表的字段定义
| 序号 | 字段名 | 类型 | 描述 | 说明 |
|---|---|---|---|---|
| 1 | studID | INT | 学号 | 主键,非空 |
| 2 | name | VARCHAR(10) | 姓名 | 非空 |
| 3 | gender | VARCHAR(4) | 性别 | |
| 4 | departID | INT | 学院编号 | 关联departments表中的记录 |
| 5 | majorID | INT | 专业编号 | 关联majors表中的记录 |
studInfo表中记录学生的所在学院采用了代码字段departID,具体的学院名称需要通过查询departments表中相同的departID的记录获得;majorID记录了专业代码,具体的专业名称需要查找majors表中的记录获取。这两个字段都是代码字段,只存储代码,具体的意义需要查询关联数据表的相应记录获得,在实际的数据库设计中经常用到这种设计方式。
Qt SQL中使用QSqlRelationalTableModel可以很方便地实现这种代码型数据表的显示与编辑,实例samp11_4显示了QSqlRelationalTableModel的使用方法。
11.1.3 Qt SQL模块的主要类
Qt SQL提供的主要类的简要功能描述见表11-6。
表11-6 Qt SQL模块包含的主要类的功能
| 类名称 | 功能描述 |
|---|---|
| QSqlDatabase | 用于建立与数据库的连接 |
| QSqlDriver | 用于访问具体的SQL数据库的底层抽象类 |
| QSqlDriverCreator | 为某个具体的数据库驱动提供SQL驱动的模板类 |
| QSqlDriverCreatorBase | 所有SQL驱动器的基类 |
| QSqlDriverPlugin | 用于定制QSqlDriver插件的抽象基类 |
| QSqlError | SQL数据库错误信息,可以用于访问上一次出错的信息 |
| QSqlField | 操作数据表或视图(view)的字段的类 |
| QSqlIndex | 操作数据库的索引的类 |
| QSqlQuery | 执行各种SQL语句的类 |
| QSqlQueryModel | SQL查询结果数据的只读数据模型(data model),用于SELECT查询结果数据记录的只读显示 |
| QSqlRecord | 封装了数据记录操作的类 |
| QSqlRelation | 用于存储SQL外键信息的类,用于QSqlRelationalTableModel数据源中设置代码字段与关联数据表的关系 |
| QSqlRelationalDelegate | 用于QSqlRelationalTableModel的一个代码字段的显示和编辑代理组件,一般是一个QComboBox组件,下拉列表中自动填充代码表的代码字段对应的实际内容 |
| QSqlRelationalTableModel | 用于一个数据表的可编辑的数据模型,支持代码字段的外键 |
| QSqlResult | 访问SQL数据库的抽象接口 |
| QSqlTableModel | 编辑一个单一数据表的数据模型类 |
| QDataWidgetMapper | 用于界面组件与字段之间实现映射,实现字段内容自动显示的类 |
QSqlDatabase用于建立与数据库的连接,一般是先加载需要的数据库驱动,然后设置数据库的登录参数,如主机地址、用户名、登录密码等,如果是单机型数据库,如SQLite,只需设置数据库文件即可。
数据库的操作一般需要将数据库的内容在界面上进行显示和编辑,Qt采用 Model/View结构进行数据库内容的界面显示。QTableView是常用的数据库内容显示视图组件,用于数据库操作的数据模型类有QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel,这几个类的继承关系见原书的图11-2。
QSqlQueryModel通过设置SELECT语句查询获取数据库的内容,但是QSqlQueryModel的数据是只读的,不能进行编辑。
QSqlTableModel直接设置一个数据表的名称,可以获取数据表的全部记录,其结果是可编辑的,设置为界面上的QTableView组件的数据模型后就可以显示和编辑数据。
QSqlRelationalTableModel编辑一个数据表,并且可以将代码字段通过关系与代码表关联,将代码字段的编辑转换为直观的内容选择编辑。
QSqlQuery是另外一个经常使用的类,它可以执行任何SQL语句,特别是没有返回记录的语句,如UPDATE、INSERT、DELETE等,通过SQL语句对数据库直接进行编辑修改。
11.2 QSqlTableModel的使用
11.2.1 实例功能
实例samp11_1使用QSqlTableModel显示实例数据库demodb中employee数据表的内容,实现编辑、插入、删除记录的操作,实现数据的排序和记录过滤,还实现BLOB类型字段Photo中存储照片的显示、导入等操作,运行界面如原书的图11-3所示。
在图11-3的中,左侧数据表格是一个QTableView组件,设置一个QSqlTableModel类的变量作为数据模型后就可以显示数据表的内容。右侧的一些编辑框、下拉列表框等。界面组件通过QDataWidgetMapper类的实例设置为与某个字段关联,自动显示字段的内容。但是没有现成的组件可以通过字段映射显示Photo字段中的照片。为此使用一个QLabel组件,通过其pixmap属性显示图片,所以照片的显示、导入和清除需要单独操作,并且与数据模块的记录移动保持同步,如同将此界面组件与Photo字段映射了一样。
QTableView显示内容有缺省的代理组件,一般是自动使用QLineEdit组件。但是对于某些字段期望通过下拉列表框来选择输入,例如“性别”和“部门”这两个字段。为此,还设计了自定义数据代理类,“性别”和“部门”两个字段使用QComboBox进行编辑。
工具栏上的按钮根据当前状态自动可用或禁用,特别是“保存”和“取消”两个按钮在数据表的内容被修改后自动变为可用,当保存或取消修改后又变为不可用。
11.2.2 主窗口设计
实例samp11_1是一个主窗口继承自QMainWindow的应用程序,使用UI设计器可视化设计界面。设计的Action如原书的图11-4所示,使用Action创建工具栏按钮,其他界面组件和布局不再详述。
以下是主窗口类MainWindow的定义(一些通用的部分,自动生成的界面组件的槽函数省略了)。
#include <QtSql>
#include <QDataWidgetMapper>
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QSqlDatabase DB; //数据库连接
QSqlTableModel *tabModel; //数据模型
QItemSelectionModel *theSelection; //选择模型
QDataWidgetMapper *dataMapper; //数据映射
QWComboBoxDelegate delegateSex; //自定义数据代理,性别
QWComboBoxDelegate delegateDepart; //自定义数据代理,部门
void openTable(); //打开数据表
void getFieldNames(); //获取字段名称,填充“排序字段”的comboBox
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_currentChanged(const QModelIndex ¤t, const QModelIndex &previous);
void on_currentRowChanged(const QModelIndex ¤t, const QModelIndex &previous);
}
MainWindow类中定义了几个私有变量。
-
QSqlDatabase DB,用于加载数据库驱动和建立与数据库之间的连接。
-
QSqlTableModel *tabModel,用于指定某一个数据表,作为数据表的数据模型。
-
QItemSelectionModel *theSelection,作为tabModel的选择模型,提供currentChanged()、currentRowChanged()等信号,在tabModel选择的字段发生变化、当前记录发生变化时发射信号,以便程序进行响应。例如在currentChanged()信号发射时,检查tabModel是否有数据被修改,从而更新界面上“保存”和“取消”两个按钮的使能状态。
-
QDataWidgetMapper *dataMapper用于实现界面组件与tabModel的字段之间的映射。例如界面上的QLineEdit类型的dbEditName组件与数据表的Name字段映射,当前记录变化时会自动更新显示当前记录的Name字段的数据。
-
QWComboBoxDelegate是自定义的基于QComboBox的代理类,delegateSex和delegateDepart用作tableView中“性别”和“部门”字段的代理组件。
私有函数openTable()用于打开数据表,getFieldNames()用于获取数据表employee的所有字段的名称,并填充界面上“排序字段”后的ComboBox组件。
定义了两个槽函数,功能如下。
-
on_currentChanged()用于检查数据表内容是否有修改,从而更新“保存”和“取消”两个按钮的使能状态。
-
on_currentRowChanged()用于在当前记录发生变化时,从新的当前记录里提取Photo字段的内容,并作为界面上的QLabel类型的dblabPhoto组件的pixmap显示出来。
MainWindow的构造函数代码如下,主要是对tableView的一些显示属性的设置。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setCentralWidget(ui->splitter);
// tableView显示属性设置
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectItems);
ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
ui->tableView->setAlternatingRowColors(true);
}
11.2.3 打开数据表
工具栏上的“打开”按钮由actOpenDB生成,单击此按钮时将添加SQLite数据库驱动、打开数据库文件、连接employee数据表并设置显示属性,并且创建tableView显示的代理组件,设置数据源与界面组件的映射等。其响应代码如下:
void MainWindow::on_actOpenDB_triggered()
{//打开数据库
QString aFile=QFileDialog::getOpenFileName(this,"选择数据库文件","",
"SQLite数据库(*.db *.db3)");
if (aFile.isEmpty())
return;
DB=QSqlDatabase::addDatabase("QSQLITE"); //添加 SQLITE数据库驱动
DB.setDatabaseName(aFile); //设置数据库名称
if (!DB.open()) //打开数据库
{
QMessageBox::warning(this, "错误", "打开数据库失败",
QMessageBox::Ok,QMessageBox::NoButton);
return;
}
openTable();//打开数据表
}
由打开文件对话框选择需要打开的SQLite数据库文件,本实例中需要打开demodb.db3数据库,此文件在第11章实例程序总目录下。
私有变量DB是QSqlDatabase类。QSqlDatabase用于数据库的操作,包括建立数据库连接,设置登录数据库的参数,打开数据库等。下面的一行语句使用QSqlDatabase的静态函数addDatabase()添加SQLite数据库的驱动。
DB=QSqlDatabase::addDatabase("QSQLITE");
然后需要使用QSqlDatabase的几个函数设置数据库登录参数,setDatabaseName()设置数据库名称,对于SQLite数据库,就设置为数据库文件。如果是网络型数据库,如Oracle、MS SQL Server等,还需要使用setHostName()设置数据库主机名,setUserName()设置数据库用户名,setPassword()设置数据库登录密码。对于SQLite数据库,只需用setDatabaseName()设置数据库文件即可。
数据库连接与登录参数设置后,调用QSqlDatabase::open()函数打开数据库。如果成功打开数据库,再调用自定义函数openTable()打开数据表employee,并进行相应的操作。openTable()函数代码如下:
void MainWindow::openTable()
{//打开数据表
tabModel=new QSqlTableModel(this,DB);//数据表
tabModel->setTable("employee"); //设置数据表
tabModel->setSort(tabModel->fieldIndex("EmpNo"),Qt::AscendingOrder);
tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
if (!(tabModel->select()))//查询数据
{
QMessageBox::critical(this, "错误", "打开数据表错误,错误信息\n"
+tabModel->lastError().text(),
QMessageBox::Ok,QMessageBox::NoButton);
return;
}
//设置字段显示名
tabModel->setHeaderData(tabModel->fieldIndex("EmpNo"), Qt::Horizontal, "工号");
tabModel->setHeaderData(tabModel->fieldIndex("Name"), Qt::Horizontal, "姓名");
tabModel->setHeaderData(tabModel->fieldIndex("Gender"),Qt::Horizontal, "性别");
tabModel->setHeaderData(tabModel->fieldIndex("Height"),Qt::Horizontal, "身高");
tabModel->setHeaderData(tabModel->fieldIndex("Birthday"),Qt::Horizontal,"出生日期");
tabModel->setHeaderData(tabModel->fieldIndex("Mobile"), Qt::Horizontal, "手机");
tabModel->setHeaderData(tabModel->fieldIndex("Province"), Qt::Horizontal, "省份");
tabModel->setHeaderData(tabModel->fieldIndex("City"), Qt::Horizontal, "城市");
tabModel->setHeaderData(tabModel->fieldIndex("Department"), Qt::Horizontal, "部门");
tabModel->setHeaderData(tabModel->fieldIndex("Education"), Qt::Horizontal, "学历");
tabModel->setHeaderData(tabModel->fieldIndex("Salary"), Qt::Horizontal, "工资");
tabModel->setHeaderData(tabModel->fieldIndex("Memo"), Qt::Horizontal, "备注");
tabModel->setHeaderData(tabModel->fieldIndex("Photo"), Qt::Horizontal, "照片");
theSelection=new QItemSelectionModel(tabModel);//选择模型
connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
connect(theSelection,SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentRowChanged(QModelIndex,QModelIndex)));
ui->tableView->setModel(tabModel);//设置数据模型
ui->tableView->setSelectionModel(theSelection); //设置选择模型
ui->tableView->setColumnHidden(tabModel->fieldIndex("Memo"),true);
ui->tableView->setColumnHidden(tabModel->fieldIndex("Photo"),true);
//tableView上为“性别”和“部门”两个字段设置自定义代理组件
QStringList strList;
strList<<"男"<<"女";
bool isEditable=false;
delegateSex.setItems(strList,isEditable);
ui->tableView->setItemDelegateForColumn(
tabModel->fieldIndex("Gender"), &delegateSex);
strList.clear();
strList<<"销售部"<<"技术部"<<"生产部"<<"行政部";
isEditable=true;
delegateDepart.setItems(strList,isEditable);
ui->tableView->setItemDelegateForColumn(
tabModel->fieldIndex("Department"),&delegateDepart);
//创建界面组件与数据模型的字段之间的数据映射
dataMapper= new QDataWidgetMapper();
dataMapper->setModel(tabModel);//设置数据模型
dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
//界面组件与tabModel的具体字段之间的联系
dataMapper->addMapping(ui->dbSpinEmpNo,tabModel->fieldIndex("EmpNo"));
dataMapper->addMapping(ui->dbEditName,tabModel->fieldIndex("Name"));
dataMapper->addMapping(ui->dbComboSex,tabModel->fieldIndex("Gender"));
dataMapper->addMapping(ui->dbSpinHeight,tabModel->fieldIndex("Height"));
dataMapper->addMapping(ui->dbEditBirth,tabModel->fieldIndex("Birthday"));
dataMapper->addMapping(ui->dbEditMobile,tabModel->fieldIndex("Mobile"));
dataMapper->addMapping(ui->dbComboProvince,
tabModel->fieldIndex("Province"));
dataMapper->addMapping(ui->dbEditCity,tabModel->fieldIndex("City"));
dataMapper->addMapping(ui->dbComboDep,
tabModel->fieldIndex("Department"));
dataMapper->addMapping(ui->dbComboEdu,tabModel->fieldIndex("Education"));
dataMapper->addMapping(ui->dbSpinSalary,tabModel->fieldIndex("Salary"));
dataMapper->addMapping(ui->dbEditMemo,tabModel->fieldIndex("Memo"));
dataMapper->toFirst();//移动到首记录
getFieldNames();//获取字段名称列表,填充ui->groupBoxSort组件
//更新actions和界面组件的使能状态
ui->actOpenDB->setEnabled(false);
ui->actRecAppend->setEnabled(true);
ui->actRecInsert->setEnabled(true);
ui->actRecDelete->setEnabled(true);
ui->actScan->setEnabled(true);
ui->groupBoxSort->setEnabled(true);
ui->groupBoxFilter->setEnabled(true);
}
函数openTable()主要是创建QSqlTableModel类型的私有变量tabModel,指定需要打开的数据表为employee,设置与界面显示组件的关联等。QSqlTableModel设置一个数据表名称后,作为数据表的数据模型,可以方便地编辑一个数据表。
使用的核心的类是QSqlTableModel,其主要的函数功能见表11-7(省略了函数中的const关键字,省略了缺省参数)。
表11-7 QSqlTableModel类的主要函数
| 函数 | 功能描述 |
|---|---|
| QSqlDatabase database() | 返回其数据库连接 |
| void setTable(QString &tableName) | 设置数据表名称,不立即读取记录,但会提取字段信息 |
| QString tableName() | 返回设置的数据表名称 |
| void setFilter(QString &filter) | 设置记录过滤条件 |
| void setSort(int column, Qt::SortOrder order) | 设置排序字段和排序规则,需调用select()才生效 |
| void sort(int column, Qt::SortOrder order) | 按列号column和排序规则立即进行排序并获取数据 |
| void setEditStrategy(EditStrategy strategy) | 设置编辑策略 |
| bool setHeaderData(int section, Qt::Orientation orientation, QVariant &value,) | 设置表头,一般用于设置字段的显示名称 |
| bool isDirty() | 若有未更新到数据库的修改,就返回true;否则返回false |
| int fieldIndex(QString &fieldName) | 根据字段名称返回其在模型中的字段序号,若字段不存在则返回-1 |
| QSqlIndex primaryKey() | 返回数据表的主索引 |
| int rowCount() | 返回记录条数 |
| bool select() | 查询数据表的数据,并使用设置的排序和过滤规则 |
| bool selectRow(int row) | 刷新获取指定行号的记录 |
| void clear() | 清除数据模型,释放所有获取的数据 |
| QSqlRecord record() | 返回一条空记录,只有字段名,可用来获取字段信息 |
| QSqlRecord record(int row) | 返回行号为row的一条记录,包含记录的数据 |
| bool setRecord(int row, QSqlRecord &values) | 更新一条记录的数据到数据模型,源和目标之间通过字段名称匹配,而不是按位置匹配 |
| bool insertRecord(int row, QSqlRecord &record) | 在行号row之前插入一条记录 |
| bool insertRows(int row, int count) | 在行号row之前插入count空行,编辑策略为OnFieldChange 或OnRowChange时只能插入一行 |
| bool removeRows(int row, int count) | 从行号row开始,删除count行。若编辑策略为OnManualSubmit,需调用submitAll()才从数据表里删除 |
| void revertRow(int row) | 取消行号为row的记录的修改 |
| void revert() | 编辑策略为OnRowChange或OnFieldChange时,取消当前行的修改,对 OnManualSubmit编辑策略无效 |
| void revertAll() | 取消所有未提交的修改 |
| bool submit() | 提交当前行的修改到数据库,对OnManualSubmit编辑策略无效 |
| bool submitAll() | 提交所有未更新的修改到数据库,若成功则返回true;否则返回false,错误的详细信息可以从lastError()获取 |
函数openTable()的代码主要包括以下几个部分的功能。
1.数据模型创建与属性设置
首先创建QSqlTableModel类型的私有变量tabModel,并且在创建时指定了数据库连接,就是前面设置的私有变量DB,然后用setTable()函数指定数据表。
setSort()函数设置排序字段和排序方式,其函数原型为:
void QSqlTableModel::setSort(int column, Qt::SortOrder order)
第1个参数column是排序字段的列号;第2个参数order是枚举类型Qt::SortOrder,表示排序方式,取值Qt::AscendingOrder表示升序,Qt::DescendingOrder表示降序。程序中设置为根据EmpNo字段升序排序,即:
tabModel->setSort(tabModel->fieldIndex("EmpNo"),Qt::AscendingOrder);
QSqlTableModel::setEditStrategy()函数用于设置编辑策略,参数是枚举类型QSqlTableModel::EditStrategy,各取值的意义如下。
-
QSqlTableModel::OnFieldChange,字段值变化时立即更新到数据库。
-
QSqlTableModel:: OnRowChange,当前行(就是记录)变换时更新到数据库。
-
QSqlTableModel:: OnManualSubmit,所有修改暂时缓存,手动调用submitAll()保存所有修改,或调用revertAll()函数取消所有未保存修改。
程序里设置编辑策略为QSqlTableModel::OnManualSubmit,在修改数据后并不直接提交更新,只是让工具栏上的“保存”和“取消”按钮可用,用户手动提交或取消修改。
对tabModel设置这些属性后,调用select()函数打开数据表,如果打开失败,可以通过QSqlTableModel::lastError()函数获取上一错误信息的文本说明。
2.表头设置
QSqlTableModel::setHeaderData()函数用于设置每个字段的表头数据,主要是设置显示标题。如果不进行表头设置,在TableView里显示时将显示字段名作为表头。为此,将每个字段的显示设置为相应的中文标题。例如,设置“Name”字段的显示标题为“姓名”的代码是:
tabModel->setHeaderData(tabModel->fieldIndex("Name"), Qt::Horizontal, "姓名");
setHeaderData()函数的第1个参数是字段的序号,通过fieldIndex()函数可以获取某个字段名在数据模型里的序号,避免直接使用数字时不便于理解和修改的问题。
3.选择模型及其信号的作用
为数据模型创建一个选择模型的代码如下:
theSelection=new QItemSelectionModel(tabModel);
选择模型的作用是当用户在TableView组件上操作时,获取当前选择的行、列信息,并且在选择的单元格变化时发射currentChanged()信号,在当前行变化时发射currentRowChanged()信号。
为currentChanged()信号编写槽函数on_currentChanged(),获取tabModel->isDirty()函数的值,根据是否有未提交的修改,更新工具栏按钮“保存”或“修改”的使能状态。槽函数on_currentChanged()的代码如下:
void MainWindow::on_currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
{//更新actSubmit和actRevert的状态
Q_UNUSED(current);
Q_UNUSED(previous);
ui->actSubmit->setEnabled(tabModel->isDirty()); //有未保存修改时可用
ui->actRevert->setEnabled(tabModel->isDirty());
}
为currentRowChanged()信号编写槽函数on_currentRowChanged(),用于在当前行变化时,从新的记录里提取Photo字段的内容,并将图片在QLabel组件中显示出来。on_currentRowChanged()的代码如下:
void MainWindow::on_currentRowChanged(const QModelIndex ¤t, const QModelIndex &previous)
{
Q_UNUSED(previous);
// 行切换时的状态控制
ui->actRecDelete->setEnabled(current.isValid());
ui->actPhoto->setEnabled(current.isValid());
ui->actPhotoClear->setEnabled(current.isValid());
if (!current.isValid())
{
ui->dbLabPhoto->clear(); //清除图片显示
return;
}
dataMapper->setCurrentIndex(current.row()); //更新数据映射的行号
int curRecNo=current.row();//获取行号
QSqlRecord curRec=tabModel->record(curRecNo); //获取当前记录
if (curRec.isNull("Photo")) //图片字段内容为空
ui->dbLabPhoto->clear();
else
{
QByteArray data=curRec.value("Photo").toByteArray();
QPixmap pic;
pic.loadFromData(data);
ui->dbLabPhoto->setPixmap(pic.scaledToWidth(
ui->dbLabPhoto->size().width()));
}
}
槽函数on_currentRowChanged()传递的参数current是行切换后新的当前行的模型索引,首先根据current是否有效,更新3个Action的使能状态。若current是有效的,更新数据映射dataMapper的当前行,即:
dataMapper->setCurrentIndex(current.row());
这将使界面上的编辑框、下拉列表框等与字段关联的界面组件显示当前记录的内容。
由于没有现成的界面组件可以通过数据映射显示BLOB字段内的图片,在此槽函数里通过编程获取Photo字段的数据,并将其显示为一个QLabel组件的pixmap。
用下面两行代码获取当前行的记录:
int curRecNo=current.row();//获取行号
QSqlRecord curRec=tabModel->record(curRecNo); //获取当前记录
curRec是QSqlRecord类型,返回了当前记录的数据,可以获取当前记录的每个字段的内容。程序中它获取Photo字段的内容,并将其转换为图片后作为ui->dbLabPhoto组件的pixmap来显示。
4.表示记录的类QSqlRecord
QSqlRecord类记录了数据表的字段信息和一条记录的数据内容,QSqlTableModel有两种参数的函数record()可以返回一条记录。
-
QSqlRecord QSqlTableModel::record(),不带参数的record()函数,返回的一个QSqlRecord对象只有记录的字段定义,但是没有数据。这个函数一般用于获取一个数据表的字段定义。
-
QSqlRecord QSqlTableModel::record(int row),返回行号为row的记录,包括记录的字段定义和数据。
QSqlRecord类封装了对记录的字段定义和数据的操作,其主要函数见表11-8(省略了函数参数中的const关键字,对于具有不同参数的同名函数,只列出一种参数形式的函数)。
表11-8 QSqlRecord类的主要函数
| 函数原型 | 功能描述 |
|---|---|
| void clear() | 清除记录的所有字段定义和数据 |
| void clearValues() | 清除所有字段的数据,将字段数据内容设置为null |
| bool contains(QString &name) | 判断记录是否含有名称为name的字段 |
| bool isEmpty() | 若记录里没有字段,返回true;否则返回false |
| int count() | 返回记录的字段个数 |
| QString fieldName(int index) | 返回序号为index的字段的名称 |
| int indexOf(QString &name) | 返回字段名称为name的字段的序号,如果字段不存在,返回-1 |
| QSqlField field(QString &name) | 返回字段名称为name的字段对象 |
| QVariant value(QString &name) | 返回字段名称为name的字段的值 |
| void setValue(QString &name, QVariant &val) | 设置字段名称为name的字段的值为val |
| bool isNull(const QString &name) | 判断字段名称为name的字段数据是否为null |
| void setNull(const QString &name) | 设置名称为name的字段的值为null |
QSqlRecord用于字段操作的函数一般有两种参数形式的同名函数,用字段序号或字段名表示一个字段,如value()函数返回一个字段的值,有如下两种参数形式的函数:
-
QVariant value(int index),返回序号为index的字段的值;
-
QVariant value(QString &name),返回字段名称为name的字段的值。
5.表示字段的类QSqlField
QSqlRecord的field()函数返回某个字段,返回数据类型是QSqlField。QSqlField封装了字段定义信息和数据。字段的定义一般在设计数据表时就固定了,不用在QSqlRecord里修改。QSqlRecord用于字段数据读写的函数见表11-9(省略了函数参数中的const关键字)。
表11-9 QSqlField类的主要函数
| 接口函数 | 功能描述 |
|---|---|
| void clear() | 清除字段数据,设置为NULL。如果字段是只读的,则不清除 |
| bool isNull() | 判断字段值是否为NULL |
| bool setReadOnly(bool readOnly) | 设置一个字段为只读,只读的字段不能用setValue()函数设置值,也不能用clear()函数清除值 |
| QVariant value() | 返回字段的值 |
| void setValue(QVariant &value) | 设置字段的值 |
6.tableView的设置
界面上用一个QTableView组件显示tabModel的表格数据内容,设置其数据模型和选择模型,并且将Memo和Photo两个字段的列设置为隐藏,因为在表格里难以显示备注文字和图片。
为在tableView中显示和编辑“性别”和“部门”两个字段,设计了基于QComboBox的自定义代理组件类QWComboBoxDelegate。自定义代理组件QWComboBoxDelegate的设计方法在5.5节有详细介绍,这里只是稍作改变,增加了一个setItems()函数,用于初始化下拉列表框和设置是否可以编辑,这样一个QWComboBoxDelegate可以创建多个代理组件实例,分别用于“性别”和“部门”两个字段。
下面是相对于5.5节的QWComboBoxDelegate修改或增加的部分的代码,其余相同的部分没有列出。
class QWComboBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
private:
QStringList m_ItemList;//选择列表
bool m_isEdit; //是否可编辑
public:
void setItems(QStringList items, bool isEdit);//初始化设置列表内容
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE;
};
下面是两个函数的实现代码:
void QWComboBoxDelegate::setItems(QStringList items, bool isEdit)
{
m_ItemList=items;
m_isEdit=isEdit;
}
QWidget *QWComboBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(option);
Q_UNUSED(index);
QComboBox *editor = new QComboBox(parent);
for (int i=0;iaddItem(m_ItemList.at(i));
editor->setEditable(m_isEdit); //是否可编辑
return editor;
}
7.数据映射
QDataWidgetMapper用于建立界面组件与数据模型之间的映射,可以将界面的QLineEdit、QCombobox等组件与数据模型的一个字段关联起来。
创建QDataWidgetMapper类的变量dataMapper后,用setModel()设置关联的数据模型,setSubmitPolicy()函数设置数据提交策略,有自动(AutoSubmit)和手动(ManualSubmit)两种方式。addMapping()用于设置界面组件与数据模型的列的映射,程序在界面上的各编辑组件与数据表的各字段之间建立了映射关系。Memo字段可以与一个QPlainTextEdit的组件映射,但是Photo是一个BLOB字段,没有组件可以直接显示其内容。
QDataWidgetMapper还有toFirst()、toPrevious()、toNext()和toLast() 4个函数用于在记录间移动,setCurrentIndex()和setCurrentModelIndex()可以直接移动到某一行记录,revert()和submit()用于手工取消或提交当前记录的修改,当提交策略设置为QDataWidgetMapper::AutoSubmit时,行切换时将自动提交修改。
8.获取数据表的所有字段名称
自定义函数getFieldNames()获取数据表的所有字段名称,并填充到界面上的“排序字段”下拉列表框里,用于选择排序字段。getFieldNames()的实现代码如下:
void MainWindow::getFieldNames()
{ //获取所有字段名称
QSqlRecord emptyRec=tabModel->record();//获取空记录,只有字段名
for (int i=0;i<emptyRec.count();i++)
ui->comboFields->addItem(emptyRec.fieldName(i));
}
这里使用了QSqlTableModel::record()函数获取一个空的记录,包括数据表的所有字段信息。
所有这些初始化完成后,更新窗口上相关Actions的使能状态。
11.2.4 添加、插入与删除记录
工具栏上有“添加”“插入”“删除”3个按钮用于记录操作。actRecAppend用于添加记录,其槽函数代码如下:
void MainWindow::on_actRecAppend_triggered()
{//添加记录
tabModel->insertRow(tabModel->rowCount(),QModelIndex());
QModelIndex curIndex=tabModel->index(tabModel->rowCount()-1,1);
theSelection->clearSelection();//清空选择
theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
int currow=curIndex.row(); //获得当前行号
tabModel->setData(tabModel->index(currow,0),2000+tabModel->rowCount());
tabModel->setData(tabModel->index(currow,2),"男");
}
QSqlTableModel::insertRow(int row)函数在数据模型的row行前面插入一行记录,如果row大于或等于数据模型的总行数,则在最后添加一行记录。
actRecInsert实现在当前行的前面插入一行,其槽函数代码如下:
void MainWindow::on_actRecInsert_triggered()
{//插入记录
QModelIndex curIndex=ui->tableView->currentIndex();
tabModel->insertRow(curIndex.row(),QModelIndex());
theSelection->clearSelection();//清除已有选择
theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}
actRecDelete用于删除当前记录,其槽函数实现代码如下:
void MainWindow::on_actRecDelete_triggered()
{//删除当前记录
QModelIndex curIndex=theSelection->currentIndex();//获取当前模型索引
tabModel->removeRow(curIndex.row()); //删除一行
}
在插入或删除记录操作,未提交保存之前,tableView的左侧表头会以标记表示记录编辑状态,“*”表示新插入的记录,“!”表示删除的记录。在保存或取消修改后,这些标记就消失,删除的记录行也从tableView里删除。
11.2.5 保存与取消修改
在打开数据表初始化时,设置数据模型的编辑策略为OnManualSubmit,即手动提交修改。当数据模型的数据被修改后,不管是直接修改字段值,还是插入或删除记录,在未提交修改前,QSqlTableModel::isDirty()函数返回true,就是利用这个函数在自定义槽函数on_currentChanged()里修改actSubmit和actRevert两个Action的使能状态。
actSubmit用于保存修改,actRevert用于取消修改,对应工具栏上的“保存”和“取消”两个按钮,下面是这两个Action的槽函数的代码。
void MainWindow::on_actSubmit_triggered()
{//保存修改
bool res=tabModel->submitAll();
if (!res)
QMessageBox::information(this, "消息", "数据保存错误,错误信息\n"
+tabModel->lastError().text(),QMessageBox::Ok,QMessageBox::NoButton);
else
{
ui->actSubmit->setEnabled(false);
ui->actRevert->setEnabled(false);
}
}
void MainWindow::on_actRevert_triggered()
{//取消修改
tabModel->revertAll();
ui->actSubmit->setEnabled(false);
ui->actRevert->setEnabled(false);
}
QSqlTableModel::submitAll()函数用于将数据表所有未提交的修改保存到数据库,revertAll()函数取消所有修改。
调用submitAll()函数保存数据时如果失败,可以通过lastError()函数获取错误的具体信息,例如,Name是必填字段,添加记录时若没有填写Name字段的内容就提交,就会出现错误信息提示对话框。
11.2.6 设置和清除照片
employee数据表的Photo字段是BLOB字段,用于存储图片文件。Photo字段内容的显示已经在自定义槽函数on_currentRowChanged()里实现了,就是在当前记录变化时提取Photo字段的内容,并显示为dbLabPhoto的pixmap。
actPhoto和actPhotoClear是设置照片和清除照片的两个Action,与工具栏上的“设置照片”和“清除照片”按钮关联。actPhoto的槽函数代码如下:
void MainWindow::on_actPhoto_triggered()
{//设置照片
QString aFile=QFileDialog::getOpenFileName(this,"选择图片", "", "照片(*.jpg)");
if (aFile.isEmpty())
return;
QByteArray data;
QFile* file=new QFile(aFile);
file->open(QIODevice::ReadOnly);
data = file->readAll();
file->close();
int curRecNo=theSelection->currentIndex().row();
QSqlRecord curRec=tabModel->record(curRecNo); //获取当前记录
curRec.setValue("Photo",data); //设置字段数据
tabModel->setRecord(curRecNo,curRec);
QPixmap pic;
pic.load(aFile); //在界面上显示
ui->dbLabPhoto->setPixmap(pic.scaledToWidth(ui->dbLabPhoto->width()));
}
代码功能是用文件对话框选择一个图片文件,读取文件内容到QByteArray类型的变量data里,获取当前记录到变量curRec后,用QSqlRecord的setValue()函数为Photo字段设置数据为data,然后用setRecord()函数更新当前记录。
curRec.setValue("Photo",data); //设置字段数据
tabModel->setRecord(curRecNo,curRec);
这里的更新只是更新到数据模型,并没有更新到数据库。
actPhotoClear用于清除照片,其槽函数实现代码如下:
void MainWindow::on_actPhotoClear_triggered()
{
int curRecNo=theSelection->currentIndex().row();
QSqlRecord curRec=tabModel->record(curRecNo); //获取当前记录
curRec.setNull("Photo");//设置为空值
tabModel->setRecord(curRecNo,curRec);
ui->dbLabPhoto->clear();
}
获取当前记录到变量curRec后,调用setNull()函数将Photo字段设置为NULL,就是清除了字段的内容,然后更新记录到数据模型。
11.2.7 数据记录的遍历
工具栏上的“涨工资”按钮用于将数据表内所有记录的salary字段的内容增加10%,演示了记录遍历的功能。actScan实现此按钮的功能,其槽函数实现代码如下:
void MainWindow::on_actScan_triggered()
{//涨工资,记录遍历
if (tabModel->rowCount()==0)
return;
for (int i=0;i<tabModel->rowCount();i++)
{
QSqlRecord aRec=tabModel->record(i); //获取当前记录
float salary=aRec.value("Salary").toFloat();
salary=salary*1.1;
aRec.setValue("Salary",salary);
tabModel->setRecord(i,aRec);
}
if (tabModel->submitAll())
QMessageBox::information(this, "消息", "涨工资计算完毕",
QMessageBox::Ok,QMessageBox::NoButton);
}
11.2.8 记录排序
QSqlTableModel的setSort()函数设置数据表根据某个字段按照升序或降序排列,实际上就是设置了SQL语句里的ORDER BY子句。
在打开数据库时,已经调用getFieldNames()函数将数据表的所有字段名添加到界面上的comboFields下拉列表框了。“排序字段”下拉列表框、“升序”和“降序”两个RadioButton的相应操作的槽函数实现按选择字段排序。
void MainWindow::on_comboFields_currentIndexChanged(int index)
{//选择字段进行排序
if (ui->radioBtnAscend->isChecked())
tabModel->setSort(index,Qt::AscendingOrder);
else
tabModel->setSort(index,Qt::DescendingOrder);
tabModel->select();
}
void MainWindow::on_radioBtnAscend_clicked()
{//升序
tabModel->setSort(ui->comboFields->currentIndex(),Qt::AscendingOrder);
tabModel->select();
}
void MainWindow::on_radioBtnDescend_clicked()
{//降序
tabModel->setSort(ui->comboFields->currentIndex(),Qt::DescendingOrder);
tabModel->select();
}
在调用setSort()函数设置排序规则后,需要调用QSqlTableModel::select()重新读取数据表的数据才会使排序规则生效。
11.2.9 记录过滤
QSqlTableModel的setFilter()函数设置记录过滤条件,实际上就是设置了SQL语句里的WHERE字句。
实例里演示针对Gender字段设置字段过滤条件,窗口界面上“数据过滤”分组框里有3个RadioButton,分别为“男”“女”“全显示”,3个按钮的clicked()信号的槽函数实现代码如下:
void MainWindow::on_radioBtnMan_clicked()
{
tabModel->setFilter(" Gender='男' ");
}
void MainWindow::on_radioBtnWoman_clicked()
{
tabModel->setFilter(" Gender='女' ");
}
void MainWindow::on_radioBtnBoth_clicked()
{
tabModel->setFilter("");
}
调用setFilter()后无需调用select()函数就可以立即刷新记录,若要取消过滤条件,只需在setFilter()函数里传递一个空字符串。
11.3 QSqlQueryModel的使用
11.3.1 QSqlQueryModel功能概述
从原书的图11-2的SQL数据模型类的继承关系看,QSqlQueryModel是QSqlTableModel的父类。QSqlQueryModel封装了执行SELECT语句从数据库查询数据的功能,但是QSqlQueryModel只能作为只读数据源使用,不可以编辑数据。
QSqlQueryModel类的主要接口函数见表11-10(省略了函数中的const关键字和缺省参数)。
表11-10 QSqlQueryModel类的主要函数
| 接口函数 | 功能描述 |
|---|---|
| void clear() | 清除数据模型,释放所有获得的数据 |
| QSqlError lastError() | 返回上次的错误,可获取错误的类型和文本信息 |
| QSqlQuery query() | 返回当前关联的QSqlQuery对象 |
| void setQuery(QSqlQuery &query) | 设置一个QSqlQuery对象,获取数据 |
| void setQuery(QString &query) | 设置一个SELECT语句创建查询,获取数据 |
| QSqlRecord record() | 返回一个空记录,包含当前查询的字段信息 |
| QSqlRecord record(int row) | 返回行号为row的记录 |
| int rowCount() | 返回查询到的记录条数 |
| int columnCount() | 返回查询的字段个数 |
| void setHeaderData(int section, Qt::Orientation orientation, QVariant &value) | 设置表头数据,一般用于设置字段的表头标题 |
使用QSqlQueryModel作为数据模型从数据库里查询数据,只需使用setQuery()函数设置一个SELECT查询语句即可。QSqlQueryModel可以作为QTableView等视图组件的数据源,也可以使用QDataWidgetMapper创建字段与界面组件的映射,只是查询出来的数据是不可编辑的。
11.3.2 使用QSqlQueryModel实现数据查询
1.实例功能
使用QSqlQueryModel可以从一个数据表或多个数据表里查询数据,只需设计好SELECT语句即可。实例samp11_2使用QSqlQueryModel从employee表里查询记录,并在界面上显示,运行窗口见原书的图11-5。
窗口工具栏上几个工具栏按钮只有打开数据库和记录移动功能,记录移动通过调用QDataWidgetMapper类的记录移动功能实现。窗口上没有数据编辑和保存功能,因为QSqlQueryModel查询出来的数据是只读的。
主窗口类的定义部分如下(去除了界面组件自动生成的槽函数的定义):
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QSqlDatabase DB; //数据库
QSqlQueryModel *qryModel; //数据模型
QItemSelectionModel *theSelection; //选择模型
QDataWidgetMapper *dataMapper;//数据界面映射
void openTable();//打开数据表
void refreshTableView();//移动记录时刷新TableView的当前行
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//selectionModel的行发生了变化
void on_currentRowChanged(const QModelIndex ¤t, const QModelIndex &previous);
};
这里定义了QSqlQueryModel类型的数据模型变量qryModel,也定义了数据库、选择模型和数据界面映射的变量。refreshTableView()函数用于记录移动后刷新tableView上的当前行位置。
自定义槽函数on_currentRowChanged()在选择模型的当前行变化时,处理Photo字段的查询与照片显示。
2.打开数据库
工具栏上的“打开数据库”按钮对应actOpenDB,选择SQLite数据库文件,然后调用openTable()函数打开数据库。槽函数on_actOpenDB_triggered()的代码与实例samp11_1完全一样,这里不再列出。
openTable()用于查询数据,建立界面显示等具体操作,其代码如下:
void MainWindow::openTable()
{//打开数据表
qryModel=new QSqlQueryModel(this);
qryModel->setQuery("SELECT EmpNo, Name, Gender, Height, Birthday, Mobile, Province, City, Department , Education, Salary FROM employee ORDER BY EmpNo");
if (qryModel->lastError().isValid())
{
QMessageBox::critical(this, "错误",
"数据表查询错误,错误信息\n"+qryModel->lastError().text(),
QMessageBox::Ok,QMessageBox::NoButton);
return;
}
qryModel->setHeaderData(0,Qt::Horizontal,"工号");
qryModel->setHeaderData(1,Qt::Horizontal,"姓名");
qryModel->setHeaderData(2,Qt::Horizontal,"性别");
qryModel->setHeaderData(3,Qt::Horizontal,"身高");
qryModel->setHeaderData(4,Qt::Horizontal,"出生日期");
qryModel->setHeaderData(5,Qt::Horizontal,"手机");
qryModel->setHeaderData(6,Qt::Horizontal,"省份");
qryModel->setHeaderData(7,Qt::Horizontal,"城市");
qryModel->setHeaderData(8,Qt::Horizontal,"部门");
qryModel->setHeaderData(9,Qt::Horizontal,"学历");
qryModel->setHeaderData(10,Qt::Horizontal,"工资");
theSelection=new QItemSelectionModel(qryModel);
connect(theSelection,SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentRowChanged(QModelIndex,QModelIndex)));
ui->tableView->setModel(qryModel);
ui->tableView->setSelectionModel(theSelection);
//创建数据映射
dataMapper= new QDataWidgetMapper();
dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
dataMapper->setModel(qryModel);
dataMapper->addMapping(ui->dbSpinEmpNo,0);//"EmpNo";
dataMapper->addMapping(ui->dbEditName,1);//"Name";
dataMapper->addMapping(ui->dbComboSex,2);//"Gender";
dataMapper->addMapping(ui->dbSpinHeight,3);//"Height";
dataMapper->addMapping(ui->dbEditBirth,4);//"Birthday";
dataMapper->addMapping(ui->dbEditMobile,5);//"Mobile";
dataMapper->addMapping(ui->dbComboProvince,6);//"Province";
dataMapper->addMapping(ui->dbEditCity,7);//"City";
dataMapper->addMapping(ui->dbComboDep,8);//"Department";
dataMapper->addMapping(ui->dbComboEdu,9);//"Education";
dataMapper->addMapping(ui->dbSpinSalary,10);//"Salary";
dataMapper->toFirst();
ui->actOpenDB->setEnabled(false);
}
程序首先创建了QSqlQueryModel类型的私有变量qryModel,然后调用setQuery()函数设置了SELECT查询语句,SELECT语句从employee表里查询除了Memo和Photo之外的所有其他字段。
使用setHeaderData()函数为每个字段设置显示标题,为使代码简化,这里直接使用了字段的序号。
为qryModel创建了选择模型theSelection,并且将其currentRowChanged()信号与自定义槽函数on_currentRowChanged()关联起来。这个自定义槽函数用来在记录移动时,查询出Memo和Photo字段的内容,并在界面上显示出来。
下面是槽函数on_currentRowChanged()的代码:
void MainWindow::on_currentRowChanged(const QModelIndex ¤t, const QModelIndex &previous)
{
Q_UNUSED(previous);
if (!current.isValid())
{ ui->dbLabPhoto->clear();
return;
}
dataMapper->setCurrentModelIndex(current); //更新数据映射的行号
bool first=(current.row()==0); //是否首记录
bool last=(current.row()==qryModel->rowCount()-1);//是否尾记录
ui->actRecFirst->setEnabled(!first); //更新使能状态
ui->actRecPrevious->setEnabled(!first);
ui->actRecNext->setEnabled(!last);
ui->actRecLast->setEnabled(!last);
int curRecNo=theSelection->currentIndex().row();
QSqlRecord curRec=qryModel->record(curRecNo); //获取当前记录
int empNo=curRec.value("EmpNo").toInt();
QSqlQuery query; //查询当前EmpNo的Memo和Photo字段的数据
query.prepare("select EmpNo, Memo, Photo from employee where EmpNo = :ID");
query.bindValue(":ID",empNo);
query.exec();
query.first();
QVariant va=query.value("Photo");
if (!va.isValid()) //图片字段内容为空
ui->dbLabPhoto->clear();
else
{//显示照片
QByteArray data=va.toByteArray();
QPixmap pic;
pic.loadFromData(data);
ui->dbLabPhoto->setPixmap(pic.scaledToWidth(
ui->dbLabPhoto->size().width()));
}
QVariant va2=query.value("Memo");//显示备注
ui->dbEditMemo->setPlainText(va2.toString());
}
这个函数实现3个功能,第1个功能是更新数据映射的行号,即:
dataMapper->setCurrentModelIndex(current);
使窗口上的字段关联的显示组件刷新显示当前记录的内容。
第2个功能是根据当前行号,判断是否是首记录或尾记录,以此更新界面上4个记录移动的Action的使能状态。
第3个功能是获取当前记录的EmpNo字段的值(即员工编号),然后用一个QSqlQuery变量query执行查询语句,只查询出这个员工的Memo和Photo字段的数据,然后在界面元件上显示。这里使用了QSqlQuery类,它用来执行任意的SQL语句。
在QSqlQueryModel类的变量qryModel里设置SELECT语句时,并没有查询所有字段,因为Photo是BLOB字段,全部查询出来后必然占用较大内存,而且在做记录遍历时,如果存在BLOB字段数据,执行速度会很慢。所以,这个实例里将普通字段的查询用QSqlQueryModel来查询并显示,而Memo和Photo字段数据的查询采用按需查询的方式,这样可以减少内存消耗,提高记录遍历时的执行速度。
openTable()函数剩余的部分是设置tableView的数据模型和选择模型,然后创建数据界面映射变量dataMapper,设置各个界面组件与字段的映射关系。
3.记录移动
用于数据映射的QDataWidgetMapper类设置数据模型后,总是指向数据模型的当前记录。QDataWidgetMapper有4个函数进行当前记录的移动,分别是toFirst()、toLast()、toNext()和toPrevious()。当前记录移动时,会引起数据模型关联的选择模型发射currentRowChanged()信号,也就会执行关联的自定义槽函数on_currentRowChanged()。
工具栏上有4个记录移动的按钮,它们调用QDataWidgetMapper的记录移动函数实现记录移动,4个Action的槽函数代码如下:
void MainWindow::on_actRecFirst_triggered()
{ //首记录
dataMapper->toFirst();
refreshTableView();
}
void MainWindow::on_actRecPrevious_triggered()
{ //前一条记录
dataMapper->toPrevious();
refreshTableView();
}
void MainWindow::on_actRecNext_triggered()
{//后一条记录
dataMapper->toNext();
refreshTableView();
}
void MainWindow::on_actRecLast_triggered()
{//最后一条记录
dataMapper->toLast();
refreshTableView();
}
使用QDataWidgetMapper的记录移动操作后,QDataWidgetMapper会移动到新的记录上,映射了字段的界面组件也会自动显示新记录的字段的数据。但是,tableView的当前行并不会自动变化,所以需要调用refreshTableView()函数刷新tableView的显示,refreshTableView()函数的代码如下:
void MainWindow::refreshTableView()
{//刷新tableView的当前选择行
int index=dataMapper->currentIndex();
QModelIndex curIndex=qryModel->index(index,1);
theSelection->clearSelection();//清空选择项
theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}
11.4 QSqlQuery的使用
11.4.1 QSqlQuery基本用法
QSqlQuery是能执行任意SQL语句的类,如SELECT、INSERT、UPDATE、DELETE等,QSqlQuery类的一些常用函数见表11-11(省略函数中的const关键字,省略缺省参数,不同参数的同名函数一般只给出一种参数形式)。
表11-11 QSqlQuery类的主要函数
| 接口函数 | 功能描述 |
|---|---|
| bool prepare(QString &query) | 设置准备执行的SQL语句,一般用于带参数的SQL语句 |
| void bindValue(QString &placeholder, QVariant &val) | 设置SQL语句中参数的值,以占位符表示参数 |
| void exec() | 执行由prepare()和bindValue()设置的SQL语句 |
| void exec(QString &query) | 直接执行一个不带参数的SQL语句 |
| bool isActive() | 如果成功执行了exec()函数,就返回true |
| bool isSelect() | 如果执行的SQL语句是SELECT语句,就返回true |
| QSqlRecord record() | 返回当前记录 |
| QVariant value(QString &name) | 返回当前记录名称为name的字段的值 |
| bool isNull(QString &name) | 判断一个字段是否为空,当query非活动、未定位在有效记录、无此字段或字段为空时都返回true |
| int size() | 对于SELECT语句,返回查询到的记录条数,其他语句返回-1 |
| int numRowsAffected() | 返回SQL语句影响的记录条数,对于SELECT语句无定义 |
| bool first() | 定位到第一条记录,isActive和isSelect都为true时才有效 |
| bool previous() | 定位到上一条记录,isActive和isSelect都为true时才有效 |
| bool next() | 定位到下一条记录,isActive和isSelect都为true时才有效 |
| bool last() | 定位到最后一条记录,isActive和isSelect都为true时才有效 |
| bool seek(int index) | 定位到指定序号的记录 |
| int at() | 返回当前记录的序号 |
使用QSqlQuery执行不带参数的SQL语句时可以用exec(QString)函数,如:
QSqlQuery query;
query.exec("SELECT * FROM employee");
query.exec("UPDATE employee SET Salary=3000 where Gender='女' ");
使用带参数的SQL语句时,先用prepare()函数准备SQL语句,然后用bindValue()函数设置参数值,再用exec()执行SQL语句,如:
QSqlQuery query;
query.prepare ("SELECT * FROM employee where EmpNo=:ID");
query.bindValue(":ID", 2003);
query.exec();
上面是SQL语句中的参数用“冒号+参数名”表示的形式,还可以直接用占位符来表示参数,如:
QSqlQuery query;
query.prepare ("UPDATE employee SET Name=?, Gender=?, Height=? where EmpNo=?");
query.bindValue(0, "高某某");
query.bindValue(1, "男");
query.bindValue(2, 1.78);
query.bindValue(3,2010);
query.exec();
11.4.2 QSqlQueryModel和QSqlQuery联合使用
1.数据表显示
QSqlQueryModel可以查询数据并作为数据模型,实现数据的显示,QSqlQuery可以执行UPDATE、INSERT、DELETE等SQL语句实现数据的编辑修改。
实例samp11_3通过联合使用QSqlQueryModel和QSqlQuery组件实现数据表的显示和编辑修改,原书的图11-6是实例samp11_3运行主窗口。
主窗口类定义如下(去掉了一些自动生成的部分):
class MainWindow : public QMainWindow
{ Q_OBJECT
private:
QSqlDatabase DB; //数据库
QSqlQueryModel *qryModel; //数据库模型
QItemSelectionModel *theSelection; //选择模型
void openTable();//打开数据表
void updateRecord(int recNo); //更新记录
public:
explicit MainWindow(QWidget *parent = 0);
};
这个主窗口类的定义相比于实例samp11_2只是增加了一个updateRecord()函数。工具栏上的“打开数据库”按钮的代码与实例samp11_2完全相同,会调用openTable()连接数据库并查询数据表的数据。
本实例的openTable()函数的代码如下:
void MainWindow::openTable()
{//打开数据表
qryModel=new QSqlQueryModel(this);
theSelection=new QItemSelectionModel(qryModel);
qryModel->setQuery("SELECT EmpNo, Name, Gender, Height, Birthday, Mobile, Province, City, Department, Education, Salary FROM employee order by EmpNo");
if (qryModel->lastError().isValid())
{
QMessageBox::information(this, "错误", "数据表查询错误,错误信息\n"
+qryModel->lastError().text(), QMessageBox::Ok,QMessageBox::NoButton);
return;
}
qryModel->setHeaderData(0,Qt::Horizontal,"工号");
qryModel->setHeaderData(1,Qt::Horizontal,"姓名");
qryModel->setHeaderData(2,Qt::Horizontal,"性别");
qryModel->setHeaderData(3,Qt::Horizontal,"身高");
qryModel->setHeaderData(4,Qt::Horizontal,"出生日期");
qryModel->setHeaderData(5,Qt::Horizontal,"手机");
qryModel->setHeaderData(6,Qt::Horizontal,"省份");
qryModel->setHeaderData(7,Qt::Horizontal,"城市");
qryModel->setHeaderData(8,Qt::Horizontal,"部门");
qryModel->setHeaderData(9,Qt::Horizontal,"学历");
qryModel->setHeaderData(10,Qt::Horizontal,"工资");
ui->tableView->setModel(qryModel);
ui->tableView->setSelectionModel(theSelection);
ui->actOpenDB->setEnabled(false);
ui->actRecInsert->setEnabled(true);
ui->actRecDelete->setEnabled(true);
ui->actRecEdit->setEnabled(true);
ui->actScan->setEnabled(true);
}
openTable()函数创建了QSqlQueryModel类对象qryModel,从数据表employee里查询出除了Memo和Photo之外的其他字段,并作为界面上的tableView的数据模型。还创建了选择模型theSelection,但是没有为选择模型的currentRowChanged()信号关联槽函数,因为不需要在记录移动时做什么处理。由于使用QSqlQueryModel作为tableView的数据源,在tableView里是无法编辑修改数据的。
2.编辑记录对话框
由于在tableView上无法编辑修改数据,只是作为一个只读的数据显示,在主窗口工具栏上提供了“插入记录”“编辑记录”“删除记录”3个按钮对数据进行编辑。“插入记录”和“编辑记录”都会打开一个对话框,编辑一条记录的所有字段数据,确认插入后用QSqlQuery执行一条INSERT语句插入一条记录,确认编辑时用QSqlQuery执行一个UPDATE语句更新一条记录。
设计了一个对话框WDialogData,用于编辑一条记录的所有字段的数据,在“插入记录”和“编辑记录”时调用此对话框,对话框运行界面如原书的图11-7所示。
WDialogData类的定义如下:
class WDialogData : public QDialog
{
Q_OBJECT
private:
QSqlRecord mRecord; //保存一条记录的数据
public:
explicit WDialogData(QWidget *parent = 0);
~WDialogData();
void setUpdateRecord(QSqlRecord &recData); //更新记录
void setInsertRecord(QSqlRecord &recData); //插入记录
QSqlRecord getRecordData();//获取录入的数据
private slots:
void on_btnClearPhoto_clicked(); //清理照片
void on_btnSetPhoto_clicked(); //设置照片
private:
Ui::WDialogData *ui;
};
QSqlRecord类型的私有变量mRecord用于存储一条记录的数据。
插入一条记录时,创建对话框后调用setInsertRecord()函数初始化对话框的数据;编辑一条记录时,创建对话框后调用setUpdateRecord ()函数初始化对话框的数据。
对话框确认修改后,调用getRecordData()时,界面数据存入mRecord,并将mRecord作为返回值,返回编辑后的一条记录的数据。
对话框WDialogData的所有自定义函数,以及对话框上的“导入照片”“清除照片”按钮的代码如下,程序代码比较简单,这里不再过多解释。
void WDialogData::setUpdateRecord(QSqlRecord &recData)
{ //编辑记录,更新记录数据到界面
mRecord=recData;
ui->spinEmpNo->setEnabled(false); //员工编号不允许编辑
setWindowTitle("更新记录");
//根据recData的数据更新界面显示
ui->spinEmpNo->setValue(recData.value("EmpNo").toInt());
ui->editName->setText(recData.value("Name").toString());
ui->comboSex->setCurrentText(recData.value("Gender").toString());
ui->spinHeight->setValue(recData.value("Height").toFloat());
ui->editBirth->setDate(recData.value("Birthday").toDate());
ui->editMobile->setText(recData.value("Mobile").toString());
ui->comboProvince->setCurrentText(recData.value("Province").toString());
ui->editCity->setText(recData.value("City").toString());
ui->comboDep->setCurrentText(recData.value("Department").toString());
ui->comboEdu->setCurrentText(recData.value("Education").toString());
ui->spinSalary->setValue(recData.value("Salary").toInt());
ui->editMemo->setPlainText(recData.value("Memo").toString());
QVariant va=recData.value("Photo");
if (!va.isValid()) //图片字段内容为空
ui->LabPhoto->clear();
else
{
QByteArray data=va.toByteArray();
QPixmap pic;
pic.loadFromData(data);
ui->LabPhoto->setPixmap(pic.scaledToWidth(
ui->LabPhoto->size().width()));
}
}
void WDialogData::setInsertRecord(QSqlRecord &recData)
{//插入记录,无需更新界面显示,但是要存储recData的字段结构
mRecord=recData; //保存recData到内部变量
ui->spinEmpNo->setEnabled(true); //插入的记录,员工编号允许编辑
setWindowTitle("插入新记录");
ui->spinEmpNo->setValue(recData.value("EmpNo").toInt());
}
QSqlRecord WDialogData::getRecordData()
{ //"确定"按钮后,界面数据保存到记录mRecord
mRecord.setValue("EmpNo",ui->spinEmpNo->value());
mRecord.setValue("Name",ui->editName->text());
mRecord.setValue("Gender",ui->comboSex->currentText());
mRecord.setValue("Height",ui->spinHeight->value());
mRecord.setValue("Birthday",ui->editBirth->date());
mRecord.setValue("Mobile",ui->editMobile->text());
mRecord.setValue("Province",ui->comboProvince->currentText());
mRecord.setValue("City",ui->editCity->text());
mRecord.setValue("Department",ui->comboDep->currentText());
mRecord.setValue("Education",ui->comboEdu->currentText());
mRecord.setValue("Salary",ui->spinSalary->value());
mRecord.setValue("Memo",ui->editMemo->toPlainText());
//照片编辑时已经修改了mRecord的Photo字段的值
return mRecord; //以记录作为返回值
}
void WDialogData::on_btnClearPhoto_clicked()
{ //清除照片
ui->LabPhoto->clear();
mRecord.setNull("Photo"); //Photo字段清空
}
void WDialogData::on_btnSetPhoto_clicked()
{//设置照片
QString aFile=QFileDialog::getOpenFileName(this,"选择图片", "", "照片(*.jpg)");
if (aFile.isEmpty())
return;
QByteArray data;
QFile* file=new QFile(aFile);
file->open(QIODevice::ReadOnly);
data = file->readAll();
file->close();
mRecord.setValue("Photo",data); //图片保存到Photo字段
QPixmap pic;
pic.loadFromData(data);
ui->LabPhoto->setPixmap(pic.scaledToWidth(ui->LabPhoto->size().width()));
}
3.编辑记录
单击主窗口工具栏上的“编辑记录”按钮,或在tableView上双击某条记录,会编辑当前记录,代码如下:
void MainWindow::on_actRecEdit_triggered()
{//编辑当前记录
int curRecNo=theSelection->currentIndex().row();
updateRecord(curRecNo);
}
void MainWindow::on_tableView_doubleClicked(const QModelIndex &index)
{ //tableView上双击,编辑当前记录
int curRecNo=index.row();
updateRecord(curRecNo);
}
两个槽函数都会调用updateRecord()函数,并且以记录的序号作为传递参数。updateRecord()函数实现当前记录的编辑,代码如下:
void MainWindow::updateRecord(int recNo)
{ //更新一条记录
QSqlRecord curRec=qryModel->record(recNo); //获取当前记录
int empNo=curRec.value("EmpNo").toInt();//获取EmpNo
QSqlQuery query; //查询出当前记录的所有字段
query.prepare("select * from employee where EmpNo = :ID");
query.bindValue(":ID",empNo);
query.exec();
query.first();
if (!query.isValid()) //是否为有效记录
return;
curRec=query.record();
WDialogData *dataDialog=new WDialogData(this);
Qt::WindowFlags flags=dataDialog->windowFlags();
dataDialog->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint);
dataDialog->setUpdateRecord(curRec);//调用对话框函数更新数据和界面
int ret=dataDialog->exec();// 以模态方式显示对话框
if (ret==QDialog::Accepted) //OK键被按下
{
QSqlRecord recData=dataDialog->getRecordData(); //获得对话框返回的记录
query.prepare("update employee set Name=:Name, Gender=:Gender,"
" Height=:Height, Birthday=:Birthday, Mobile=:Mobile, Province=:Province,"
" City=:City,Department=:Department, Education=:Education, Salary=:Salary,"
" Memo=:Memo, Photo=:Photo where EmpNo = :ID");
query.bindValue(":Name",recData.value("Name"));
query.bindValue(":Gender",recData.value("Gender"));
query.bindValue(":Height",recData.value("Height"));
query.bindValue(":Birthday",recData.value("Birthday"));
query.bindValue(":Mobile",recData.value("Mobile"));
query.bindValue(":Province",recData.value("Province"));
query.bindValue(":City",recData.value("City"));
query.bindValue(":Department",recData.value("Department"));
query.bindValue(":Education",recData.value("Education"));
query.bindValue(":Salary",recData.value("Salary"));
query.bindValue(":Memo",recData.value("Memo"));
query.bindValue(":Photo",recData.value("Photo"));
query.bindValue(":ID",empNo);
if (!query.exec())
QMessageBox::critical(this, "错误",
"记录更新错误\n"+query.lastError().text(),
QMessageBox::Ok,QMessageBox::NoButton);
else//重新执行SQL语句查询
qryModel->query().exec();
}
delete dataDialog;
}
函数updateRecord(int recNo)根据行号recNo从qryModel获取当前记录的EmpNo字段的值,即员工编号,然后使用一个QSqlQuery对象从数据表里查询出这一个员工的所有字段的一条记录。使用QsqlQuery时用到了参数SQL语句:
query.prepare("select * from employee where EmpNo = :ID");
query.bindValue(":ID",empNo);
query.exec();
由于EmpNo是数据表employee的主键字段,不允许出现重复,所以只会查询出一条记录,查询出的一条完整记录保存到变量curRec。
然后创建WDialogData类型的对话框dataDialog,调用setUpdateRecord(curRec)将完整记录传递给对话框。对话框执行后,如果是“确定”返回,则通过getRecordData()函数获取对话框编辑后的记录数据:
QSqlRecord recData=dataDialog->getRecordData();
recData里包含了编辑后的最新数据,然后使用QSqlQuery对象执行带参数的UPDATE语句更新一条记录。更新成功后,将数据模型qryModel的SELECT 语句重新执行一次,可刷新tableView的显示。
4.插入记录
工具栏上的“插入记录”可以插入一条新记录,代码如下:
void MainWindow::on_actRecInsert_triggered()
{//插入记录
QSqlQuery query;
query.exec("select * from employee where EmpNo =-1"); //只查询字段信息
QSqlRecord curRec=query.record();//获取当前记录,实际为空记录
curRec.setValue("EmpNo",qryModel->rowCount()+3000);
WDialogData *dataDialog=new WDialogData(this);
Qt::WindowFlags flags=dataDialog->windowFlags();
dataDialog->setWindowFlags(flags | Qt::MSWindowsFixedSizeDialogHint);
dataDialog->setInsertRecord(curRec); //插入记录
int ret=dataDialog->exec();// 以模态方式显示对话框
if (ret==QDialog::Accepted) //OK键被按下
{
QSqlRecord recData=dataDialog->getRecordData();
query.prepare("INSERT INTO employee (EmpNo,Name,Gender, Height,Birthday, "
"Mobile,Province, City,Department,Education,Salary,Memo,Photo) "
" VALUES(:EmpNo,:Name, :Gender,:Height,:Birthday, :Mobile, :Province,"
" :City, :Department, :Education,:Salary,:Memo,:Photo)");
query.bindValue(":EmpNo",recData.value("EmpNo"));
query.bindValue(":Name",recData.value("Name"));
query.bindValue(":Gender",recData.value("Gender"));
query.bindValue(":Height",recData.value("Height"));
query.bindValue(":Birthday",recData.value("Birthday"));
query.bindValue(":Mobile",recData.value("Mobile"));
query.bindValue(":Province",recData.value("Province"));
query.bindValue(":City",recData.value("City"));
query.bindValue(":Department",recData.value("Department"));
query.bindValue(":Education",recData.value("Education"));
query.bindValue(":Salary",recData.value("Salary"));
query.bindValue(":Memo",recData.value("Memo"));
query.bindValue(":Photo",recData.value("Photo"));
if (!query.exec())
QMessageBox::critical(this, "错误",
"插入记录错误\n"+query.lastError().text(),
QMessageBox::Ok,QMessageBox::NoButton);
else //重新执行SQL语句查询
qryModel->query().exec();
}
delete dataDialog;
}
程序首先用QSqlQuery类对象query执行一个SQL语句“select * from employee where EmpNo =-1”,这样不会查询到任何记录,目的就是得到一条空记录curRec。
创建WDialogData类型的对话框dataDialog后,调用setInsertRecord()函数对话框进行初始化设置。
对话框dataDialog运行“确认”返回后,使用query执行INSERT语句插入一条新记录。若插入记录执行成功,需要重新执行数据模型qryModel的SQL语句查询数据,才会更新界面上的tableView的显示。
5.删除记录
工具栏上的“删除记录”删除tableView上的当前记录,代码如下:
void MainWindow::on_actRecDelete_triggered()
{//删除当前记录
int curRecNo=theSelection->currentIndex().row();
QSqlRecord curRec=qryModel->record(curRecNo); //获取当前记录
if (curRec.isEmpty()) //当前为空记录
return;
int empNo=curRec.value("EmpNo").toInt();//获取员工编号
QSqlQuery query;
query.prepare("delete from employee where EmpNo = :ID");
query.bindValue(":ID",empNo);
if (!query.exec())
QMessageBox::critical(this, "错误",
"删除记录出现错误\n"+query.lastError().text(),
QMessageBox::Ok,QMessageBox::NoButton);
else //重新执行SQL语句查询
qryModel->query().exec();
}
从数据模型的当前记录中获取员工编号,然后使用一个QSqlQuery类的对象执行一条DELETE语句删除这条记录。
删除记录后需要重新设置数据模型qryModel的SQL语句并查询数据,以更新数据和tableView的显示。
6.记录遍历
工具栏上的“涨工资”按钮通过记录遍历,修改所有记录的Salary字段的值,其实现代码如下:
void MainWindow::on_actScan_triggered()
{//涨工资,记录遍历
QSqlQuery qryEmpList;
qryEmpList.exec("SELECT EmpNo,Salary FROM employee ORDER BY EmpNo");
qryEmpList.first();
QSqlQuery qryUpdate; //临时 QSqlQuery
qryUpdate.prepare("UPDATE employee SET Salary=:Salary WHERE EmpNo = :ID");
while (qryEmpList.isValid()) //当前记录有效
{
int empID=qryEmpList.value("EmpNo").toInt(); //获取EmpNo
float salary=qryEmpList.value("Salary").toFloat(); //获取Salary
salary=salary+1000; //涨工资
qryUpdate.bindValue(":ID",empID);
qryUpdate.bindValue(":Salary",salary); //设置SQL语句参数
qryUpdate.exec(); //执行update
if (!qryEmpList.next()) //移动到下一条记录,并判断是否到末尾了
break;
}
qryModel->query().exec();//重新查询数据,更新tableView的显示
QMessageBox::information(this, "提示", "涨工资计算完毕",
QMessageBox::Ok,QMessageBox::NoButton);
}
程序里使用了两个QSqlQuery类变量,qryEmpList查询EmpNo和Salary两个字段的全部记录,qryUpdate用于执行一个带参数的UPDATE语句,每次更新一条记录。
遍历qryEmpList的所有记录,QSqlQuery有first()、previous()、next()、last()等函数记录移动,若到了最后一条记录后再执行next(),将返回false,以此判断是否遍历完所有记录。
11.5 QSqlRelationalTableModel的使用
11.5.1 关系型数据表和实例功能
从原书的图11-2的类继承关系图上看,QSqlRelationalTableModel是QSqlTableModel的子类。QSqlRelationalTableModel可以处理关系数据表,所谓关系数据表,是指将主表里的某个字段存储为代码型字段,而代码的具体意义在另外一个数据表里。
表11-5给出了studInfo的字段定义,studInfo数据表实际存储的数据示例见表11-12,字段departID和majorID存储的是学院代码和专业代码。
表11-12 表studInfo的数据记录示例
| 序号 | studID | name | gender | departID | majorID |
|---|---|---|---|---|---|
| 1 | 2017200211 | 张三 | 男 | 20 | 2003 |
| 2 | 2017200102 | 李四 | 男 | 10 | 1002 |
| 3 | 2017300212 | 小明 | 男 | 50 | 5001 |
| 4 | 2017400102 | 小雅 | 女 | 50 | 5002 |
学院代码字段departID的具体意义在数据表departments里定义,departments表的字段结构见表11-3,departments表的示例数据见表11-13。
表11-13 表departments的数据记录示例
| 序号 | departID | department |
|---|---|---|
| 1 | 10 | 生物科学学院 |
| 2 | 20 | 数理学院 |
| 3 | 50 | 化工学院 |
同样,专业数据表majors定义了专业代码及其专业名称,majors表的字段结构见表11-4,majors表的示例数据见表11-14。majors数据表不仅定义了专业代码的意义,还使用了学院代码字段departID,与departments数据表发生关联。
表11-14 表majors的数据记录示例
| 序号 | majorID | major | departID |
|---|---|---|---|
| 1 | 1001 | 生物遗传学 | 10 |
| 2 | 1002 | 生物工程 | 10 |
| 3 | 5001 | 计算机网络 | 50 |
| 4 | 5002 | 自动化 | 50 |
在数据库设计中使用代码字段和代码表的好处;一是可以减少数据表的存储量,一个大的数据表存储代码远比存储具体文字用的存储空间少;二是代码表示的文字可能会被修改,例如学院的名称可能会修改,这时只需修改代码表里一条记录而已。
QSqlRelationalTableModel类专门用来编辑这种具有代码字段的数据表,可以很方便地将代码字段与关系数据表建立关系,在显示和编辑数据表时,直接使用关系数据表的代码意义字段的内容。实例samp11_4演示关系型数据表使用的方法,原书的图11-8是程序运行界面。
实例samp11_4使用QSqlRelationalTableModel作为tableView的数据源,显示和编辑studInfo数据表。学院和专业两个字段与代码表建立了关系,tableView中直接显示这两个字段的文字内容,编辑时有一个下拉列表框,列表框里是代码表里全部代码意义文字。
11.5.2 关系型数据模型功能实现
1.主窗口类定义
主窗口类定义如下,定义了一个QSqlRelationalTableModel类型的变量tabModel作为数据模型。
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QSqlDatabase DB; //数据库连接
QSqlRelationalTableModel *tabModel;//数据模型
QItemSelectionModel *theSelection; //选择模型
void openTable(); //打开数据表
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_currentChanged(const QModelIndex ¤t, const QModelIndex &previous);
};
2.打开数据表
主窗口工具栏的“打开”按钮选择数据库文件demodb.db3,然后调用openTable()打开数据表,openTable()函数的代码如下:
void MainWindow::openTable()
{//打开数据表
tabModel=new QSqlRelationalTableModel(this,DB);
tabModel->setTable("studInfo"); //设置数据表
tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
tabModel->setSort(0,Qt::AscendingOrder);
tabModel->setHeaderData(0,Qt::Horizontal,"学号");
tabModel->setHeaderData(1,Qt::Horizontal,"姓名");
tabModel->setHeaderData(2,Qt::Horizontal,"性别");
tabModel->setHeaderData(3,Qt::Horizontal,"学院");
tabModel->setHeaderData(4,Qt::Horizontal,"专业");
//设置代码字段的查询关系数据表
tabModel->setRelation(3,
QSqlRelation("departments","departID","department")); //学院
tabModel->setRelation(4,
QSqlRelation("majors","majorID","major"));//专业
theSelection=new QItemSelectionModel(tabModel);
connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
ui->tableView->setModel(tabModel);
ui->tableView->setSelectionModel(theSelection);
//为关系型字段设置缺省代理组件
ui->tableView->setItemDelegate(
new QSqlRelationalDelegate(ui->tableView));
tabModel->select(); //打开数据表
ui->actOpenDB->setEnabled(false);
ui->actRecAppend->setEnabled(true);
ui->actRecInsert->setEnabled(true);
ui->actRecDelete->setEnabled(true);
ui->actFields->setEnabled(true);
}
QSqlRelationalTableModel类的主要函数与QSqlTableModel相同,有一个新的函数setRelation()用于设置代码字段的关联数据表和关联字段。setRelation()函数的定义如下:
void setRelation(int column, const QSqlRelation &relation)
其中column是主表中代码字段的序号,relation是QSqlRelation类型的表示关联数据表的关系。设置代码字段departID的关系的代码如下:
tabModel->setRelation(3,
QSqlRelation("departments","departID","department"));
第1个参数(数字3)是departID字段在studInfo表中的字段序号。
第2个参数用QSqlRelation(“departments”,“departID”,“department”)创建了一个QSqlRelation类型对象,其中,参数"departments"是代码表departments,参数"departID"是代码字段名称,参数"department"是代码意义字段名称。
openTable()函数中还有关键的一行:
ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));
这是在tableView中为代码字段创建缺省的关系型代理组件,这样在tableView中编辑代码字段的内容时,才会出现一个下拉列表框,列出代码表的所有可选内容。
3.实际字段列表
使用QSqlRelationalTableModel类设置代码字段的关系后,在tableView中以代码意义显示代码字段的内容,其实际字段有没有变化呢?
工具栏上的按钮“字段列表”列出了tabModel的所有字段的名称,其代码如下:
void MainWindow::on_actFields_triggered()
{//获取字段列表
QSqlRecord emptyRec=tabModel->record();//获取空记录,只有字段名
QString str;
for (int i=0;i<emptyRec.count();i++)
str=str+emptyRec.fieldName(i)+'\n';
QMessageBox::information(this, "所有字段名", str,
QMessageBox::Ok,QMessageBox::NoButton);
}
字段列表的对话框如原书的图11-9所示,可以看到两个代码字段departID和majorID被代码表中的代码意义字段department和major替换了。但是在界面上修改数据后,数据能以代码的形式保存到原数据表里。
4.其他功能的实现
工具栏上其他“添加”“插入”“删除”“保存”“取消”等按钮的功能的实现与实例samp11_1相同,自定义槽函数on_currentChanged()的实现也与samp11_1相同,这些功能的实现就不具体介绍了。

3228

被折叠的 条评论
为什么被折叠?



