简介这是一套基于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.dbQt 会自动创建空文件但表结构不会自动建所以最好封装一个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 QStandardItemModelModel 与视图分离清空 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 BYSELECT 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 lockedSQLite 多写并发很脆弱现象连续快速点击“保存”几次后弹出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几个目录。整套系统做完我也交过不只一次“只靠三个源文件交差”的作业被一句“换个机器能跑吗”问住后才把打包检查变成固定习惯。项目本身不大但把“能跑”和“能用”之间的路走通比多写十个界面都值。希望帮到你。本文还有配套的精品资源点击获取