简介面向教务管理场景的QT选课系统毕业设计项目压缩包内提供完整源码、可执行程序与配套论文适合本科毕业设计、课程设计或初学者学习Qt与教务业务逻辑。系统按功能划分为基本信息管理、选课与退课冲突检测、排课管理三大模块学生、教师、课程三类信息均支持增删改查、排序与查找学生可在线选课退课教师可提交课程管理员负责课程审核添加数据以文件方式持久化图形界面交互友好。压缩包共114个文件大小6.05MB以cpp/h源码、ui界面文件、qm资源与png图片为主另有csv数据文件、dll动态库、exe可执行程序、docx论文文档等便于直接运行与二次开发。已有1870人学习下载源码模块结构清晰并涵盖管理员端、学生端、教师端入口及课程管理、模型设置等界面配论文和可执行demo能帮助读者理解QT的事件处理、界面布局、类封装以及文件读写等关键实现适合需要快速上手完整教务选课项目开发的读者。1. 教务选课管理系统的 QT 工程解压之后才是真开始拿到这个标题你先别急着找解压密码。这个 zip 在毕设、课程设计和中小型教务系统外包需求里出现频率很高核心目标就一句话用 QT 的 C 桌面开发能力把学生、教师、课程、选课、成绩单这几类教务实体的增删改查流程做成一个可运行的图形界面系统。它适合的人群很具体——打算用 QT 做课程设计的学生、刚接单做桌面管理软件的自由开发者、以及想把 QT 的 Model/View 编程和 SQLite 事务机制一次摸熟的在职工程师。很多人把这类工程当成能跑就行的作业但真等你在 QT 5.15.2 MSVC2019 64 位环境下把源码打开往往先撞上一堵墙要么是依赖路径失效导致编译报错不少见要么是数据库驱动没带上导致程序启动后黑屏闪退。这篇笔记我就按自己的落地习惯从工程结构拆起一路讲到表的自增主键、选课防超选的 UPDATE 原子操作、release 构建和依赖打包最后落在一组避坑记录上。做完这套流程你拿到的就不只是能交差的 demo而是一个能真正部署给教务老师点开用的桌面工具。2. 先把工程的骨架讲透为什么教务选课系统天生适合 QTSQL 实体建模2.1 表结构设计与 QT 里的数据落地选型我不建议一上来就写界面。任何教务系统的底层都是数据模型第一步应该把实体关系画清楚。常见做法是拆成 6 张核心表学生表student_id 为主键存学号、姓名、专业、班级、教师表teacher_id、姓名、所属院系、课程表course_id、课程名、教师编号、学分、容量上限、选课记录表enrollment_id 自增、学生编号、课程编号、选课时间、教学班表可选处理一个课程多个老师的情况、系统配置表存学期学期名等全局参数单行即可。选课意愿与学生课程冲突检测这两块QT 的 QSqlRelationalTableModel 天然适合做外键关联展示——一个课程表字段关联教师表和教学班QTableView 上直接下拉显示教师姓名而不是裸 ID。数据库引擎选 SQLite理由很实际教务系统单机部署场景居多不需要独立的数据库服务进程SQLite 单文件备份方便老师拿 U 盘拷走整个系统也不丢数据。唯一要留意的是 SQLite 并发写锁——选课瞬间多个用户同时写你需要把事务打开一次提交一批操作。-- 建表脚本示例SQLite 方言 CREATE TABLE courses ( course_id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, teacher_id INTEGER NOT NULL, credit REAL DEFAULT 2.0, capacity INTEGER DEFAULT 60, FOREIGN KEY (teacher_id) REFERENCES teachers(teacher_id) );逻辑说明course_id用自增主键避免像手工维护字符串编号那样在CS101 和 CS102 之间跳号错乱。capacity的默认值保证插入语句少写一个字段也不会立刻出错。所有外键必须先建父表再建子表SQLite 默认PRAGMA foreign_keys是关闭的QT 连接建立后建议手动开一次否则外键约束形同虚设。参数说明credit 用 REAL 类型而不是 INTEGER是为了支持 2.5 学分的课程capacity 选 INTEGER 上限 60 只是初始值后续要在每次选课前用事务重查剩余名额。2.2 界面分层单窗口布局加三个核心 View架子搭好后进入 UI 层。这套系统最常见的布局是 QMainWindow 作为主窗体中央区域放 QTabWidget分成课程管理选课管理成绩查询三个页签。每个页签内部基本就是 QTableView 搜索框 按钮区的组合。左侧用一个 QTreeWidget 按学院或年级筛选算是加分项不做也不影响可用性。关键在 Model 与 View 的绑定。我的习惯是所有 QTableView 统一不直接操作界面控件拿数据而是全部通过 QSqlTableModel 或 QSqlQueryModel 完成读写。原因是 QT 的 Model/View 架构会自动帮你把界面刷新和编辑状态同步下来——当你在选课页签里点了提交选课只要调 model-submitAll()对应的视图会立刻反映数据库变化不需要手动去遍历每一行控件。// 课程表的 model 绑定示例 QSqlTableModel *courseModel new QSqlTableModel(this); courseModel-setTable(courses); courseModel-setHeaderData(1, Qt::Horizontal, QStringLiteral(课程名)); courseModel-setHeaderData(2, Qt::Horizontal, QStringLiteral(任课教师)); courseModel-setHeaderData(3, Qt::Horizontal, QStringLiteral(学分)); courseModel-select(); // select() 之后 view 上才看得到数据 ui-tableView-setModel(courseModel); ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); ui-tableView-setEditTriggers(QAbstractItemView::NoEditTriggers);逻辑说明先setTable再select这是 QSqlTableModel 的固定节奏直接 setTable 后不 select 视图是空白的。setEditTriggers设成 NoEditTriggers 是给选课页签用的——学生不应在表格里直接改已选记录编辑能力只留给管理端页面。参数说明setHeaderData只是改了显示列头不是改表结构如果想调整显示顺序用setColumnHidden隐藏 id 列例如courseModel-setColumnHidden(0, true)别去动 courses 表字段顺序。2.3 登录与权限区分学生端和教务端的下策与上策不少教学 demo 把登录做成了只校验用户名密码是否匹配。放在真实教务系统里这是不够的因为它无法阻止学生进入管理员功能页。我给你一个既简单又可靠的方案users 表加 role 字段LOAD 时按角色决定主窗口哪些 Tab 可见。实现上用 QStackedWidget 装两套界面面板或者干脆在主构造函数里把课程管理页签从 TabWidget 中移除。// 登录后按角色裁剪界面 bool isAdmin (user.role admin); ui-tabWidget-setTabVisible(ui-tabWidget-indexOf(ui-tab_manage), isAdmin); ui-tabWidget-setTabVisible(ui-tabWidget-indexOf(ui-tab_enroll), true);这个方案的边界也很清楚只做界面裁剪属于下策因为懂行的学生改一行代码或直接连 SQLite 文件就能绕过权限真正要求高的教务系统至少要在业务类里再判断一次当前用户角色甚至对登录后的操作记录写审计日志。但作为课程设计和中小型内部工具界面管控已经足够挡住绝大多数误点。3. 把核心功能跑通登录鉴权、选课防超选与课程冲突检测3.1 登录鉴权密码摘要落库与 LoginService 结构密码保存是这类系统最容易被人拿来说的地方。明文密码存库是绝对不行的——一旦 SQLite 文件被拷走这很常见所有账号全部暴露。正确做法是存加盐的哈希值。QT 里没有一键调用的 MD5 封装QCryptographicHash 支持 MD5但 MD5 不建议用于口令存储我建议用 SHA-256 搭配每个用户独立的盐值// 登录验证逻辑QT 5.15 SQLite QString salt QString::number(QRandomGenerator::global()-generate()); QString hash QCryptographicHash::hash( (salt password).toUtf8(), QCryptographicHash::Sha256).toHex(); // 验证时重新计算比对 QSqlQuery q; q.prepare(SELECT password_hash, salt FROM users WHERE username :u); q.bindValue(:u, username); q.exec(); if (q.next() q.value(0).toString() QCryptographicHash::hash((q.value(1).toString() password).toUtf8(), QCryptographicHash::Sha256).toHex()) { // 通过 }逻辑说明密码输入后先拼接盐再哈希数据库里存的是盐 哈希值两列。每次随机生成新盐即使两个用户设了同一个密码存储的哈希值也完全不同攻击者无法用彩虹表批量碰撞。这个流程没写进界面的原因是它属于业务服务层你可以在企业内部叫它 LoginService在这里它就是一个私有函数负责接收窗口传来的用户名和密码字符串返回角色枚举和用户 ID。注意别在登录函数里弹 MessageBox 做提示——界面反馈交给按钮的槽函数处理业务层只负责返回错误码比如 0 成功、1 用户不存在、2 密码错误。3.2 选课事务与防超选一条 UPDATE 的原子性扛住并发选课场景的经典坑是超选明明课程容量只有 60A 和 B 同时点击选课各自查出还剩 1 个名额然后同时插入两条记录结果变成 61 人。避免这个问题的核心不是加锁而是把检查名额 更新名额 插入选课记录合并成一个数据库事务并用一条带条件判断的 UPDATE 保证原子性// 事务式选课核心片段 db.transaction(); QSqlQuery q; q.prepare(UPDATE courses SET capacity capacity - 1 WHERE course_id :cid AND capacity 0); q.bindValue(:cid, courseId); int affected q.numRowsAffected(); q.exec(); if (affected 0) { db.rollback(); return 课程已经满员选课失败; } QSqlQuery q2; q2.prepare(INSERT INTO enrollments (student_id, course_id, enroll_time) VALUES (:sid, :cid, :time)); q2.bindValue(:sid, stuId); q2.bindValue(:cid, courseId); q2.bindValue(:time, QDateTime::currentDateTime().toString(Qt::ISODate)); if (!q2.exec()) { db.rollback(); return 选课失败请稍后重试; } db.commit();逻辑说明UPDATE 后面带上capacity 0条件SQLite 执行这条语句时天然串行化同一时刻只有一个写事务能成功更新到这个行。第一个事务把容量扣掉第二个事务再进来时容量已经是 0affected 为 0直接回滚。这比先 SELECT 再判断再 UPDATE 的老流程安全得多因为 SELECT 和 UPDATE 之间始终隔着一段窗口期。参数说明numRowsAffected()必须在 UPDATE 执行后下一次语句执行前读取如果你多执行了一条无关语句拿到的影响行数可能已经变了QT 的 QSqlQuery 在重用时才会重置状态。另外db.transaction()之前要确保db是单例连接多线程环境每个线程不能共享同一个 QSqlDatabase 连接对象。3.3 课程冲突检测时间段重叠判断的边界处理选课系统的另一个隐藏需求是课程冲突检测。比如学生选了周一 3-4 节的数据库原理就不能再选同一时间段的机器学习。时间段数据我建议用两个字段day_of_weekslot_start而不是存第几节到第几节的字符串。判断冲突时按学生的已选列表加载所有课程逐个比较星期几相同且时间区间重叠// 冲突检测辅助函数 bool isConflict(int dayOfWeek, float start, float end, const QVectorCourseTime enrolled) { for (const auto t : enrolled) { if (t.dayOfWeek dayOfWeek start t.end end t.start) { return true; } } return false; } // 调用处 QVectorQSqlRecord enrolled loadStudentCourses(stuId); bool conflict isConflict(day, 10.0f, 11.5f, enrolled);逻辑说明重叠判断的核心是start existing_end end existing_start这个公式能覆盖所有包含、相交、相等情况比写一堆if/else嵌套省心。用 float 表示时间节点属于基本方案够用如果用真实时间 QTime 类型也完全等价只是比较时用msecsTo或直接运算符重载。参数说明课程设计阶段把冲突检测做成选课按钮执行事务前的预检查即可不依赖数据库触发器。原因是 SQLite 的触发器能写但调试麻烦而且出错时定位成本高业务层用 C 做更直观后续也容易移植到 Qt6。3.4 数据加载性能为什么你的界面打开要卡 1 秒这套系统数据量不大几百个学生、几十门课、上千条选课记录正常情况下 QSqlTableModel 的 select() 毫秒级返回。但如果你的已选课程列表是每次打开 Tab 页签时重新 new 一个 QSqlQueryModel 全表查询时间就会增长。我的做法是主窗口构造函数里就把该填的 Model 都填充好只在数据发生变更时执行 select()界面操作和数据库操作分离能明显降低卡顿感。改数据后同步刷新模型的固定用法是// 数据变更后统一刷新指定的 model courseModel-select(); enrollmentModel-select(); ui-tabWidget-setCurrentIndex(1);这里的 select() 是 QSqlTableModel 的公开槽函数不是 QAbstractItemModel 的虚函数。调用后表格里的过滤器、排序字段如果用 QSortFilterProxyModel会自动重新执行不必重建整个 model 指针。4. 把工程跑在真实环境里QT 5.15.2 MSVC2019 windeployqt4.1 环境选择学 QT 该装哪一套组件QT 的装法不是只有一条路。仅从工程落地角度我推荐你用官方在线安装器装 QT 5.15.2尽管维护模式有点旧、勾选 Qt Charts如果要做成绩图表、Qt SQL SQLite 驱动和 Qt Debugger Tools。如果你是 VS2022 QT 的组合那么 MSVC2019_64 的 Kit 就行——VS2022 生成的二进制和 MSVC2019 的 QT 库兼容性很好。常见的翻车点装了 QT 却忘记勾选 Qt SQL 驱动QSQLITE 插件没进 Qt 安装目录程序运行时 QSqlDatabase::addDatabase(QSQLITE) 返回的 driver 为 null。排查方法是打印QSqlDatabase::drivers()如果列表里没有 QSQLITE说明你安装时没选数据库驱动模块只能回安装器补装。# 查看当前环境的可用数据库驱动 # 这是 QSqlDatabase::drivers() 输出的一个典型内容 QSQLITE QMARIADB QMYSQL QODBC QPSQL如果只有 QSQLITE 那一行说明环境正常。驱动列表里没有的引擎不能用这是 QT 规则和你的项目里是否 include 了头文件无关。4.2 构建配置的 3 个必修参数进入 QT Creator 后在构建套件Kit里要确认三件事不然很容易编译通过、运行闪退一是 Qt version 与 msvc 版本匹配。QT 5.15.2 配 MSVC2019_64 是标准组合配 msvc2015 会报 runtime mismatch配 msvc2022_64 虽然能用但你不能直接把 QT 6.x 的库链接进 5.15 工程链接器会优先找同一套环境的库。二是编译输出目录不要带中文或空格QT Creator 默认生成的构建目录如果包含非 ASCII 路径windeployqt 会因为路径解析问题漏拷文件。三是 debug 与 release 分开设置发布版务必切到 Release 模式——debug 模式下的依赖库名字带 d 后缀Qt5Cored.dll拷到没装 QT 的机器上必然报找不到。# 构建完成后用 windeployqt 做依赖收集release 模式 cd /d D:\build\CourseSystem-Desktop_Qt_5_15_2_MSVC2019_64-Release\release D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe --release --no-translations CourseSystem.exe逻辑说明windeployqt 会自动扫描 exe 的导入表把需要的 Qt5Core.dll、Qt5Widgets.dll、Qt5Sql.dll含 sqlite 驱动插件复制到 exe 同目录还会把 platforms/qwindows.dll 放进去。--no-translations是为了省掉用不上的一堆翻译文件。参数说明如果你的程序用了Qt Charts只运行上面这条命令还不够charts 模块的 dll 不会自动拷全生成后手动把 Qt5Charts.dll 和目标机缺失的 qml 或 imageformats 插件一并复制到发布目录即可。发布目录最终长这样发布目录/ CourseSystem.exe Qt5Core.dll Qt5Widgets.dll Qt5Gui.dll Qt5Sql.dll Qt5Charts.dll platforms/ qwindows.dll sqldrivers/ qsqlite.dll styles/ qmodernwindowsstyle.dll4.3 记录一次典型启动崩溃的排查过程有同学在目标机器上双击 exe提示Qt: qsqlite driver not loaded然后程序自动退出。这个问题的直接原因是 sqldrivers/qsqlite.dll 没被复制到发布目录或它在 QT 目录里没被构建出来。排查路径是先回到开发机确认QSqlDatabase::drivers()输出包含 QSQLITE然后用 Vitual Studio 的 dependency walker或更轻量的 Dependencies 工具打开 qsqlite.dll检查依赖的 Qt5Sql.dll 是否带 d 后缀、路径是否正确最后确认发布目录位置是否放在 exe 同级的 sqldrivers 子目录下。QT 加载插件目录固定是exe所在目录/sqldrivers/和exe所在目录/platforms/不是任意路径。5. 从开发机到教师电脑发布打包与运行时避坑5.1 运行时缺 Qt 平台插件 windows 的完整处理这是每次给客户交付必出现的问题。现象是双击后弹窗报错qt.qpa.plugin: could not find the Qt platform plugin windows in 。很多人第一反应去网上找 dll 下载这是最危险的做法——dll 来源不明可能导致运行时崩溃甚至安全风险。正确做法只有一步重新跑一次 windeployqt让它把 platforms 目录彻底生成。# 提示缺失 platform 插件时的第一行动 D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe --release --no-translations --no-system-d3d-compiler --no-opengl-sw CourseSystem.exe说明--no-opengl-sw省掉软件渲染的 opengl32sw.dll大部分机器有显卡驱动--no-system-d3d-compiler跳过 D3D 编译器 dll缩减体积。如果目标机器是学校机房的老电脑记得不省略 opengl32sw.dll否则可能在集成显卡上显示黑屏。参数说明这里面的 QPAQt Platform Abstraction插件 qwindows.dll 决定了 QT 窗口能不能在 Windows 上打开。缺失时不管主程序多大、依赖多全QT 直接拒绝启动。校验发布目录是否完整看 platforms 子目录是否存在、其下 qwindows.dll 文件是否非 0 字节。5.2 数据库初始化首次启动是建库还是用现成文件教务系统交付时目标电脑多半是洁净环境没有任何依赖。你要决定首次启动时是生成一个新数据库还是复制一个已导入好的学校数据文件。我见过最稳的做法是程序启动时先检查数据库文件是否存在不存在就执行 resources 里内嵌的建表 SQL 脚本然后插入系统管理员默认账号、基础院系列表等种子数据最后弹出初始密码请第一时间修改的 MessageBox。这种做法的好处是不需要额外发布一个 db 文件目标机总能从干净状态启动。// 启动时自动初始化数据库 QFile sqlFile(:/res/schema.sql); if (!db.exists()) { if (sqlFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QString sql sqlFile.readAll(); QSqlQuery q; q.exec(sql); // 一次性执行多条建表语句 } // 插入默认 admin 账户 QSqlQuery q; q.prepare(INSERT INTO users (username, password_hash, salt, role) VALUES (admin, :hash, :salt, admin)); // ... 绑定后 exec }逻辑说明:resources/schema.sql是编译时内嵌的资源不暴露路径也不怕目标机误删。用单条q.exec(sql)执行多语句时SQLite 驱动要求句子以分号分隔QT 内部会分段执行。如果你担心某一条建表语句失败导致初始化中断可以在 exec 后检查q.lastError()日志并写入本地 debug 文件便于交付排障。提示如果首次启动建库失败多半是资源文件没有正确加载——检查 .qrc 里的路径前缀是否以/开头以及文件是否真的加进去了。这个坑我踩过三次。5.3 中文字符显示与乱码从编码层面一次性解决QT 5.15 默认源码编码是 UTF-8除非你在项目文件里显式指定了其他编码。但 MSVC 编译器在 Windows 默认用本地代码页GBK解析源文件。如果你的源码里写了中文但不带 UTF-8 BOMMSVC 就会把中文当作 GBK 读导致界面按钮上出现乱码。解决方式有两个二选一一是给 .cpp/.h 源文件统一加上 UTF-8 BOM让 MSVC 自动识别为 UTF-8二是工程内所有中文字符串用 QStringLiteral() 或 QString::fromUtf8() 包住。我偏好前者因为 QStringLiteral 能防乱码但你写漏一个字符串就会出问题BOM 一劳永逸。# 用 powershell 批量给项目源文件加 BOM Get-ChildItem -Recurse -Include *.cpp,*.h | ForEach-Object { $content [System.IO.File]::ReadAllText($_.FullName) [System.IO.File]::WriteAllText($_.FullName, $content, [System.Text.UTF8Encoding]::new($true)) }逻辑说明加了 BOM 之后MSVC 正确按 UTF-8 编译QT 里的中文直接呈现。注意别对 .qrc、.ui 文件做这个操作它们有自己的编码规则。这个版本问题在 QT6 里基本消失编译器默认 UTF-8但 5.15.2 的工程必须先处理。5.4 多线程与界面卡顿什么时候不建议用 QThread选课高峰期、导入 Excel 名单、批量生成成绩表这类操作会在主线程里跑数据库循环界面会进入未响应状态。正确做法是把耗时操作移到 QThread 里。但 QThread 不是万能药如果你在线程里直接操作主线程创建的 QWidget 或 QSqlDatabase 连接必然崩溃或者行为诡异。我在这个工程里推荐的方式是 QThread 信号槽传结果代码如下class ImportWorker : public QObject { Q_OBJECT public slots: void run() { QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE, import_conn); db.setDatabaseName(system.db); db.open(); // 循环导入... emit finished(rowCount); db.close(); } signals: void finished(int count); }; // 调用侧 QThread *thread new QThread; ImportWorker *worker new ImportWorker; worker-moveToThread(thread); connect(thread, QThread::started, worker, ImportWorker::run); connect(worker, ImportWorker::finished, this, [this](int count){ QMessageBox::information(this, tr(导入完成), tr(共导入 %1 行).arg(count)); }); connect(worker, ImportWorker::finished, thread, QThread::quit); thread-start();注意每个线程要使用独立的数据库连接QSqlDatabase::addDatabase 的第二个参数必须是一个新的连接名import_conn否则 QT 直接 warning 并在运行时拒绝访问报connection already in use。6. 基于这个 zip 继续往上走MVVM 改造、权限细化和重生课表的查重写到这里你已经能把工程跑通、发布并交付了。接下来说两个我建议你投入的方向它们比单纯加功能更值钱。第一个方向是顺路把工程往 MVVM 架构上靠。头图里那个结构体、写入内存缓冲区都是底层细节但在界面端你可以引入 QSortFilterProxyModel 做展示层过滤把学生按学院筛选和课程按学分排序这些逻辑全部从 widget 槽函数剥离出来只改 model 的 setFilterFixedString界面不变可维护性上一个大台阶。第二个方向是成绩导出。教务系统交到老师手上后最频繁的操作其实是导 Excel——做成绩登记表、打印选课名单。QT 侧没有现成的 xlsx 库但我建议不要引入重量级第三方库自己写一个 CSV 导出即可用 QTextStream 写 UTF-8 with BOMExcel 直接打开不乱码。如果将来必须导出 XLSX再考虑编译 QXlsx 源码CSV 方案永远是最低成本的后悔药。QFile file(选课名单.csv); file.open(QIODevice::WriteOnly); QTextStream out(file); out.setEncoding(QStringConverter::Utf8); out \xEF\xBB\xBF; // UTF-8 BOM out 学号,姓名,课程名,学分\n; for (int row 0; row model-rowCount(); row) { out model-record(row).value(student_id).toString() , model-record(row).value(name).toString() , model-record(row).value(course_name).toString() , model-record(row).value(credit).toString() \n; }这里用 QTextStream 手动设置 UTF-8 编码确保 Excel 2016 及以上版本打开不乱码。如果你接手的项目里已有 QXlsx那直接走xlsx.write(row, col, value)接口但没必要为一个教务表引入第三方依赖。最后说一句我的使用习惯这类 zip 解压后我永远先建一个独立的测试库test.db把课程容量设到 2、用学生 A/B 各选一次课去验证事务防超选而不是直接灌真实数据。这个习惯救过我很多次——数据坏了重建很容易但教学环境里学生账号被误删就很麻烦。项目本身不难难的是你有没有把边界情况和失败路径想清楚。希望这篇笔记能在你接手类似选课系统时不踩太多坑顺利交付一套让使用者真正打开能用的桌面端。本文还有配套的精品资源点击获取