公司动态
基于Qt的选课与成绩管理系统开发实践:架构设计与避坑指南
简介一套基于C与Qt框架开发的学生选课与成绩管理系统覆盖学生、教师、管理员三类角色适用于高校课程设计、毕业设计或Qt入门实践。学生可登录查询成绩、自主选课教师能够录入、修改和删除课程成绩并可管理所授班级的教学信息管理员则统一维护学生、教师与课程数据支持账户创建、权限调整和密码重置等操作。压缩包共计106个文件以cpp源文件、h头文件、ui界面文件为主分别对应业务逻辑、类声明和窗口设计另有pro工程配置、makefile及编译好的exe可执行程序整体大小约6.99MB。通过阅读源码可学习Qt控件布局、信号与槽机制、SQL数据库读写以及多角色权限管理的实现方式直接运行exe则能快速预览系统各模块的完整交互流程便于二次开发或功能扩展。目前已有4762人学习下载适合需要完整项目参考的初学开发者及相关专业学生。 做选课系统这种老掉牙的题目大多数人第一反应都是“又来了”“有什么好做的”。但真把需求捋清楚、界面做得像回事、权限边界划分明白坑其实不少。尤其用Qt来做很多人在第一步就栽了——不会搭项目结构或者用QWidget一把梭最后代码全堆在一个mainwindow.cpp里改一个需求要翻三四个文件。我这段时间正好把这么一套QT学生选课与成绩管理系统完整地写了跑了测试也踩了若干坑。这篇把设计思路、模块拆分、关键技术点和实际遇到的问题一次性讲清楚给后来人留个可直接抄作业的底稿。1. 项目整体设计与思路拆解1.1 核心需求解析这套系统名义上是“选课成绩”但拆开来看其实是三个C端学生、教师、管理员和一个公共数据层之间的协作问题。学生要能登录、改密码、浏览课程列表、选课/退课、查成绩教师要能登录、查看自己名下课程、给学生录成绩、维护课程基本信息管理员最麻烦要做账号管理、开课审核、数据统计、学期切换。三个角色不能越权界面不能混用数据必须统一落库。很多人在这里会犯一个错误把三种角色的界面全部塞到同一个Widget里通过登录后的身份判断去隐藏/显示按钮。这样不是不能跑但代码会迅速腐化。我的做法是三个独立的界面类分别继承自QMainWindow只通过登录控制模块决定进入哪个窗口。这样每个角色模块内部高内聚互相之间不直接依赖后续加功能、改样式都不容易影响其他端。1.2 技术选型为什么用Qt而不是Web或Electron选技术栈这件事本身就是一个说服需求方的过程。Qt最大的优势在于它是编译型、跨平台的桌面框架启动快、内存占用可控不像Electron动不动就几百MB。更重要的是Qt的信号槽机制天然适合这种多窗口、多角色间消息传递的场景。三个窗口之间虽然各自独立但都依赖同一个数据服务层共享登录状态、操作日志和数据库连接这一层同步机制用信号槽来处理非常自然。数据库我选的是SQLite这是做本地系统最务实的选择部署不需要单独安装服务QSqlDatabase原生支持。如果后续要改成MySQL只需要在数据库创建代码里替换连接类型和相关查询方言。1.3 项目结构千万别把代码全堆一个文件夹Qt的工程文件.pro管理程度决定了后期能走多远。我按照MVC的分层思想把目录拆成了这样CourseSystem/ ├── CourseSystem.pro ├── common/ # 公共工具、常量、数据库操作封装 │ ├── dbmanager.h/cpp │ ├── session.h/cpp │ └── utils.h/cpp ├── model/ # 数据模型针对QTableView的后端模型 │ ├── coursemodel.h/cpp │ ├── stumodel.h/cpp │ └── grademodel.h/cpp ├── view/ # 界面 │ ├── loginwindow.h/cpp │ ├── studentwindow.h/cpp │ ├── teacherwindow.h/cpp │ └── adminwindow.h/cpp └── resources/ # 样式表、图标、字体资源这样切的好处是界面部分不直接操作SQL语句所有数据操作都通过DbManager走数据访问接口。举个实际场景管理员想要新增“批量导入学生”的功能只需要在DbManager里加一个事务方法然后AdminWindow里调用并刷新表格完全不碰其他模块的代码。2. 核心功能模块与数据模型设计2.1 用户登录与会话管理登录是整个系统的入口这块如果做不好后面所有权限控制全是空谈。我做的登录窗口非常简单就是一个用户名框、密码框和角色下拉框但后台逻辑需要注意几个点。第一密码不能明文存储。虽然咱们这是个课程设计级别的东西但用Qt的QCryptographicHash做一次SHA-256散列存储是基本的职业素养。注册时散列一次存库登录时把输入用同样的算法散列后去比对数据库里永远不出现明文密码。第二登录成功后的“会话状态”需要全局可访问。我在common里单独维护了一个Session单例保存当前登录用户的ID、姓名、角色、登录时间。三个主窗口都从Session::instance()里取身份信息而不是把用户名当作参数层层传递。这种设计在后续写“记录用户操作日志”时特别好加因为日志模块只需要在DbManager的写操作里统一读一下Session就知道是谁干的。第三按角色跳转窗口。登录验证通过后根据role字段决定是StudentWindow、TeacherWindow还是AdminWindow登录窗口本身hide()而非close()保证退出登录时还能回来。这里有朋友会问为什么不直接销毁登录窗口因为如果窗口在栈上声明跳转后生命周期不好控制如果在堆上new则需要在主窗口里保存指针方便调回。我是在main函数里把LoginWindow设置为QW用show()切换的。2.2 数据库表设计数据库表是整个系统的心脏。直接罗列一下我建的表结构这个结构基本覆盖了常见的选课和成绩管理需求。用户表userid INTEGER PRIMARY KEY AUTOINCREMENTusername TEXT UNIQUE NOT NULLpassword TEXT NOT NULL 存SHA-256散列role INTEGER NOT NULL 1学生 2教师 3管理员name TEXT NOT NULLcreate_time TEXT学生表studentid INTEGER PRIMARY KEY AUTOINCREMENTuser_id INTEGER NOT NULL REFERENCES user(id)student_no TEXT UNIQUE NOT NULL 学号class_name TEXTmajor TEXT教师表teacherid INTEGER PRIMARY KEY AUTOINCREMENTuser_id INTEGER NOT NULL REFERENCES user(id)teacher_no TEXT UNIQUE NOT NULL 工号title TEXT 职称课程表courseid INTEGER PRIMARY KEY AUTOINCREMENTcourse_no TEXT UNIQUE NOT NULL 课程号course_name TEXT NOT NULLteacher_id INTEGER REFERENCES teacher(id)credit REAL NOT NULLcapacity INTEGER NOT NULL DEFAULT 60selected_count INTEGER NOT NULL DEFAULT 0schedule TEXT 上课时间比如“周一3-4节”status INTEGER DEFAULT 1 1可选0封版选课表electiveid INTEGER PRIMARY KEY AUTOINCREMENTstudent_id INTEGER NOT NULL REFERENCES student(id)course_id INTEGER NOT NULL REFERENCES course(id)std_score REAL DEFAULT -1 -1代表未录入成绩期末成绩不会录入是-1关于selected_count这个字段很多人在设计时会想“这个不是能从选课表里count出来吗为什么要冗余出来”。实际情况是列表页要显示每个课程的当前人数/容量比如果每次都去elective表里做COUNT统计课程一多就会卡。用冗余字段配合事务更新在选课成功时加一、退课时减一性能会明显更稳。后面“选课冲突检查”也依赖它。2.3 学生模块选课与退课的事务边界学生模块做了三个功能课程列表含已选/可选状态、我的课表、成绩查看。课程列表用QTableView QSqlTableModel实现直接绑定course表筛掉status为0的数据。但要实现“显示我是否已选”我在查询里做了LEFT JOIN把elective表中当前学生的选课记录合并进来。这样表格里可以多一列“状态”显示“已选/未选”。选课操作是整个系统中最容易出并发问题的地方。两个学生同时选同一门只剩一个名额的课可能出现超选。我用的是数据库事务条件更新的方式BEGIN TRANSACTION; UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; -- 检查是否更新成功rowCount()是否为1 INSERT INTO elective(student_id, course_id) VALUES(?, ?); COMMIT;先执行条件更新如果更新行数不是1说明此时容量已满或者被其他事务抢占了直接回滚事务并提示“课程容量已满”。这种方式比先查容量再插入靠谱得多因为它把“检查和更新”做成了原子操作。退课逻辑类似先删elective记录再对course表执行selected_count selected_count - 1。我这边还加了个限制如果该课程成绩已经录入std_score不是-1不允许退课只能联系管理员处理。这算是一个比较贴近真实教务场景的约束。2.4 教师模块成绩录入的批量处理和格式校验教师登录后看到的界面只包含自己创建的课程。这部分我用teacher_id关联来过滤course表杜绝教师看到别的老师的课。点击某个课程后右侧显示选修这门课的学生列表关联elective和student表然后在“成绩”列录入平时分或期末总分。成绩录入最让人头大的是格式和范围。我在QTableWidget的itemChanged信号里做了实时校验输入不是数字直接拒绝并还原超出0-100范围提示“成绩应在0~100之间”。有同行说这会影响输入手感但实测下来直接限制总比录完统一校验返工要省事。如果有批量导入成绩的需求可以用QFileDialog选择CSV文件按学号匹配写入并把失败的行号记录到日志。录完后点“提交”我对整列数据做一次事务提交。这样一旦中间有某一行格式异常全部回滚不会出现录了一半就保存的尴尬。2.5 管理模块用户管理、数据统计与开课维护管理员的界面元素最多我分了三个Tab用户管理、课程管理、数据看板。用户管理里管理员可以新增学生/教师账号输入学号、姓名、初始密码默认123456也可以禁用某账号把user表status置0。这里有个注意点禁用账号不是删除因为删除会产生外键孤儿数据。一个学生如果已经选了课强行删除用户记录会导致选课表里的student_id悬空。应该用status字段控制能否登录。课程管理里管理员可以审批教师提交的新课程申请也可以直接创建课程、分配授课教师。分配教师时用QComboBox动态加载教师表数据方便选择。这一块的界面设计我用了QTabWidget 左右分栏左边是教师列表右边是负责的课程列表视觉上清晰很多。数据看板做了三个统计卡片总学生数、总教师数、本学期选课人次。用QChart画了一个简单的柱状图展示各门课程选课人数对比方便管理员掌握哪些课是“爆款”哪些是“冷门”。QChart搞这个非常快缺点是Qt Charts模块需要单独在.pro里QT charts万一用的低版本还要手动安装这个在后面问题部分展开。3. 实操过程与核心环节实现3.1 环境准备与项目初始化我开发时用的是Qt 5.15.2 Qt Creator MinGW 64bit这套组合最省心在线安装时选择Qt 5.15.2组件包的MinGW 8.1.0 64-bit即可。国内网络下载慢就挂镜像源qt镜像社区里找。装完记得勾选“Qt Charts”和“Qt SQL”模块别等到编译报错再回头装。新建项目时选Qt Widgets ApplicationKeeps the traditional widget-based desktop app这也是学生系统的主流选择不建议用QML/Qt Quick虽然动画效果好看但做这种密集型表单交互并不顺手而且对新手不友好。项目名我起的是CourseSystem。.pro文件里的核心配置如下QT core gui sql charts greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET CourseSystem TEMPLATE app CONFIG c11 # 包含各子目录 INCLUDEPATH common model view SOURCES main.cpp \ common/dbmanager.cpp \ common/session.cpp \ common/utils.cpp \ view/loginwindow.cpp \ view/studentwindow.cpp \ view/teacherwindow.cpp \ view/adminwindow.cpp \ model/coursemodel.cpp HEADERS ... RESOURCES resources/resources.qrc3.2 数据库初始化代码建表不用手搓数据库的连接和建表我全部放在DbManager::init()里。这是个静态方法程序启动后调用一次不存在就创建文件表不存在就自动建。这样整个项目完成之后把可执行文件拷到别的电脑不用额外装数据库双击就能跑起来。关键代码如下示意bool DbManager::init() { QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(course_system.db); if (!db.open()) { qDebug() 数据库打开失败 db.lastError().text(); return false; } QSqlQuery query; query.exec(CREATE TABLE IF NOT EXISTS user (...)); // 其他建表语句... return true; }建表之后顺手做一件事插入默认管理员账号。这样系统打包给其他人用时至少能用一个初始管理员账号登进去。默认账号admin/123456重点是在首次登录时强制修改密码。3.3 登录窗口到主窗口的切换逻辑main.cpp里最核心的部分就是控制登录到主界面的连接。这里用的是Qt经典的单窗口轮换模式int main(int argc, char *argv[]) { QApplication a(argc, argv); DbManager::init(); LoginWindow login; login.show(); return a.exec(); }LoginWindow内部的跳转逻辑放在LoginWindow::onLoginSuccess(int role)里。实例化对应的主窗口后用stackedWidget的动态替换或者简单的指针法切换。我实际用的是// 给每个主窗口的内存管理留一个指针退出时delete if (role 1) { StudentWindow *w new StudentWindow(); w-setAttribute(Qt::WA_DeleteOnClose); w-show(); } else if (role 2) { ... } this-close();这里有个细节close()之后登录窗口并没有真正销毁因为它是栈对象只要主事件循环还在运行它就还在。新窗口设置WA_DeleteOnClose可以防止内存泄漏。3.4 模型视图设计QSqlTableModel还是自定义Model在课程列表界面直接绑QSqlTableModel是最快的方法但它在排序、筛选上需要跑SQL稍显笨重。我的做法是自定义了一个CourseModel类继承QAbstractTableModel内部持有的数据源是QListCourseInfo每次刷新从DbManager查询后填充list并beginResetModel()。好处有两点切页、刷新很快不用每次都重新查询可以灵活在data()里做自定义渲染比如已选课程用绿色前景色人数已满用灰色。QVariant CourseModel::data(const QModelIndex index, int role) const { if (!index.isValid()) return QVariant(); if (role Qt::DisplayRole) { // 按列返回CourseInfo的字段 } else if (role Qt::ForegroundRole) { CourseInfo info m_courses.at(index.row()); if (info.selected) return QColor(Qt::darkGreen); if (info.selectedCount info.capacity) return QColor(Qt::gray); } return QVariant(); }初学者总是纠结“到底用现成的还是自己写Model”我的建议是凡是只读展示的场景直接上Model凡是需要自定义颜色、图标、按钮的再继承重写别一上来就全部自定义浪费时间还容易出bug。3.5 Qt Charts绘制统计图10行代码出图管理员数据看板里那个柱状图其实是整个系统最不需要动脑的部分因为QChart把它封装到了“不能再简单”的程度。QtCharts::QBarSet *set new QtCharts::QBarSet(选课人数); for (int i 0; i courseNames.size(); i) { *set selectedCounts[i]; } QtCharts::QBarSeries *series new QtCharts::QBarSeries(); series-append(set); QtCharts::QChart *chart new QtCharts::QChart(); chart-addSeries(series); chart-setTitle(各课程选课人数对比); chart-createDefaultAxes(); ui-chartView-setChart(chart);有个操作要点是chartView必须在界面里先放好一个QChartView控件而不是手动new出来再layout。直接在设计器里拖一个QChartView代码里ui-chartView-setChart(chart)刷新、替换实例都很方便。4. 常见问题与排查技巧实录4.1 跨平台XCB、xrandr报错Linux下运行Qt的坎儿如果你是在Linux上开发或部署启动时很容易碰到类似这种报错qt.qpa.plugin: Could not load the Qt platform plugin xcb in even though it was found. this application failed to start because no Qt platform plugin could be initialized. qxcbconnection: failed to initialize xrandr Qt: XKEYBOARD extension not present on the X server.原因基本都是系统缺少X11依赖库。遇到这种情况99%是少装了libxcb-*相关库或者XKEYBOARD扩展没开。我在Ubuntu 20.04上测试时提示XKEYBOARD extension not present后来是装齐了这些包解决的sudo apt install libxcb-xinerama0 libxcb-cursor0 libxcb-icccm4 \ libxcb-keysyms1 libxcb-shape0 libxkbcommon-x11-0还有种情况是远程SSH连接时没有DISPLAY环境变量导致Qt试图初始化GUI但找不到显示服务。本地测试还好一旦放到开发板上要特别注意设置export DISPLAY:0。4.2 qsqlite数据库驱动没有编译进去比较隐蔽的一个坑程序在开发机上跑得好好的换台电脑双击exe就崩或者提示“driver not loaded”。原因通常是发布时只拷了exe没有拷贝sqldrivers目录下的qsqlite.dllWindows或libqsqlite.soLinux。Qt的数据库驱动是插件机制必须在程序运行时能找到对应的插件路径。解决方式是用Qt自带的windeployqt.exe工具部署windeployqt CourseSystem.exe --no-translations它会自动把依赖的Qt DLL和插件目录包括sqldrivers、platforms、styles一并拷贝到exe所在目录。拷贝完后可以检查一下目录里是否有sqldrivers/qsqlite.dll没有就手动从Qt安装目录复制。4.3 QSqlQuery执行UPDATE却没有生效这类问题我在群里帮人排查过好多次典型的“症状”是日志里显示执行成功但数据库里的数据没变或者重启程序后数据回来了。核心原因有两个第一是忘了提交事务。如果用QSqlDatabase::transaction()开启事务后面必须显式commit()否则所有改动在连接关闭时全部回滚。第二是数据库路径不对。SQLite是文件数据库如果程序的工作目录working directory和数据库文件实际所在目录不一致不同模块可能打开的是不同路径下的两个文件。这个问题只在你打印QSqlDatabase::database().connectionName()和QCoreApplication::applicationDirPath()时才会发现。更隐蔽一点的问题对同一个SQLite文件同时建立多个QSqlDatabase连接并且又没设置WAL模式会造成“database is locked”的报错。稳妥做法是全局只维护一个数据库连接所有查询都从DbManager获取不要在每个窗口里各自addDatabase。4.4 选课事务导致的死锁问题选课的高并发场景下我踩过一次事务嵌套的坑。原先我在StudentWindow::selectCourse里调了DbManager::selectCourse()这个函数内部开了事务。但如果在调用前界面上某个信号连接里已经手动开启了一个未提交的事务就会嵌套。SQLite对嵌套事务的处理是内层BEGIN会隐式提交外层事务这与预期不一致。最终表现是学生选课成功后教师端录成绩时发现数据丢了当时排查了很久。解决办法是统一规范所有写入操作入口都走DbManager事务只在这层开启和提交界面层绝不允许直接transaction()。我还在每个写事务结束后用Q_EMIT dataChanged()通知所有相关界面刷新而不是依赖界面自动刷新。4.5 中文字体与编码问题Qt 5默认的UTF-8对中文支持很好但Windows下如果用了MSVC编译器源文件编码必须保存为UTF-8 with BOM否则中文字符串会乱码。这个属于老生常谈但每次换环境还会有人中招。在main.cpp顶部可以加一句QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8));源码文件统一用UTF-8编码保存界面上的中文字符串就不会出幺蛾子。SQLite里查询中文也同样没问题但要注意QSqlQuery的prepare语句里中文字符串必须经过QString::fromUtf8()或者直接用QString字面量别随手用const char*拼接SQL。5. 发布与后续扩展建议整个系统开发完发布这块是很多新手的最后一堵墙。在Windows上建议用Release模式构建编译完用windeployqt做依赖收集数据库文件可以在程序第一次启动时自动生成。如果想要更专业的安装体验可以用Qt Installer Framework打一个安装包但那个学习成本不低在课程设计或者小范围部署的语境下绿色解压版完全够用。代码整洁度这块多说一句系统做完后我特意花了半天重构了所有硬编码的SQL语句统一收拢到DbManager里。这个改动短期看只是“搬代码”但对后续加功能比如加一个“教师查看所授课程学生名单并导出Excel”的需求极其有帮助。你只需要在DbManager加一个方法返回数据列表再在界面上加一个导出按钮调QFileDialog选择保存路径用QAxObject或者第三方库导出Excel即可。6. 避坑总结与个人体会最后分享我在实际开发中的几个体会。第一个是权限控制一定要从第一步就做不要先做功能再做权限否则最后会发现到处都在判断身份代码混乱到不敢动。第二个是数据库和界面的分层要彻底界面尽量不出现SQL语句所有的数据变更都走封装好的方法未来加功能、换数据库都会轻松很多。第三个是不要迷信QSS美化系统稳定的核心在数据一致性上界面简洁干净够用就行花太多时间调圆角和阴影对课程设计来说是本末倒置。另外测试环节建议多写一些“非法输入”用例。比如选课容量为负、成绩录入超过100、用户名带空格、退课时该课程成绩已录入等等。Qt的QIntValidator、QRegularExpressionValidator这些控件级校验能省下很多后端判断但核心的数据完整性校验还是要在写入数据库前做一遍不能只靠界面控件拦着。这套系统目前跑下来的状态是学生选课退课流畅、教师录成绩顺手、管理员看板直观整个项目在同级别课程设计里属于“足够交差且有一定亮点”的水平。后面如果继续扩展可以加的模块包括公告通知、课程评价、培养方案计划等核心架构不用动。有正在做同类项目的朋友如果卡在哪部分过不去顺着我这个设计思路逐块对照排查大概率能找到问题所在。本文还有配套的精品资源点击获取