Qt C++ MySQL学生管理系统:从源码解析到功能实现 简介这是一份基于C与Qt框架、以MySQL为后台数据库的学生信息管理系统完整工程源码适合课程设计、毕业设计及QtMySQL入门学习者使用。系统覆盖登录认证、学生信息增删改查、课程成绩、宿舍、奖助及缴费管理等模块可对照界面布局与业务代码理解典型桌面应用的分层实现。资源共79个文件包含26个cpp、25个h、23个ui另含pro工程文件、sql建库脚本、docx使用说明与README文档压缩包仅211KB。cpp/h承载业务逻辑与类定义ui文件为Qt Designer界面模板sql脚本用于初始化数据库整体目录按管理功能划分便于按模块研读。项目在Qt环境中导入pro文件即可构建结合使用说明文档可快速跑通登录、查询与维护流程不仅适合作为课设/毕设直接提交也为后续扩展和二次开发提供了清晰参考。目前已有312人学习下载受到同类项目学习者的关注。1. 拿到这个 Qt 学生管理系统压缩包先别编译看清它值不值得改每年这个时候都能看到有人在求助帖里贴出一张编译失败的截图配文是“下载了 C Qt 基于 MySQL 学生信息管理系统.zip打开就报错”。这个标题指向的是一套非常典型的学生信息管理系统源码C 写业务逻辑Qt 搭界面MySQL 存数据。它解决的需求也很直白——需要一套带图形界面的学生档案增删改查工具或者作为课程设计、毕设、公司内部小工具的原型。判断一个源码包值不值得留别看代码量看三件事有没有分层、SQL 是不是参数化、数据库连接是不是可配置。这三样全占值得花两天吃透一样不占也别急着删当反面教材对照着改进步反而更快。下面按“先读工程结构再建库建表然后功能落地最后排坑进阶”的顺序把这条完整链路讲透。文中涉及的具体源码以最常见的工程实现路径为准你手上那份压缩包解压后可能略有差异但只要沿着“工程文件 → 数据库 → 功能代码 → 运行排错”这条线走绝大多数问题都能定位到具体环节。2. 拆解 .zip 里的 C/Qt 骨架工程文件、界面与数据层各管什么解压后先别急着双击 .pro先把目录结构和构建文件读明白。这一章解决的是“这个包是怎么组织起来的”和“我改哪里才能不把别的地方改坏”两个问题。常见的课程设计包通常有三层文件构建描述文件.pro / .vcxproj、界面描述文件.ui、源码文件.h/.cpp。搞清这三类文件的职责再动手改任何一行逻辑。2.1 先读 .pro 与目录结构QT sql 是哪一行决定了技术栈QMake 工程的核心是 .pro 文件它决定了编译器怎么看待这个项目。很多第一次接触 Qt 的人直接双击 .pro 让 Qt Creator 加载随后编译报错“找不到 QSqlDatabase”根源往往是 .pro 里少了 sql 模块。一个典型的学生信息管理系统 .pro 大概长这样QT core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET StudentManager TEMPLATE app CONFIG c17 SOURCES \ main.cpp \ MainWindow.cpp \ StudentDao.cpp \ LoginDialog.cpp HEADERS \ MainWindow.h \ StudentDao.h \ LoginDialog.h FORMS \ MainWindow.ui \ LoginDialog.ui这段配置里最关键的其实是第一行QT core gui sql。core和gui是 Qt 基础模块几乎每个界面程序都要sql才是连接 MySQL 的开关。少写sql程序里所有 QSqlDatabase、QSqlQuery 符号都会直接编译失败而且报错位置可能在几十处容易让人误以为是代码本身有问题。CONFIG c17决定编译器按 C17 标准处理源码涉及 lambda 表达式和智能指针的代码对标准版本敏感。如果压缩包里是 .sln / .vcxproj 而不是 .pro说明作者用的是 Visual Studio Qt VS Tools对应的操作是在 Qt Project Settings 里勾选 Qt Modules把 Sql 和 Widgets 勾上。逻辑一样只是配置入口不同。2.2 UI 与逻辑分离的两种写法.ui 文件与纯代码构建拿到源码后你会发现界面实现通常分两种风格。第一种是用 Qt Designer 画好 .ui 文件代码里通过ui-setupUi(this)加载第二种是纯代码手写控件用new QLineEdit搭配布局器拼界面。.ui 方案更适合后期维护界面改布局不用动 C 代码纯代码方案适合动态生成表格行、批量创建输入框的场景但代码量会明显变大。最常见的 .ui 初始化代码是这个模式// MainWindow.cpp 构造函数中的典型写法 #include MainWindow.h #include ui_MainWindow.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); // 界面生成后把按钮点击信号接到自定义槽函数 connect(ui-btnQuery, QPushButton::clicked, this, MainWindow::onQueryStudents); connect(ui-btnAdd, QPushButton::clicked, this, MainWindow::onAddStudent); } MainWindow::~MainWindow() { delete ui; }ui_MainWindow.h是由 .ui 文件通过 uic 工具自动生成的不要手动改它。connect 这里用的是函数指针语法编译期会检查信号和槽是否存在比起老式connect(ui-btnQuery, SIGNAL(clicked()), this, SLOT(onQueryStudents()))更安全写错槽函数名会直接编译报错而不是运行没反应。btnQuery和btnAdd是你在 .ui 文件里给按钮设置的 objectName这个名称和代码不一致时编译期不报错运行期点击毫无反应排查时要先确认这一层。2.3 分层约定的价值界面、业务、数据访问三条线分开很多课程设计包的通病是 MainWindow 里既写界面逻辑又写 SQL一个窗口文件上千行看着热闹改起来心惊胆战。我一般会把数据库操作单独拆一个 DAO 层界面只调方法不直接面对 SQL。好处很直接改表结构时只动 DAO换数据库时也只动 DAO界面层完全不受影响。// StudentDao.h —— 数据访问层头文件 #ifndef STUDENTDAO_H #define STUDENTDAO_H #include QVector #include Student.h class StudentDao { public: QVectorStudent queryAll(); bool insert(const Student s); bool update(const Student s); bool removeById(int id); }; #endif // STUDENTDAO_H这套接口暴露的是业务对象Student不是 QSqlQuery调用方完全感知不到底层是 MySQL 还是 SQLite。QVectorStudent返回值是值语义即便 DAO 内部先拿到 QSqlQuery 再逐行填充最终交到界面层的数据也是干净的副本。界面层拿到列表后只需要负责往表格控件里塞数据这层划分在后续做功能扩展时会省下大量时间。3. 在 MySQL 上把表建对并接通字段选型、连接串与字符集系统能不能跑起来数据库这一环占一半。见过的翻车现场里有一大半不是代码逻辑问题而是表结构设计不合理、连接驱动缺失、字符集不统一。这一章把建表、连接、字符集三件事拆开讲每一步都给出可以直接复制的命令和代码照着做至少能保证“本机跑通”。3.1 一张能落地的学生表字段选型会决定后续查询的写法先建库建表SQL 本身不复杂但字段类型的选择会在后面不断反噬。比如学号字段用 INT存00123就会变成123性别用 ENUM以后想加一个“未知”就要改表结构。下面这张表是实践中踩过坑之后调整出来的稳一点的设计CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE IF NOT EXISTS student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键, student_no VARCHAR(20) NOT NULL COMMENT 学号保留前导零, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1男 0女, birth_date DATE NULL COMMENT 出生日期, class_name VARCHAR(50) NOT NULL COMMENT 班级, phone VARCHAR(20) NULL COMMENT 联系方式, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除位 0未删 1已删, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB COMMENT学生信息表;student_no用 VARCHAR 而不是 INT是为了保住前导零同时给未来可能出现的字母后缀留余地。deleted字段是逻辑删除位删除操作走 UPDATE 而不是 DELETE误删还能恢复相当于给自己留了后悔药。gender用 TINYINT 比 ENUM 好扩展后面想加值只需改注释不用 ALTER TABLE。建库时DEFAULT CHARACTER SET utf8mb4必须定好否则连接层设置字符集只能管当前连接表结构的默认字符集不对写进去的中文照样乱。字段选型这里最容易犯的错是图省事把所有字段都塞成 VARCHAR。下面这张对比表可以对照着判断自己的设计字段场景推荐的类型不推荐原因学号 / 工号VARCHAR(20)INT / BIGINT前导零会丢扩展性差出生日期 / 入学日期DATEVARCHAR可排序、可做日期函数计算性别 / 状态TINYINTENUM改枚举值不用动表结构删除标记TINYINT直接物理删除误删可恢复统计可过滤如果已经是旧表且学号变成 INT 存坏了补救语句是ALTER TABLE student MODIFY student_no VARCHAR(20) NOT NULL;再用 LPAD 把位数补回来注意补位宽度按实际学号规则来。3.2 Qt 连接 MySQL 的最小连接串与驱动检查建好表之后Qt 侧需要一小段连接代码。最小可运行版本如下#include QSqlDatabase #include QSqlError #include QDebug bool openMysql(QString err) { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(student_db); db.setUserName(root); db.setPassword(your_password); if (!db.open()) { err db.lastError().text(); return false; } // 打开后立刻统一连接字符集 QSqlQuery query(db); query.exec(SET NAMES utf8mb4); return true; }这里的QMYSQL是 Qt SQL 驱动的插件名不是数据库实例名。setHostName填 MySQL 所在机器地址本机开发用 127.0.0.1 最省事setPort默认 3306如果本机装了多个 MySQL 实例端口不同就必须显式指定。setDatabaseName对应的是 3.1 节创建的student_db写成别的名字 open 会失败。lastError()是排错的第一入口连接失败时不要靠猜先把它打印出来看具体原因。驱动缺失是另一类高频问题。Qt 官方安装包经常不自带 QMYSQL 驱动。用一行代码就能确认qDebug() QSqlDatabase::drivers();如果输出里有QMYSQL说明驱动插件存在问题在连接参数或客户端库如果只有QSQLITE说明驱动压根没装。缺驱动时有两个走向一是去 Qt 源码里编译 mysql 驱动插件需要本机有 MySQL 客户端开发库且 Qt 与客户端库位数一致二是先改用 SQLite 把功能逻辑跑通最后再切回 MySQL。对课程设计来说先 SQLite 后 MySQL 是成本最低的路线。注意驱动插件、Qt 运行库、MySQL 客户端库三者位数不一致时open 会持续报 driver not loaded这类问题重装软件不如检查位数来得快。3.3 连接管理两种姿势全局单例还是局部获取连接管理是第二个高频雷区。很多源码在每一个按钮的槽函数里都写一次addDatabase(QMYSQL)第二次运行就会报duplicate connection name。原因是addDatabase默认使用同一个连接名重复注册会覆盖旧连接并触发 Qt 警告严重时直接崩溃。我一般把连接注册放在程序启动阶段全局只注册一次后续统一用QSqlDatabase::database()获取默认连接QSqlDatabase db QSqlDatabase::database(); // 获取默认连接 if (!db.isOpen()) { qWarning() 数据库连接已断开; return; } QSqlQuery query(db);这段代码的好处是不再重复创建连接也不用手动管理引用计数。QSqlDatabase::database()返回默认连接内部引用计数自动维护程序退出时 Qt 会统一清理。如果工程里多个模块需要不同的数据库addDatabase(QMYSQL, main_conn)这种命名连接方式更合适之后每次取用都带这个名字避免模块之间互相抢连接。还要注意连接对象不能跨线程使用。后台线程如果要做耗时查询不要直接拿主线程的连接对象正确做法是在子线程内部单独addDatabase再关闭。很多管理系统在导出大批量数据时崩溃就是因为主线程连接被子线程抢用。4. 把学生信息管理的硬功能写透登录鉴权、增删改查与分组统计数据库通了之后真正体现工程能力的其实是业务功能代码登录、查询、维护、统计。这一章给的都是可以直接抄进工程的最小实现同时也说明每个关键参数为什么这么设。新手照着做能跑通熟手能看出哪些写法为后续扩展留了余地。4.1 登录与鉴权密码别用明文给一个可运行的加盐哈希课程设计包里最常见的登录写法是直接把密码明文存数据库登录时SELECT * FROM admin WHERE password 输入的密码。这在单机演示环境确实能跑但稍微有点工程意识的团队都不会接受。给一个改进版密码字段存的是加盐哈希不是明文// 登录校验核心逻辑 QString salt 每位用户注册时生成的随机盐; QString inputHash QString(QCryptographicHash::hash( (salt password).toUtf8(), QCryptographicHash::Sha256).toHex()); QSqlQuery query(db); query.prepare(SELECT id FROM admin_user WHERE account ? AND password_hash ?); query.addBindValue(account); query.addBindValue(inputHash); if (query.exec() query.next()) { qDebug() 登录成功, uid query.value(0).toInt(); } else { // 统一提示“账号或密码错误”不要区分到底哪个错 }把盐和密码拼在一起做哈希能避免两个相同密码的用户产生相同哈希值盐一般是在创建账号时随机生成并单独存在用户表里。SHA-256 是 Qt 内置支持的标准算法对课程设计来说够用生产环境则建议换成更慢的哈希算法来对抗暴力破解。这里需要明确一点它只是比明文强不是万能方案。登录失败时的提示也值得注意。不要返回“用户不存在”或“密码错误”两种信息统一提示“账号或密码错误”避免攻击者通过提示信息枚举有效账号。演示系统虽然威胁模型低但接口设计习惯可以早点建立。4.2 增删改SQL 用 prepare bindValue别拿字符串拼新增学生是管理系统最核心的操作也是 SQL 注入翻车现场的重灾区。学生姓名里如果带单引号比如OBrien用字符串拼接 SQL 时要么报错要么被截断。正确的写法是参数绑定QSqlQuery query(db); query.prepare(INSERT INTO student (student_no, name, gender, birth_date, class_name, phone) VALUES (?, ?, ?, ?, ?, ?)); query.addBindValue(ui-edtNo-text().trimmed()); query.addBindValue(ui-edtName-text().trimmed()); query.addBindValue(ui-cbGender-currentIndex()); query.addBindValue(ui-dateBirth-date().toString(yyyy-MM-dd)); query.addBindValue(ui-edtClass-text().trimmed()); query.addBindValue(ui-edtPhone-text().trimmed()); if (!query.exec()) { QMessageBox::warning(this, 错误, query.lastError().text()); return; }?是位置占位符值由驱动层做转义处理安全性远好于手动拼字符串。addBindValue按顺序对应省事但可读性略差如果 SQL 里的字段很多建议改成bindValue(:name, ...)这种命名占位符后续维护时不用数问号。trimmed()是给手输界面的防御用户不小心输入前后空格时不会写进数据库。日期转成yyyy-MM-dd是因为 MySQL 的 DATE 类型默认识别这个格式不要在业务层做多余时区转换。修改和删除本质上是同一套思路。编辑时需要先从表格选中行拿到主键 id再执行 UPDATE删除则建议走逻辑删除// 逻辑删除把 deleted 置 1而不是物理 DELETE query.prepare(UPDATE student SET deleted 1 WHERE id ?); query.addBindValue(currId); query.exec();这里有个容易忽略的点列表查询必须带WHERE deleted 0过滤条件否则删除过的学生还会出现在界面上。逻辑删除牺牲了一点查询性能换来的是一次点击就能恢复误删数据的能力。对管理系统这类以人工操作为主的场景这个交换非常划算。4.3 模糊查询与分组统计动态条件怎么拼才不出错查询模块通常支持按姓名、学号、班级三个条件组合筛选条件可能为空也可能多个同时生效。动态拼 SQL 时最容易把用户输入直接拼进字符串正确做法是拼条件结构、绑定用户值QString sql SELECT id, student_no, name, gender, class_name, phone FROM student WHERE deleted 0; QSqlQuery query(db); if (!name.isEmpty()) { sql AND name LIKE ?; query.addBindValue(% name %); } if (!no.isEmpty()) { sql AND student_no LIKE ?; query.addBindValue(% no %); } query.prepare(sql); query.exec();这段代码的边界在于拼接的部分是AND name LIKE ?这样的条件结构来自代码分支而不是用户输入用户输入的内容全部通过addBindValue进入查询。LIKE里的%位置决定匹配方式姓名用%xx%包含匹配最符合直觉学号用xx%前缀匹配可以利用索引效率更高。三个条件全为空时SQL 退化成纯列表查询逻辑依然成立。统计功能同样走聚合查询按班级统计人数时把结果列表展示出来即可SELECT class_name, COUNT(*) AS cnt FROM student WHERE deleted 0 GROUP BY class_name ORDER BY cnt DESC;注意分组统计的结果不要和明细列表共用一个表格模型一个是多列详情展示一个是两列分组聚合混在一起界面会很乱。常见做法是单独用一个 QTableWidget 承载统计结果。如果演示时班级很多加LIMIT 10只展示人数最多的前十个班界面更有重点。5. 高频踩坑与排查驱动加载失败、中文乱码、表格不刷新这个章节写给两类人一类是压缩包解压后一直编译不过、运行崩溃的另一类是程序能跑但数据不对、界面不对的。每个坑都按“现象 → 原因 → 解决”的顺序写你可以直接对照自己的报错信息定位。踩过这些坑之后这个项目的维护成本会明显降下来。5.1 现象连接 MySQL 报 driver not loaded或者认证插件无法加载最常见的是 open 一直返回 false错误信息是driver not loaded或者 MySQL 8 环境里报Authentication plugin caching_sha2_password cannot be loaded。原因有三档按顺序排查Qt 安装包里确实没有 QMYSQL 驱动插件驱动插件存在但 Qt 与 MySQL 客户端库位数不一致MySQL 用户认证插件与驱动支持的插件列表不匹配。先运行qDebug() QSqlDatabase::drivers();看驱动列表。如果列表里没有 QMYSQL解决方向是编译驱动插件或更换带驱动的 Qt 版本编译时需要 MySQL 客户端开发库且位数与 Qt 一致。如果列表里有 QMYSQL则是认证插件或客户端库问题可以先把 MySQL 用户改成兼容的认证方式再测。改认证方式属于临时手段生产环境更推荐升级驱动来支持新的认证插件。注意驱动插件是否加载成功和 debug/release 版本也有关换构建模式之后要重新确认驱动路径在运行目录里。5.2 现象插入的中文在 MySQL 里是问号界面里也是乱码中文乱码的原因要拆成两层看解决顺序不能反。第一层是库表字符集不是 utf8mb4第二层是源码文件本身编码不对编译时读出来的字符串就是乱码。先查表结构SHOW CREATE TABLE student;如果 CHARSET 不是 utf8mb4执行ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接层在 open 之后执行SET NAMES utf8mb4保证客户端、连接、表三端字符集一致。界面乱码则回到源码.cpp 和 .ui 文件用 UTF-8 保存MSVC 编译时追加/utf-8编译选项。在 .pro 里可以这样控制msvc { QMAKE_CXXFLAGS /utf-8 }这段配置只对 MSVC 生效MinGW 不需要加因为 MinGW 默认按 UTF-8 处理源码。解决顺序必须是“表字符集 → 连接字符集 → 源码编码”漏掉任何一层都可能残留乱码。5.3 现象执行了 INSERT表格里看不到新数据如果用 QSqlQueryModel 绑定表格插入数据后表格里没反应原因通常是模型没有重新查询。QSqlQueryModel 是查询结果的快照不会自动感知数据库变化。解决方法是操作完成后刷新模型model-clear(); model-setQuery(SELECT id, student_no, name, class_name FROM student WHERE deleted 0, db); ui-tableView-setModel(model);clear()先清掉旧结果集setQuery再拉取新数据这里的 db 必须和之前注册的连接是同一个否则模型可能自动创建默认连接在多连接工程里会让人误以为“数据没写进库”。如果用的是 QTableWidget对应的操作是先setRowCount(0)清空再逐行插入逻辑类似只是 API 不同。5.4 现象程序退出时崩溃报 QSqlDatabasePrivate 析构错误退出崩溃常见于多个对话框各持有一个 QSqlDatabase 对象的工程。原因是连接对象在局部作用域创建析构顺序失控或者同一连接名被反复addDatabaseQt 内部引用计数乱掉。解决方法是把连接管理收敛到一个入口不要在按钮槽函数里创建连接。确实需要显示移除连接时注意三个动作的顺序db.close(); QSqlDatabase::removeDatabase(main_conn);先close()再removeDatabase()而且要在所有持有该连接的 QSqlQuery 对象析构之后执行否则 Qt 会警告connection still in use。如果工程里从不主动移除连接绝大多数场景也不会崩但排查退出崩溃时第一个怀疑对象就是这里。5.5 现象Debug 模式正常Release 模式一闪而过Debug 能跑 Release 闪退几乎可以断定是运行库和依赖的 dll 没带全。Qt 的 debug 构建默认带着调试运行库路径解析相对宽松Release 版本靠自己携带依赖缺了就容易启动即崩。尤其要注意Qt 的部署工具不会自动带上 MySQL 驱动插件需要手动拷贝qsqlmysql.dll到 plugins/sqldrivers 目录。部署时用 Qt 提供的部署工具处理主程序依赖然后手动补充 MySQL 客户端库和 SQL 驱动插件。常见的命令形态是先把部署工具指向 Release 下的 exe再把 MySQL 的客户端 dll 复制到 exe 同目录。如果数据库部署在远程机器本地开发机不需要 MySQL 服务端但客户端库仍然要有因为驱动插件运行时依赖它。5.6 现象带 0 开头的学号存进去就少一位这个坑基本没有报错提示数据悄悄就错了。原因几乎都是建表时把学号字段设成了 INT00123存进去自动变成123。修复分两步先改字段类型再补回前导零ALTER TABLE student MODIFY student_no VARCHAR(20) NOT NULL; UPDATE student SET student_no LPAD(student_no, 5, 0);LPAD的第二参数是最终长度需要按真实学号规则来有的学校学号 8 位有的 10 位写错了会截断或补错。这个坑最坑的地方在于它不影响程序运行也不报任何错误只有对账或导出时才发现。解决之后还要在程序入口加一个校验学号输入框只允许数字和字母避免用户手滑再次写入畸形数据。6. 从课设到可交付给系统加一套 DAO 层与可视化统计如果时间只够做两件事建议把 DAO 封装和班级统计图表加上。前者让后续改需求时不心惊胆战后者让演示效果上一个台阶。这两件事做完这套代码的气质就和普通课程设计拉开差距了。6.1 把 DAO 补齐成抽象接口第 2 章给出的 StudentDao 只有声明落地时每个函数在 .cpp 里完成。以查询为例实现要把 SQL 完全收进 DAO界面层只拿到学生对象列表// StudentDao.cpp 查询实现摘要 QVectorStudent StudentDao::queryAll() { QVectorStudent list; QSqlQuery query(QSqlDatabase::database()); query.prepare(SELECT id, student_no, name, class_name FROM student WHERE deleted 0 ORDER BY id DESC); if (!query.exec()) { qWarning() query.lastError().text(); return list; } while (query.next()) { Student s; s.id query.value(0).toInt(); s.studentNo query.value(1).toString(); s.name query.value(2).toString(); s.className query.value(3).toString(); list.append(s); } return list; }ORDER BY id DESC让新录入的学生排在最前面演示时输入完数据立刻能看到变化体验比按 id 升序好很多。有了这一层MainWindow 里就不会再出现 SQL 字串后续把 MySQL 换成 SQLite界面代码一行都不用动这个收益会在你改数据库时体会得非常深。6.2 用 Qt Charts 画班级人数柱状图在 .pro 里追加一行QT charts然后就可以在统计页用三行核心代码出图QBarSet *set new QBarSet(人数); for (const auto item : statList) { *set item.count; } QBarSeries *series new QBarSeries; series-append(set); QChart *chart new QChart; chart-addSeries(series); chart-createDefaultAxes();statList来自第 4.3 节的聚合查询结果每个元素包含班级名和人数。createDefaultAxes()会自动生成坐标轴但 X 轴刻度需要额外把班级名列表绑定到 QBarCategoryAxis否则柱子上不会显示班级标签。图表放进 QChartView 后再嵌到 QTabWidget和明细列表分页展示比挤在同一页清楚得多。班级超过 20 个时建议只看前十个柱状图太密反而没有演示效果。6.3 给新人的三条手感和一个习惯拿到陌生工程先跑通再改不要把编译当“边改边猜”改任何 SQL 之前先把表结构和数据备份一份防止把开发数据弄丢复制别人代码时数据库地址、账号密码一定改成自己的保留原作者配置还提交到公共仓库是事故隐患。我自己的习惯是每个子系统收尾时压缩成一个能自测的最小工程数据库脚本、部署说明、源码三层放同一目录拿到任何一台干净机器按说明走一遍能跑才算真正交付。你把这个学生管理系统按这个标准收尾它就不再是解压后吃灰的压缩包而是一个能写进简历的完整作品。希望帮到你。本文还有配套的精品资源点击获取