017-Qt 的数据库模块

auther: abinng date: 2026-05-14 15:47 createDate:2026-05-14 15:47

复习路线

这篇笔记要回答的问题是:Qt 如何连接数据库、执行 SQL,以及怎样通过模型视图架构将数据库中的数据高效地展示在界面上?

Qt SQL 模块总体上分为 3 层:

1
2
3
4
5
驱动层(桥接具体数据库)

SQL 接口层(QSqlDatabase 连接 + QSqlQuery 执行 SQL)

用户接口层(QSqlQueryModel / QSqlTableModel / QSqlRelationalTableModel → QTableView)

下次忘记时,可以按这个顺序复习:

  1. 先看“数据库连接“,理解 QSqlDatabase 的分层设计以及如何创建连接。
  2. 再看“执行SQL语句“,掌握 QSqlQuery 的增删改查和批量操作。
  3. 看“MVD 模型视图架构“,理解 Model / View / Delegate 三个角色的分工与通信。
  4. 看“三个SQL模型“,对比 QSqlQueryModel / QSqlTableModel / QSqlRelationalTableModel 的能力差异和继承关系。
  5. 看“QSqlTableModel 实战“,用完整代码走通单表的增删改查 + 事务提交。
  6. 看“QSqlRelationalTableModel 实战“,理解外键关联的展示和过滤。
  7. 最后看“自定义委托“,掌握如何定制单元格的显示和编辑行为。

1. 数据库连接

1.1 分层架构

Qt 对数据库的支持采用分层设计,保证了“一份代码多处使用“——无论底层是 MySQL、SQLite 还是 PostgreSQL,上层业务代码基本无需改动。

Qt SQL 模块中的类大致分为 3 层:

  • 驱动层:为具体数据库和 SQL 接口层之间提供底层桥接。
  • SQL 接口层:提供对数据库的访问。QSqlDatabase 用来创建连接,QSqlQuery 使用 SQL 语句与数据库交互。
  • 用户接口层:将数据库中的数据链接到窗口部件上。这些类基于模型视图框架实现,即便不熟悉 SQL 也可以操作数据库。

要使用 Qt SQL 模块,需要在 CMake 中配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 4.1)
project(xxx)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

set(CMAKE_PREFIX_PATH "/path/to/Qt/6.5.3/mingw_64")

find_package(Qt6 COMPONENTS
Core
Gui
Widgets
Sql # ← 加上 Sql 模块
REQUIRED)

add_executable(xxx main.cpp)
target_link_libraries(xxx
Qt::Core
Qt::Gui
Qt::Widgets
Qt::Sql) # ← 链接 Sql 库

1.2 连接的四层模型

一次成功的数据库连接,从上到下包含四层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
业务代码 (QSqlQuery 等)


┌───────────────────────────┐
│ QSqlDatabase (通用连接管理) │
└──────────┬────────────────┘
│ (依赖)

┌───────────────────────────┐
│ Qt SQL 驱动插件动态库 │ ← QSqlDriver 的具体实现(Qt 提供)
│ (例: qsqlmysql.dll) │
└──────────┬────────────────┘
│ (调用)

┌───────────────────────────┐
│ 数据库厂商客户端动态库 │ ← 真正实现网络通信的底层接口
│ (例: libmysqlclient.dll) │ (数据库官网提供)
└──────────┬────────────────┘
│ (TCP/IP 网络通信等)

【具体的数据库服务器 (如 MySQL Server)】
  • 业务代码层:我们写的 QSqlDatabaseQSqlQuery 代码。
  • Qt 驱动插件层:Qt 官方提供的动态库(如 qsqlmysql.dll / libqsqlite.so),实现了 QSqlDriver 接口。
  • 数据库厂商客户端库层:各大数据库官网提供的动态库(如 MySQL 的 libmysqlclient.dll),负责真正的网络通信。
  • 具体的数据库服务器:数据库本身。

可以通过 QSqlDatabase::drivers() 查看 Qt 当前支持哪些数据库驱动,也可以在 Qt 安装目录下的 plugins/sqldrivers 文件夹中看到所有的驱动插件文件。

1.3 创建连接

通过 QSqlDatabase::addDatabase() 来新建数据库连接:

点击展开
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
#include <QApplication>
#include <QSql>
#include <QSqlDatabase>
#include <QDebug>
#include <QSqlError>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

// 1. 查看当前支持的数据库驱动
QStringList drivers = QSqlDatabase::drivers();
qDebug() << "查看驱动列表";
foreach(QString d, drivers) {
qDebug() << d;
}

// 2. 创建数据库连接
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "first");
db.setDatabaseName("/path/to/your/test.db");
if (!db.open()) {
qDebug() << "database open failed" << db.lastError().text();
return -1;
}
qDebug() << "open database success!";

db.close();
return QApplication::exec();
}

addDatabase() 的第一个参数是驱动名称(如 "QSQLITE""QMYSQL"),第二个参数是连接名,用于区分同一应用中的多个数据库连接。

1.4 驱动加载问题

可能会遇到 QSQLITE driver not loaded 错误,同时也不会打印驱动列表。

原因通常是:虽然 Qt 安装目录下的 plugins/sqldrivers 文件夹中有驱动插件文件,但插件搜索路径可能指向了项目目录下的 debug/plugins/sqldrivers,那里缺少驱动文件。

CLion 的 CMakeLists.txt 末尾有一段拷贝 pluginsQt6Core.dllQt6Gui.dllQt6Widgets.dll 的代码,删除那段拷贝代码并清除已拷贝的文件后,程序就能正确找到 Qt 安装目录下的驱动插件了。

CLion 也可以很方便地连接数据库:右侧工具栏有数据库插件,相当于一个数据库客户端,新建连接 → 数据源选择 SQLite:

初次连接 SQLite 会让下载驱动文件:

下载好后填写属性信息,可以测试连接:

2. 执行SQL语句

Qt 通过 QSqlQuery 对象来执行 SQL 语句,核心方法是 exec()

2.1 创建表

先准备好 SQL 语句,然后调用 exec() 即可:

1
2
3
4
5
6
7
8
9
10
void create_table01(const QSqlDatabase &db)
{
QSqlQuery query(db);
bool ret = query.exec("CREATE TABLE student (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
if (ret) {
qDebug() << "Create student table success";
} else {
qDebug() << "Create student table failed:" << query.lastError().text();
}
}

2.2 查询数据

执行完 exec() 后,QSqlQuery 的内部指针位于第一条记录之前。必须调用一次 next() 使其前进到第一条记录,然后可以反复调用 next() 遍历所有记录,直到返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void query_table(const QSqlDatabase &db)
{
QSqlQuery query(db);
if (query.exec("SELECT * FROM student")) {
while (query.next()) {
// 按字段名取值
// QString name = query.value("name").toString();
// int age = query.value("age").toInt();

// 按字段序号取值(从 0 开始)
QString name = query.value(1).toString();
int age = query.value(2).toInt();
qDebug() << "-------: " << name << " " << age;
}
}
}

value() 返回 QVariant,不同的数据库类型会自动映射为 Qt 中最接近的相应类型。

2.3 插入数据(单条)

插入单条记录有两种绑定方式——名称绑定位置绑定

1
2
3
4
5
6
7
8
9
10
11
// 名称绑定
query.prepare("INSERT INTO student (id, name) VALUES (:id, :name)");
query.bindValue(":id", idValue);
query.bindValue(":name", nameValue);
query.exec();

// 位置绑定
query.prepare("INSERT INTO student (id, name) VALUES (?, ?)");
query.addBindValue(idValue);
query.addBindValue(nameValue);
query.exec();

2.4 批量插入

当要插入多条记录时,只需 prepare() 一次,然后使用 QVariantList 绑定多组数据,最后调用 execBatch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void insert_test(const QSqlDatabase &db)
{
QSqlQuery query(db);
query.prepare("INSERT INTO student (name, age) VALUES (:_name, :_age)");

QVariantList names;
names << "x1" << "x2" << "x3";
QVariantList ages;
ages << 18 << 19 << 20;

query.bindValue(":_name", names);
query.bindValue(":_age", ages);
query.execBatch(); // 批量执行
}

3. MVD 模型视图架构

3.1 架构概述

Qt 中的模型/视图架构(MVD)是经典 MVC 架构的演进,主要用来将数据的存储界面的展示进行解耦和分离。该架构包含 3 个核心组件:模型(Model)、视图(View)和委托(Delegate)。

3.2 核心组件

模型(Model)

  • 定义:是应用对象,用来表示数据。
  • 职责:模型与底层的真实数据源(如数据库、文件、内存结构)进行通信,为架构中的其他组件提供标准的数据交互接口。

视图(View)

  • 定义:是模型的用户界面,用来显示数据。
  • 职责:负责数据的整体布局和展示。视图从模型中获得模型索引(Model Index),模型索引用来精确表示具体的数据项。

委托 / 代理(Delegate)

  • 定义:用于定制单个数据项的渲染和编辑方式。
  • 职责:在标准的视图中,委托负责渲染(绘制)具体的数据项。它接管了 View 中局部 UI 的展示逻辑。

3.3 组件间的通信机制

MVD 架构中,各个组件通过模型索引信号与槽进行高效通信:

View 与 Model 的直接通信(展示数据)

  • 读取:视图根据需要,拿着模型索引直接向模型请求数据用于展示。
  • 通知:当后台模型的数据发生变化时,模型会发出信号通知视图,视图随即请求新数据刷新界面。

Delegate 与 Model 的直接通信(编辑数据)

  • 编辑机制:当用户双击视图中的某个项目进入编辑状态时,委托会被唤起。
  • 数据回写:编辑完成后,委托使用模型索引直接与模型进行通信,将修改后的新数据写回模型中。

4. 三个SQL模型

4.1 概述

除了直接使用 QSqlQuery 执行 SQL 语句,Qt 还提供了 3 个基于 MVD 架构的高级模型类。它们都继承自 QAbstractTableModel,可以直接绑定到 QTableView / QListView 等视图上展示数据库数据:

继承自 读写 适用场景
QSqlQueryModel QAbstractTableModel 只读 任意 SQL 查询结果展示
QSqlTableModel QSqlQueryModel 可读写 单表增删改查,无需手写 SQL
QSqlRelationalTableModel QSqlTableModel 可读写 含外键关联的表,自动将外键 ID 替换为可读名称

三者能力递增,下面逐个说明。

4.2 QSqlQueryModel —— 查询模型

是什么:一个只读模型,执行一条 SQL 查询后将结果集缓存在内存中,供视图显示。它不关心表结构、不区分主键外键——只负责“你给 SQL,我出结果“。

1
2
3
auto *model = new QSqlQueryModel(this);
model->setQuery("SELECT * FROM student", QSqlDatabase::database("first"));
ui->tableView->setModel(model);

用到的接口

  • setQuery(sql, db) — 设置 SQL 查询并执行,结果集存入模型

适合展示统计报表、多表联查结果等不需要编辑的场景。

4.3 QSqlTableModel —— 表格模型

是什么:继承自 QSqlQueryModel,专门操作单张数据库表,且可读可写。它把 INSERT / UPDATE / DELETE 操作封装成了方法调用,不需要拼接 SQL 字符串。配合 QTableView 就能快速搭建一个数据库表格编辑界面。

下面按功能分组介绍其常用接口。

初始化

1
2
3
4
5
m_model = new QSqlTableModel(this, m_db);
m_model->setTable("student"); // 绑定表
m_model->select(); // 执行查询,等价于 SELECT * FROM student
m_model->setHeaderData(0, Qt::Horizontal, "ID号"); // 自定义表头列名
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交模式
  • setTable("表名") — 指定要操作的表
  • select() — 执行查询、拉取数据。修改排序/过滤条件后需重新调用才会生效
  • setHeaderData(col, Qt::Horizontal, "名称") — 自定义列的表头文字
  • setEditStrategy(QSqlTableModel::OnManualSubmit) — 设置为“手动提交“模式:所有修改先缓存在内存中,调用 submitAll() 后才写入数据库

增删行

1
2
3
4
5
int row_num = m_model->rowCount();
m_model->insertRow(row_num); // 在末尾插入空行

int cur_row = ui->tableView->currentIndex().row();
m_model->removeRow(cur_row); // 删除当前选中行
  • insertRow(row) — 在指定位置插入一个空行
  • removeRow(row) — 删除指定行
  • rowCount() — 获取当前总行数

提交与撤销

1
2
m_model->submitAll();    // 提交所有修改到数据库
m_model->revertAll(); // 撤销所有未提交的修改

事务

结合数据库事务确保数据安全——要么全部成功写入,要么全部回滚:

1
2
3
4
5
6
m_model->database().transaction();   // 开启事务
if (m_model->submitAll()) {
m_model->database().commit(); // 成功 → 提交
} else {
m_model->database().rollback(); // 失败 → 回滚
}
  • database().transaction() — 开启事务
  • database().commit() — 提交事务
  • database().rollback() — 回滚事务

排序与过滤

1
2
3
4
m_model->setSort(2, Qt::AscendingOrder);   // 按第2列升序
m_model->select(); // 重新查询使排序生效

m_model->setFilter(QString("name = '%1'").arg(input_name)); // WHERE 过滤
  • setSort(col, order) — 设置排序列和方向(Qt::AscendingOrder / Qt::DescendingOrder),需配合 select() 生效
  • setFilter("条件") — 设置过滤条件,等价于 SQL 的 WHERE 子句。设为空字符串 "" 可清除过滤

4.4 QSqlRelationalTableModel —— 关联外键模型

是什么:继承自 QSqlTableModel,专门解决外键关联的显示问题。

举个例子:books 表的 category_id 字段存的是分类 ID(12),直接用 QSqlTableModel 显示出来就是一串数字,用户根本看不懂。QSqlRelationalTableModel 通过 setRelation() 告诉模型“这一列是外键,去另一张表查对应的名称来显示“。同时配合 QSqlRelationalDelegate,编辑时该列会自动弹出下拉框让用户选择。

1
2
3
4
5
6
7
8
9
model = new QSqlRelationalTableModel(this, m_db);
model->setTable("books");
// 第4列是外键,关联到 category 表,用 id 匹配,显示 name 字段
model->setRelation(4, QSqlRelation("category", "id", "name"));
model->setHeaderData(4, Qt::Horizontal, "图书分类");
model->select();
ui->tableView->setModel(model);
// 让外键列在编辑时显示下拉框
ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));

用到的接口

  • setRelation(col, QSqlRelation("关联表", "匹配列", "显示列")) — 将第 col 列设为外键:用“匹配列“去“关联表“中查找,将“显示列“的值替代原始 ID 展示给用户
  • QSqlRelationalDelegate — Qt 内置的外键列代理,编辑时自动生成 QComboBox 列出关联表的所有可选值供用户选择

这三个模型的能力是递增的:只读查询 → 单表读写 → 外键关联。实际开发中根据需求选择合适的即可。

5. QSqlTableModel 实战

在实际开发中,QSqlTableModel 配合 QTableView 可以极大地简化数据库表的增删改查逻辑。它隐藏了复杂的 SQL 语句拼接过程,下面用完整代码走通整个流程。

5.1 模型与视图的初始化

要正确展示数据,需要分别对 Model(数据层)和 View(展示层)进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 初始化 Model 并绑定数据表
m_model = new QSqlTableModel(this, m_db);
m_model->setTable("student"); // 绑定数据库中的 student 表
m_model->select(); // 执行查询,拉取数据

// 修改表头显示的列名
m_model->setHeaderData(0, Qt::Horizontal, "ID号");
m_model->setHeaderData(1, Qt::Horizontal, "姓名");
m_model->setHeaderData(2, Qt::Horizontal, "年龄");

// 设置修改策略:OnManualSubmit 表示所有修改先缓存在内存,手动提交后才写库
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);

// 2. 将 Model 绑定到 View 上
ui->tableView->setModel(m_model);

// 3. View 的常用视觉与交互优化
ui->tableView->setAlternatingRowColors(true); // 开启斑马线(隔行换色)
ui->tableView->verticalHeader()->hide(); // 隐藏自带的左侧行序号
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // 列宽自动拉伸填满控件
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 点击单元格时选中整行
ui->tableView->setColumnHidden(0, true); // 隐藏第 0 列 (通常是主键ID)

setEditStrategy(QSqlTableModel::OnManualSubmit) 是本例的关键:选择了“手动提交“,所有增删改先在内存中缓存,只有调用 submitAll() 才真正写入数据库。这样做的好处是可以配合数据库事务,确保一组操作要么全部成功、要么全部回滚。

5.2 增删行

1
2
3
4
5
6
7
// 【新增一行】
int row_num = m_model->rowCount(); // 获取当前总行数
m_model->insertRow(row_num); // 在末尾插入一个空行等待用户输入

// 【删除一行】
int cur_row = ui->tableView->currentIndex().row(); // 获取当前选中的行号
m_model->removeRow(cur_row); // 标记删除该行

5.3 事务提交与撤销

配合 OnManualSubmit 策略,结合数据库事务确保数据安全:

1
2
3
4
5
6
7
8
9
10
11
12
// 【撤销修改】放弃所有尚未提交的修改
m_model->revertAll();

// 【提交修改】将缓存中的增删改统一写入数据库(结合事务)
m_model->database().transaction(); // 开启事务
if (m_model->submitAll()) {
m_model->database().commit(); // SQL 全部执行成功 → 提交
QMessageBox::information(this, "提示", "数据修改成功");
} else {
m_model->database().rollback(); // 出错 → 回滚到修改前的状态
QMessageBox::warning(this, "错误", "数据库错误");
}

5.4 排序与过滤

QSqlTableModel 封装了 ORDER BYWHERE 语句,直接调用方法即可实现:

1
2
3
4
5
6
7
8
9
// 按某一列排序 (等同于 ORDER BY)
m_model->setSort(2, Qt::AscendingOrder); // 第 2 列升序
// m_model->setSort(2, Qt::DescendingOrder); // 降序
m_model->select(); // ⚠️ 设置排序规则后,必须重新 select 才能生效

// 数据过滤查询 (等同于 WHERE)
QString input_name = "小明";
m_model->setFilter(QString("name = '%1'").arg(input_name));
// 设置 filter 后,模型会自动过滤出符合条件的数据

5.5 完整代码

点击展开
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
#ifndef WIN_DEMO_H
#define WIN_DEMO_H
#include <QWidget>
#include <QSqlDatabase>
#include <QSqlTableModel>

QT_BEGIN_NAMESPACE
namespace Ui {
class WinDemo;
}
QT_END_NAMESPACE

class WinDemo : public QWidget {
Q_OBJECT

public:
explicit WinDemo(QWidget *parent = nullptr);
~WinDemo() override;
public slots:
void on_btnSubmit_clicked();
void on_btnCancel_clicked();
void on_btnAdd_clicked();
void on_btnDel_clicked();
void on_btnAsce_clicked();
void on_btnDesc_clicked();
void on_btnQuery_clicked();
void on_btnAll_clicked();
private:
Ui::WinDemo *ui;
QSqlDatabase m_db;
QSqlTableModel *m_model;
void setupView();
void setupModel();
};

#endif //WIN_DEMO_H
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include "win_demo.h"
#include "ui_win_demo.h"
#include <QMessageBox>
#include <QSqlError>
#include "GenderDelegate.h"

WinDemo::WinDemo(QWidget *parent) : QWidget(parent), ui(new Ui::WinDemo) {
ui->setupUi(this);
m_db = QSqlDatabase::addDatabase("QSQLITE", "first");
m_db.setDatabaseName("/path/to/your/test.db");
if (!m_db.open()) {
QMessageBox::critical(this, "警告", QString("database open failed: %1").arg(m_db.lastError().text()));
return;
}
m_model = new QSqlTableModel(this, m_db);
m_model->setTable("student");
m_model->select();
setupModel();

ui->tableView->setModel(m_model);
setupView();
ui->tableView->setItemDelegateForColumn(3, new GenderDelegate(this));
}

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

void WinDemo::on_btnSubmit_clicked() {
m_model->database().transaction();
if (m_model->submitAll()) {
if (m_model->database().commit()) {
QMessageBox::information(this, "TableModel", "数据修改成功");
}
} else {
m_model->database().rollback();
QMessageBox::warning(this, "TableModel", QString("数据库错误: %1").arg(m_model->lastError().text()));
}
}

void WinDemo::on_btnCancel_clicked() {
m_model->revertAll();
}

void WinDemo::on_btnAdd_clicked() {
int row_num = m_model->rowCount();
m_model->insertRow(row_num);
}

void WinDemo::on_btnDel_clicked() {
int cur_row = ui->tableView->currentIndex().row();
m_model->removeRow(cur_row);
int ok = QMessageBox::warning(this, "删除当前行", "你确定删除当前行?", QMessageBox::Yes, QMessageBox::No);
if (ok == QMessageBox::Yes) {
m_model->submitAll();
} else {
m_model->revertAll();
}
}

void WinDemo::on_btnAsce_clicked() {
m_model->setSort(2, Qt::AscendingOrder);
m_model->select();
}

void WinDemo::on_btnDesc_clicked() {
m_model->setSort(2, Qt::DescendingOrder);
m_model->select();
}

void WinDemo::on_btnQuery_clicked() {
QString input_name = ui->query_data->text();
m_model->setFilter(QString("name = '%1'").arg(input_name));
}

void WinDemo::on_btnAll_clicked() {
m_model->setTable("student");
m_model->select();
setupModel();
setupView();
}

void WinDemo::setupView() {
ui->tableView->setAlternatingRowColors(true);
ui->tableView->verticalHeader()->hide();
ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->tableView->setColumnHidden(0, true);
}

void WinDemo::setupModel() {
m_model->setHeaderData(0, Qt::Horizontal, "ID号");
m_model->setHeaderData(1, Qt::Horizontal, "姓名");
m_model->setHeaderData(2, Qt::Horizontal, "年龄");
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
}

6. QSqlRelationalTableModel 实战

这一节用一个完整的例子说明外键关联的使用场景和方法。

6.1 问题场景

假设有两张表:

  • books 表:id, title, author, price, category_id
  • category 表:id, name

books.category_id 通过外键引用 category.id。如果用 QSqlTableModel 直接显示 books 表,用户看到的分类列是一串数字 ID,完全不可读。我们需要的是显示分类名称(如“计算机““文学“),并且编辑时提供下拉框来选择分类。

这就是 QSqlRelationalTableModel 的用武之地。

6.2 完整代码

点击展开
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
#ifndef DB_WIDGET_H
#define DB_WIDGET_H
#include <QWidget>
#include <QSqlDatabase>
#include <QSqlRelationalTableModel>
#include <QSqlTableModel>

QT_BEGIN_NAMESPACE
namespace Ui {
class DBWidget;
}
QT_END_NAMESPACE

class DBWidget : public QWidget {
Q_OBJECT
public:
explicit DBWidget(QWidget *parent = nullptr);
~DBWidget() override;
public slots:
void on_btnQuery_clicked();
void on_btnAll_clicked();
private:
Ui::DBWidget *ui;
QSqlDatabase m_db;
QSqlTableModel *categoryModel;
QSqlRelationalTableModel *model;
};

#endif //DB_WIDGET_H
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
#include "db_widget.h"
#include "ui_db_widget.h"
#include <QSqlTableModel>
#include <QSqlRelationalTableModel>
#include <QSqlRelationalDelegate>

DBWidget::DBWidget(QWidget *parent) : QWidget(parent), ui(new Ui::DBWidget) {
ui->setupUi(this);
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName("/path/to/your/test.db");
m_db.open();

model = new QSqlRelationalTableModel(this, m_db);
model->setTable("books");
// 核心:设置外键关联 —— 第4列(category_id) 关联到 category 表,用 id 匹配,显示 name
model->setRelation(4, QSqlRelation("category", "id", "name"));
model->setHeaderData(4, Qt::Horizontal, "图书分类");
model->setEditStrategy(QSqlTableModel::OnManualSubmit);
model->select();
ui->tableView->setModel(model);
// 关键:为外键列装上 QSqlRelationalDelegate,编辑时自动弹出下拉框
ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));

// 填充分类下拉框
categoryModel = new QSqlTableModel(this, m_db);
categoryModel->setTable("category");
categoryModel->select();
ui->filterCombo->setModel(categoryModel);
ui->filterCombo->setModelColumn(1);
}

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

void DBWidget::on_btnQuery_clicked() {
int current_row = ui->filterCombo->currentIndex();
QModelIndex idIndex = categoryModel->index(current_row, 0);
QString categoryID = categoryModel->data(idIndex).toString();

model->setFilter(QString("category_id = '%1'").arg(categoryID));
model->select();
}

void DBWidget::on_btnAll_clicked() {
model->setFilter("");
model->select();
}

6.3 按外键分类过滤

上面的完整代码中已经演示了一个实用场景:使用下拉框按分类筛选图书。

思路是:用一个独立的 QSqlTableModelQComboBox 提供分类列表,用户选择某个分类后,用该分类的 ID 去 QSqlRelationalTableModel 中设置 setFilter()

关键点在于:QComboBox 本身也用了 MVD 架构——setModel() 绑定数据源,setModelColumn() 指定显示哪一列。而 on_btnQuery_clicked() 中通过 categoryModel->index(current_row, 0) 拿到分类 ID,再用 model->setFilter() 去过滤主表,on_btnAll_clicked() 则清空过滤条件恢复全量显示。

7. 自定义委托

默认情况下,双击 QTableView 的单元格弹出的都是普通文本输入框。如果我们需要特定的显示和编辑方式(例如:数据库里存的是整数 0/1,界面上需要显示 女/男,且编辑时弹出下拉菜单),就需要使用自定义委托(Delegate)

实现一个自定义委托,需要继承 QStyledItemDelegate 并重写四个核心方法。以下是“性别下拉框代理“的完整实现:

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef GENDER_DELEGATE_H
#define GENDER_DELEGATE_H
#include <QStyledItemDelegate>

class GenderDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
GenderDelegate(QObject *parent = nullptr);
~GenderDelegate() override = default;

QString displayText(const QVariant &value, const QLocale &locale) const override;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
};

#endif //GENDER_DELEGATE_H
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
#include "GenderDelegate.h"
#include <QComboBox>

GenderDelegate::GenderDelegate(QObject *parent) : QStyledItemDelegate(parent) {
}

// 1. 拦截显示:将底层数据(1/0)转成文字(男/女)让 View 显示
QString GenderDelegate::displayText(const QVariant &value, const QLocale &locale) const {
int val = value.toInt();
return (val == 1) ? "男" : "女";
}

// 2. 创建编辑器:双击单元格时,生成并返回一个 QComboBox(下拉框)
QWidget *GenderDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option, const QModelIndex &index) const {
QComboBox *editor = new QComboBox(parent);
editor->addItem("女", 0);
editor->addItem("男", 1);
return editor;
}

// 3. 数据到编辑器:读取 Model 里的旧数据(0或1),让下拉框默认选中对应的项
void GenderDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {
int value = index.model()->data(index, Qt::EditRole).toInt();
QComboBox *cb = static_cast<QComboBox *>(editor);
cb->setCurrentIndex(cb->findData(value));
}

// 4. 编辑器到数据:用户选完下拉框后,拿到当前选中的值(0或1),写回到 Model 中
void GenderDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const {
QComboBox *cb = static_cast<QComboBox *>(editor);
int value = cb->currentData().toInt();
model->setData(index, value, Qt::EditRole);
}

写好委托类后,只需在初始化时将其分配给视图的特定列即可:

1
2
// 将 GenderDelegate 代理,专门绑定给表格的第 3 列
ui->tableView->setItemDelegateForColumn(3, new GenderDelegate(this));

View 在处理这一列时会自动走我们自定义的显示和编辑逻辑。这 4 个方法的调用时机是:

  1. 渲染单元格时 → displayText() 被调用,决定显示什么文字
  2. 用户双击进入编辑 → createEditor() 创建编辑控件
  3. 编辑控件出现后 → setEditorData() 把 Model 的旧值填进去
  4. 用户编辑完成 → setModelData() 把新值写回 Model

这样就完成了从“数据库原始值“到“用户友好界面“的完整映射。