简介:这是一份面向高校计算机相关专业课程设计或毕业设计的图书管理系统完整方案,基于Qt框架与SQL数据库实现,适合具备C++与数据库基础、希望快速搭建可演示项目或系统学习桌面应用开发的学习者。压缩包共一百三十二个文件,约六点六七MB,其中四十个头文件与二十四个源文件构成项目核心逻辑,十八个界面文件用于可视化窗体设计,并配有流程图、设计报告、界面截图、资源文件等,便于在代码、界面与文档之间对照理解。系统区分管理员和普通用户两种角色,覆盖图书入库、检索、借还、逾期统计等常用业务流,还通过绘图组件展示运营数据,整体功能完整、模块边界清晰。源码均经过本地编译验证,按照文档配置数据库和开发环境即可直接运行,能帮助节省大量排错时间。目前已有二百二十一人学习下载,无论是用于课程设计答辩、综合实训,还是作为Qt与数据库结合开发的入门范本,都具有较好的借鉴意义。
1. 图书管理系统为什么值得选 QT 来做
图书管理系统几乎是 QT 课程设计里出现频率最高的题目之一,但它的坑不在界面控件,而在“登录后怎么按角色切换模块、借还书怎么保证数据库一致、统计图怎么不卡界面”这三件事上。这套资源恰好覆盖了完整链路:mainwindow.cpp 负责登录和主框架,admin/user 两个窗口分别处理管理员和普通用户业务,paint.cpp 封装图表绘制,qcustomplot.cpp 直接以源码形式提供绘图能力,再配合 SQLite 和配套文档,能当作一份可运行、可交差的课设模板。适合正在选型 C++/Qt 课设的同学,也适合想快速翻一下 Qt Widgets 业务分层写法的从业者参考,后面我按文件结构、数据事务、图表绘制、打包排错四个角度拆开讲。
2. 源码结构拆解:mainwindow 与 admin/user 的职责边界
这套源码从文件命名就能看出典型的 Qt Widgets 单窗口单类风格,不是把所有功能塞进 mainwindow.cpp,而是按角色拆成管理员端和用户端。对课程设计来说,这种拆分的好处是评审老师问“某个功能在哪”时,你能直接指出文件,而不是在一千行的主窗口里翻按钮的 connect 所在行。
2.1 从 .cpp 文件名还原模块划分
拿到压缩包先不要急着编译,按文件列表过一遍,基本能还原作者的设计意图:
| 文件 | 对应模块 | 主要职责 |
|---|---|---|
| mainwindow.cpp | 登录窗口与主框架 | 登录验证、角色判定、页面切换 |
| adiminster.cpp | 管理员端 | 图书增删改查、读者管理、借阅记录维护 |
| user.cpp | 普通用户端 | 书目检索、借书、还书、个人借阅历史 |
| paint.cpp | 图表封装 | 把 SQL 统计结果转成 QCustomPlot 数据并刷新 |
| qcustomplot.cpp | 第三方绘图库 | QCustomPlot 全部实现,随源码一并编译 |
源码里管理员窗口的文件名是 adiminster.cpp,这个拼写不算标准,编译运行没问题,但如果你要交报告,建议在类名里用 AdminWindow,并在文档里注明文件名对应关系,避免答辩时被问住。qcustomplot.cpp 是第三方开源库,体积不小,但也正因为它是单文件源码,不需要额外装模块,放到工程里加进 .pro 的 SOURCES 就能编。
2.2 登录后的页面切换:QStackedWidget 比反复 new 窗口更顺
很多课设喜欢登录成功之后 loginWindow->hide(),再 new 一个 AdminWindow 出来 show()。这样做的缺点是每次登录都要重新初始化一次界面,而且两个窗口各自持有数据库连接状态,逻辑容易散。常见做法是 MainWindow 里放一个 QStackedWidget,把登录页、管理员页、用户页都预先加进去,登录成功后直接切页。
// mainwindow.cpp 构造函数中完成页面注册 stackWidget = new QStackedWidget(this); stackWidget->addWidget(loginPage); // index 0 stackWidget->addWidget(adminPage); // index 1 stackWidget->addWidget(userPage); // index 2 // 登录成功后由信号槽触发切换 connect(loginPage, &LoginWidget::loginSucceeded, this, &MainWindow::onLoginSucceeded); void MainWindow::onLoginSucceeded(int role, const QString &userId) { if (role == 1) { stackWidget->setCurrentWidget(adminPage); adminPage->setCurrentUser(userId); } else { stackWidget->setCurrentWidget(userPage); userPage->setCurrentUser(userId); } }这段代码把页面切换收敛到 MainWindow 一个地方。role 为 1 表示管理员,其他值走用户端;setCurrentUser 负责把登录者的 id 传给业务页面,后续查询“我借了哪些书”都依赖这个成员变量。相比反复 new 窗口,QStackedWidget 的优势是切页不重建界面,表格筛选条件、图表缩放状态都能保留。
2.3 管理员端用 QSqlTableModel 绑表,避免逐行 setText
图书管理界面最忌讳的是用 QTableWidget 手动 setItem 填充数据,一旦图书数量上千,刷新就会明显卡顿,而且增删改之后还要自己同步 UI。课设能接受的写法是 QSqlTableModel 直接绑表,配合 QTableView 显示。
bookModel = new QSqlTableModel(this); bookModel->setTable("books"); bookModel->setEditStrategy(QSqlTableModel::OnManualSubmit); bookModel->setHeaderData(1, Qt::Horizontal, QStringLiteral("书名")); bookModel->setHeaderData(2, Qt::Horizontal, QStringLiteral("作者")); ui->tableView->setModel(bookModel); bookModel->select();setEditStrategy 用 OnManualSubmit 的意思是界面上改了单元格不会立刻写库,必须手动调用 submitAll() 才生效,这样可以避免用户误操作直接污染数据。删除时先 removeRows,再统一 submitAll(),如果要取消修改,调用 revertAll() 就能回到上次提交状态。user.cpp 里查“我的借阅记录”也用同样的套路,只是 setTable 换成 borrow_records,再用 setFilter 限定 user_id。
2.4 共享当前登录用户:成员变量比全局变量稳
有些不成熟的课设会把当前用户 id 存成 static 全局变量,表面省事,但多窗口环境下排查问题很痛苦。更合理的做法是给每个页面提供一个 setCurrentUser 接口,把登录信息作为成员变量缓存。
void AdminWindow::setCurrentUser(const QString &userId) { currentUserId = userId; // 刷新该用户相关的审批、借阅记录 refreshBorrowList(); }2.5 模块之间通信尽量走信号槽
adminPage 要通知 MainWindow“退出登录”,不要直接操作 stackWidget 的索引,而是发一个 logoutRequested 信号,由 MainWindow 决定切回登录页并清理密码框缓存。这样每个页面只关心自己该做什么,页面之间的耦合降到最低,后续如果要加“修改密码后强制重新登录”之类的功能,也只要在 MainWindow 里多接一个信号。
3. 数据层设计:借书事务和 QSqlQuery 的边界
图书管理系统无论界面怎么写,最终比拼的是数据不丢、不错、不乱。课设里的常见扣分点集中在:还书日期超出 30 天怎么算逾期、库存为 0 时还能不能借出、连续借两本书会不会把库存扣成负数。这三个问题的解法都指向表结构设计和事务控制。
3.1 三张表的字段设计
常规课设最少需要三张表:读者表、图书表、借阅记录表。注意借阅记录表不直接放“读者姓名”和“书名”,而是放 id 外键,这样改书名或改读者姓名时不需要动历史记录。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| users | user_id, username, password, role | user_id 用学号/工号字符串,role 区分管理员 |
| books | book_id, title, author, publisher, category, stock, location | stock 是剩余可借数量 |
| borrow_records | record_id, book_id, user_id, borrow_date, due_date, return_date, status | status: 0 借出中,1 已归还 |
users 表里如果直接用自增 int 而不是字符串 id,后面做“某读者借过多少本”统计时还得 join,反而麻烦。borrow_records 的 due_date 在插入记录时直接用 SQLite 的 date('now', '+30 days') 生成,减少 C++ 端日期格式化代码。
3.2 连接数据库前先检查驱动
项目文档里说配置好环境就能运行,最常见的环境问题就是 QSQLITE 驱动没加载。Qt 的 SQLite 插件编译后是一个 sqldrivers 文件夹下的 dll,如果程序报错 “QSqlDatabase: QSQLITE driver not loaded”,通常不是代码问题,而是插件目录缺失或程序运行目录不对。
if (!QSqlDatabase::isDriverAvailable("QSQLITE")) { QMessageBox::critical(nullptr, QStringLiteral("错误"), QStringLiteral("未找到 QSQLITE 驱动,请检查 sqldrivers 目录")); return; } QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("library.db"); if (!db.open()) { qDebug() << db.lastError().text(); }这里是先判断驱动存在再创建连接。addDatabase 的第二个参数可以传连接名,如果你在管理员端和用户端各开一个连接,务必使用不同的连接名,比如 addDatabase("QSQLITE", "adminConn"),否则第二次 addDatabase 默认覆盖第一次连接,导致第一个窗口查询全部失败。
3.3 借书流程用事务包起来
借书看起来是“插一条记录、减一个库存”,但实际上两个 SQL 必须要么都成功,要么都失败。如果插了借阅记录但库存没减,书就“凭空消失”了;如果减了库存但记录没插上,读者会莫名少一本书。正确做法是把两步放进一个事务。
// 借书槽函数 if (!db.transaction()) return; QSqlQuery check(db); check.prepare("SELECT stock FROM books WHERE book_id = ?"); check.addBindValue(bookId); if (!check.exec() || !check.next() || check.value(0).toInt() <= 0) { db.rollback(); QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("该图书已无可借库存")); return; } QSqlQuery insert(db); insert.prepare("INSERT INTO borrow_records(book_id, user_id, borrow_date, due_date, status) " "VALUES(?, ?, date('now'), date('now', '+30 days'), 0)"); insert.addBindValue(bookId); insert.addBindValue(currentUserId); if (!insert.exec()) { db.rollback(); return; } QSqlQuery update(db); update.prepare("UPDATE books SET stock = stock - 1 WHERE book_id = ?"); update.addBindValue(bookId); if (!update.exec()) { db.rollback(); return; } db.commit(); refreshBookTable();注意这里的事务边界:先 check 库存,再 insert 借阅记录,最后 update 库存。任何一个 exec 失败都直接 rollback,不会留下半截状态。addBindValue 的作用是参数绑定,比拼接字符串多一层安全保护,书名里带单引号也不会破坏 SQL。SQLite 单机场景下事务粒度不建议开得太大,一次借书一个事务就够了。
3.4 关键词检索的 LIKE 过滤怎么防注入
QSqlTableModel 没有 prepare 接口,setFilter 拼字符串时最容易踩的坑是书名里含单引号,比如 “SQL'S 入门”,一个未转义的单引号会让整条查询失败。
QString keyword = ui->editKeyword->text().trimmed(); keyword = keyword.replace("'", "''"); bookModel->setFilter(QString("title LIKE '%%1%' OR author LIKE '%%1%'") .arg(keyword)); bookModel->select();replace("'", "''") 是 SQL 标准里的转义写法,两个单引号代表一个字面单引号。LIKE 后面那两个 % 是通配符,其中 %1 会被 arg 替换成用户输入,首尾的 % 表示“包含关系”。如果只写一半,检索逻辑就从包含变成前缀或后缀匹配,容易得出与预期不符的结果。
4. QCustomPlot 实战:paint.cpp 里的统计图怎么画
管理端除了表格,评审老师通常还想看到“界面有点可视化”,比如月度借阅量柱状图、热门图书排行。这套资源里放 qcustomplot.cpp 而不是用 Qt Charts,原因很直接:Qt Charts 在 Qt 5 里需要通过 QT += charts 单独添加模块,发布时还牵扯插件的额外拷贝;QCustomPlot 只需要把 .cpp 和 .h 放进工程,所有绘制能力都在你的程序里。
4.1 paint.cpp 的定位:隔离绘图逻辑
paint.cpp 在这个项目里建议只做一件事:接收 SQL 查出来的统计结果,转换成 QCustomPlot 需要的 QVector 数据,然后触发重绘。不要在 paint.cpp 里写 SQL,也不要在查询函数里直接操作 plot 控件,这样图表和业务数据可以各自单独测试。
4.2 月度借阅量柱状图
先看 SQL 端怎么取数。借阅日期是字符串,格式形如 2025-01-15,用 substr 截取前 7 位就能得到月份:
SELECT substr(borrow_date, 1, 7) AS month, COUNT(*) AS cnt FROM borrow_records GROUP BY month ORDER BY month;C++ 端拿到查询结果后填充 QCPBars,这是 QCustomPlot 里的柱状图 plottable,一组柱子对应一个 QCPBars 对象。
// paint.cpp 中绘制柱状图 QCustomPlot *plot = ui->widgetPlot; // UI 里放的 QCustomPlot 控件 QVector<double> ticks, counts; QStringList labels; QSqlQuery q("SELECT substr(borrow_date, 1, 7) AS month, COUNT(*) " "FROM borrow_records GROUP BY month ORDER BY month"); while (q.next()) { labels << q.value(0).toString(); ticks << ticks.size(); // 0,1,2... counts << q.value(1).toDouble(); } QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText); textTicker->setTicks(ticks, labels); plot->xAxis->setTicker(textTicker); plot->yAxis->setLabel(QStringLiteral("借阅数量")); QCPBars *bars = new QCPBars(plot->xAxis, plot->yAxis); bars->setData(ticks, counts); bars->setPen(Qt::NoPen); bars->setBrush(QColor(70, 130, 180, 180)); plot->rescaleAxes(); plot->replot();这里 x 轴不能直接拿月份字符串当数值,而是用 0、1、2 这样的整数刻度,再把月份文本映射到刻度上,否则月份文本会被当成普通分类坐标处理。setPen 设为 NoPen 是去掉柱子描边,setBrush 里的第四个参数是 alpha 透明度,能让重叠色块更好看。rescaleAxes 会根据当前数据自动调整坐标范围,避免柱子跑到绘图区外面。
4.3 热门图书 TOP10 排行
借阅排行和月度统计共用同一套 QCPBars 逻辑,区别只在 SQL:
SELECT b.title, COUNT(r.record_id) AS borrow_count FROM borrow_records r JOIN books b ON r.book_id = b.book_id GROUP BY b.book_id ORDER BY borrow_count DESC LIMIT 10;图书管理系统这类数据量不大的课设,用 GROUP BY 和 LIMIT 就够,没必要在 C++ 端做排序。但有个细节需要注意:QCPBars 的柱子宽度默认是按数据点数量自动算的,如果你的数据只有五条,柱子会非常宽,建议显式设置 bars->setWidth(0.6 或 0.8) 控制比例。
4.4 图表刷新与内存释放
刷新图表时最容易犯的错误是每次查询都 new 一个 QCPBars,旧对象一直保留在 plot 里,导致刷新越多次、柱子越密集。正确做法是复用已有的 QCPBars:
if (!bars) { bars = new QCPBars(plot->xAxis, plot->yAxis); } else { bars->data()->clear(); } bars->setData(ticks, counts); plot->replot(QCustomPlot::rpQueuedRepaint);data()->clear() 只清空柱子数据,不销毁 plottable。replot 的参数 rpQueuedRepaint 表示延迟到事件循环空闲时重绘,连续刷新多组数据时能减少闪烁。QCustomPlot 在数据量几千点以内的重绘开销都很小,课设的图书数据不涉及性能瓶颈,真正要避免的是反复 new 对象造成的内存泄漏。
| QCustomPlot 方法 | 作用 | 使用场景 |
|---|---|---|
| replot() | 立即重绘 | 回调、按钮触发 |
| replot(QCustomPlot::rpQueuedRepaint) | 延迟重绘 | 高频刷新、拖动条联动 |
| rescaleAxes() | 按数据重新缩放坐标轴 | 数据范围变化后 |
| data()->clear() | 清空 plottable 数据 | 复用柱状图刷新 |
5. 发布、打包与排错:从源码到绿色版本
很多课设源码在开发机上跑得好好的,拷到别的电脑双击 exe 就报“缺少 DLL”或者“QT_QPA_PLATFORM_PLUGIN_PATH not set”。这通常是构建版本和运行环境没对齐,不是代码逻辑问题。
5.1 环境不一致的典型报错
- 报错找不到 msvcp140.dll:程序是 MSVC 编译器构建的,目标机器没有安装 VC++ 运行库。
- 报错无法定位程序输入点:你用了高版本 Qt 构建,目标机器跑的是旧版本核心 DLL,程序路径里混入了别处的 qt*.dll。
- 报错 QT_QPA_PLATFORM_PLUGIN_PATH:程序没找到 platforms/qwindows.dll 平台插件,通常发生在用 Qt Creator 直接拷贝 exe 而未做部署时。
用 windeployqt 可以一次性补齐 Qt 运行库:
cd /d D:\build\LibrarySystem\release set PATH=C:\Qt\5.15.2\msvc2019_64\bin;%PATH% windeployqt LibrarySystem.exe第一条命令切到 release 构建目录,注意必须是 release,debug 版带调试符号且体积大得多。第二条把 Qt 的 bin 目录临时加进 PATH,目的是让 windeployqt 能定位到同版本的 Qt 库。第三条执行后,工具会自动把 exe 依赖的 Qt5Core.dll、Qt5Widgets.dll、platforms 插件等全部复制到当前目录。
5.2 打包前把数据库路径改为自动定位
课设里 setDatabaseName("library.db") 用的相对路径,开发时没有“当前目录”概念,因为 exe 在 build 目录,源程序里也能通过绝对路径连接。但发布之后,如果把程序安装在 D 盘,library.db 就会被放下 D:\LibrarySystem 下;把程序拷到 U 盘给别人运行,当前目录可能是任意盘符。更稳的课设做法是把数据库放到 QStandardPaths 指定的用户数据目录:
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir().mkpath(dataDir); QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName(dataDir + "/library.db");AppDataLocation 在 Windows 上会落到 C:/Users/用户名/AppData/Roaming/程序名 下,首次运行自动创建目录,避免“无法打开数据库文件”的权限类问题。如果你的课设报告里想体现 QT 国际化能力,还可以在 main.cpp 里用 QTranslator 加载 .qm 文件,配合 QStringLiteral 标记需要翻译的文本,但这部分作为加分项即可,先把运行环境和数据库路径稳定下来更有价值。
本文还有配套的精品资源,点击获取