简介:这是一套基于Qt框架开发的学生信息管理系统完整工程,面向计算机专业学生、毕业设计及期末大作业场景,分为教师端与学生端:教师端支持学生信息增删查改、班级成绩分析,学生端可登录查看个人成绩,并实现了登录验证与角色权限区分,功能覆盖高校教务管理的常见需求。压缩包共70个文件、约5.85MB,以cpp/h源文件、ui界面设计文件、pro工程配置为主,另含qrc资源、SQLite数据库文件、release/debug构建目录及可直接运行的exe,便于对照学习界面布局、信号槽机制与SQLite数据操作。目前已有414人学习下载。源码模块按教师端、学生端、班级管理等模块分类组织,整体结构直观,便于二次开发,附带说明文档,适合作为Qt/C++课程设计模板或毕业设计基础,也可据此扩展登录权限、成绩导出等功能,也适合Qt初学者逐步调试。
1. 为什么“基于Qt的学生信息管理系统”值得照着做一遍
每个学期末都有人问同一个问题:想找一个能把C++、数据库、图形界面一次串起来的项目。基于Qt的学生信息管理系统就是这类问题里最常见的答案——教师端管增删查改和班级成绩分析,学生端只做成绩查看,一个压缩包就把角色权限、数据操作、统计图表全包圆了。它解决的不只是“交一份课程设计”,而是把课本里的单文件Demo推进到真实项目形态:界面层、数据层、逻辑层怎么分,登录后按角色跳转,统计结果落到图表上。适合刚学完C++语法、想用Qt补上完整项目经验的开发者,也适合拿到压缩包后想快速看懂再改成自己课题的从业者。
2. 先从数据表动手:教师端和学生端如何共用一套库
拿到这类压缩包,先别急着点运行。第一步打开.pro文件,确认QT +=那行里有没有sql和charts;第二步打开main.cpp,看数据库连接是在哪初始化的。常见做法是在main.cpp里先初始化数据库,再创建登录窗口,登录成功后按角色字段分发到教师主窗口或学生主窗口。这套流程跟界面美观程度无关,但决定了后面所有功能能不能在一个统一的数据层上跑起来。
2.1 双角色模型拆解:教师端能做什么,学生端只能看什么
教师端登录后进入主窗口,拥有学生档案的增删查改和班级成绩分析两个大模块;学生端登录后只看到本人成绩列表,没有任何修改入口。实现时两个窗口不共享功能代码,而是通过t_user表的role字段做分发:0 开教师窗口,1 开学生窗口。
这里有一个典型翻车点:有人为了省事,只用一个主窗口加按钮隐藏来模拟权限隔离,答辩时被老师点开调试器问一句“把按钮取消隐藏是不是就能改了”,场面立刻失控。权限隔离不是藏界面,而是在数据访问层就区分开——教师端代码里出现写操作 SQL,学生端整个模块压根不包含任何写语句。我见过某开发者的改版,学生端确实只写了查询,这才是正确的边界。
除此之外,账号设计上要注意:教师账号不需要绑定学号,学生账号必须关联到t_student.student_id,否则学生端登录后不知道该查谁的成绩。可以把t_user里加一个可空的student_id字段,教师账号填空,学生账号填学号。
2.2 三张表和一段连接代码:SQLite让整个系统单文件分发
选 SQLite 而不是 MySQL,理由很直接:Qt 自带QSQLITE驱动,不需要装数据库服务,数据库就是一个文件,拷走整个项目目录就能演示。对这类双端本地系统来说,这是最低成本的方案。建表语句我一般拆成三张表,职责边界清楚:
-- 登录与角色表:role 0=教师, 1=学生 CREATE TABLE IF NOT EXISTS t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 0, student_id TEXT ); -- 学生档案表 CREATE TABLE IF NOT EXISTS t_student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT NOT NULL, gender TEXT, phone TEXT ); -- 成绩表:考试类型区分期中/期末 CREATE TABLE IF NOT EXISTS t_score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course_name TEXT NOT NULL, score REAL NOT NULL, exam_type TEXT DEFAULT '期末' );t_user.role用整数不用字符串,查询快且不容易写错,注释留在建表 SQL 里就够。t_score里没有冗余班级字段,班级信息从t_student取,避免同一份数据存两处——很多改版越改越乱,就是因为在成绩表里又加了一个class_name,学生转班后两处不一致。
数据库连接代码一般在main.cpp里,放在登录窗口构造之前:
#include <QSqlDatabase> #include <QSqlError> #include <QSqlQuery> QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("school.db"); if (!db.open()) { qWarning() << "open db failed:" << db.lastError().text(); return -1; }addDatabase默认使用qt_sql_default_connection这个别名,一个程序里保留这一个连接就够;后续所有窗口的QSqlQuery都会自动复用这个连接,不需要再addDatabase一次。setDatabaseName传相对路径时,程序启动目录下如果没有school.db,Qt 会自动创建空文件,但表结构不会自动建,所以最好封装一个initDatabase()函数,把上面三段CREATE TABLE IF NOT EXISTS按顺序执行一遍,保证首次运行不会报no such table。写不写这个函数,直接决定了压缩包在别人电脑上能不能一把跑起来。
3. 教师端增删查改:Model/View 与 SQL 的完整落地
增删查改是教师端的骨架。很多压缩包里这部分的写法是QTableWidget加clearContents()再逐行new QTableWidgetItem,功能没问题,但数据量一上来刷新就闪烁。我一般用QTableView配合QStandardItemModel,代码量多几行,换来的是 Model 层面的整体替换,以及后续成绩分析图表对同一份数据的复用。
3.1 两种表格方案怎么选
| 方案 | 数据存放方式 | 刷新方式 | 适合场景 |
|---|---|---|---|
| QTableWidget | 单元格直接持有数据 | clearContents 后重建 | 行数少、只做一次性展示 |
| QTableView + QStandardItemModel | Model 与视图分离 | 清空 Model 后整体 setItem | 频繁增删改、数据需要复用 |
几百人的班级,两种方案都跑得动。选型主要看两点:一是后续成绩分析要不要复用同一份学生数据,Model 方案可以直接拿model指针继续干活;二是表格刷新频率高不高,Model 方案整批替换数据不会引起界面闪烁。这两条这个系统都占,所以我选 Model/View。如果你完全不熟 Model/View,直接用QTableWidget也完全能交差,代码量还更少,只是第 4 章做图表时要把数据重新从表格里抓一遍,会多绕一段路。
3.2 增删查改的完整代码:查询、插入、删除、刷新
初始化 Model 和 View 的关系,通常放在教师窗口构造函数里:
model = new QStandardItemModel(this); ui->tableView->setModel(model); refreshData();刷新函数是核心,增删查改之后都要调它:
void TeacherWindow::refreshData(const QString &keyword) { model->clear(); model->setHorizontalHeaderLabels({"学号", "姓名", "班级", "性别", "电话"}); QSqlQuery query; if (keyword.isEmpty()) { query.exec("SELECT student_id, name, class_name, gender, phone " "FROM t_student ORDER BY student_id"); } else { query.prepare("SELECT student_id, name, class_name, gender, phone " "FROM t_student WHERE student_id LIKE ? OR name LIKE ? " "ORDER BY student_id"); QString like = "%" + keyword + "%"; query.addBindValue(like); query.addBindValue(like); } int row = 0; while (query.next()) { for (int col = 0; col < 5; ++col) { QStandardItem *item = new QStandardItem(query.value(col).toString()); model->setItem(row, col, item); } row++; } }逻辑说明:关键字为空时查全表,非空时走LIKE模糊查询,学号和姓名都支持搜索。模糊串要拼成%关键词%,通配符拼在绑定参数里,不要直接拼 SQL 文本。model->clear()会连表头一起清掉,所以每次刷新都要重新setHorizontalHeaderLabels,这是 Model/View 方案里最容易漏的一步。
插入功能的槽函数,关键在预处理和字段拦截:
void TeacherWindow::addStudent() { QString id = ui->editId->text().trimmed(); QString name = ui->editName->text().trimmed(); QString cls = ui->editClass->text().trimmed(); if (id.isEmpty() || name.isEmpty()) { QMessageBox::warning(this, "提示", "学号和姓名不能为空"); return; } QSqlQuery query; query.prepare("INSERT INTO t_student(student_id, name, class_name, gender, phone) " "VALUES(?, ?, ?, ?, ?)"); query.addBindValue(id); query.addBindValue(name); query.addBindValue(cls); query.addBindValue(ui->comboGender->currentText()); query.addBindValue(ui->editPhone->text().trimmed()); if (!query.exec()) { QMessageBox::critical(this, "错误", "插入失败:" + query.lastError().text()); return; } refreshData(); }prepare加addBindValue是为了防 SQL 注入,本地单机系统同样要坚持这个习惯。空学号直接拦截,避免主键冲突后弹一堆数据库错误。失败时把lastError().text()弹出来,而不是只弹一句“失败”,排查问题能省一半时间。
删除学生要注意顺序:
void TeacherWindow::deleteStudent() { QString id = ui->editId->text().trimmed(); if (id.isEmpty()) { QMessageBox::warning(this, "提示", "请先选择或输入学号"); return; } QSqlQuery query; db.transaction(); query.prepare("DELETE FROM t_score WHERE student_id = ?"); query.addBindValue(id); query.exec(); query.prepare("DELETE FROM t_student WHERE student_id = ?"); query.addBindValue(id); query.exec(); db.commit(); refreshData(); }先删成绩再删学生,顺序反了会留下孤儿成绩记录,第 4 章的成绩统计里 Count 就会对不上。事务包裹是为了防止第一次 DELETE 成功、第二次失败时出现半删状态。
查询框的textChanged信号可以接一个带 300 毫秒定时器的槽函数,输入停顿后才触发refreshData,而不是每敲一个字符就刷一次全表。这个细节在几百行数据时感觉不明显,数据上千后体感差异很大。
4. 班级成绩分析:一条 GROUP BY SQL 加一张 QChart 条形图
成绩分析是这个系统拉开档次的模块。教师端通常选择一个课程再点“分析”,界面显示各班级的平均分、及格率、成绩段分布。数据量不大,没必要引数据分析库,一条聚合 SQL 加一张 QChart 图就能落地,而且答辩时更容易讲清楚。
4.1 平均分与及格率:JOIN 之后 GROUP BY
SELECT s.class_name, COUNT(DISTINCT s.student_id) AS stu_count, ROUND(AVG(sc.score), 1) AS avg_score, SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) * 1.0 / COUNT(sc.id) AS pass_rate, SUM(CASE WHEN sc.score >= 85 THEN 1 ELSE 0 END) * 1.0 / COUNT(sc.id) AS excellent_rate FROM t_student s LEFT JOIN t_score sc ON s.student_id = sc.student_id GROUP BY s.class_name;逻辑说明:LEFT JOIN保证还没录成绩的班级也出现在结果里,教师在成绩分析界面能看到“这个班还没数据”,而不是班级直接消失。SUM(CASE WHEN ...)是在聚合过程中做条件计数,比把结果全部拉回来在 C++ 里遍历再判断要快得多。COUNT(DISTINCT s.student_id)统计的是实际学生数,及格率的分母用COUNT(sc.id),也就是成绩记录数——一个学生有多科成绩时,每科都参与及格率计算。所以这个及格率准确说是“本次考试各科成绩的及格率”,不是“学生及格率”,答辩时要说清这个口径。
如果只想分析某一门课,加一个WHERE sc.course_name = ?参数即可。分数段分布是另一个常用口径,也是同一个套路:
SELECT s.class_name, SUM(CASE WHEN sc.score >= 90 THEN 1 ELSE 0 END) AS seg90, SUM(CASE WHEN sc.score >= 80 AND sc.score < 90 THEN 1 ELSE 0 END) AS seg80, SUM(CASE WHEN sc.score >= 60 AND sc.score < 80 THEN 1 ELSE 0 END) AS seg60, SUM(CASE WHEN sc.score < 60 THEN 1 ELSE 0 END) AS segLow FROM t_student s LEFT JOIN t_score sc ON s.student_id = sc.student_id GROUP BY s.class_name;分数段比单一平均分更能看出班级两极分化,界面里可以做一个下拉框,切换“平均分/及格率/分数段”三种口径,SQL 对应换一条。后端返回的数据可以用一个结构体数组暂存,图表和旁边的统计表格共用这份内存。
4.2 把统计结果画成条形图
.pro文件里要加QT += charts,否则QChart相关类全部编译不过。绘制部分常见写法是这样的:
#include <QtCharts/QChart> #include <QtCharts/QBarSeries> #include <QtCharts/QBarSet> #include <QtCharts/QBarCategoryAxis> #include <QtCharts/QValueAxis> QBarSet *set = new QBarSet("平均分"); for (double v : avgList) { set->append(v); } QBarSeries *series = new QBarSeries(); series->append(set); QChart *chart = new QChart(); chart->addSeries(series); chart->setTitle("各班级平均分对比"); QBarCategoryAxis *axisX = new QBarCategoryAxis(); axisX->append(classList); QValueAxis *axisY = new QValueAxis(); axisY->setRange(0, 100); chart->addAxis(axisX, Qt::AlignBottom); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisX); series->attachAxis(axisY); ui->chartView->setChart(chart); chart->legend()->hide();参数说明:axisY->setRange(0, 100)必须显式设置,否则 QChart 会自动压缩坐标轴范围,把 78 分和 85 分的差距画成巨差,观感失真。addAxis和attachAxis是两件事,坐标轴先挂到 chart 上,series 再关联坐标轴,顺序反了会出现坐标轴不显示。图例在只有一个序列时可以legend()->hide(),多个班级对比时才需要保留。QChart 的命名空间在 Qt5 和 Qt6 里都是QtCharts,代码不用改,但 CMake 里 Qt6 要写find_package(Qt6 COMPONENTS Charts)。
5. Qt 学生信息系统避坑清单:乱码、锁库、刷新失败、驱动缺失
功能写完后,真正耗时间的从来不是业务逻辑,而是运行环境的问题。下面四个坑我在这类系统里反复碰到,每个都按“现象 → 原因 → 解决”的顺序记录,也都是真实排查过的血泪经验。
5.1 中文乱码:从源码到 SQLite 再到界面的编码链路
现象:按钮文字变成??,或者数据库里读出来的中文学生姓名显示成乱码。原因:Windows 下用 MSVC 编译器时,源码文件是 UTF-8 无 BOM 编码,但编译器默认按系统本地编码解析,中文字符串字面量就被解释错了。解决:MSVC 工程在源码第一行加#pragma execution_character_set("utf-8");MinGW 和 Linux 下一般没有这个问题。.ui文件里复制粘贴进来的中文也经常混进 GBK,用文本编辑器打开.ui文件,检查头部encoding属性,统一改成UTF-8。数据库端乱码多半是写入时编码就不对,先解决前两步再回头看数据。
5.2 database is locked:SQLite 多写并发很脆弱
现象:连续快速点击“保存”几次后,弹出database is locked unable to fetch row。原因:最常见的是多个窗口各自执行了一次QSqlDatabase::addDatabase,导致同一个school.db被两个连接打开。SQLite 对同一时刻多个写事务支持很弱,一个连接没提交,另一个写操作就会撞锁。解决:全程序只保留main.cpp里那一个默认连接,其他窗口用QSqlQuery时不要再次addDatabase。批量写操作(比如导入成绩)用db.transaction()和db.commit()包起来,出错时db.rollback()。自查方式:全文搜索addDatabase,出现次数超过一次就是隐患。
5.3 数据改完表格不刷新
现象:插入学生成功,控制台也打印了执行成功,但界面上看不到新记录。原因:SQL 执行成功只动了数据库,QStandardItemModel里的数据没同步。解决:所有写操作槽函数尾部统一调用refreshData(),不要在多个地方各自拼界面刷新代码。另一个隐蔽问题是清 Model 的方式:model->clear()会连表头一起清掉,下次刷新必须重新设置表头;如果只想清数据行,用model->removeRows(0, model->rowCount()),表头结构保留,代码更干净。
5.4 打包后 Driver not loaded
现象:开发机上跑得正常,整个 Release 文件夹拷到别的电脑,启动报QSqlDatabase: QSQLITE driver not loaded。原因:发布目录里只有 exe,缺少 Qt 的 SQLite 插件sqldrivers/qsqlite.dll。Qt 的数据库驱动是插件机制,不是静态编译进 exe 的。解决:用windeployqt.exe处理 Release 目录,命令是windeployqt --release --compiler-runtime <exe路径>,处理完检查目录下是否存在sqldrivers/qsqlite.dll。如果没有,从 Qt 安装目录的plugins/sqldrivers里手动复制一份。我早期直接把 release 版 exe 单独拷给别人,被当成“没做完”的项目,后来养成的习惯是:每次发布前跑一遍部署工具,再在另一台机器上启动一次才算完。
6. 上点强度:从能跑的 Demo 到能交付的成品
功能全通只是起点,把系统打磨到能交给别人用,还有三件小事值得做。
6.1 密码不再存明文
t_user表里的password字段改成存 SHA-256 摘要,登录时对输入密码算哈希再比对:
QByteArray hash = QCryptographicHash::hash( password.toUtf8(), QCryptographicHash::Sha256).toHex();改动很小,但老师或同事问“密码安全怎么处理”时,能拿出实际代码而不是一句“系统是单机的”。
6.2 一键导出 CSV 成绩
教师端加一个“导出成绩”按钮,用QFile和QTextStream写 CSV。记得输出 UTF-8 BOM,否则用 Excel 打开 CSV 时中文列名会乱码:
QFile file("scores.csv"); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&file); out << "\xEF\xBB\xBF"; // UTF-8 BOM out << "学号,姓名,课程,成绩\n"; // 遍历查询结果逐行写入 }CSV 的好处是不依赖任何第三方库,生成的.csv文件拷到没有 Office 的机器也可以用记事本打开。
6.3 发布三步走
Release 构建,windeployqt收集依赖,最后压成 zip。发布物里至少包含 exe、platforms、sqldrivers、styles几个目录。整套系统做完,我也交过不只一次“只靠三个源文件交差”的作业,被一句“换个机器能跑吗”问住后,才把打包检查变成固定习惯。项目本身不大,但把“能跑”和“能用”之间的路走通,比多写十个界面都值。希望帮到你。
本文还有配套的精品资源,点击获取