本项目不仅涵盖数据库表设计(支持书籍、作者、分类的多表关联)、QSqlRelationalTableModel的高级用法(自动解析外键关系),还深度实践了自定义UI委托技术(如星级评分控件)和双向数据绑定(QDataWidgetMapper)。无论是学习Qt SQL模块的集成开发,还是探索企业级桌面应用的架构设计,本案例都将为您提供清晰的实现路径与工业级代码范例,助您掌握从数据存储到界面呈现的全栈开发能力。
一:【工程项目】运行效果
二:【工程项目】源码分享
1:工程项目UI设计与实现
2:initdb.h文件
/*
* 文件名:initdb.h
* 描述:该头文件用于初始化SQLite数据库,创建表结构并插入示例数据
* 作者:vico
*/
#ifndef INITDB_H
#define INITDB_H
#include <QtSql> // 包含Qt SQL模块头文件
// 向books表添加书籍记录
void addBook(QSqlQuery &q, const QString &title, int year, const QVariant &authorId,
const QVariant &genreId, int rating)
{
// 绑定插入参数(顺序需与INSERT语句中的占位符对应)
q.addBindValue(title); // 书籍标题
q.addBindValue(year); // 出版年份
q.addBindValue(authorId); // 作者ID(外键)
q.addBindValue(genreId); // 类型ID(外键)
q.addBindValue(rating); // 评分(0-5)
q.exec(); // 执行插入操作
}
// 向genres表添加新类型,并返回新插入记录的ID
QVariant addGenre(QSqlQuery &q, const QString &name)
{
q.addBindValue(name); // 类型名称
q.exec();
return q.lastInsertId(); // 返回自动生成的主键ID
}
// 向authors表添加新作者,并返回新插入记录的ID
QVariant addAuthor(QSqlQuery &q, const QString &name, QDate birthdate)
{
q.addBindValue(name); // 作者姓名
q.addBindValue(birthdate); // 作者出生日期
q.exec();
return q.lastInsertId(); // 返回自动生成的主键ID
}
// 创建books表的SQL语句
const auto BOOKS_SQL = QLatin1String(R"(
create table books(
id integer primary key,
title varchar,
author integer, -- 作者ID(逻辑外键)
genre integer, -- 类型ID(逻辑外键)
year integer, -- 出版年份
rating integer -- 用户评分(0-5)
))");
// 创建authors表的SQL语句
const auto AUTHORS_SQL = QLatin1String(R"(
create table authors(
id integer primary key,
name varchar, -- 作者姓名
birthdate date -- 出生日期
))");
// 创建genres表的SQL语句
const auto GENRES_SQL = QLatin1String(R"(
create table genres(
id integer primary key,
name varchar -- 类型名称
))");
// 预编译插入authors表的SQL语句(使用占位符?)
const auto INSERT_AUTHOR_SQL = QLatin1String(R"(
insert into authors(name, birthdate) values(?, ?)
)");
// 预编译插入books表的SQL语句
const auto INSERT_BOOK_SQL = QLatin1String(R"(
insert into books(title, year, author, genre, rating)
values(?, ?, ?, ?, ?)
)");
// 预编译插入genres表的SQL语句
const auto INSERT_GENRE_SQL = QLatin1String(R"(
insert into genres(name) values(?)
)");
// 初始化数据库的主函数
QSqlError initDb()
{
// 创建并配置SQLite内存数据库(无需磁盘存储)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:"); // 使用内存数据库
// 尝试打开数据库
if (!db.open())
return db.lastError(); // 返回打开错误
// 检查表是否已存在(避免重复初始化)
QStringList tables = db.tables();
if (tables.contains("books", Qt::CaseInsensitive) &&
tables.contains("authors", Qt::CaseInsensitive))
return QSqlError(); // 表已存在,直接返回
// 执行建表操作
QSqlQuery q;
if (!q.exec(BOOKS_SQL)) return q.lastError(); // 创建books表
if (!q.exec(AUTHORS_SQL)) return q.lastError(); // 创建authors表
if (!q.exec(GENRES_SQL)) return q.lastError(); // 创建genres表
// 预编译并插入作者数据
if (!q.prepare(INSERT_AUTHOR_SQL)) return q.lastError();
QVariant asimovId = addAuthor(q, "Isaac Asimov", QDate(1920, 2, 1));
QVariant greeneId = addAuthor(q, "Graham Greene", QDate(1904, 10, 2));
QVariant pratchettId = addAuthor(q, "Terry Pratchett", QDate(1948, 4, 28));
// 预编译并插入类型数据
if (!q.prepare(INSERT_GENRE_SQL)) return q.lastError();
QVariant sfiction = addGenre(q, "Science Fiction");
QVariant fiction = addGenre(q, "Fiction");
QVariant fantasy = addGenre(q, "Fantasy");
// 预编译并插入书籍数据
if (!q.prepare(INSERT_BOOK_SQL)) return q.lastError();
// 添加Isaac Asimov的书籍
addBook(q, "Foundation", 1951, asimovId, sfiction, 3);
addBook(q, "Foundation and Empire", 1952, asimovId, sfiction, 4);
// ...(其他书籍添加省略)
// 添加Terry Pratchett的书籍
addBook(q, "Going Postal", 2004, pratchettId, fantasy, 3);
return QSqlError(); // 返回无错误
}
#endif // INITDB_H
3:bookwindow.h文件
/*
* 文件名:BookWindow.h
* 描述:定义主窗口类,用于展示和管理书籍数据库的GUI界面
* 作者:vico
*/
#ifndef BOOKWINDOW_H
#define BOOKWINDOW_H
#include <QtWidgets> // 包含Qt Widgets模块基础类(QMainWindow等)
#include <QtSql> // 包含Qt SQL数据库模块
#include "ui_bookwindow.h" // 包含Qt Designer生成的UI类头文件
// 主窗口类,继承自QMainWindow
class BookWindow: public QMainWindow
{
Q_OBJECT // 启用Qt元对象系统(信号槽机制)
public:
BookWindow(); // 构造函数
private slots:
// 槽函数 - 显示"关于"对话框
void about();
private:
// 显示数据库操作错误信息
void showError(const QSqlError &err);
// 初始化菜单栏
void createMenuBar();
// 成员变量
Ui::BookWindow ui; // UI组件容器(包含所有设计器创建的控件)
/* 关系型数据表模型指针
* 功能:
* - 连接数据库表与视图组件
* - 支持外键关系映射(如将author_id显示为作者姓名)
* - 提供数据编辑和过滤能力 */
QSqlRelationalTableModel *model = nullptr;
// 模型列索引缓存(避免重复查找)
int authorIdx = 0; // 作者字段在模型中的列索引
int genreIdx = 0; // 类型字段在模型中的列索引
};
#endif
4:bookwindow.cpp文件
/* 文件名:bookwindow.cpp
* 描述:实现书籍管理主窗口的业务逻辑,集成数据库操作与UI交互
* 作者:vico
*/
#include "bookwindow.h"
#include "bookdelegate.h" // 自定义表格委托,用于数据呈现和编辑
#include "initdb.h" // 数据库初始化函数
#include <QtSql> // SQL数据库模块
/* 构造函数:初始化UI、数据库连接和模型视图 */
BookWindow::BookWindow()
{
ui.setupUi(this); // 加载并布局UI设计文件中的组件
// 检查SQLITE驱动是否可用
if (!QSqlDatabase::drivers().contains("QSQLITE"))
QMessageBox::critical(
this,
"Unable to load database",
"This demo needs the SQLITE driver"
);
// 初始化数据库连接并创建表结构
QSqlError err = initDb();
if (err.type() != QSqlError::NoError) {
showError(err); // 显示初始化错误
return;
}
/* 创建关系型表格模型
* 参数:
* - ui.bookTable:父对象,用于内存管理
* - 功能:将数据库表映射为可编辑的表格模型 */
model = new QSqlRelationalTableModel(ui.bookTable);
model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交修改
model->setTable("books"); // 关联数据库中的books表
// 获取外键字段的列索引(优化后续操作)
authorIdx = model->fieldIndex("author"); // 作者字段索引
genreIdx = model->fieldIndex("genre"); // 类型字段索引
/* 设置外键关系映射
* 参数说明:
* - authorIdx:当前模型的字段索引
* - QSqlRelation("authors", "id", "name"):关联authors表的id到name字段 */
model->setRelation(authorIdx, QSqlRelation("authors", "id", "name"));
model->setRelation(genreIdx, QSqlRelation("genres", "id", "name"));
// 设置本地化表头(中文字段名)
model->setHeaderData(authorIdx, Qt::Horizontal, tr("Author Name"));
model->setHeaderData(genreIdx, Qt::Horizontal, tr("Genre"));
model->setHeaderData(model->fieldIndex("title"), Qt::Horizontal, tr("Title"));
model->setHeaderData(model->fieldIndex("year"), Qt::Horizontal, tr("Year"));
model->setHeaderData(model->fieldIndex("rating"), Qt::Horizontal, tr("Rating"));
// 加载数据到模型(执行SELECT * FROM books)
if (!model->select()) {
showError(model->lastError());
return;
}
/* 配置表格视图
* - 设置模型和自定义委托(处理数据显示和编辑)
* - 隐藏ID列(model->fieldIndex("id")获取列索引)
* - 设置单选模式 */
ui.bookTable->setModel(model);
ui.bookTable->setItemDelegate(new BookDelegate(ui.bookTable)); // 自定义委托
ui.bookTable->setColumnHidden(model->fieldIndex("id"), true);
ui.bookTable->setSelectionMode(QAbstractItemView::SingleSelection);
/* 配置作者下拉框
* - 使用关系模型获取authors表数据
* - 显示name字段 */
ui.authorEdit->setModel(model->relationModel(authorIdx));
ui.authorEdit->setModelColumn(
model->relationModel(authorIdx)->fieldIndex("name"));
// 同理配置类型下拉框
ui.genreEdit->setModel(model->relationModel(genreIdx));
ui.genreEdit->setModelColumn(
model->relationModel(genreIdx)->fieldIndex("name"));
/* 设置评分列宽度策略
* - 固定宽度,根据内容自动调整
* - 防止用户手动调整列宽 */
ui.bookTable->horizontalHeader()->setSectionResizeMode(
model->fieldIndex("rating"),
QHeaderView::ResizeToContents);
/* 数据映射器配置(UI控件与模型数据绑定)
* 功能:将表单控件与模型字段实时同步 */
QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->setItemDelegate(new BookDelegate(this)); // 复用委托
// 添加字段映射
mapper->addMapping(ui.titleEdit, model->fieldIndex("title"));
mapper->addMapping(ui.yearEdit, model->fieldIndex("year"));
mapper->addMapping(ui.authorEdit, authorIdx);
mapper->addMapping(ui.genreEdit, genreIdx);
mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating"));
/* 连接表格行选择信号
* 当用户选择不同行时,自动更新表单控件内容 */
connect(ui.bookTable->selectionModel(),
&QItemSelectionModel::currentRowChanged,
mapper,
&QDataWidgetMapper::setCurrentModelIndex
);
// 初始化选中第一行
ui.bookTable->setCurrentIndex(model->index(0, 0));
ui.bookTable->selectRow(0);
createMenuBar(); // 创建菜单栏
}
/* 显示数据库错误弹窗
* 参数:QSqlError对象,包含错误详细信息 */
void BookWindow::showError(const QSqlError &err)
{
QMessageBox::critical(this, "Database Error",
"Error: " + err.text()); // 显示错误描述
}
/* 创建菜单栏及动作 */
void BookWindow::createMenuBar()
{
// 创建动作对象
QAction *quitAction = new QAction(tr("&Exit"), this);
QAction *aboutAction = new QAction(tr("&About"), this);
QAction *aboutQtAction = new QAction(tr("About &Qt"), this);
// 构建文件菜单
QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(quitAction); // 添加退出动作
// 构建帮助菜单
QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(aboutAction);
helpMenu->addAction(aboutQtAction);
// 连接信号与槽
connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); // 退出应用
connect(aboutAction, &QAction::triggered, this, &BookWindow::about); // 关于本程序
connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); // 关于Qt
}
/* 显示关于对话框 */
void BookWindow::about()
{
QMessageBox::about(this, tr("About Book Manager"),
tr("<p>This <b>Book Manager</b> demonstrates how to use "
"Qt SQL with model/view architecture.</p>"));
}
5:bookdelegate.h文件
/*
* 文件名:bookdelegate.h
* 描述:自定义委托类,用于实现星级评分显示和编辑功能
* 作者:vico
*/
#ifndef BOOKDELEGATE_H
#define BOOKDELEGATE_H
#include <QModelIndex>
#include <QPixmap>
#include <QSize>
#include <QSqlRelationalDelegate> // 继承自关系型数据库委托基类
QT_FORWARD_DECLARE_CLASS(QPainter) // 前向声明QPainter,减少头文件依赖
// 继承QSqlRelationalDelegate以支持关系型字段处理
class BookDelegate : public QSqlRelationalDelegate
{
public:
// 构造函数
explicit BookDelegate(QObject *parent = nullptr);
// 重写绘制方法(实现星级评分显示)
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
// 返回单元格建议尺寸(根据星图大小计算)
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
// 处理编辑事件(鼠标点击修改评分)
bool editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
// 创建编辑器控件(默认使用QSpinBox编辑评分)
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
private:
QPixmap star; // 星形图标位图(用于绘制评分)
};
#endif // BOOKDELEGATE_H
6:bookdelegate.cpp文件
/* 文件名:bookdelegate.cpp
* 描述:实现书籍表格的自定义委托,主要处理评分星标显示和年份输入控制
* 作者:vico
*/
#include "bookdelegate.h"
#include <QtWidgets> // 包含Qt Widgets模块头文件
/* 构造函数
* 参数:parent - 父对象指针(通常为QTableView)
* 功能:初始化星标图片资源 */
BookDelegate::BookDelegate(QObject *parent)
: QSqlRelationalDelegate(parent), // 调用基类构造函数
star(QPixmap(":images/star.png")) // 从资源文件加载星标图片
{
// 注:":images/star.png"为Qt资源系统路径,需在.qrc文件中定义
}
/* 自定义单元格绘制方法
* 参数:
* painter - 绘制工具
* option - 样式选项(包含位置、状态等信息)
* index - 数据模型索引
* 功能:对评分列(第5列)进行星标绘制,其他列使用默认绘制 */
void BookDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
// 非评分列(第5列)使用基类默认绘制
if (index.column() != 5) {
QSqlRelationalDelegate::paint(painter, option, index);
} else {
// 获取数据模型和颜色组状态
const QAbstractItemModel *model = index.model();
QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ?
(option.state & QStyle::State_Active ? QPalette::Normal : QPalette::Inactive)
: QPalette::Disabled;
// 绘制选中状态背景
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.color(cg, QPalette::Highlight));
}
// 获取评分值并绘制星标
int rating = model->data(index, Qt::DisplayRole).toInt();
int width = star.width(); // 单颗星宽度
int height = star.height(); // 单颗星高度
int x = option.rect.x(); // 起始X坐标
int y = option.rect.y() + (option.rect.height() / 2) - (height / 2); // 垂直居中
// 绘制实心星标(根据评分值循环)
for (int i = 0; i < rating; ++i) {
painter->drawPixmap(x, y, star); // 在(x,y)位置绘制星标
x += width; // 水平移动绘制位置
}
}
// 绘制单元格底部和右侧边框线(覆盖默认边框)
QPen originalPen = painter->pen();
painter->setPen(option.palette.color(QPalette::Mid));
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); // 底部线
painter->drawLine(option.rect.topRight(), option.rect.bottomRight()); // 右侧线
painter->setPen(originalPen); // 恢复原始画笔
}
/* 单元格建议尺寸计算
* 返回值:QSize对象表示建议尺寸
* 功能:评分列根据星标尺寸计算,其他列使用基类计算 */
QSize BookDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == 5) {
// 5颗星的宽度 + 1像素边框,高度+1像素边框
return QSize(5 * star.width(), star.height()) + QSize(1, 1);
}
// 其他列使用基类计算并增加1像素边框补偿
return QSqlRelationalDelegate::sizeHint(option, index) + QSize(1, 1);
}
/* 编辑事件处理
* 返回值:事件是否已处理
* 功能:实现点击评分列修改星标数量 */
bool BookDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
// 非评分列使用基类处理
if (index.column() != 5) {
return QSqlRelationalDelegate::editorEvent(event, model, option, index);
}
// 处理鼠标左键点击事件
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
// 计算点击位置对应的星标数量
qreal clickPosX = mouseEvent->position().toPoint().x() - option.rect.x();
int stars = qBound(0, static_cast<int>(0.7 + clickPosX / star.width()), 5);
// 更新模型数据(触发视图刷新)
model->setData(index, QVariant(stars));
// 返回false允许选择状态变更
return false;
}
return true; // 其他事件已处理
}
/* 创建单元格编辑器
* 返回值:QWidget指针指向创建的编辑器
* 功能:为年份列(第4列)创建带范围的SpinBox */
QWidget *BookDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
// 非年份列使用基类编辑器(组合框等)
if (index.column() != 4) {
return QSqlRelationalDelegate::createEditor(parent, option, index);
}
// 创建年份输入SpinBox(范围-1000到2100)
QSpinBox *yearSpinBox = new QSpinBox(parent);
yearSpinBox->setFrame(false); // 无边框
yearSpinBox->setMinimum(-1000); // 最小年份
yearSpinBox->setMaximum(2100); // 最大年份
return yearSpinBox; // 返回编辑器控件
}
7:main.cpp文件
/* 文件名:main.cpp(通常)
* 描述:Qt应用程序的主入口文件,创建并显示主窗口
* 作者:vico
*/
#include "bookwindow.h" // 包含自定义的主窗口类头文件
#include <QtWidgets> // 包含Qt Widgets模块所有头文件(实际开发建议按需包含)
/* 主函数 - 应用程序入口点
* 参数:
* argc : 命令行参数个数
* argv : 命令行参数数组指针
* 返回值:
* int : 应用程序退出码 */
int main(int argc, char *argv[])
{
// 创建QApplication实例,管理GUI应用程序控制流和主设置
QApplication app(argc, argv);
/* 创建主窗口对象
* 说明:
* - 在栈上分配,生命周期随作用域结束自动销毁
* - 构造函数内会初始化数据库连接和UI组件 */
BookWindow win;
// 显示主窗口(默认隐藏,需显式调用show())
win.show();
/* 进入Qt事件主循环
* 功能:
* - 监听和处理窗口系统事件(鼠标、键盘、重绘等)
* - 阻塞直到所有窗口关闭或调用quit()
* 返回值:
* - 传递应用程序退出状态码(通常0表示正常退出) */
return app.exec();
}