简介:基于Qt框架开发的成绩管理系统,完整覆盖C++课程设计常见需求,适合计算机、自动化、电子信息等专业的在校学生、教师及企业人员下载参考,也可作为课程作业或毕业设计的起步项目。压缩包内含98个文件,体积约187.59MB,主要由C++源文件、Qt界面文件、数据库文件、动态链接库及VS工程文件构成,同时涵盖资源脚本、调试日志与编译中间产物,整体目录结构清晰,便于定位源码、界面和配置模块。项目实现了登录注册、教师学生信息管理、成绩录入与查询等核心功能,代码已通过运行测试,逻辑完整且注释友好,可帮助初学者理解Qt信号槽机制与界面开发流程,也可在现有基础上针对功能或界面做二次开发。目前已有62人浏览学习,适合需要快速获取可运行样例、熟悉完整工程结构或搭建相似管理系统的读者。
1. 基于QT的成绩管理系统课设:从选型到交付的一次完整推演
每年这个时候,都会有不少人被"C++课程设计:基于QT的成绩管理系统"这个题目卡住。说它难,其实拆开看就是一张学生表加一张成绩表,再加几个统计按钮;说它容易,很多人卡在QT环境配置上一天装不出来,或者在QTableView和QTreeWidget之间反复横跳。这个项目真正考验的不是C++语法,而是你能否把一个"能跑起来的控制台程序"升级成"有界面、能交互、能交付演示"的桌面应用。这篇笔记打算按我做课设辅导时最常用的路径来讲:先定版本和编译器,再定数据结构,然后处理界面交互,最后补上打包和答辩验证。整个过程会穿插具体的代码和参数,尽量让你照着走就能在两天内拿出一个能演示的版本。
2. QT版本与编译器选型:先定工具链,再写第一行代码
课设项目最容易翻车的地方往往不是业务逻辑,而是环境。C++课程设计一般要求用QT完成界面,但你打开Qt官网时会发现下载入口对新手并不友好,尤其当你选了MSVC版本的QT,却忘了装对应的Visual Studio C++ Redistributable,后面编译会报出一串看不懂的链接错误。所以第一步不是写代码,而是把工具链理顺。
2.1 用清华镜像装QT 5.15.2:版本号和编译器怎么搭配
选择QT版本时,我的建议是不要追新,直接选5.15.2 LTS版本。原因有三:第一,5.15.2带离线安装包,在校园网环境下比在线安装器稳定得多;第二,网上能搜到的绝大多数QT课设代码都基于5.x的语法风格,你复制资料里的代码时不容易遇到接口不兼容的问题;第三,5.15.2同时支持MSVC2019_64和MinGW两种工具链,后者对只装了Qt Creator没装Visual Studio的同学更友好。
打开清华镜像站,进入qt/archive/qt目录,找到5.15.2的安装包。Windows平台注意区分文件名里的编译器标识:qt-opensource-windows-x86-msvc2019_64-5.15.2.exe对应MSVC2019 64位,适合安装了Visual Studio 2019或2022的机器;qt-opensource-windows-x86-mingw810-5.15.2.exe对应MinGW 8.1.0编译器,适合不想装Visual Studio的场景。
安装时有个容易忽略的细节:组件勾选阶段,务必展开Qt 5.15.2节点,勾选"Qt Charts"和"Qt SQL"模块。成绩管理系统后期如果要做分数分布图或连接SQLite数据库,这两个模块少了的话得重装整个QT,非常浪费时间。编译器组件里,MinGW版本会自动匹配,MSVC版本则需要在安装完成后到"工具→选项→Kits"里手动确认编译器路径。
2.2 验证工具链:构建套件和最小工程
装完QT后,不要急着写成绩管理系统,先用一个空白窗口工程验证工具链。在Qt Creator里新建项目时选"Application→Qt Widgets Application",构建系统选qmake。
验证工程的关键是确认主窗口能正常编译运行。这里有一个很多人会踩的坑:新建项目时默认的main.cpp里QApplication对象是局部变量,如果你之前复制过某个教程改成了QApplication *app = new QApplication(argc, argv);,那么主窗口关闭时程序可能不退出。空窗口至少能弹出来,再继续后面的开发。
// main.cpp 最小验证工程 #include <QApplication> #include <QWidget> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle(QString::fromLocal8Bit("成绩系统启动测试")); window.resize(800, 600); window.show(); return app.exec(); }这段代码做三件事:创建QApplication实例、创建QWidget窗口、进入事件循环。app.exec()会阻塞直到窗口关闭。如果这一步能跑出窗口,说明QT环境、编译器、构建系统三条链路都通了。此时再回到项目里删掉测试代码,开始面向成绩管理系统的开发。
3. 成绩数据从结构体到表格模型:统计、排序与界面绑定的完整链路
环境通了之后,先做数据层。成绩管理系统的核心数据是学生信息和课程成绩,用结构体承载是C++课程设计最受老师认可的做法。很多同学一上来就想着用二维数组或直接往界面控件里塞数据,后边要加统计功能时发现数据是散的,改起来痛不欲生。正确顺序是先定义结构体,再做排序和统计,最后把数据塞进表格模型。
3.1 定义Student结构体与管理类:存什么、怎么存
结构体承载一门课的基本成绩信息,至少要包含学号、姓名、课程名、平时成绩、考试成绩和总评成绩。平时成绩和考试成绩的加权比例通常由课程性质决定,比如三七开或五五开,我在代码里把比例做成常量,避免魔法数字散落在各处。总评成绩用double类型存储,因为后续要算平均分和排名,浮点精度比整数更稳妥。
// student.h #ifndef STUDENT_H #define STUDENT_H #include <QString> struct Student { QString id; // 学号 QString name; // 姓名 QString course; // 课程名 double usualScore; // 平时成绩 满分100 double examScore; // 考试成绩 满分100 double totalScore; // 总评成绩 }; #endif // STUDENT_H这里有一个C++课程设计里常见的分歧点:用QString还是std::string存姓名。我建议在QT项目里用QString,因为后续所有控件方法都接受QString,比如QTableWidgetItem的构造函数直接吃QString,如果你用std::string还得先转换,多出来的代码在答辩时毫无加分点。另外totalScore不要在定义时手算,等数据填充完成后统一计算,这样后面改加权比例时只改一处。
管理类ScoreManager负责维护一个QVector<Student>容器,提供增删改查接口。这里不直接用std::vector是因为QVector和QT的隐式共享机制配合更好,foreach遍历也更自然。课程设计这种规模的数据量,从性能上讲两者没有差别,选QVector更多是让代码风格和QT生态保持一致。
3.2 冒泡排序与平均分统计:参数化排名和一键统计的坑
排序是成绩管理系统里老师最爱检查的功能点。课设要求里如果写了"按总评成绩排名",很多同学第一反应是写个冒泡排序。我不建议你用std::sort直接糊弄过去——虽然它更高效,但课程设计答辩时老师会问排序过程,能用白话讲清楚冒泡排序的交换逻辑,比甩一句"我用了STL"更容易拿高分。下面这段代码我在课设里用了很多次,注意看降序比较的写法。
// ScoreManager::sortByTotal 冒泡排序降序 void ScoreManager::sortByTotal() { int n = students.size(); for (int i = 0; i < n - 1; ++i) { for (int j = 0; j < n - 1 - i; ++j) { if (students[j].totalScore < students[j + 1].totalScore) { Student temp = students[j]; students[j] = students[j + 1]; students[j + 1] = temp; } } } }冒泡排序在数据量超过上千条时性能会明显下降,但成绩管理系统一个班最多几十人,这个复杂度完全够用,老师也更好理解。参数上注意j < n - 1 - i这个边界条件:每轮排序后最大值已经沉到末尾,不需要再参与比较,漏掉这个"-i"会导致多一次无意义的循环遍历,虽然不影响结果,但如果你把逻辑写错成j < n - 1,排序仍然正确,只是效率低一点,很难被发现。我习惯在注释里写明"内层循环边界减去已完成轮数",答辩时能说清楚这是性能优化点。
平均分、及格率这些统计指标,建议单独写一个方法返回结构体,而不是把计算结果直接printf到控制台或窗体标题栏。因为后面做表格刷新时要重新取数据,统计结果可能是中间变量,拆出来方便复用。
// 统计结果结构体 struct StatResult { double average; // 平均分 int passCount; // 及格人数 大于等于60 double passRate; // 及格率 百分比 int maxScore; // 最高分 int minScore; // 最低分 }; StatResult ScoreManager::calcStatistics() const { StatResult result = {0.0, 0, 0.0, 0, 100}; int count = students.size(); if (count == 0) return result; double sum = 0.0; int passCnt = 0; int maxS = -1, minS = 101; for (const Student &s : students) { sum += s.totalScore; if (s.totalScore >= 60.0) passCnt++; if (s.totalScore > maxS) maxS = static_cast<int>(s.totalScore); if (s.totalScore < minS) minS = static_cast<int>(s.totalScore); } result.average = sum / count; result.passCount = passCnt; result.passRate = passCnt * 100.0 / count; result.maxScore = maxS; result.minScore = minS; return result; }注意passRate的100.0必须写成double,如果写成passCnt * 100 / count,整数除法会把90.6%算成90%,虽然结果接近但精度不对,答辩时老师会盯着这个问。我在给学生review代码时发现这个错误至少出现过三次,都是因为图省事直接写了整数。另外minScore初始化为100,最高分maxScore初始化为-1,这是遍历求最大最小值时的常用初值策略,能让空数据集的边界情况也能返回合理值,不会出现“初始化为0导致没有学生时最低分显示0”的尴尬。
3.3 从QVector到QStandardItemModel:为什么不用QTableWidget
到了把数据呈现出来的环节,很多人会直接往QTableWidget里逐行setItem。QTableWidget用起来确实简单,但它把数据和界面耦合死了,一旦你要做"按课程筛选""按学号查找",就得手动遍历控件再取值,代码写起来像在用爬虫处理HTML。我推荐改用QStandardItemModel配合QTableView,这是QT官方推荐的做法,数据存储在模型中,视图只负责渲染,逻辑上清晰很多。
// 表格模型刷新 void MainWindow::refreshTable() { // 假设 model_ 是 QStandardItemModel* 成员 model_->clear(); QStringList headers; headers << "学号" << "姓名" << "课程" << "平时分" << "考试分" << "总评"; model_->setHorizontalHeaderLabels(headers); const QVector<Student> &list = manager_.students(); for (int row = 0; row < list.size(); ++row) { const Student &s = list.at(row); QList<QStandardItem*> items; items << new QStandardItem(s.id) << new QStandardItem(s.name) << new QStandardItem(s.course); QStandardItem *usualItem = new QStandardItem(QString::number(s.usualScore, 'f', 1)); QStandardItem *examItem = new QStandardItem(QString::number(s.examScore, 'f', 1)); QStandardItem *totalItem = new QStandardItem(QString::number(s.totalScore, 'f', 1)); // 按分数段设置背景色 if (s.totalScore >= 90.0) { totalItem->setBackground(QColor("#e1f5e1")); // 浅绿色表示优秀 } else if (s.totalScore < 60.0) { totalItem->setBackground(QColor("#ffebee")); // 浅红色表示不及格 } items << usualItem << examItem << totalItem; model_->appendRow(items); } ui->tableView->setModel(model_); }QString::number(s.usualScore, 'f', 1)里的'f'表示浮点数格式,1表示保留1位小数,这是QT里double转字符串最常用的方式。如果你直接写QString::number(s.usualScore),默认只输出6位有效数字,像89.5还好,88.75会变成88.75,但88.705会被截断,答辩时数据精度对不上会被一眼识破。按分数段染色这个技巧能体现你对"用户体验"的理解,但注意QTableView默认的格子是白色的,如果你数据量小也可以用QTableWidget替代,但我不建议——因为后面如果加了"按课程筛选"功能,QStandardItemModel的setFilterRegExp配合QSortFilterProxyModel能省一半代码。
整体来看这一层的主要任务是:把结构体数组里的数据映射到QStandardItem,再让QTableView自动渲染。你只需要处理从数据到Item的映射方向,后续用户在表格里点击、排序、选择行都交给QT内部处理,不用写任何控件遍历逻辑。
4. 界面交互与增删改查落地:信号槽、TreeWidget选型与对话框联动
数据模型有了,接下来是整个课设里最耗费时间的一个环节:把界面上需要的功能做成真正能用鼠标操作的交互。成绩管理系统的基本功能是录入、修改、删除、查询,外加统计信息展示。很多同学做这一层时会陷入"控件选择困难症",其实只要想清楚交互场景,用什么控件是水到渠成的事。
4.1 QTreeWidget做课程/班级导航,QTableView做成绩明细
界面布局上,我习惯左侧放一个QTreeWidget,右侧放QTableView。左侧以"课程"为顶级节点,展开后是该课程下的班级或分组,点击任意节点时右侧表格联动显示对应数据。这种结构在课设答辩时非常好讲:它展示了你能把"数据筛选"和"数据展示"分离,而不是把所有信息堆在一张平铺的表格里。
// 初始化左侧课程树 void MainWindow::initCourseTree() { ui->treeWidget->clear(); QStringList courses = manager_.allCourses(); // 从数据中提取所有课程名 for (const QString &course : courses) { QTreeWidgetItem *courseItem = new QTreeWidgetItem(ui->treeWidget); courseItem->setText(0, course); // 第0列显示课程名 // 给每个课程挂一个"全部"子节点 QTreeWidgetItem *allItem = new QTreeWidgetItem(courseItem); allItem->setText(0, "全部"); allItem->setData(0, Qt::UserRole, course); // 存课程名到自定义角色 } ui->treeWidget->expandAll(); }setData(0, Qt::UserRole, course)这行代码很多新手看不懂。Qt::UserRole是自定义数据角色的起始值,你可以把任意QVariant类型的业务数据挂到节点上,访问时用item->data(0, Qt::UserRole).toString()取回来。这里挂课程名是因为在onTreeWidgetClicked信号里,你需要通过节点的自定义数据知道用户点的是哪门课,而不是靠遍历节点文字反推,那样效率低且容易错。信号槽连接方式直接在设计器里鼠标右键"转到槽"即可,QT会自动生成槽函数声明和连接,别手写connect,课设项目里手写connect容易漏参数。
4.2 增删改查的槽函数实现:对话框取值和行号定位的边界
新增和编辑功能需要弹一个对话框,让用户输入学号、姓名、分数。不要在MainWindow里用一堆QLineEdit组件堆表单,那样代码量暴增且界面混乱。正确做法是新建一个QDialog,在Qt Designer里拖几个LineEdit和Label,然后通过dialog.exec()模态运行,返回值代表用户点了确定还是取消。
// 新增成绩弹窗 void MainWindow::onAddButtonClicked() { ScoreEditDialog dialog(this); if (dialog.exec() == QDialog::Accepted) { Student s; s.id = dialog.idEdit()->text().trimmed(); s.name = dialog.nameEdit()->text().trimmed(); s.course = dialog.courseEdit()->text().trimmed(); s.usualScore = dialog.usualSpin()->value(); s.examScore = dialog.examSpin()->value(); s.totalScore = s.usualScore * 0.3 + s.examScore * 0.7; if (s.id.isEmpty() || s.name.isEmpty()) { QMessageBox::warning(this, "输入错误", "学号和姓名不能为空"); return; } manager_.addStudent(s); refreshTable(); refreshStats(); } }dialog.idEdit()这类访问器需要你在对话框类里手动封装getter,Qt Designer生成的ui里控件是private的,不能直接ui->idEdit访问。注意trimmed()去空格,这是防止用户粘贴数据时带入不可见字符。QMessageBox::warning是模态对话框,第二个参数是标题、第三个是正文,和QMessageBox::information的区别是图标不同,按课设要求用warning提示校验错误比弹错误码更友好。
删除功能有一个坑:在QTableView里选中行然后删除,你以为ui->tableView->currentIndex().row()就是行号。但当表格被点击过表头排序后,视图里的行号和数据容器里的下标是不一致的。因为QTableView的排序发生在视图层,不修改模型内部顺序。解决方式有两个:一是给QTableView设置setSortingEnabled(false),禁用表头排序,换取逻辑简单;二是通过QStandardItemModel的index(row, 0).data()拿到学号,再按学号删除。我建议课设阶段用后者,因为老师很可能手动点击表头排序来测试,你禁用排序反而会显得功能缺失。
// 按选中行的学号删除 void MainWindow::onDeleteButtonClicked() { QModelIndex current = ui->tableView->currentIndex(); if (!current.isValid()) { QMessageBox::information(this, "提示", "请先选中一行"); return; } QString id = model_->data(model_->index(current.row(), 0)).toString(); QMessageBox::StandardButton confirm = QMessageBox::question(this, "确认删除", QString("确定删除学号为%1的学生吗?").arg(id)); if (confirm == QMessageBox::Yes) { manager_.removeById(id); refreshTable(); refreshStats(); } }current.row()只对当前没有排序的状态有效,但通过model_->index(current.row(), 0)取第一列学号,再按学号去manager_.removeById(id)里删,就能避开排序后下标错位的隐患。QModelIndex::isValid()在没有任何选中行时返回false,直接弹提示比用空学号继续往下走靠谱得多。这是我强烈建议你抄下来的一段代码,几乎所有"表格选中+删除"需求都能套。
4.3 查询和统计面板:按学号精确匹配与分数段分布展示
成绩管理系统还有一个高频功能是查询。常见的查询维度是学号和姓名。学号适合精确匹配,姓名适合包含匹配(因为可能重名,或用户只记得前两个字)。实现上不要在手写循环里去==比较,而是用QString::contains配合Qt::CaseInsensitive忽略大小写,防止用户在学号里混入字母时查不到。
// 按关键字查询 void MainWindow::onSearchTextChanged(const QString &keyword) { model_->clear(); QStringList headers; headers << "学号" << "姓名" << "课程" << "平时分" << "考试分" << "总评"; model_->setHorizontalHeaderLabels(headers); const QVector<Student> &list = manager_.students(); for (const Student &s : list) { if (s.id.contains(keyword, Qt::CaseInsensitive) || s.name.contains(keyword, Qt::CaseInsensitive)) { // 重新添加匹配行,代码同refreshTable内部一致,可抽函数复用 } } ui->tableView->setModel(model_); }这里有个小技巧:查询函数里直接调用model_->clear(),相当于每次查询时重建表格,不需要考虑隐藏行的状态。缺点是输入法整段拼音时可能触发多次重查,但课设数据量小,完全没问题。如果用QLineEdit::textChanged信号每次重查,你要注意它会在程序初始化时因为空白文本触发一次空查询,空关键字应该返回全表而不是空表,所以方法开头最好判断一下if (keyword.isEmpty()) { refreshTable(); return; }。
统计面板建议放在窗口右侧或底部,用一组QLabel展示"平均分、最高分、最低分、及格率"等信息,而不是每算一次都弹窗。这样视觉上很直观,答辩时老师一眼就能看到你的统计数据不是写死的。每次数据增删改后记得调用refreshStats(),把calcStatistics()的返回值格式化后填到QLabel上,格式字符串用QString("平均分: %1").arg(stats.average, 0, 'f', 2),arg的多参数重载非常灵活,注意'f'和精度参数的位置不能写反。
5. QT成绩管理系统避坑排查:6个让同学反复编译失败的高频问题
这一章单独拎出来放在最后而不是藏在前面章节里,是因为课设提问里比重最大的就是"编译报错怎么办"和"运行崩溃为什么"。这里挑6个我认为最高频的踩坑记录,每条按现象→原因→解决来写,你照着排查通常几分钟内能找到问题。
5.1 中文乱码:界面和源码里直接写字出现问号
现象:QMessageBox::information(NULL, "提示", "保存成功")弹窗标题和内容显示成"???",或者使用VS Code写的源文件在Qt Creator里乱码。
原因:Windows下QT 5.x默认UTF-8读取源码,但Visual Studio的MSVC编译器默认按本地代码页GBK读取文件。如果你的源文件保存为GBK编码,又是中文写死在字符串里,QT读出来的就是乱码。
解决:统一用UTF-8编码保存所有.cpp和.h文件。Qt Creator里点"编辑→编码→按UTF-8重新载入",VS Code右下角把编码切换到UTF-8即可。代码里尽量用QString::fromLocal8Bit("中文")或者QStringLiteral。注意QStringLiteral在MSVC下也有编码坑,最省心的是fromLocal8Bit,虽然运行时开销可以忽略。
提示:如果在Qt Creator里看到"无法反编译"或中文注释乱码,先检查文件编码,别去改编译参数,改了可能引入更多问题。
5.2 Qt Creator里出现:-1: error: dependent '..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist
现象:编译时报dependent '.../qt/5.15.2/msvc2019_64/include/qtwidgets' does not exist,但QT明明安装成功了。
原因:Qt 5.15.2安装时,某些组件的include目录没有正确写入Qt Creator的Kit配置里,或者你在安装时取消了部分模块。qtwidgets这个头文件路径不存在的直接原因是Qt Widgets模块没装上。
解决:重新运行安装程序,进入组件选择界面,勾选"Qt 5.15.2→Qt Widgets"这一项。装完之后到"工具→选项→Kits"里,看到"Qt Version"标签页里5.15.2的路径是否正确指向你安装的目录。如果你安装的是MSVC2019_64版本,检查编译套件里的"编译器"是否为"Microsoft Visual C++ Compiler 16.x (x86-x64)",编译器选错也会导致它去找错误目录。此问题在热榜上频繁出现,说明安装时勾选不仔细是初中级用户最常见的事故源之一。
5.3 Qt运行提示"无法定位程序输入点"或缺Qt5Core.dll
现象:在开发机运行正常,把exe发送到别的电脑上双击,弹出"由于找不到Qt5Core.dll,无法继续执行代码"。
原因:Qt Creator里调试运行时,自动从QT/5.15.2/msvc2019_64/bin目录找DLL;独立运行时路径变量没有配置,需要把依赖DLL和环境相关的动态库一并拷贝过去。
解决:用Qt自带的windeployqt工具生成发布目录。管理员身份运行Qt命令行工具,切到项目生成的exe所在目录,执行:
windeployqt --release --no-translations ScoreManager.exe如果没有windeployqt命令,先确认Qt的bin目录在PATH里。运行成功后,exe同级目录会自动拷入Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll等必要文件。如果程序里还用了Charts模块,可能要手动把Qt5Charts.dll拷过去,windeployqt对新模块的识别有时会漏。发布之前把整个目录发给别的机器测试,这是最稳妥的验证方式。
5.4 QTableView点击表头排序后,删除的却是另外一个人的记录
现象:点击“总分”列表头,表格立即按分数从高到低排序。选中第一行点删除,结果删掉的是原数据顺序里的第一行,不是显示在最高分位置的那条记录。
原因:setSortingEnabled(true)时,QTableView内部通过QSortFilterProxyModel维护排序视图,但你的currentIndex().row()取到的是视图行号,不是数据源中的行号。直接用视图行号去删结构体数组里的元素,自然错位。
解决:给QStandardItemModel设置setSortRole(Qt::DisplayRole),把排序交给模型层。在onDeleteButtonClicked里仍然按学号删除,不要再按行号定位。如果嫌排序乱,也可以最简单地设置table->setSortingEnabled(false),牺牲排序交互,换取删除逻辑的简单可靠。课程设计答辩时,我会建议保留排序功能,然后说清楚"我按学号删除而非行号,因为排序会改变视图行号",这反而是加分点。
5.5ui->tableView->setModel(model_)之后表格空白
现象:已经执行了setModel,表格区域却空白,刷新不出来数据。但打印s.count()时数量是对的,数据确实进了model。
原因:最常见的两个原因,一是QStandardItemModel的父对象被提前销毁,比如在refreshTable里写的是QStandardItemModel model;,函数返回时model销毁,视图拿到的指针悬空;二是model_->setHorizontalHeaderLabels(headers)里的headers类型是QStringList,漏了#include <QStringList>,编译不报错但运行行为异常。
解决:把model_声明为MainWindow的成员指针,在构造函数里model_ = new QStandardItemModel(this);,让主窗口持有模型生命周期。每次刷新时调用model_->clear()而不是重建对象。额外建议在refreshTable()函数开头model_->setRowCount(0),防止清空不彻底。这条经验是重灾区,很多同学写的是局部变量模型导致偶发崩溃。
5.6 修改数据后表格不刷新,或者刷新后滚动条位置跳回顶部
现象:点"保存修改"按钮,数据已写入manager_的容器里,但表格仍显示旧数据。刷新后表格弹回第一行,用户正在看第20名同学的数据也被迫跳走。
原因:刷新函数里没有把模型里的数据重新加载;滚动条位置由视图维护,model_->clear()重置了视图状态,滚动条自然回零。
解决:所有增删改操作完成后统一调用refreshTable(),函数内先model_->removeRows(0, model_->rowCount())再加新行;如果只想保持滚动位置,可以在refreshTable前记录ui->tableView->verticalScrollBar()->value(),刷新后setValue恢复。不过课设阶段我更建议直接清空重建,因为代码更容易看懂,答辩讲起来也坦率——你不需要在性能上炫技,保证功能正确即可。
6. 扩展到可持续维护:从单文件存储到SQLite接入和数据校验
课程设计交一版能跑的就算完成任务,但如果你还有余力,或者想让自己用于答辩的作品比同学高半个档次,我建议增加两个方向的功能:一是把数据存储从普通文件升级到SQLite数据库,二是加入重复学号校验和分数范围校验。这两个方向考察的是"工程化思维",在答辩时非常讨巧。
使用SQLite替代QHash或QDataStream保存数据,好处是可以随时用SQL查询语句对成绩做复杂的条件筛选,比如"查出所有总评大于80且考试不及格的学生"。QT对SQLite有内置支持,不需要额外安装数据库引擎。需要在.pro文件里加一行QT += sql,然后建立数据库连接,这是你用QSqlDatabase前必须的第一步。
// databasehelper.cpp 初始化SQLite并建表 bool initDatabase() { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("score.db"); // 数据库文件名,相对路径生成在exe同级目录 if (!db.open()) { qWarning() << "数据库打开失败: " << db.lastError().text(); return false; } QSqlQuery query; query.exec("CREATE TABLE IF NOT EXISTS students (" "id TEXT PRIMARY KEY," "name TEXT NOT NULL," "course TEXT NOT NULL," "usual_score REAL," "exam_score REAL," "total_score REAL)"); return true; }注意QSqlDatabase::addDatabase的静态函数,如果不指定连接名,默认连接"qt_sql_default_connection",重复调用会覆盖之前的连接配置。在MainWindow里只用一次就行。执行SQL写操作时,建议用QSqlQuery::prepare加占位符绑定值,不要直接拼字符串拼接,不仅能防止SQL注入(课设一般不会被攻击,但这是好习惯),还能避免学号里有单引号导致SQL语法错误。这一步是代码评审时一个很容易被提出来的点。
数据校验方向我强烈建议添加:新增学生时检查学号是否在现有数据中重复,输入分数时检查0到100范围。前者防止你的表格里出现两行一模一样的学号,后者防止用户录入负数或超过100的分数把统计结果搞坏。把校验逻辑写在ScoreManager::addStudent里,而不是散落在对话框按钮的槽函数中,这样控制台测试和界面调用都走同一套规则,代码内聚性好。
这段扩展内容看起来多,实际落地也就多一个数据库辅助类和两个检查函数。如果你时间不够,做不好SQLite接入,那么退而求其次,把数据文件保存改成JSON格式也比QDataStream的二进制更利于出错排查。无论选哪条路,期末答辩时你能说出的维护理由都比"我能把程序跑起来"强得多。
最后我想分享一个自己带课设时养成的习惯:写完所有功能后,我会故意删掉路径下的score.db或数据文件再运行一次,看程序能不能优雅提示而不是崩溃。很多同学的课设程序都直接在首次运行时因为没有配置文件而黑屏或闪退,这在答辩演示时几乎算事故。给程序加上一个简单的"文件不存在则创建空数据"的初始化流程,比多做一个酷炫功能实际得多。这种对边界情况的处理能力,也是区分"会写代码"和"会做软件"的一条分水岭。希望这篇笔记能让你在课设这条路上少走几步,按着这个思路,你完全可以在两天内交付一套能演示、能回答老师提问的成绩管理系统。
本文还有配套的精品资源,点击获取