拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Qt C/S图书管理系统实战:从1.zip拆解到QTableView性能优化

Qt C/S图书管理系统实战:从1.zip拆解到QTableView性能优化

简介:这份资源是一套基于Qt框架与MySQL数据库实现的C/S架构图书管理系统完整源码,面向学习Qt桌面开发、数据库编程及客户端/服务器通信的开发者与课程设计学生。项目覆盖用户登录注册、图书检索与详情查看、借阅归还、预约取消、分类浏览及个人中心等客户端模块,服务器端则负责数据库管理、业务逻辑处理与SQL增删改查,并借助Qt信号槽机制与QSqlDatabase模块完成异步通信和数据持久化。压缩包共120个文件,以34个h头文件、21个cpp源文件为核心代码,辅以41个png界面素材、pro工程文件、ts翻译文件、qrc资源文件及1个sql建库脚本,整体约382KB,结构清晰便于按模块阅读。目前已有307人学习下载,适合作为Qt入门到进阶的实战参考,帮助读者理解C/S分层设计、界面与逻辑解耦及数据库连接封装等关键思路。

1. 从一份 1.zip 说起:Qt C/S 图书管理系统到底能跑出什么

如果你手上正好有一个叫1.zip的压缩包,解压后看到一堆.pro、.ui、.cpp、.h文件,目录里还夹着book、manager、login这类命名,那大概率就是一份 Qt 写的 C/S 架构图书管理系统。这类资源在课程设计、毕业设计、公司内部小工具里出现频率极高,但真正能一次编译跑通、数据库连上、界面不崩的并不多。我见过太多人卡在:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...'这种路径报错上,或者 Qt 版本一换,QTableWidget直接白屏。

这份资源的核心价值在于:它把图书管理这个经典业务场景,用 Qt 的 C/S 模式完整落地了一遍——客户端负责界面交互,服务端或本地数据库负责数据持久化,中间通过 Qt 的网络模块或数据库模块打通。适合三类人:正在找 Qt 课程设计参考的学生、想快速搭一个桌面端管理工具原型的开发者、以及需要一份能改造成自己业务的管理系统骨架的工程师。它不是什么高深框架,但胜在结构清晰、模块齐全,登录、增删改查、借阅归还、用户权限这些基础功能都能找到对应实现。下面我按实际拆包和复现的顺序,把这份资源从环境到代码到坑,一层层拆开讲。

2. 环境与工程结构:Qt 版本、编译器与 .pro 文件怎么对齐

2.1 先确认 Qt 版本和编译器组合

拿到1.zip后第一件事不是急着打开.pro,而是先看工程里有没有README、.pri或者注释里写的 Qt 版本。常见的是 Qt 5.12、5.14、5.15 这几个 LTS 版本,编译器分 MinGW 和 MSVC 两派。如果你用 Qt Creator 打开后提示dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...',说明工程里写死了绝对路径,或者你当前 Kit 和工程原本的 Kit 不一致。

我一般会先做三件事:

  1. 在 Qt Creator 里点「项目」→「Build & Run」,看当前 Kit 的 Qt 版本和编译器。
  2. 打开.pro文件,检查有没有QT +=里缺模块,比如sql、network、widgets。
  3. 如果.pro.user文件存在,直接删掉,让 Qt Creator 重新生成,避免旧路径干扰。
# 查看当前系统里装了哪些 Qt 版本(Linux/macOS 常见路径) ls /opt/Qt* 2>/dev/null ls ~/Qt* 2>/dev/null # Windows 下一般在 C:\Qt 或 D:\Qt # 确认 qmake 版本 qmake -v

上面命令是帮你快速定位本机 Qt 安装情况。qmake -v输出的版本号要和工程原本使用的版本尽量一致,跨大版本(比如 5.12 到 6.x)会有大量 API 变动,QTextCodec、QRegExp这些类在 Qt 6 里要么移除要么改名,直接编译会报一堆错。

2.2 工程目录的典型分层

一份结构正常的 Qt C/S 图书管理系统,目录大致长这样:

目录/文件作用
BookManage.pro工程入口,定义模块、源文件、头文件
main.cpp程序入口,通常只做QApplication和登录窗口启动
login/登录界面,含用户名密码校验
mainwindow/主界面,菜单、工具栏、表格展示
book/图书实体类、图书操作类
user/用户实体、权限管理
database/数据库连接、SQL 执行封装
network/如果走 C/S 网络模式,这里放 socket 通信

如果解压后发现所有文件都堆在根目录,没有分层,那说明这份资源是「能跑就行」的风格,后期改起来会比较痛苦。我一般会先按功能把文件挪到对应子目录,再同步改.pro里的SOURCES、HEADERS、FORMS路径。这一步别嫌麻烦,后面加功能时你会感谢自己。

2.3 .pro 文件的关键配置

.pro是 qmake 的工程描述文件,几个关键点必须确认:

QT += core gui widgets sql network greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = BookManage TEMPLATE = app # 源文件列表 SOURCES += \ main.cpp \ login/login.cpp \ mainwindow/mainwindow.cpp \ book/book.cpp \ database/dbhelper.cpp HEADERS += \ login/login.h \ mainwindow/mainwindow.h \ book/book.h \ database/dbhelper.h FORMS += \ login/login.ui \ mainwindow/mainwindow.ui

QT += sql是数据库模块,network是网络模块,这两个如果漏了,编译时会出现undefined reference to QSqlDatabase或QTcpSocket相关错误。TEMPLATE = app表示生成可执行程序,如果是库才用lib。greaterThan(QT_MAJOR_VERSION, 4): QT += widgets这行是兼容 Qt 4 和 Qt 5 的写法,Qt 5 以上必须加widgets,否则QMainWindow、QWidget都找不到。

提示:如果.pro里用了$$PWD或相对路径引用第三方库,换机器后路径很可能失效,优先改成相对工程根目录的写法。

3. 数据库连接与图书增删改查:从 QSqlDatabase 到界面刷新

3.1 数据库选型与连接封装

这类图书管理系统最常见的数据库是 SQLite 和 MySQL。SQLite 胜在零配置、单文件,适合课程设计和单机使用;MySQL 适合多客户端共享数据。资源里如果带.db文件,基本就是 SQLite;如果带.sql脚本,多半是 MySQL。

我一般会把数据库连接封装成一个单例类,避免每次操作都重新addDatabase:

// dbhelper.h #ifndef DBHELPER_H #define DBHELPER_H #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QDebug> class DBHelper { public: static DBHelper& instance(); bool connect(const QString& dbPath); QSqlDatabase& db(); void close(); private: DBHelper() {} Q_DISABLE_COPY(DBHelper) QSqlDatabase m_db; }; #endif // DBHELPER_H
// dbhelper.cpp #include "dbhelper.h" DBHelper& DBHelper::instance() { static DBHelper inst; return inst; } bool DBHelper::connect(const QString& dbPath) { if (m_db.isOpen()) { return true; } m_db = QSqlDatabase::addDatabase("QSQLITE"); m_db.setDatabaseName(dbPath); if (!m_db.open()) { qDebug() << "数据库打开失败:" << m_db.lastError().text(); return false; } return true; } QSqlDatabase& DBHelper::db() { return m_db; } void DBHelper::close() { if (m_db.isOpen()) { m_db.close(); } }

这段代码的逻辑是:用静态局部变量保证全局只有一个DBHelper实例,connect里指定驱动为QSQLITE,数据库文件路径由调用方传入。Q_DISABLE_COPY禁止拷贝构造,防止多个连接对象互相干扰。参数dbPath一般写成QApplication::applicationDirPath() + "/book.db",这样打包发布后数据库文件跟着 exe 走,不会因为工作目录变化而找不到。

3.2 图书表的建表与基础操作

图书管理系统的核心表通常包括book、user、borrow_record三张。建表语句可以在程序启动时执行,也可以单独用 SQL 脚本初始化:

CREATE TABLE IF NOT EXISTS book ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL, author TEXT, publisher TEXT, total_count INTEGER DEFAULT 1, available_count INTEGER DEFAULT 1 ); CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role TEXT DEFAULT 'reader' ); CREATE TABLE IF NOT EXISTS borrow_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER, user_id INTEGER, borrow_date TEXT, return_date TEXT, status TEXT DEFAULT 'borrowed' );

book表里total_count和available_count分开存,是为了处理同一本书多本副本的情况。isbn加UNIQUE约束,防止重复录入。borrow_record用status字段标记借出还是归还,比直接删记录更利于追溯。

对应的插入和查询代码:

// 添加图书 bool addBook(const QString& isbn, const QString& title, const QString& author, const QString& publisher) { QSqlQuery query(DBHelper::instance().db()); query.prepare("INSERT INTO book (isbn, title, author, publisher) " "VALUES (:isbn, :title, :author, :publisher)"); query.bindValue(":isbn", isbn); query.bindValue(":title", title); query.bindValue(":author", author); query.bindValue(":publisher", publisher); if (!query.exec()) { qDebug() << "插入失败:" << query.lastError().text(); return false; } return true; } // 查询图书,支持按书名模糊搜索 QList<Book> searchBooks(const QString& keyword) { QList<Book> list; QSqlQuery query(DBHelper::instance().db()); query.prepare("SELECT id, isbn, title, author, publisher, " "total_count, available_count FROM book " "WHERE title LIKE :kw OR author LIKE :kw"); query.bindValue(":kw", "%" + keyword + "%"); if (!query.exec()) { qDebug() << "查询失败:" << query.lastError().text(); return list; } while (query.next()) { Book b; b.id = query.value(0).toInt(); b.isbn = query.value(1).toString(); b.title = query.value(2).toString(); b.author = query.value(3).toString(); b.publisher = query.value(4).toString(); b.totalCount = query.value(5).toInt(); b.availableCount = query.value(6).toInt(); list.append(b); } return list; }

prepare+bindValue是防 SQL 注入的标准做法,别直接用字符串拼接。LIKE :kw配合%keyword%实现模糊匹配,注意bindValue传的是"%"+keyword+"%",不是keyword本身。查询结果用while(query.next())逐行读取,字段顺序要和SELECT里一致,否则value(2)取到的可能不是你想要的列。

3.3 界面表格刷新与数据绑定

Qt 里展示图书列表,新手常用QTableWidget,直接setItem填充;数据量大时更推荐QTableView+ 自定义QAbstractTableModel。这份资源如果用的是QTableWidget,在几百条数据以内没问题,上千条就会明显卡顿。

void MainWindow::refreshBookTable(const QList<Book>& books) { ui->tableWidget->setRowCount(0); ui->tableWidget->setRowCount(books.size()); for (int i = 0; i < books.size(); ++i) { const Book& b = books.at(i); ui->tableWidget->setItem(i, 0, new QTableWidgetItem(QString::number(b.id))); ui->tableWidget->setItem(i, 1, new QTableWidgetItem(b.isbn)); ui->tableWidget->setItem(i, 2, new QTableWidgetItem(b.title)); ui->tableWidget->setItem(i, 3, new QTableWidgetItem(b.author)); ui->tableWidget->setItem(i, 4, new QTableWidgetItem(b.publisher)); ui->tableWidget->setItem(i, 5, new QTableWidgetItem(QString::number(b.availableCount))); } }

先setRowCount(0)清空再重新设置行数,比逐行删除快。每列对应一个QTableWidgetItem,数字字段记得QString::number转一下。如果表格列头在.ui里已经设好,这里只需要填数据。刷新时机一般放在增删改操作成功之后,或者搜索按钮的槽函数里。

注意:QTableWidget每次setItem都会触发视图更新,数据量大时建议先setUpdatesEnabled(false),填完再setUpdatesEnabled(true)。

4. 登录、权限与借阅逻辑:业务层怎么串起来

4.1 登录校验与密码存储

登录窗口通常是程序启动后的第一个界面,校验通过再show()主窗口。密码存储如果明文写在数据库里,属于典型的安全隐患,但课程设计里很常见。稍微规范一点的做法是存 MD5 或 SHA256 哈希:

#include <QCryptographicHash> QString hashPassword(const QString& plain) { QByteArray ba = plain.toUtf8(); QByteArray hash = QCryptographicHash::hash(ba, QCryptographicHash::Sha256); return hash.toHex(); } bool checkLogin(const QString& username, const QString& password) { QSqlQuery query(DBHelper::instance().db()); query.prepare("SELECT password, role FROM user WHERE username = :u"); query.bindValue(":u", username); if (!query.exec() || !query.next()) { return false; } QString storedHash = query.value(0).toString(); QString inputHash = hashPassword(password); if (storedHash != inputHash) { return false; } // 可以把 role 存到全局变量或主窗口成员里 return true; }

QCryptographicHash::hash第二个参数指定算法,Sha256比Md5更稳。toHex()把二进制哈希转成十六进制字符串存库。登录成功后,role字段决定主界面里哪些按钮可用,比如reader只能查和借,admin才能增删改。

4.2 借书与还书的库存联动

借书操作不是简单插一条borrow_record就完事,还要扣减available_count,并且要防止并发下超借。SQLite 单机场景可以用事务包起来:

bool borrowBook(int bookId, int userId) { QSqlDatabase db = DBHelper::instance().db(); db.transaction(); QSqlQuery query(db); // 先检查库存 query.prepare("SELECT available_count FROM book WHERE id = :id"); query.bindValue(":id", bookId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int available = query.value(0).toInt(); if (available <= 0) { db.rollback(); return false; } // 扣库存 query.prepare("UPDATE book SET available_count = available_count - 1 WHERE id = :id"); query.bindValue(":id", bookId); if (!query.exec()) { db.rollback(); return false; } // 插借阅记录 query.prepare("INSERT INTO borrow_record (book_id, user_id, borrow_date, status) " "VALUES (:bid, :uid, :date, 'borrowed')"); query.bindValue(":bid", bookId); query.bindValue(":uid", userId); query.bindValue(":date", QDate::currentDate().toString("yyyy-MM-dd")); if (!query.exec()) { db.rollback(); return false; } db.commit(); return true; }

db.transaction()开启事务,任何一步失败就rollback(),全部成功才commit()。库存检查放在扣减之前,避免负数。还书逻辑反过来:available_count + 1,同时把borrow_record的status改成returned,并填上return_date。

4.3 主界面菜单与权限控制

主窗口一般用QMenuBar+QToolBar+QStackedWidget或QTabWidget组织功能页。权限控制可以在MainWindow构造函数里根据登录返回的role决定哪些QAction可见:

void MainWindow::applyRole(const QString& role) { bool isAdmin = (role == "admin"); ui->actionAddBook->setVisible(isAdmin); ui->actionDeleteBook->setVisible(isAdmin); ui->actionEditBook->setVisible(isAdmin); ui->actionBorrow->setVisible(true); ui->actionReturn->setVisible(true); }

setVisible(false)只是隐藏,快捷键如果绑定了还是可能触发,更严格的做法是setEnabled(false)或者直接在槽函数里再判一次权限。别只靠界面隐藏做安全,业务层也要校验。

提示:借阅记录表建议加索引CREATE INDEX idx_borrow_user ON borrow_record(user_id);,查某用户借阅历史时快很多。

5. 避坑与排查:编译、运行、数据三层常见问题

5.1 编译报错 dependent 路径找不到

现象:Qt Creator 编译时提示:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...' does not exist。

原因:.pro或.pro.user里残留了原作者的 Qt 安装绝对路径,或者你当前 Kit 的 Qt 版本和工程不匹配。

解决:删掉.pro.user,在 Qt Creator 里重新配置 Kit;检查.pro里有没有INCLUDEPATH +=写死的外部路径,改成相对路径或直接删掉;确认QT +=模块和当前 Qt 版本一致。

5.2 数据库驱动加载失败

现象:程序运行后提示QSqlDatabase: QSQLITE driver not loaded或QMYSQL driver not loaded。

原因:Qt 安装时没有勾选对应的数据库驱动插件,或者发布时sqldrivers目录没拷全。

解决:SQLite 驱动一般在Qt/版本/编译器/plugins/sqldrivers/qsqlite.dll,确认这个文件存在;MySQL 需要额外编译驱动,或者改用 SQLite 先跑通。发布时把plugins/sqldrivers整个目录拷到 exe 同级。

5.3 中文乱码

现象:界面显示的中文变成问号或方块,数据库里读出来的也是乱码。

原因:源文件编码、Qt 字符串编码、数据库编码三者不一致。Qt 5 默认 UTF-8,但 MSVC 编译器可能按 GBK 解析源文件。

解决:在.pro里加QMAKE_CXXFLAGS += /utf-8(MSVC)或QMAKE_CXXFLAGS += -finput-charset=UTF-8(GCC);数据库连接后执行PRAGMA encoding = "UTF-8";;main.cpp里 Qt 5 可以设QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));。

5.4 表格数据量大时界面卡死

现象:图书上千条时,切换页面或搜索后界面卡住几秒。

原因:QTableWidget逐格setItem触发大量重绘,且没有关闭更新。

解决:改用QTableView+QAbstractTableModel,只提供数据接口,视图按需渲染;或者临时setUpdatesEnabled(false),填完再开。搜索时加LIMIT分页,别一次查全表。

5.5 发布后缺少 DLL 无法运行

现象:在自己电脑上能跑,拷到别人电脑提示缺少Qt5Core.dll、Qt5Gui.dll等。

原因:没有把 Qt 运行时库和平台插件一起打包。

解决:用windeployqt(Windows)或macdeployqt(macOS)自动拷贝依赖:

# Windows 下在 Qt 命令行环境执行 windeployqt BookManage.exe # 确认 platforms/qwindows.dll 存在

windeployqt会把Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、Qt5Sql.dll以及platforms、sqldrivers等插件目录拷到 exe 旁边。如果用了 MySQL,还要手动补libmysql.dll。

6. 进阶改造:把 QTableWidget 换成 QTableView + 自定义 Model

如果你打算在这份资源基础上继续加功能,比如支持几千条图书、按列排序、分页加载,那QTableWidget迟早会成为瓶颈。我一般会直接换成QTableView+ 自定义QAbstractTableModel,这是 Qt 里处理表格数据的标准姿势,也是这份资源最值得动手改的一处。

先定义一个BookModel:

// bookmodel.h #ifndef BOOKMODEL_H #define BOOKMODEL_H #include <QAbstractTableModel> #include <QList> #include "book.h" class BookModel : public QAbstractTableModel { Q_OBJECT public: explicit BookModel(QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void setBooks(const QList<Book>& books); Book bookAt(int row) const; private: QList<Book> m_books; QStringList m_headers; }; #endif // BOOKMODEL_H
// bookmodel.cpp #include "bookmodel.h" BookModel::BookModel(QObject* parent) : QAbstractTableModel(parent) { m_headers << "ID" << "ISBN" << "书名" << "作者" << "出版社" << "可借"; } int BookModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_books.size(); } int BookModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_headers.size(); } QVariant BookModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() >= m_books.size()) return QVariant(); const Book& b = m_books.at(index.row()); if (role == Qt::DisplayRole) { switch (index.column()) { case 0: return b.id; case 1: return b.isbn; case 2: return b.title; case 3: return b.author; case 4: return b.publisher; case 5: return b.availableCount; default: return QVariant(); } } if (role == Qt::TextAlignmentRole) { return Qt::AlignCenter; } return QVariant(); } QVariant BookModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) { return m_headers.value(section); } return section + 1; } void BookModel::setBooks(const QList<Book>& books) { beginResetModel(); m_books = books; endResetModel(); } Book BookModel::bookAt(int row) const { if (row < 0 || row >= m_books.size()) return Book(); return m_books.at(row); }

beginResetModel()和endResetModel()包住数据替换,通知视图整体刷新。data里按index.column()返回对应字段,Qt::TextAlignmentRole让内容居中。headerData提供列头文字。这个 Model 不持有数据库连接,只负责把QList<Book>暴露给视图,职责单一,后面加分页、排序都从这里扩展。

在MainWindow里绑定:

m_model = new BookModel(this); ui->tableView->setModel(m_model); ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); ui->tableView->setAlternatingRowColors(true); ui->tableView->horizontalHeader()->setStretchLastSection(true);

搜索或刷新时调用m_model->setBooks(searchBooks(keyword));即可。取当前选中行用ui->tableView->currentIndex().row(),再通过m_model->bookAt(row)拿到完整对象。

改完这一步,最直观的感受是:一千条数据滚动不再卡顿,列宽拖动、排序、隐藏列这些QTableView自带能力直接可用。我自己的习惯是,凡是表格行数可能超过 500 的 Qt 项目,一律从QTableView起步,不给自己留返工的后悔药。从那以后我每次拿到类似1.zip这种资源,都会先看表格用的是什么控件,如果是QTableWidget且业务有增长预期,第一件事就是把它换掉。希望这份拆解能帮到你,少走几个我当年踩过的弯路。

本文还有配套的精品资源,点击获取

返回列表