连接数据库

写在前面:数据库连接是不安全的,子线程不可以复用主线程连接!

要使用QSqlQuery或者QSqlQueryModel访问数据库,首先要建立与数据库的连接。创建连接+打开连接

首先要说明的是标识数据库的应是连接名称而非数据库名称。在调用QSqlQuery或者QSqlQueryModel中的成员函数时,如果不传递连接名称参数,将使用默认连接。下例为创建并打开一个默认连接

1
2
3
4
5
6
7
8
QSqlDatabase db = QSqlDatabase::addDatabase("QMySql")//创建默认连接
db.setHostName("bigblue");//主机名字,一般是localhost
db.setDatabaseName("flightdb");//数据库连接的名字,就是理解意义上的数据库名
db.setUserName("root");
db.setPassword("123456");
bool ok = db.open();//打开默认连接
QSqlDatabase firstDB = QSqlDatabase::addDatabase("QMYSQL", "first");//创建有名连接
QSqlDatabase secondDB = QSqlDatabase::addDatabase("QMYSQL", "second");

如果open()失败,通过QSqlDatabase::lastError()获取错误信息

连接建立后,可以在任意地方调用函数获取数据库指针

QSqlDatabase p = QSqlDatabase::database();//可带参数:连接名称

要关闭数据库,先QSqlDatabase::close()关闭,再使用静态方法QSqlDatabase::removeDatabase()删除

QSqlQuery

QSqlQuery 类提供了执行 SQL 语句和浏览查询结果集的接口.

执行查询

要执行查询语句,只需要建立一个QSqlQuery对象,调用其方法即可

QSqlQuery构造函数接受QSqlDatabase对象,用于指定要使用的数据库连接,如果为空则使用默认连接

1
2
QSqlQuery q;
bool ok = q.exec("SELECT name, salary FROM employee WHERE salary > 50000");

导航结果集

QSqlQuery提供对结果集一条记录一条记录的访问,调用exec()后,q的内部指针位于第一条记录之前的位置即at()=-1,需调用一次QSqlQuery::next(),来前进到第一条记录,然后重复调用next来访问其他记录直到false。

1
2
3
4
while(query.next()) { QStringname=query.value(0).toString();
intsalary=query.value(1).toInt();
qDebug() << name << salary;
}

QSqlQuery::value() 函数返回当前记录中字段的值。字段指定为基于零的索引。QSqlQuery::value() 返回一个QVariant ,该类型可容纳各种 C++ 和 Qt Core 数据类型,如intQStringQByteArray 。不同的数据库类型会自动映射为最接近的 Qt 对应类型。在代码片段中,我们调用QVariant::toString() 和QVariant::toInt() 将变体转换为QStringint

导航结果集除了next之外,还有first(只要有结果集,强制跳转第一行而非索引-1的位置),previous,last,seek,at

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 演示数据集导航函数
void navigateDataset() {
QSqlQuery query("SELECT id, name, age FROM student ORDER BY id");
// 1. 检查查询是否成功(仅当查询返回结果集时,导航函数才有效)
if (!query.exec()) {
qDebug() << "查询失败:" << query.lastError().text();
return;
}
// 2. 获取总行数(SQLite 支持 size(),部分数据库可能不支持,需提前确认)
int totalRows = query.size();
qDebug() << "结果集总行数:" << totalRows << "\n";
// 3. 导航函数用法演示
qDebug() << "=== 1. first():强制跳转到第一行即便最开始指向-1 ===";
if (query.first()) { // 成功返回 true,失败(空结果集)返回 false
int id = query.value("id").toInt();
QString name = query.value("name").toString();
int age = query.value("age").toInt();
qDebug() << "当前行索引(at()):" << query.at() // 返回 0(第一行索引从 0 开始)
<< " | 数据:id=" << id << ", name=" << name << ", age=" << age << "\n";
}

qDebug() << "=== 2. next():跳转到下一行 ===";
if (query.next()) { // 从第一行跳转到第二行
int id = query.value("id").toInt();
QString name = query.value("name").toString();
int age = query.value("age").toInt();
qDebug() << "当前行索引(at()):" << query.at() // 返回 1
<< " | 数据:id=" << id << ", name=" << name << ", age=" << age << "\n";
}

qDebug() << "=== 3. last():跳转到最后一行 ===";
if (query.last()) { // 跳转到第五行
int id = query.value("id").toInt();
QString name = query.value("name").toString();
int age = query.value("age").toInt();
qDebug() << "当前行索引(at()):" << query.at() // 返回 4(总行数 5,索引 0-4)
<< " | 数据:id=" << id << ", name=" << name << ", age=" << age << "\n";
}

qDebug() << "=== 4. previous():跳转到上一行 ===";
if (query.previous()) { // 从最后一行跳转到第四行
int id = query.value("id").toInt();
QString name = query.value("name").toString();
int age = query.value("age").toInt();
qDebug() << "当前行索引(at()):" << query.at() // 返回 3
<< " | 数据:id=" << id << ", name=" << name << ", age=" << age << "\n";
}

qDebug() << "=== 5. seek(int row):跳转到指定行 ===";
if (query.seek(2)) { // 跳转到第 3 行(索引从 0 开始,2 对应第三行)
int id = query.value("id").toInt();
QString name = query.value("name").toString();
int age = query.value("age").toInt();
qDebug() << "当前行索引(at()):" << query.at() // 返回 2
<< " | 数据:id=" << id << ", name=" << name << ", age=" << age << "\n";
}

// 6. 边界情况:跳转到超出范围的行
qDebug() << "=== 6. 边界测试 ===";
if (!query.seek(10)) { // 跳转到索引 10(超出总行数 5),返回 false
qDebug() << "跳转到索引 10 失败(超出范围)";
}
if (!query.previous()) { // 从第一行再往前跳,返回 false
qDebug() << "从第一行往前跳失败(已到开头)";
}
}

要确定数据库驱动程序是否允许使用某功能,使用QSqlDriver::hasFeature(),如下面计算结果集行数

1
2
3
4
5
6
7
8
QSqlDatabase defaultDB = QSqlDatabase::database();
if (defaultDB.driver()->hasFeature(QSqlDriver::QuerySize)) {
numRows = query.size();//支持则直接使用
} else {
// this can be very slow
query.last();//不支持则只能如此计算
numRows = query.at() + 1;
}

特定优化:如果在结果集中导航,并且只在向前浏览时使用 next() 和 seek(),则可以在调用 exec() 之前调用QSqlQuery::setForwardOnly(true)。这是一个简单的优化方法,在操作大型结果集时,可以显著加快查询速度。

执行插入

创建query对象,插入一条记录q.exec(插入语句)即可

1
2
"INSERT INTO employee (id, name, salary) "
"VALUES (1001, 'Thad Beaumont', 65000)"

插入多条记录更推荐将查询与实际插入值分开,依赖命令绑定和位置绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QSqlQuery query;
query.prepare("INSERT INTO employee (id, name, salary) "
"VALUES (:id, :name, :salary)");
query.bindValue(":id", 1001);
query.bindValue(":name", "Thad Beaumont");
query.bindValue(":salary", 65000);
query.exec();
//位置绑定
query.prepare("INSERT INTO employee (id, name, salary) "
"VALUES (?, ?, ?)");
query.addBindValue(1001);
query.addBindValue("Thad Beaumont");
query.addBindValue(65000);
query.exec();

好处在,插入多条记录时,只需要调用一次prepare语句,后面的记录按需执行bindValue/addBindValue,及exec即可。

执行更新

更新操作类似于插入,也可以通过位置或者命名绑定

执行删除

1
2
QSqlQuery query;
query.exec("DELETE FROM employee WHERE id = 1007");

!!事务!!

如果底层数据库引擎支持事务,即QSqlDriver::hasFeature(QSqlDriver::Transaction)返回true,则可以

  1. QSqlDatabase::transaction(),启动事务
  2. 输入要在事务上下文中执行的SQL命令
  3. QSqlDatabase::commit()/rollback()
1
2
3
4
5
QSqlDatabase p_db = QSqlDatabase::database();
p_db.transaction();
//sql
//...
p_db.commit();

SQL模型类

除了使用查询语句访问数据库,Qt还提供了三个高级类。

QSqlQueryModel 基于任意 SQL 查询的只读模型。
QSqlTableModel 基于单个表的读写模式。
QSqlRelationalTableModel 支持外键的QSqlTableModel子类。

好处

  1. 这些类都继承自QAbstractTableModel (而又继承自QAbstractItemModel ),可以方便地在QListViewQTableView 等项目视图类中显示数据库中的数据
  2. 可以使代码更容易适应其他数据源。例如,如果使用QSqlTableModel ,随后又决定使用 XML 文件而不是数据库来存储数据,那么这基本上只是将一种数据模型替换为另一种数据模型的问题。

QSqlQueryModel

基于任意查询【可以使一个表部分字段也可以是多个表的组合字段】的只读模型

1
2
3
4
5
6
7
QSqlQueryModel model;
model.setQuery("SELECT * FROM employee");
for(int i= 0; i<model.rowCount();++i) {
int id=model.record(i).value("id").toInt();
QString name=model.record(i).value("name").toString();
qDebug() << id << name;
}

使用QSqlQueryModel::setQuery() 设置查询后,可以使用QSqlQueryModel::record(int) 访问单条记录。

还可以使用QSqlQueryModel::data() 和从QAbstractItemModel 继承的任何其他函数。

此外,还有一个setQuery() 重载,它接收一个QSqlQuery 对象并对其结果集进行操作。这样,就可以使用QSqlQuery 的任何功能来设置查询(例如,准备查询)。

QSqlTableModel

提供了一种读写模式,每次只对一种表进行读写操作。不需要sql语法知识

1
2
3
4
5
6
7
8
9
10
QSqlTableModel model;
model.setTable("employee");
model.setFilter("salary > 50000");
model.setSort(2, Qt::DescendingOrder);//(按照第二列降序排列)
model.select();
for(int i= 0; i<model.rowCount();++i){
QStringname=model.record(i).value("name").toString();
intsalary=model.record(i).value("salary").toInt();
qDebug() << name << salary;
}

使用QSqlTableModel::record() 检索表中的一行,使用QSqlTableModel::setRecord() 修改该行。例如,以下代码将使每个雇员的工资增加 10%

1
2
3
4
5
6
7
8
for (int i = 0; i < model.rowCount(); ++i) {
QSqlRecord record = model.record(i);
double salary = record.value("salary").toInt();
salary *= 1.1;
record.setValue("salary", salary);
model.setRecord(i, record);
}
model.submitAll();

上面是一种根据字段去设置的方法,还可以根据行列去设置值,代码如下:

1
2
3
4
5
6
7
model.insertRows(row, 1);
model.setData(model.index(row, 0), 1013);
model.setData(model.index(row, 1), "Peter Gordon");
model.setData(model.index(row, 2), 68500);
model.submitAll();
model.removeRows(row, 5);//(起始索引,删除数目)
model.submitAll();

可以注意到执行任意修改操作后都需要提交记录submitAll。何时提交以及是否需要提交?

取决于表的编辑策略editstrategy

QSqlTableModel::OnRowChange 规定当用户选择不同的记录时,即跳出编辑状态,待处理的更改将应用到数据库
QSqlTableModel::OnManualSubmit 所有更改都缓存在模型中,直到调用 submitAll()
QSqlTableModel::OnFieldChange 不缓存更改

看样子只需要OnFielsChange就可以不用调用submitAll,但存在两个问题,一是显而易见的实时修改带来的性能问题,二是如果修改了主键,试图填充时该条记录可能会丢失

QSqlRelationalTableModel

该模型主要是对QSqlTableModel的扩展,为外键提供支持。如果book 表中有一个名为authorid 的字段,它指向作者表的id 字段(主键),那么authorid 是一个外键。

1
2
3
model->setTable("employee");
model->setRelation(2, QSqlRelation("city", "id", "name"));
model->setRelation(3, QSqlRelation("country", "id", "name"));

视图结合以显示

可以将同一模型用作多个视图的数据源。如果用户通过其中一个视图编辑模型,其他视图将立即反映更改

视图类在顶部显示一个标题来标注列。要更改页眉文本,在模型上调用setHeaderData() 。标题的标签默认为表的字段名。

1
2
3
4
model->setHeaderData(0, Qt::Horizontal, QObject::tr("ID"));
model->setHeaderData(1, Qt::Horizontal, QObject::tr("Name"));
model->setHeaderData(2, Qt::Horizontal, QObject::tr("City"));
model->setHeaderData(3, Qt::Horizontal, QObject::tr("Country"));

QTableView 也在左侧有一个垂直标题,用数字标识行。如果使用QSqlTableModel::insertRows() 插入行,在使用submitAll() 提交之前,或在用户移动到另一条记录时(假设edit strategyQSqlTableModel::OnRowChange )自动提交之前,新行都将标有星号 (*)。

img

同样,如果使用removeRows() 删除记录,在提交更改之前,这些记录将用感叹号(!)标记。

视图中的项与目均是通过代理实现的delegate,想要自定义代理需要集成QAbstractStyleItemDelegate

或者QStyledItemDelegate,重写createEditor,setEditorData,setModelData,updateEditorGeometry等函数。

QSql示例

驱动准备

Qt 的 MySQL 驱动(QMYSQL)不是 “开箱即用” 的,需要满足两个条件:

  1. Qt 安装目录下有 qsqlmysql.dll(MySQL 驱动文件);
  2. 系统中存在 MySQL 客户端库(libmysql.dll),Qt 驱动需要依赖它才能连接 MySQL 服务器。

而 SQLite 是嵌入式数据库,驱动无需额外依赖,这是两者的关键区别!

由于我使用的Mysql,需要安装额外的sql驱动包,但是官方现在不提供了。只能靠第三方编译好的dll使用。贴个链接https://github.com/thecodemonkey86/qt_mysql_driver。一定要选择与自己qt版本完全相同的release下载

一个粘在qt安装目录

img

一个粘在构建目录和exe一起

img

再次运行程序可以看到链接成功

img

表准备

img

动作准备

增加如上菜单项,目前没有找到办法平铺在菜单栏上。

右键这些动作,点击转到槽,选择triggered信号编写槽函数

img

简要界面设计

大致如下

img

打开数据库表并显示

比较麻烦,包含数据模型创建,选择模型创建。

1.创建数据模型,打开表

数据保存方式:这里选择先存缓冲区,由用户点击action再submit。

img

2.设置字段,显示标题

其实就是设置界面可见的表的标题字段,用数据库表的字段建立一个映射关系。

tabModel->setHeaderData(tabModel->fieldIndex(“empNo”),Qt::Horizontal,”工号”);

3.创建选择模型

因为想把是否提交修改数据的选择权交给客户,所以在当前行或者单元格发生变化时发送信号。通过model的isDirty()方法判断是否有未提交到数据库的修改,据此给菜单栏提交/撤回动作使能。

而选中行槽函数是为了从当前记录里面提取photo字段内容,再将图片显示出来

场景 :行 0 列 0 → 行 0 列 1(同行动列)

  • currentChanged:触发,current 是行 0 列 1 的 QModelIndexprevious 是行 0 列 0;
  • currentRowChanged:不触发(行号还是 0,仅列变)。
信号名 完整签名 触发条件 传递信息
currentChanged void currentChanged(const QModelIndex &current, const QModelIndex &previous) 「当前项」发生变化(无论行 / 列改变),比如:1. 从行 0 列 0 → 行 0 列 1(同行动列);2. 从行 0 列 0 → 行 1 列 0(列同行变);3. 从行 0 列 0 → 行 1 列 1(行 + 列都变) current:新的当前项索引(含行、列、模型信息);previous:旧的当前项索引
currentRowChanged void currentRowChanged(int currentRow) 仅当「当前项的行号」发生变化时触发,列变化不触发:✅ 行 0→行 1(触发);❌ 行 0 列 0→行 0 列 1(不触发) currentRow:新的当前项行号(仅整数,无列 / 模型信息)
1
2
3
4
5
6
7
8
9
10
11
selModel = new QItemSelectionModel(tabModel,this);
//当前行或列发生变化时触发槽函数
connect(selModel,&QItemSelectionModel::currentChanged,this,&MainWindow::do_currentChanged);
connect(selModel,&QItemSelectionModel::currentRowChanged,this,&MainWindow::do_currentRowChanged);
void MainWindow::do_currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
Q_UNUSED(current);
Q_UNUSED(previous);
ui->actionSubmit->setEnabled(tabModel->isDirty());
ui->actionRevert->setEnabled(tabModel->isDirty());
}

选中行后显示照片,并且自动修改datamapp,这使得界面上与字段关联的组件显示当前行记录的内容。

因为没有现成组件可以通过数据映射Blob类型的图片,所以在槽函数里先获取该字段的数据(是二进制数据所以用字节数组承接),在为label显示图片。

QSqlRecord类记录的是数据表的字段信息一条记录的数据内容。model.record()可以返回字段信息,model.record(int row)返回字段定义和数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void MainWindow::do_currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
{
Q_UNUSED(previous);
//current是当前行的模型索引,需要判断是否有效
ui->actionRecDelete->setEnabled(current.isValid());
ui->actionPhoto->setEnabled(current.isValid());
ui->actionPhotoClear->setEnabled(current.isValid());
if(!current.isValid()){
ui->dbLabelPhoto->clear();
return;
}
dataMapper->setCurrentIndex(current.row());
int curRecNo = current.row();
//获取当前行记录
QSqlRecord curRec = tabModel->record(curRecNo);
if(curRec.isNull("Photo"))
ui->dbLabelPhoto->clear();
else{
QByteArray data = curRec.value("Photo").toByteArray();
QPixmap pic;
pic.loadFromData(data);
ui->dbLabelPhoto->setPixmap(pic.scaledToWidth(ui->dbLabelPhoto->size().width()));
}
}

不得不提的是SQLRecord类提供了返回字段对象的函数QSqlField field(name);所以提一嘴QSqlField,封装了访问 字段定义信息和字段数据的一些函数。如返回字段名称,字段所在表名称,字段类型字段是否必填自增等定义信息,返回清空设置字段值等字段信息。

4.MV视图模型绑定
1
2
3
4
ui->tableView->setModel(tabModel);
ui->tableView->setSelectionModel(selModel);
ui->tableView->setColumnHidden(tabModel->fieldIndex("Memo"),true);
ui->tableView->setColumnHidden(tabModel->fieldIndex("Photo"),true);
5.为部分字段【性别】【部门】设置代理
1
2
3
4
5
6
7
8
9
10
11
12
13
QStringList strList;
strList<<"男"<<"女";
bool isEditable = false;
delegateSex.setItems(strList,isEditable);
//设置代理
ui->tableView->setItemDelegateForColumn(tabModel->fieldIndex("Gender"),&delegateSex);

strList.clear();
strList<<"计算机学院"<<"电子信息学院";
isEditable = true;
delegateSex.setItems(strList,isEditable);
//设置代理
ui->tableView->setItemDelegateForColumn(tabModel->fieldIndex("Department"),&delegateDepart);
6.创建界面组件与模型字段的数据映射

先讲一下数据映射类QDataWidgetMapper

这个类主要用来建立界面组件与模型字段的映射关系,这样这些组件就可以自动显示关联字段的数据并且在组件中修改数据后也可以提交到模型!【没到数据库!】

数据提交方式 setSubmitPolicy 同数据模型一样也需要设置界面组件上数据更新到模型的方式。autosubmit:组件丢失焦点所进行的修改自动提交manualSubmit:手动提交,需调用submit
添加/移除映射 add/removeMapping clearMapping可以清除所有映射
返回某个组件映射字段的序号返回某个序号的字段映射的组件 int mappedSection()QWidget* mappedWidgetAt()
记录移动并刷新界面数据 toFirst/toPreviours/toNext/toLast/setCurrentIndex()
数据保存 bool submit()void revert() 将组件上修改的数据提交到模型重新从模型获取数据显示,所有未提交修改将会丢失
信号 void currentIndexChanged 当前行发生变化时发射

还需要注意的是该类没有选择模型,所以当tableview单元格点击发生变化,要想右边组件也同步变化则需要在槽函数调用dataMa->setCurrentIndex(current.row())

这里的界面组件指的是非TableView里面的组件,即我们右半区域的一些组件,希望把数据也映射到对应位置。

addMapping 第三个参数 currentData 表示「用 某组件(如QComboBox)currentData() 绑定数据库字段」,而非默认的 currentText(),这样数据库的 1/0 会对应选项的 1/0 值,显示中文文本;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ui->dbComboSex->addItem("男", 1);  // 显示“男”,对应值 1
ui->dbComboSex->addItem("女", 0); // 显示“女”,对应值 0
dataMapper = new QDataWidgetMapper(this);
dataMapper->setModel(tabModel);
dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
//界面组件与模型具体字段的映射
dataMapper->addMapping(ui->dbSpinEmpNo,tabModel->fieldIndex("empNo"));
dataMapper->addMapping(ui->dbEditName,tabModel->fieldIndex("Name"));
dataMapper->addMapping(ui->dbComboSex,tabModel->fieldIndex("Gender"));
dataMapper->addMapping(ui->dbEditBirth,tabModel->fieldIndex("Birthday"));
dataMapper->addMapping(ui->dbComboProvince,tabModel->fieldIndex("Province"));
dataMapper->addMapping(ui->dbComboDep,tabModel->fieldIndex("Department"));
dataMapper->addMapping(ui->dbEditMemo,tabModel->fieldIndex("Memo"));
dataMapper->toFirst();//移动到首个记录

img

【新】勘误,是因为没有在代码里写与工资组件的映射。。。

还要解决右边组件组中combox等非lineedit组件明明设置了映射但是却不显示的问题。

原因是这些组件和lineEdit数据模型/显示机制的不同,需要先设置下拉列表并确保下拉列表和字段值匹配才可以显示。只需要在.ui里面提前写好就可以。

而spinbox显示0是因为数据库中工资字段类型为float,不支持。有三种解决办法

  1. 换用 QDoubleSpinBox(支持小数)
  2. 保留 QSpinBox 并设置 decimals=0 自动取整。
  3. 若无法修改数据库字段类型,可通过 QDataWidgetMappersetItemDelegate 实现类型转换:

通过构造时传入列索引来判断是否是需要自己处理的列,其他列调用默认设置方法即可,为了简单直接采用方法1。注意设置最大值,否则显示0.0

7.获取字段名称列表,填充’排序字段‘下拉框

一定要在model获取到数据后,才有页眉数据【是从页眉取而不是数据库】,才可以获取。在填充逻辑里面仅仅添加需要排序的字段

getFieldNames();

img我只填充了工号及工资,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
ui->comboBoxFieldNames->clear();
int colCnt = tabModel->columnCount();
for(int col =0;col<colCnt;++col){
QString fieldName = tabModel->headerData(col,Qt::Horizontal).toString();
if (fieldName.isEmpty() || fieldName == "Name") { // 跳过空字段名或主键 id
continue;
}
ui->comboBoxFieldNames->addItem(fieldName);
}
if(ui->comboBoxFieldNames->count()>0){//如果要显示的条目大于0默认显示第一项
ui->comboBoxFieldNames->setCurrentIndex(0);
}
}

上面代码比较麻烦,可以直接通过QSqlRecord类的当前记录来设置(因为其中存储了字段定义信息和字段数据)

1
2
3
QSqlRecord rec =tabModel->record()
for(int i=0;i<rec.count();++i)
ui->comboBoxFieldNames->addItem(rec.fieldName(i));
8.更新action和界面组件的使能状态
1
2
3
4
5
6
7
ui->actionOpenDB->setEnabled(false);
ui->actionRecAppend->setEnabled(true);
ui->actionRecInsert->setEnabled(true);
ui->actionRecDelete->setEnabled(true);

ui->groupBoxSort->setEnabled(true);
ui->groupBoxFilter->setEnabled(true);

到这里已经实现表显示数据库数据+点击任意记录右边组件同步变化+修改数据模型组件同步变化了

但是模型改了,还没有到提交数据库那一步,所以只是视图上修改了。接下来完善我们的手动保存逻辑

其他功能实现

添加、插入、删除记录

主要基于QSqlTableModel类定义的两个操作记录的函数

bool insertRecord(int row,consr QSqlRecord &record);在row行前插入

bool setRecord(int row,const QSqlRecord &value);修改row行记录

及其父类定义的删除记录的函数

bool removeRow(int row,consr QModelIndex &parent =QModelIndex())

插入数据需要注意,数据库中必填项必须要填,否则报错。找不到动作编辑器只需要在ui设计界面空白部分右键。

为什么这三种方法里面获取行索引的函数都不同?

先看添加的场景tabModel->index这是在添加一个新的索引关联某行提供用于访问的索引对象。

而插入和删除场景,大部分情况下结果是相同的,只是特殊情况不同。视图的currentIndex关注在焦点,选择模型的currentIndex关注在选中状态。这两个因为默认关联所以会带来聚焦了也同时选中的效果,但必须强调聚焦!=选中。编辑焦点用视图的索引,处理选中状态用选择模型的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void MainWindow::on_actionRecAppend_triggered()
{//添加=尾增
QSqlRecord rec = tabModel->record();//只获取字段信息
rec.setValue(tabModel->fieldIndex("empNo"),1000+tabModel->rowCount());
rec.setValue(tabModel->fieldIndex("Gender"),"男");
tabModel->insertRecord(tabModel->rowCount(),rec);
//让选择模型选中新加的记录
selModel->clearSelection();
QModelIndex curIndex = tabModel->index(tabModel->rowCount()-1,1);
selModel->setCurrentIndex(curIndex,QItemSelectionModel::Select);
showRecordCount();
}


void MainWindow::on_actionRecInsert_triggered()
{
//先取得当前行的模型索引
QModelIndex curIndex = ui->tableView->currentIndex();
QSqlRecord rec = tabModel->record();
//rec definition
tabModel->insertRecord(curIndex.row(),rec);

selModel->clearSelection();
QModelIndex Index = tabModel->index(tabModel->rowCount()-1,1);
selModel->setCurrentIndex(Index,QItemSelectionModel::Select);
showRecordCount();
}


void MainWindow::on_actionRecDelete_triggered()
{
QModelIndex curIndex = selModel->currentIndex();
tabModel->removeRow(curIndex.row());
showRecordCount();
}

img

保存或取消修改

我们先前给数据模型设置的提交策略是手动提交,需要我们调用submitAll等提交方法。在未提交之前,不管作何修改或新增,tabModel->isDirty()始终返回true,即暂存区有东西未提交到数据库。比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void MainWindow::on_actionSubmit_triggered()
{
bool res = tabModel->submitAll();
if(!res){
QMessageBox::critical(this,"Message","Failed to Save Data,error:"+tabModel->lastError().text());
}else{
ui->actionSubmit->setEnabled(false);
ui->actionRevert->setEnabled(false);
}
showRecordCount();
}


void MainWindow::on_actionRevert_triggered()
{
tabModel->revertAll();
ui->actionSubmit->setEnabled(false);
ui->actionRevert->setEnabled(false);
showRecordCount();
}
设置和清除照片

要设置照片,将图片数据存到photo字段。读图片数据到字节数组,取到当前记录,设置字段,更新当前记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void MainWindow::on_actionPhoto_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 = selModel->currentIndex().row();//获取行数
QSqlRecord rec = tabModel->record(curRecNo);//根据行数获得该条记录
rec.setValue("Photo",data);
tabModel->setRecord(curRecNo,rec);
QPixmap pic;
pic.load(afile);
ui->dbLabelPhoto->setPixmap(pic.scaledToWidth(ui->dbLabelPhoto->width()));

ui->actionSubmit->setEnabled(tabModel->isDirty());
ui->actionRevert->setEnabled(tabModel->isDirty());
}


void MainWindow::on_actionPhotoClear_triggered()
{
int row = selModel->currentIndex().row();
QSqlRecord rec = tabModel->record(row);
rec.setNull("Photo");
tabModel->setRecord(row,rec);
ui->dbLabelPhoto->clear();

ui->actionSubmit->setEnabled(tabModel->isDirty());
ui->actionRevert->setEnabled(tabModel->isDirty());
}

img

其他功能都正常,保存的时候报错了这是因为我们数据库图片字段容量太小了,设计表-》将该字段类型改成mediumblob即可。

img

遍历数据记录效果

通过涨工资这个动作来演示遍历记录的功能,无非就是获取行数,循环获取记录然后修改值。

如果过滤了男性,那么遍历也就只会遍历女性条目,需要注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::on_actionScan_triggered()
{
if(tabModel->rowCount()==0)
return;
for(int i=0;i<tabModel->rowCount();++i){
QSqlRecord rec = tabModel->record(i);
double salary = rec.value("Salary").toDouble();
salary *=1.1;
rec.setValue("Salary",salary);
tabModel->setRecord(i,rec);
}
if(tabModel->submitAll()){
QMessageBox::information(this,"消息","涨工资完毕");
}
}
记录排序实现

实现根据combobox选中字段与单选框选项来排序

主要用到QSqlTableModel的两个函数

void setSort(int col,Qt::SortOrder order) 设置排序条件,不会立即刷新数据模型
void sort(int col,Qt::SortOrder order) 立即排序,直接根据字段即排序方式排序,自动运行select(刷新模型)
记录过滤实现

void QSqlTableModel::setFilter(const QString &filter)

让模型仅从数据库中查询 “符合过滤条件的数据” 并加载,模型本身不会保留非过滤数据,仅维护当前过滤条件下的查询结果集。——这也是过滤后点击涨工资只会修改所见条目的数据的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void MainWindow::on_comboBoxFieldNames_currentIndexChanged(int index)
{
if(ui->radioButtonAscend->isChecked())
tabModel->setSort(index,Qt::AscendingOrder);
else
tabModel->setSort(index,Qt::DescendingOrder);
tabModel->select();
}


void MainWindow::on_radioButtonAscend_clicked()
{
tabModel->setSort(ui->comboBoxFieldNames->currentIndex(),Qt::AscendingOrder);
tabModel->select();
}


void MainWindow::on_radioButtonDescend_clicked()
{
tabModel->sort(ui->comboBoxFieldNames->currentIndex(),Qt::DescendingOrder);
}


void MainWindow::on_radioButtonMan_clicked()
{
tabModel->setFilter("Gender='男'");
showRecordCount();
}


void MainWindow::on_radioButtonWoman_clicked()
{
tabModel->setFilter("Gender='女'");
showRecordCount();
}


void MainWindow::on_radioButtonBoth_clicked()
{
tabModel->setFilter("");
showRecordCount();
}

QSqlRelationalTableModel示例

其为QSqlTableModel的扩展,作为关系型数据表的模型

对于有外键的表如果直接采用sqltable的话,编码字段会是编码,非常不直观。采用关系表模型,并设计好编码字段与编码表的关系,就可以直接显示值

表准备

这里用到了三张关联表,部门表的主键作为另两张表的外键,专业表的主键作为学生信息表的外键。

这些表内的外键如学生表的部门字段实际存储的是编码,编码的含义有需要查询部门表中同一departID值的记录中的department字段才可以知道。

这些外键字段被称为编码字段,对应的主键存在的那张表被称为编码表,编码表的一条记录有编码字段和编码含义字段。如部门表中departId字段为编码字段,department字段为编码含义字段。

imgimgimg

最重要的其实就是给外键字段建立同编码表的联系这部分。整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "anotherwindow.h"
#include "ui_anotherwindow.h"
#include<QSqlError>
#include<QMessageBox>
AnotherWindow::AnotherWindow(QWidget *parent)
: QDialog(parent)
, ui(new Ui::AnotherWindow)
{
ui->setupUi(this);

ui->tableView->setSelectionBehavior(QAbstractItemView::SelectItems);
ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
ui->tableView->setAlternatingRowColors(true);
DB = QSqlDatabase::addDatabase("QMYSQL","TestDB");
DB.setDatabaseName("qwe");
DB.setHostName("localhost");
DB.setUserName("root");
DB.setPassword("123456");
if(!DB.open()){
QSqlError error = DB.lastError();
QString errorstr = error.text();
QMessageBox::warning(this,"ERROR",errorstr);
}
else{
QMessageBox::information(this,"SUCCESS","SUCCESSED TO OPEN DB");
openTable();//调用自定义函数打开studinfo表
}
}

AnotherWindow::~AnotherWindow()
{
delete ui;
}

void AnotherWindow::openTable()
{
tabModel = new QSqlRelationalTableModel(this,DB);
tabModel->setTable("studinfo");
tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit);//数据保存方式,这里是缓存后由submitAll保存
tabModel->setSort(tabModel->fieldIndex("empNo"),Qt::AscendingOrder);//排序
selModel = new QItemSelectionModel(tabModel,this);
//当前行或列发生变化时触发槽函数
connect(selModel,&QItemSelectionModel::currentChanged,this,&AnotherWindow::do_currentChanged);
ui->tableView->setModel(tabModel);
ui->tableView->setSelectionModel(selModel);
tabModel->setHeaderData(tabModel->fieldIndex("studID"),Qt::Horizontal,"学号");
tabModel->setHeaderData(tabModel->fieldIndex("name"),Qt::Horizontal,"姓名");
tabModel->setHeaderData(tabModel->fieldIndex("gender"),Qt::Horizontal,"性别");
tabModel->setHeaderData(tabModel->fieldIndex("departID"),Qt::Horizontal,"部门");
tabModel->setHeaderData(tabModel->fieldIndex("majorID"),Qt::Horizontal,"专业");
//设置编码字段的关系
tabModel->setRelation(tabModel->fieldIndex("departID"),
QSqlRelation("departments","departID","department"));
tabModel->setRelation(tabModel->fieldIndex("majorID"),
QSqlRelation("majors","majorID","major"));
//为外键字段设置默认代理组件
ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));
tabModel->select();
ui->pushButton->setEnabled(true);

}

结果

可以看出数据库里值明明是编码,但实际显示出来的是编码表中建立关系的字段的值(即编码含义),双击可以看到列出了编码表中编码含义字段的所有记录的数据。

imgimg

总结

新建连接,建立连接,连接数据库,打开数据库,新建数据模型,模型绑定数据库,模型绑定数据库表。

视图,设置代理,设置数据映射

获取记录,修改提交(提交策略)