
简介这是一套面向计算机专业学生与C/Qt初学者的图书管理系统完整项目源码可作为毕业设计、课程大作业或自学练手参考。项目采用C与Qt框架开发后端结合MySQL实现数据存储涵盖登录验证、图书增删改查、批量导入、读者管理、密码修改与补偿处理等典型业务模块并运用信号与槽机制完成界面交互响应体现了良好的分层设计思想。压缩包共56个文件约3.86MB其中21个cpp与20个h构成核心业务逻辑9个ui文件负责界面布局另含pro工程文件、qrc资源文件及图片素材结构清晰便于二次开发。目前已有120人学习下载。通过研读源码读者可掌握Qt信号槽通信、自定义控件封装、数据库连接与界面美化等实用技能并借鉴其模块划分与目录组织方式快速搭建属于自己的桌面管理应用。1. 从一份课程设计到能跑的生产工具图书管理系统为什么值得用 C/Qt/MySQL 重写一遍很多同学第一次接触「图书管理系统」是在 C 语言课设里用控制台加文件读写糊出来的——借书还书靠fscanf查询靠遍历数组数据一多就卡程序一崩数据全丢。这套东西交作业能过但离「能用」差得远。而基于 C、Qt 和 MySQL 的图书管理系统恰好是把这份课设升级成桌面级应用的最短路径C 负责业务逻辑和数据结构Qt 负责跨平台图形界面MySQL 负责把数据真正落到关系型数据库里。它适合三类人想找一个完整项目练手的 C 初学者、需要交一份像样课程设计的学生、以及想从「只会写算法题」过渡到「能搭一个带界面的数据管理系统」的开发者。下面我按自己实际搭过一遍的顺序把选型、建库、界面、连接和踩坑讲清楚。2. 技术选型与开发环境为什么是 Qt 5.15.2 MySQL 8 而不是别的组合2.1 三个技术点各自解决什么问题先把职责分清楚后面写代码才不会乱。C 在这里不是拿来炫模板元编程的它承担的是实体建模和业务规则一本书有 ISBN、书名、作者、出版社、库存总量和可借数量借阅记录要关联读者和图书还要处理「库存为 0 不能借」「同一本书不能重复借」这类判断。用类封装这些规则比在界面代码里到处写 if-else 干净得多。Qt 解决的是界面和事件循环。图书管理系统本质是一个表单密集型应用登录框、图书列表、增删改查按钮、借还书对话框。Qt 的QWidget、QTableView、QSqlTableModel这套组合能把「数据库表」直接映射成「界面表格」省掉大量手写刷新逻辑。而且 Qt 的信号槽机制让「点按钮 → 触发业务 → 刷新界面」这条链路非常清晰。MySQL 解决的是持久化和并发。文件存储最大的问题是没法做事务借书时「扣库存」和「写借阅记录」必须同时成功或同时失败否则账就对不上。MySQL 的 InnoDB 引擎支持事务这一点在图书管理系统里是刚需不是可选项。2.2 环境版本怎么定别一上来就追新我一般会锁定一套经过验证的组合而不是全部装最新版。Qt 用 5.15.2 的 LTS 版本原因是它的Qt SQL模块和 MySQL 驱动兼容性最稳网上关于QMYSQL driver not loaded的解决方案也大多针对这个版本。MySQL 用 8.0 系列字符集统一utf8mb4避免中文书名乱码。编译器在 Windows 上用 MinGW 或 MSVC 都行但要注意Qt 的 MySQL 驱动必须和你的编译器位数一致32 位 Qt 配 32 位驱动64 位配 64 位混用必翻车。安装顺序建议是先装 MySQL确认服务能启动、能用命令行登录再装 Qt安装时勾选Qt 5.15.2下的MinGW 64-bit和Qt Debug Information Files最后确认Qt Creator能新建一个空的Qt Widgets Application并编译运行。这一步跑通后面才有意义。提示MySQL 安装时如果选了Use Strong Password Encryption老版本的 Qt MySQL 驱动可能连不上改成Use Legacy Authentication Method能省掉一堆caching_sha2_password的报错。2.3 建库建表先把数据模型定死在写任何界面之前先把数据库结构定下来。图书管理系统的核心表就三张books、readers、borrow_records。下面是我常用的建表语句字段类型和约束都按实际业务来。-- 创建数据库字符集必须用 utf8mb4否则中文书名会变问号 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE library_db; -- 图书表isbn 唯一stock 是总库存available 是可借数量 CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, publisher VARCHAR(50), stock INT NOT NULL DEFAULT 0, available INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 读者表card_no 是借书证号唯一 CREATE TABLE readers ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, phone VARCHAR(20), max_borrow INT NOT NULL DEFAULT 5 ) ENGINEInnoDB; -- 借阅记录表status 0 表示未还1 表示已还 CREATE TABLE borrow_records ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, status TINYINT NOT NULL DEFAULT 0, FOREIGN KEY (book_id) REFERENCES books(id), FOREIGN KEY (reader_id) REFERENCES readers(id) ) ENGINEInnoDB;这段 SQL 的逻辑说明books表里stock和available分开存是因为总库存不会因为借书而变可借数量才会。borrow_records用status而不是靠return_date是否为空来判断查询时条件更明确。外键约束保证不会出现「借了一本不存在的书」这种脏数据。参数说明utf8mb4_general_ci排序规则对中文够用INT存库存足够别用VARCHARDATE存借阅日期比DATETIME更符合业务因为借书是按天算的。建完表后手动插几条测试数据确认SELECT能查出中文再进入下一步。3. 用 Qt Designer 搭界面从登录窗到主窗口的布局与信号槽3.1 界面文件怎么组织别把所有控件堆一个窗口Qt Designer 的.ui文件本质是 XML用uic工具转成 C 头文件。我一般会拆成三个界面login.ui登录、mainwindow.ui主窗口带菜单和表格、bookdialog.ui新增/编辑图书的弹窗。主窗口用QTabWidget分「图书管理」「读者管理」「借阅记录」三个页签每个页签放一个QTableView。在 Qt Designer 里拖控件时记住一个原则能用布局管理器就别用绝对定位。选中主窗口点工具栏上的「垂直布局」再往里放QTabWidget这样窗口拉伸时控件会跟着变。表格列宽用QHeaderView::Stretch让每列自动平分比手动设像素值靠谱。3.2 信号槽连接按钮点击到业务逻辑的桥界面搭好后在mainwindow.h里声明槽函数在mainwindow.cpp里用connect绑定。下面是一个典型的「查询图书」按钮的连接和实现。// mainwindow.cpp #include mainwindow.h #include ui_mainwindow.h #include QSqlQueryModel #include QSqlQuery #include QMessageBox MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); // 把查询按钮的 clicked 信号连接到自定义槽函数 connect(ui-btnSearch, QPushButton::clicked, this, MainWindow::onSearchBook); // 初始化时加载全部图书 loadAllBooks(); } void MainWindow::onSearchBook() { QString keyword ui-editKeyword-text().trimmed(); QSqlQuery query; // 用 LIKE 做模糊匹配绑定参数防止 SQL 注入 query.prepare(SELECT id, isbn, title, author, publisher, stock, available FROM books WHERE title LIKE :kw OR author LIKE :kw); query.bindValue(:kw, % keyword %); if (!query.exec()) { QMessageBox::warning(this, 查询失败, query.lastError().text()); return; } QSqlQueryModel *model new QSqlQueryModel(this); model-setQuery(query); model-setHeaderData(0, Qt::Horizontal, ID); model-setHeaderData(1, Qt::Horizontal, ISBN); model-setHeaderData(2, Qt::Horizontal, 书名); model-setHeaderData(3, Qt::Horizontal, 作者); model-setHeaderData(4, Qt::Horizontal, 出版社); model-setHeaderData(5, Qt::Horizontal, 总库存); model-setHeaderData(6, Qt::Horizontal, 可借); ui-tableViewBooks-setModel(model); ui-tableViewBooks-resizeColumnsToContents(); }逻辑说明query.prepare加bindValue是防 SQL 注入的标准写法别用字符串拼接。QSqlQueryModel是只读模型适合展示查询结果如果要支持直接在表格里编辑得换成QSqlTableModel并设置EditStrategy。setHeaderData把数据库字段名换成中文表头用户体验会好很多。参数说明trimmed()去掉用户输入的首尾空格避免「空格查不到东西」这种玄学问题。resizeColumnsToContents()让列宽自适应内容但数据量大时会慢可以改成固定宽度。3.3 借书还书的事务处理这是整个系统最容易翻车的地方借书操作要同时做两件事把books.available减 1往borrow_records插一条记录。这两步必须在一个事务里否则扣了库存但记录没写进去账就乱了。下面是我用的实现。bool MainWindow::borrowBook(int bookId, int readerId) { QSqlDatabase db QSqlDatabase::database(); if (!db.transaction()) { // 开启事务 QMessageBox::warning(this, 错误, 无法开启事务); return false; } QSqlQuery query(db); // 第一步检查可借数量用 FOR UPDATE 锁住这一行防止并发超借 query.prepare(SELECT available FROM books WHERE id :id FOR UPDATE); query.bindValue(:id, bookId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int available query.value(0).toInt(); if (available 0) { db.rollback(); QMessageBox::information(this, 提示, 该书已无可借库存); return false; } // 第二步扣减可借数量 query.prepare(UPDATE books SET available available - 1 WHERE id :id); query.bindValue(:id, bookId); if (!query.exec()) { db.rollback(); return false; } // 第三步插入借阅记录应还日期设为 30 天后 query.prepare(INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date, status) VALUES (:bid, :rid, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0)); query.bindValue(:bid, bookId); query.bindValue(:rid, readerId); if (!query.exec()) { db.rollback(); return false; } if (!db.commit()) { // 提交事务 db.rollback(); return false; } return true; }逻辑说明FOR UPDATE是行级锁保证两个用户同时借同一本书时第二个会等第一个提交后再读到最新的available不会出现「库存 1 本却借出 2 本」的超借。db.transaction()和db.commit()必须成对出现任何一步失败都要rollback()。参数说明INTERVAL 30 DAY是借期可以改成从配置表读。CURDATE()取当前日期比在 C 里算好再传更省事。注意QSqlDatabase::database()默认用第一个连接如果你有多个连接要显式传连接名。4. 连接 MySQL 与驱动配置QMYSQL driver not loaded 的排查与解决4.1 驱动加载失败是最高频的坑Qt 默认不带 MySQL 驱动QSqlDatabase::addDatabase(QMYSQL)之后如果open()失败lastError().text()会告诉你QMYSQL driver not loaded。这不是代码问题是驱动文件没放对位置。Qt 的 SQL 驱动是插件形式放在sqldrivers目录下文件名类似qsqlmysql.dllWindows或libqsqlmysql.soLinux。排查顺序先确认QSqlDatabase::drivers()返回的列表里有没有QMYSQL。如果没有说明驱动没被加载。这时候检查 Qt 安装目录下的plugins/sqldrivers里有没有qsqlmysql.dll。如果没有需要自己编译或者从对应版本的 Qt 包里找现成的。4.2 自己编译驱动的最小步骤如果确实没有现成驱动用 Qt 自带的源码编译。进入 Qt 安装目录下的Src/qtbase/src/plugins/sqldrivers/mysql用 Qt 的命令行工具执行# 在 Qt 的 MinGW 命令行里执行注意 qmake 要用对应版本的 qmake -- MYSQL_INCDIRC:/Program Files/MySQL/MySQL Server 8.0/include \ MYSQL_LIBDIRC:/Program Files/MySQL/MySQL Server 8.0/lib mingw32-make mingw32-make install逻辑说明MYSQL_INCDIR指向 MySQL 的头文件目录MYSQL_LIBDIR指向libmysql.dll和libmysql.lib所在目录。编译成功后qsqlmysql.dll会被安装到 Qt 的plugins/sqldrivers下。参数说明路径里的空格要用引号包住否则 qmake 会解析错。编译完把libmysql.dll复制到你的可执行文件同目录否则运行时会报「找不到 libmysql.dll」。4.3 连接代码与连接池的简单做法单连接够用但如果你有多个线程或者频繁开关连接建议用连接池。下面是一个最简连接函数。#include QSqlDatabase #include QSqlError #include QMessageBox bool connectDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(library_db); db.setUserName(root); db.setPassword(your_password); // 设置连接选项避免长时间空闲断开 db.setConnectOptions(MYSQL_OPT_RECONNECT1); if (!db.open()) { QMessageBox::critical(nullptr, 数据库连接失败, db.lastError().text()); return false; } return true; }逻辑说明addDatabase不传连接名就用默认连接全局唯一。MYSQL_OPT_RECONNECT1让驱动在连接断开后自动重连适合桌面应用长时间挂着的场景。参数说明127.0.0.1比localhost更稳因为localhost在有些系统上会走 socket 而不是 TCP报error 2002。密码别硬编码在代码里正式项目放配置文件。5. 避坑与常见问题那些让我熬夜的报错和它们的解法5.1 中文乱码从建库到界面全链路都要 utf8mb4现象数据库里查出来是中文但显示到QTableView里变成问号或方块。原因通常是连接字符集没设对。解决在connectDatabase里加db.exec(SET NAMES utf8mb4)或者直接在连接选项里指定。同时确认建库建表时用的就是utf8mb4Qt 源文件保存为 UTF-8 带 BOMWindows 下 MSVC 需要 BOMMinGW 不需要。5.2 编译报错 cannot mix incompatible Qt library现象程序启动时弹窗提示fatal: cannot mix incompatible Qt library (version ex50601) with this library。原因是你的程序链接了某个版本的 Qt 库但运行时PATH里找到了另一个版本的 Qt DLL。解决把 Qt 的bin目录加到PATH最前面或者用windeployqt把依赖 DLL 拷到可执行文件同目录确保版本一致。5.3 借书时库存扣成负数现象并发测试时两本库存的书借出三本。原因是没有用事务加行锁两个请求同时读到available1各自减 1 后都提交。解决按第 3.3 节的写法SELECT ... FOR UPDATE锁行再在事务里更新。如果业务量小也可以在UPDATE语句里加WHERE available 0作为兜底。5.4 Qt 平台插件找不到现象Linux 下运行报qt.qpa.plugin: could not find the Qt platform plugin linuxfb。原因是 Qt 找不到平台插件或者插件依赖的库缺失。解决设置环境变量export QT_DEBUG_PLUGINS1看详细加载日志确认platforms目录下有libqxcb.so并用ldd检查它的依赖是否齐全。嵌入式场景才用linuxfb桌面用xcb。5.5 数据库连接在子线程里失效现象把查询放到QThread里执行报QSqlDatabasePrivate::database: requested database does not belong to the calling thread。原因是 Qt 的数据库连接不能跨线程共享。解决每个线程用addDatabase创建自己的连接连接名带线程 ID 区分用完removeDatabase释放。6. 进阶技巧用存储过程封装借阅逻辑以及一个验证数据一致性的习惯当借书还书的逻辑越来越复杂——比如要检查读者借阅上限、要算逾期罚金、要发通知——把这些全写在 C 里会让代码膨胀。我后来把核心的借阅逻辑下沉到 MySQL 存储过程里C 只负责调用和展示结果。这样做的好处是业务规则集中在数据库层多个客户端比如以后加个 Web 端能复用同一套逻辑。下面是一个借书的存储过程示例把检查库存、检查借阅上限、扣库存、写记录四步封在一起。DELIMITER // CREATE PROCEDURE borrow_book(IN p_book_id INT, IN p_reader_id INT, OUT p_result VARCHAR(50)) BEGIN DECLARE v_available INT; DECLARE v_borrowed INT; DECLARE v_max INT; -- 开启事务 START TRANSACTION; -- 检查库存 SELECT available INTO v_available FROM books WHERE id p_book_id FOR UPDATE; IF v_available IS NULL OR v_available 0 THEN SET p_result 库存不足; ROLLBACK; ELSE -- 检查该读者当前未还数量 SELECT COUNT(*) INTO v_borrowed FROM borrow_records WHERE reader_id p_reader_id AND status 0; SELECT max_borrow INTO v_max FROM readers WHERE id p_reader_id; IF v_borrowed v_max THEN SET p_result 已达借阅上限; ROLLBACK; ELSE UPDATE books SET available available - 1 WHERE id p_book_id; INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date, status) VALUES (p_book_id, p_reader_id, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0); COMMIT; SET p_result 借阅成功; END IF; END IF; END // DELIMITER ;在 Qt 里调用这个存储过程用QSqlQuery的prepare加CALL即可p_result用bindValue的QSql::Out类型接收。这样 C 侧代码从几十行缩到几行而且事务边界在数据库内部不会因为客户端崩溃导致半途而废。参数说明OUT p_result返回文本结果方便界面直接弹提示。FOR UPDATE依然要加存储过程内部同样有并发问题。DELIMITER //是 MySQL 命令行里改分隔符的写法在 Navicat 等工具里可以直接执行不带DELIMITER的版本。最后说一个我养成的习惯每次改完借阅相关代码手动跑一遍「借书 → 查库存 → 查记录 → 还书 → 再查库存」的闭环并且用SELECT * FROM books WHERE available 0和SELECT * FROM books WHERE available stock两条 SQL 做兜底检查。这两条查询只要返回任何一行就说明数据一致性被破坏了。这个习惯帮我提前发现过好几次事务没提交的 bug比事后修数据省心得多。希望帮到你。本文还有配套的精品资源点击获取