ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++/Qt图书管理系统:模型层、信号槽与SQLite持久化全解析

C++/Qt图书管理系统:模型层、信号槽与SQLite持久化全解析 简介基于C与QT实现的图书管理系统是一份面向计算机相关专业在校学生、课程设计与毕业设计场景的完整项目源码包。项目覆盖图书管理、用户管理、借阅管理等常见功能模块代码均通过运行测试尤其适合需要参考完整桌面应用开发流程的初学者或需要快速搭建演示项目的开发者。压缩包共517个文件约24.48MB主要包含cpp/h源码文件、ui界面文件、qrc资源文件以及编译生成的obj、dat等工程文件另有png/jpg图片与README说明文档可便捷地对照代码与界面理解实现逻辑且目录结构清晰。目前已有191人学习下载内容包含完整工程、文档说明及作者提供的远程教学支持便于解决运行配置问题对于课程设计、作业或毕业设计初期立项均有较好的参考价值。1. 图书管理系统用 C/Qt 写之前先想清楚模型层和界面层谁说了算图书管理系统是 C 课程设计里最常被挑中的一个题业务边界清楚借书还书、增删改查都是表格能表达的操作数据量不大控制台能跑Qt 也能跑。也正是因为常见同类代码一搜一大片但大部分实现把 Book 类里塞满控件指针界面一改数据就跟着散。真正拿高分的写法是先把纯 C 模型层立住再让 Qt Widgets 只做视图和转发。下面这套做法从零走一遍先定实体类和管理类的继承边界再用信号槽串起借阅流程然后落 SQLite最后把源代码和文档说明整理到能直接交付的状态。适合正在做课程设计、准备复试或者刚工作想补 Qt 对象模型基本功的人。看完能照着写出来也能看出哪些地方是为了答辩不被问倒而不该省的。2. 图书管理系统的 C 模型层实体类和管理类谁该继承 QObject写 Qt 程序第一反应是找控件但这恰恰是把图书管理系统写乱的开端。界面上的表格、按钮、输入框都是视图真正的业务规则——书名不能为空、可借数量不能为负、同一本书同一读者不能重复借——应该放在不依赖界面的 C 类里。这一层在 Qt 里一般拆成两类承载一行数据的实体类和负责增删改查及规则判断的管理类。它们对 Qt 对象模型的态度完全不同这一章先把分界线划清楚。2.1 实体类不继承 QObject管理类必须继承这是答辩时最容易被问倒的一个选择。实体类如 Book、User、BorrowRecord代表一条业务数据生命周期由容器或管理类掌控不需要事件循环也不产生信号。让它们继承 QObject 会带来三个具体问题QObject 禁用拷贝放进 QVector、QStandardItemModel 的数据流里都要改成指针管理每个实例多一份 meta-object 信息和动态属性表几百本书无感几万条数据时内存和构造时间能明显拉开差距更关键的是语义不清——实体类的方法应该是纯同步的 setter/getter和信号槽没有关系。管理类则相反。图书管理系统的核心交互是“数据变了界面要跟着变”这个通知机制天然就是 Qt 的信号槽所以 BookManager、BorrowService 这类对象必须继承 QObject用 Q_OBJECT 宏声明信号让主窗口 connect。下面把选择依据列成表格按这个思路答基本不会卡壳对比项实体类Book 等管理类BookManager 等是否继承 QObject否是且写 Q_OBJECT是否允许拷贝是值语义否指针或引用持有是否定义信号/槽否是数据变更时 emit生命周期由容器管理由 mainwindow 或 app 管理典型方法id() / setAvailable()addBook() / borrowBook()2.2 Book 与 BorrowRecord 的最小定义// Book.h #pragma once #include QString class Book { public: Book() default; Book(QString id, QString title, QString author, int total, int available) : m_id(std::move(id)), m_title(std::move(title)), m_author(std::move(author)), m_total(total), m_available(available) {} QString id() const { return m_id; } QString title() const { return m_title; } QString author() const { return m_author; } int total() const { return m_total; } int available() const { return m_available; } void setId(const QString id) { m_id id; } void setAvailable(int n) { m_available n; } private: QString m_id; // 书号/条码界面唯一标识 QString m_title; QString m_author; int m_total 1; // 馆藏总数 int m_available 0; // 当前可借 };这个类刻意保持极简。id 用 QString 而不是 int是因为图书条码、ISBN 都可能包含字母或横线主键类型定窄了后面改起来要动整个数据库。total 和 available 分开存借出一本只减 available还书时加回随时能算在馆情况。getter 写成available()而不是getAvailable()是 Qt 风格而非 JavaBean 风格在 Qt 容器和属性系统里能少写一套命名转换。构造函数用缺省实参和初始化列表兜底避免出现Book b;之后裸赋值的写法后续数据统一走管理类进入。BorrowRecord 类似但多两个时间字段#include QDate class BorrowRecord { public: QString bookId; QString readerId; QDate borrowDate; QDate dueDate; bool returned false; };这里故意把成员写成公有因为 BorrowRecord 只是数据载体没有需要保护的不变量真正借出、归还的判断放在管理类里做规则才不会散落在各个界面的槽函数中。QDate 是值类型可以直接存入容器和数据库相比自拼2025-01-01字符串它自带的addDays()、daysTo()在算逾期天数时能少写很多边界代码。2.3 BookManager信号槽和业务规则放同一层// BookManager.h #pragma once #include QObject #include QVector #include Book.h class BookManager : public QObject { Q_OBJECT public: explicit BookManager(QObject *parent nullptr); QVectorBook allBooks() const { return m_books; } bool addBook(const Book book); bool borrowBook(const QString bookId); bool returnBook(const QString bookId); signals: void booksChanged(); private: QVectorBook m_books; };关键在于booksChanged()信号。界面层不需要知道数据怎么变的只需把reloadTable()挂到booksChanged()上之后任何增删改都会自动刷新表格比手动到处调刷新的写法少一半 bug。allBooks()这里返回 QVector 的值拷贝课程设计数据量小拷贝开销可忽略真要接几万条数据改成const QVectorBook再配合 QSqlQueryModel 更合适。借出规则的实现bool BookManager::borrowBook(const QString bookId) { for (Book b : m_books) { if (b.id() ! bookId) continue; if (b.available() 0) return false; b.setAvailable(b.available() - 1); emit booksChanged(); return true; } return false; // 找不到书 }循环里先按 id 定位再检查 available失败直接 return false成功才 emit。这样主窗口的槽只需要判断返回值并弹提示不用自己遍历 bookList。把“剩余数量是否大于 0”收进管理类是答辩时能讲清楚“分层”的关键证据。3. 用 Qt Widgets 把借书还书流程接起来信号槽、对话框与按钮状态模型层立住之后界面层就是一层薄皮。主窗口常见布局是顶部菜单栏放“图书管理”“借阅管理”中部用 QTableView 显示图书列表右侧操作区放新增、编辑、借出、还书、删除按钮底部放搜索框。按钮初始状态全部置灰只有表格选中行时才启用与行相关的操作。这样做能把“没有选中行”这类不可能操作直接排除槽函数里少一层判断。3.1 用 QStandardItemModel 做表格数据源QTableView 本身不存数据需要一个 model 接口。常见做法是用 QStandardItemModel简单直接适合课程设计规模只有要做分组、懒加载时才会换 QSqlQueryModel 或自写 QAbstractTableModel。初始化m_model new QStandardItemModel(this); m_model-setHorizontalHeaderLabels( {书号, 书名, 作者, 馆藏, 可借}); ui-tableView-setModel(m_model); ui-tableView-horizontalHeader()-setSectionResizeMode( QHeaderView::Stretch);刷新函数把 BookManager 中的数据折射成行void MainWindow::reloadTable() { m_model-removeRows(0, m_model-rowCount()); for (const Book b : m_manager-allBooks()) { QListQStandardItem* row; row new QStandardItem(b.id()) new QStandardItem(b.title()) new QStandardItem(b.author()) new QStandardItem(QString::number(b.total())) new QStandardItem(QString::number(b.available())); m_model-appendRow(row); } }这里不直接操作表格控件而是把数据转成 QStandardItem 塞进 model排序、选中都由 model 管理。注意重复调用 reloadTable 前要把旧行清掉否则每点一次新增按钮列表就翻倍。QHeaderView::Stretch 让列宽平均分配显示中文书名时避免出现截断的省略号。接着把模型层和界面接上connect(m_manager, BookManager::booksChanged, this, MainWindow::reloadTable);这样前面 borrowBook 里 emit 的 booksChanged 会自动触发 reloadTable界面始终和模型层一致。代价是任何一次变更都会全量刷新表格当前数据量完全没问题如果表格里正有用户编辑中的单元格全量刷新会打断输入后续可以考虑只刷新局部行。3.2 新增图书对话框exec() 返回值与数据校验新增和编辑共用一个 BookDialog继承 QDialog。里面放几个 QLineEdit 和一个 QSpinBox用户点确定后先校验再 acceptvoid BookDialog::onConfirm() { if (m_titleEdit-text().trimmed().isEmpty()) { QMessageBox::warning(this, 校验失败, 书名不能为空); return; } m_book Book(m_idEdit-text().trimmed(), m_titleEdit-text().trimmed(), m_authorEdit-text().trimmed(), m_totalSpin-value(), m_totalSpin-value()); accept(); }主窗口在 lambda 里调用 exec()根据返回值决定是否提交connect(ui-btnAdd, QPushButton::clicked, this, [this]() { BookDialog dlg(this); if (dlg.exec() QDialog::Accepted) { if (!m_manager-addBook(dlg.book())) { QMessageBox::warning(this, 添加失败, 书号重复或已存在); } } });返回值的含义要讲清楚QDialog::Accepted 表示对话框按了确定且校验通过Rejected 表示取消或关闭。校验放对话框里、持久化放管理类里分工明确。这里不需要自定义信号因为 exec() 是阻塞的调用点直接能拿到结果如果对话框要打开后异步加载数据才需要走信号。3.3 借出/还书的按钮联动与边界状态表格选中行时借出、还书按钮才可用用 selectionModel 的信号connect(ui-tableView-selectionModel(), QItemSelectionModel::selectionChanged, this, [this](const QItemSelection , const QItemSelection ) { bool hasRow ui-tableView-currentIndex().isValid(); ui-btnBorrow-setEnabled(hasRow); ui-btnReturn-setEnabled(hasRow); });按钮可用性只依赖“有没有选中行”至于这本书是否可借交给模型层在 borrowBook 里再检查。界面负责禁用不可能的操作模型层负责业务规则两层各管一段别在一个地方写完。操作前置状态失败时界面反馈借出选中行且 available 0提示“该记录存在但没有可借副本”还书选中行且有未归还记录提示“未找到该读者的未归还记录”删除选中行二次确认后再调 deleteBook还书的实现和借用类似在 BookManager 里按 bookId 找到记录、把 available 加回再 emit booksChanged。“借出成功”可以不弹窗表格里“可借”列减一本身就是反馈频繁弹 QMessageBox 会打断操作节奏。3.4 一个常见的槽函数返回值误区很多初学者会把槽写成bool onBorrowClicked()试图用返回值判断执行是否成功。信号槽机制在 connect 时会把返回值丢弃调用方根本拿不到。正确做法是状态交给模型层持有界面通过模型返回的 bool、参数引用、或者操作后再查一次模型来判断。遇到“槽执行完要拿结果”的场景建议改用普通成员函数加 lambda必要时再发一个携带数据的信号而不是依赖槽的返回值。4. 图书管理系统接 SQLite建表、参数化 CRUD 与搜索防抖模型层用 QVector 能跑通界面但程序一关数据就丢。落 SQLite 是图书管理系统最自然的扩展方向也是文档里“数据库设计”那一章的素材。Qt 自带 QSQLITE 驱动不需要额外安装数据库服务打包时会带上sqldrivers/qsqlite.dll很适合做课程设计的持久层。4.1 初始化数据库连接与建表bool initDatabase(const QString dbPath) { if (!QSqlDatabase::isDriverAvailable(QSQLITE)) { qWarning() QSQLITE driver not available; return false; } QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(dbPath); if (!db.open()) { qWarning() open error: db.lastError().text(); return false; } QSqlQuery query(db); return query.exec( CREATE TABLE IF NOT EXISTS books ( id TEXT PRIMARY KEY, title TEXT NOT NULL, author TEXT, total INT NOT NULL DEFAULT 1, available INT NOT NULL DEFAULT 1)); }数据库文件路径建议用QCoreApplication::applicationDirPath() /library.db放在程序运行目录旁边而不是依赖当前工作目录。从 Qt Creator 里按 F5 运行的当前目录和双击 exe 运行的当前目录不一定一致用拼接出来的绝对路径最稳。isDriverAvailable 的检查很关键插件缺失时后面所有 SQL 都会报 “driver not loaded”启动时先判断能给用户明确提示而不是到操作时才崩。4.2 用参数绑定做增删改查别拼字符串界面上新增的 Book落库时如果直接拼 SQL书名里带个英文单引号就会让语句报错。参数绑定是必做项QSqlQuery query; query.prepare(INSERT INTO books(id, title, author, total, available) VALUES(?, ?, ?, ?, ?)); query.addBindValue(book.id()); query.addBindValue(book.title()); query.addBindValue(book.author()); query.addBindValue(book.total()); query.addBindValue(book.available()); if (!query.exec()) { qWarning() insert failed: query.lastError().text(); return false; }?是位置占位符也可以用:id这样的命名占位符SQL 较长时后者可读性更好。addBindValue 会把 QString 转成 TEXT、把 int 存成整型类型转换交给数据库端完成拼字符串常见的转义问题也在这层挡掉。两种写法差异如下写法书名含单引号数字类型可读性推荐字符串拼接SQL 报错手动 toString一眼看懂×参数绑定安全自动转换稍显仪式感√注意 SQLite 的 TEXT 主键不会自动增长新增书籍时由界面录入书号如果改成INTEGER PRIMARY KEY AUTOINCREMENT就要把 Book 的 id 改成 int并在 insert 时省略该列。两种设计都常见图书条码场景建议保留 TEXT。4.3 借阅记录表与归还状态别再把记录放内存借阅行为不能只改 books 表的 available还要留下每一笔“谁借了哪本、什么时候该还”。建表CREATE TABLE IF NOT EXISTS borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id TEXT NOT NULL, reader_id TEXT NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT );归还时先更新 borrow_records 的 return_date再把 books 表的 available 加回两个操作放进同一个事务db.transaction(); QSqlQuery q1(db), q2(db); q1.prepare(UPDATE borrow_records SET return_date? WHERE book_id? AND reader_id? AND return_date IS NULL); q1.addBindValue(QDate::currentDate().toString(Qt::ISODate)); q1.addBindValue(bookId); q1.addBindValue(readerId); q1.exec(); q2.prepare(UPDATE books SET available available 1 WHERE id ?); q2.addBindValue(bookId); q2.exec(); db.commit();为什么必须用事务如果只执行第一条 UPDATE 而第二条失败数据就变成“书还了但库存没加回来”的脏状态。SQLite 默认每条语句自动提交把两步包在 transaction 和 commit 之间要么都生效要么都回滚这是回答“如何保证数据一致性”时最直接的证据。4.4 搜索框防抖QLineEdit 加 300ms 定时器搜索框每敲一个字符都会触发 textChanged。如果每次都立刻 SELECT敲“三体”两个字会连打两次数据库数据量一大明显卡顿。常见做法是加一个 singleShot 定时器QTimer *debounce new QTimer(this); debounce-setInterval(300); debounce-setSingleShot(true); connect(ui-searchEdit, QLineEdit::textChanged, this, [](const QString ) { debounce-start(); // 重置计时用户停手后才触发 }); connect(debounce, QTimer::timeout, this, []() { applySearch(ui-searchEdit-text().trimmed()); });applySearch 里用 LIKE 做模糊匹配void MainWindow::applySearch(const QString kw) { QSqlQuery query; query.prepare(SELECT * FROM books WHERE title LIKE ? OR author LIKE ? ORDER BY title); query.addBindValue(% kw %); query.addBindValue(% kw %); query.exec(); m_model-removeRows(0, m_model-rowCount()); while (query.next()) { QListQStandardItem* row; row new QStandardItem(query.value(id).toString()) new QStandardItem(query.value(title).toString()) new QStandardItem(query.value(author).toString()); m_model-appendRow(row); } }LIKE 的 % 是任意长度通配符放在关键词前后实现包含匹配。如果关键词本身含有 % 或 _会被当成通配符严谨的做法是先把它们转义再拼进 pattern并配合 ESCAPE 子句。搜索框为空时不走 LIKE直接 SELECT 全表有内容时才过滤增删改和搜索两种模式交替使用就能共存。5. 交付前的源代码与文档说明收尾注释规范、部署检查和冒烟验证界面和数据库都通了之后剩下的是把工程整理成“能提交、能答辩、能运行”的状态。源代码层面先过一遍命名与注释文件和类都加文件头注释说明用途和修改日期函数注释只写“做什么、边界条件”不写“怎么实现”。/*! * 借出一本书。 * 成功条件书存在且 available 0。 * 失败时返回 false界面层据此提示读者。 */ bool BookManager::borrowBook(const QString bookId);文档说明一般跟三个README 写环境要求、编译步骤和默认测试账号数据库设计文档列清每张表的字段、类型、约束和数据流接口说明按增删改查列出入口与返回值。文档的价值在于别人按步骤能跑起来所以编译步骤里要写明 Qt 版本与编译器套件是 MinGW 还是 MSVC这两个不一致是常见的环境坑。评分人拿到工程后先看 README再看代码结构最后跑一遍程序这三点都顺才算交付完成。部署检查我一般走三步。先 Release 编译再在 Qt 命令行环境执行windeployqt --release --no-translations bookmanager.exe这个命令把 Qt 运行库、platforms 插件和 sqldrivers 拷到 exe 同目录--no-translations表示不要 Qt 自带的语言包如果做了 qt 国际化要去掉这个参数保留翻译文件。最后把整个目录拷到一台没装 Qt 的机器上冒烟启动、新增一本书、搜索、借出、还书、重启程序确认数据还在这六个动作过一遍就比大多数人直接交工程文件完整得多。如果冒烟时报“当前不会命中断点”或启动即闪退优先检查 sqldrivers 插件是否存在以及 exe 路径是否含中文导致 Qt 解析相对路径失败。SQLite 数据文件在拷贝目录后容易遇到目录只读问题把 library.db 所在目录设为可写即可。本文还有配套的精品资源点击获取
返回列表