欢迎光临
我们一直在努力

【Qt】《Qt 5.9 C++开发指南》笔记-Chapter11-数据库

第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 &current, const QModelIndex &previous);
void on_currentRowChanged(const QModelIndex &current, 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 &current, 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 &current, 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 &current, 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 &current, 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的数据记录示例

序号studIDnamegenderdepartIDmajorID
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的数据记录示例

序号departIDdepartment
1 10 生物科学学院
2 20 数理学院
3 50 化工学院

同样,专业数据表majors定义了专业代码及其专业名称,majors表的字段结构见表11-4,majors表的示例数据见表11-14。majors数据表不仅定义了专业代码的意义,还使用了学院代码字段departID,与departments数据表发生关联。

表11-14 表majors的数据记录示例

序号majorIDmajordepartID
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 &current, 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相同,这些功能的实现就不具体介绍了。

赞(0)
未经允许不得转载:171主机测评 » 【Qt】《Qt 5.9 C++开发指南》笔记-Chapter11-数据库
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址