
简介基于MFC框架的增进版学生选课系统面向高校、培训机构的教务选课场景也适合MFC初学者、C课程设计与毕业设计参考。系统在传统选课基础上优化了操作流程涵盖用户角色管理、课程信息发布、选课退课、数据统计、消息通知与权限验证等模块能缓解选课冲突、高峰期响应慢等问题。资源包为rar压缩格式共85个文件、约3.11MB核心是20个头文件与19个cpp源文件另含1个可直接运行的exe、1个mdb数据库、7个位图与2个图标以及dsp/dsw等VC6.0工程文件目录结构清晰便于加载编译和二次开发。目前已有256人学习下载。阅读源码可掌握MFC文档视图架构、数据库连接与用户权限控制等实现细节配合界面资源和数据库文件能快速梳理选课系统的完整业务流程系统设计采用模块化思路按登录、课程、学生、教师等业务类拆分适合作为课程设计、期末项目或MFC入门练习的参考资料。1. 增进版学生选课系统到底值不值得做先认清它解决的是“交差”而不是“炫技”老读者应该都见过这种场景某高校的数据库课程设计答辩现场前面几位同学打开网页版选课系统卡在登录页半天进不去轮到某同学双击一个 exe三秒进主界面课程列表、余量、已选课程清清楚楚点一下选课按钮余量当场减一台下老师直接点头。这个 exe 就是传说中口碑两极分化的“增进版学生选课系统 MFC 选课系统”。不做 C 的人觉得它老气做过的人知道它其实是最稳的课程设计选题之一。MFC 选课系统本质是一个基于微软基础类库的 Windows 桌面程序配合 Access 或 SQLite 数据库完成学生登录、课程浏览、选课退课、管理员维护课程这些闭环操作。这篇文章不吹不黑只讲清楚它怎么做、参数怎么调、坑在哪以及它为什么至今仍是“能交差”的代名词。2. 先把系统拆清楚需求边界、角色权限与四张表的取舍2.1 需求拆解三张表的系统只能做演示四张表才能谈“改进”很多人拿到“学生选课系统”这个题目第一反应是建三张表学生表、课程表、选课记录表。学生登录看到全部课程点选课记录表里插一行完事。这种三表模型在演示时确实跑得通但答辩老师随口问一句“怎么防止同一门课选两次”或者“管理员怎么修改课程信息”当场就卡壳。增进版选课系统的核心增量在“管理端”。我做完这个系统的第一版后回头加了一张管理员表把“维护课程数据”和“查看统计数据”从学生视角里拆出去整个程序的逻辑才真正闭合。四张表的结构是学生表存账号密码和基本信息管理员表独立存放并且单独加密课程表带容量和已选人数字段选课记录表负责学生和课程的关联。这一层拆完之后“谁能干什么”就清楚了学生只能看课、选课、退课、看自己的课表管理员能增删改课程、查选课统计、重置学生密码。这个边界一旦明确代码里所有的权限判断都变得有据可依。2.2 数据库设计建表语句与关键字段约束无论你最后用 Access 还是 SQLite表结构是一样的。我习惯先用 SQL 把表建好再在 MFC 里用 ODBC 连过去。下面这套建表语句是经过多次课程设计验证的骨架字段命名简单直接方便在 CRecordset 里映射。-- 学生表 CREATE TABLE student ( stu_id TEXT PRIMARY KEY, -- 学号登录账号 stu_name TEXT NOT NULL, -- 姓名 stu_pwd TEXT NOT NULL, -- 登录密码 stu_major TEXT -- 专业 ); -- 管理员表 CREATE TABLE admin ( admin_id TEXT PRIMARY KEY, admin_pwd TEXT NOT NULL ); -- 课程表 CREATE TABLE course ( course_id TEXT PRIMARY KEY, -- 课程编号 course_name TEXT NOT NULL, -- 课程名称 teacher TEXT, -- 授课教师 credit REAL DEFAULT 2.0, -- 学分 capacity INTEGER NOT NULL, -- 课程容量 selected INTEGER DEFAULT 0 -- 已选人数需要手动维护 ); -- 选课记录表 CREATE TABLE enroll ( enroll_id INTEGER PRIMARY KEY AUTOINCREMENT, stu_id TEXT NOT NULL, course_id TEXT NOT NULL, enroll_time TEXT DEFAULT (datetime(now,localtime)), UNIQUE(stu_id, course_id) -- 防止同一学生重复选同一门课 );这里有一个值得说明的设计决策课程表里的 selected 字段是“冗余字段”。明明选课记录表能统计出已选人数为什么还要在课程表里存一个因为选课系统最常见的操作是“看所有课程并显示余量”如果每次刷新列表都去 enroll 表做 GROUP BY 统计课程多的时候界面会明显变慢。selected 字段带回来的问题是数据一致性这个在第 4 章讲事务时会专门处理。另一个重点是 UNIQUE 约束它是数据库层面防止重复选课的底线哪怕代码里漏判了数据库也会报错兜底。2.3 表关系与业务约束谁管数据、谁只做操作四张表的关系其实只有一条主线enroll 表通过 stu_id 和 course_id 分别关联 student 表和 course 表。开发者最容易忽略的是删除课程时的关联处理。管理员删除一门课时如果 enroll 表里还有记录要么先删 enroll 再删 course要么强制提示“该课程已有学生选课不能删除”。我第一版就是没处理这个直接删 course 导致程序崩溃后来才明白这叫外键约束缺失的连锁反应。Access 和 SQLite 默认都不强开外键MFC 程序里更不会自动帮你管所以业务逻辑必须自己做。再往细看“增进版”还应该包含一个学分上限的约束比如每个学生最多选 20 学分超过就不能再选。这个约束放哪最合适放数据库层可以通过触发器实现但 MFC 课程设计里用触发器调试不方便我一般放在程序逻辑里选课前先查该生已选课程的学分总和加上新课程学分超过上限就弹窗拒绝。这个判断写在业务层的好处是错误信息可以给得很友好比如“你已选 18 学分这门课 4 学分超过 20 学分上限”答辩时这种细节非常加分。3. 从空窗口到主界面MFC 框架选型、ODBC 连接与课程列表加载3.1 框架选型为什么 MFC 课程设计最稳的是“登录框 对话框主界面”MFC 程序可以做成单文档、多文档、基于对话框三种形态。选课系统这种界面固定、操作密集的应用我最推荐基于对话框。原因很简单单文档的 Document/View 架构会把“课程列表、已选列表、操作按钮”拆到多个视图里切换逻辑写起来繁琐而且 MFC 的文档序列化机制对数据库程序几乎是多余的。对话框程序则是一块画布左边放一个树控件或按钮组做导航右边放列表控件显示课程数据逻辑清晰直观。登录单独用一个模态对话框密码校验通过后再进入主对话框这个模式也是实际项目里最常见的组合。主对话框的控件排布我给出一个经过多次调整的参考布局左侧用 CTreeCtrl 做导航节点是“全部课程”“我的课表”“个人信息”中间是一个 CListCtrl 报表视图列依次是课程编号、课程名称、教师、学分、容量、已选、余量底部放两个按钮“选课”和“退课”再加一个“刷新”按钮。这个布局的好处是拿不到界面上新但功能一目了然答辩时老师扫一眼就知道系统完整。3.2 用 ODBC 连接 Access 数据库连接代码与参数说明数据库连接是整个系统的命门。Access 数据库文件.mdb通过 ODBC 驱动连接是最常见做法因为 Visual Studio 自带的数据源工具对 Access 支持最好而且不需要额外安装数据库服务。连接代码通常封装在一个全局函数里程序启动时调用一次#include afxdb.h CDatabase g_db; // 全局数据库连接对象 BOOL InitDatabase(LPCTSTR lpszFilePath) { // 构造 ODBC 连接字符串指定驱动和数据库文件路径 CString strConn; strConn.Format(_T(DRIVER{Microsoft Access Driver (*.mdb, *.accdb)};DBQ%s), lpszFilePath); // 打开数据库第二个参数为 FALSE 表示不显示 ODBC 连接对话框 // 第三个参数是打开选项只读模式用 CDatabase::openReadOnly BOOL bOK g_db.OpenEx(strConn, CDatabase::noOdbcDialog); if (!bOK) { AfxMessageBox(_T(数据库连接失败请检查 student.mdb 是否位于程序目录)); return FALSE; } return TRUE; }参数说明OpenEx 的第一个参数是完整连接字符串驱动名称必须和本机安装的 ODBC 驱动一致64 位系统装 64 位 Office 时驱动名是“Microsoft Access Driver (*.mdb,.accdb)”32 位程序配 32 位驱动则更早版本的驱动名可能是“Microsoft Access Driver (.mdb)”。如果你的程序死活连不上先查这个驱动名这是新手最容易翻车的地方。第二个参数 noOdbcDialog 一定要给否则程序跑起来会弹一个 ODBC 配置窗口答辩现场非常尴尬。连接字符串里的 DBQ 是 Access 文件的绝对路径建议把数据库文件和 exe 放同一目录程序启动时用 GetModuleFileName 获取当前目录再拼接文件名避免硬编码路径。3.3 把课程表装进 CListCtrl加载、排序与刷新连接上数据库之后第一个要实现的交互就是课程列表的展示。CListCtrl 的用法有几个固定的坑这里给出一个加载课程列表的完整函数我在多个项目里都是这个模板void CMainDlg::LoadCourseList(CListCtrl listCtrl, LPCTSTR lpszFilter) { // 1. 刷新前清空旧数据否则二次加载会残留上一次的行 listCtrl.DeleteAllItems(); // 2. 打开记录集查询条件由外部传入支持搜索 CRecordset rs(g_db); CString strSQL; if (lpszFilter NULL || _tcslen(lpszFilter) 0) strSQL _T(SELECT course_id, course_name, teacher, credit, capacity, selected FROM course ORDER BY course_id); else strSQL.Format(_T(SELECT course_id, course_name, teacher, credit, capacity, selected FROM course WHERE course_name LIKE %%%s%% ORDER BY course_id), lpszFilter); rs.Open(CRecordset::forwardOnly, strSQL); // 3. 逐行读取记录插入列表控件InsertItem 后 SetItemText 填充各列 int nIndex 0; while (!rs.IsEOF()) { CString id, name, teacher, credit, cap, sel; rs.GetFieldValue(_T(course_id), id); rs.GetFieldValue(_T(course_name), name); rs.GetFieldValue(_T(teacher), teacher); rs.GetFieldValue(_T(credit), credit); rs.GetFieldValue(_T(capacity), cap); rs.GetFieldValue(_T(selected), sel); // 余量字段动态计算容量 - 已选 int nCap _ttoi(cap), nSel _ttoi(sel); CString remain; remain.Format(_T(%d), nCap - nSel); listCtrl.InsertItem(nIndex, id); listCtrl.SetItemText(nIndex, 1, name); listCtrl.SetItemText(nIndex, 2, teacher); listCtrl.SetItemText(nIndex, 3, credit); listCtrl.SetItemText(nIndex, 4, cap); listCtrl.SetItemText(nIndex, 5, sel); listCtrl.SetItemText(nIndex, 6, remain); nIndex; rs.MoveNext(); } // 4. 关闭记录集释放数据库游标 rs.Close(); }这段代码里有三个要点值得反复看。第一DeleteAllItems 必须放在最前面否则第二次刷新时数据会叠在旧数据下面这是 CListCtrl 最典型的“数据看着像没更新”问题。第二GetFieldValue 拿到的都是 CString数字字段需要转成 int 再算余量直接拿字符串拼接会导致余量显示成“12-5”这种诡异格式。第三forwardOnly 类型的记录集只能向前移动所以必须用 MoveNext 循环不能用随机定位。如果你需要实现点击列头排序就不能再用 forwardOnly要改成 dynaset 并把列表数据先装进内存数组。4. 选课与退课的核心逻辑事务、余量判断与冲突检测4.1 选课前要做哪三道检查顺序不能乱选课按钮的逻辑是整个系统里最容易写漏的。很多新手直接写一句“INSERT INTO enroll VALUES(...)”就完事然后出现一人选多门相同课、课程爆满还能选进去、选完发现学分超限。我在第三版系统里总结出了一个固定的检查顺序按这个顺序踩过所有坑第一查重复选课先查 enroll 表里是否已有该生该课的记录有就直接提示不再往下走。第二查余量把课程表里的 capacity 和 selected 拿出来比较selected capacity 就提示课程已满。第三查学分上限把该生已选课程的学分累加加上当前课程学分超过设定阈值就拒绝。这三步顺序不能乱因为它们的代价是递增的查重复是最快的单条 SELECT查余量需要读课程表查学分上限需要 JOIN 多张表先做便宜的检查再做贵的检查逻辑上更合理。4.2 用一行 UPDATE 做余量扣减和防超选查完三道检查后真正的选课操作包含两个步骤往 enroll 表插入记录以及把 course 表的 selected 字段加一。这两个操作必须放在一个事务里否则插入成功但余量没扣下次选课就会把课程撑爆。更关键的是余量扣减不能写成“先 SELECT selected在代码里加一再 UPDATE”因为两个窗口同时操作同一门课时会出现竞态条件也就是经典的“最后一席被两个人同时抢到”。正确的做法是用一条带条件的 UPDATE 原子完成扣减让数据库来判断余量是否足够BOOL EnrollCourse(LPCTSTR stuId, LPCTSTR courseId) { // 事务开始前先检查重复选课此处省略查重代码 // ... CString strSQL; // 原子扣减只有当 selected capacity 时才会更新成功 // 使用 CDatabase::ExecuteSQL 直接执行不走记录集 strSQL.Format(_T(UPDATE course SET selected selected 1 WHERE course_id %s AND selected capacity), courseId); // 开始事务 g_db.BeginTrans(); BOOL bOK TRUE; try { // 第一步扣减余量检查影响行数 // CDatabase 没有直接返冄影响行数的接口这里用 ExecuteSQL // 更可靠的做法是用 CRecordset 的 GetRowCount但需要驱动支持 g_db.ExecuteSQL(strSQL); // 第二步插入选课记录 strSQL.Format(_T(INSERT INTO enroll (stu_id, course_id, enroll_time) VALUES (%s, %s, datetime(now,localtime))), stuId, courseId); g_db.ExecuteSQL(strSQL); // 都成功再提交事务 g_db.CommitTrans(); bOK TRUE; } catch (CDBException* e) { // 任何一步失败都回滚保证余量和记录一致 g_db.RollbackTrans(); e-ReportError(); e-Delete(); bOK FALSE; } return bOK; }这段代码的核心技巧是把“判断余量”和“扣减余量”合并进同一条 UPDATE 的 WHERE 条件里。selected capacity 既是一个业务判断也是一个并发锁数据库执行 UPDATE 时会对该行加锁两个并发请求只有一个能成功另一个更新影响行数为 0程序据此判定选课失败。这就绕开了 MFC 里 CRecordset 做并发控制的复杂问题原理不复杂但非常实用。事务的边界要圈住“扣减余量”和“插入记录”两个操作任何一个失败都回滚这样就不会出现 enroll 有记录但 course 余量不动或者余量扣了但选课记录没写进去的脏数据。4.3 退课与“后悔药”恢复余量、释放记录退课是选课的逆操作同样需要事务保护。逻辑上很简单从 enroll 表删除记录同时把 course 表的 selected 减一。删除前要确认这条记录确实存在而且只能由选课人本人操作。这里有一个细节如果用户已经因为超学分被限制选课退课时同样要先算余量——这门课本来就满的selected 减一之后不能变成负数。虽然正常情况不会发生但做一下边界判断总归更稳。退课的代码结构跟选课完全对称区别在于 UPDATE 的方向是递减WHERE 条件里加一个 selected 0 防止负值。另外退课完成后主界面的课程列表和“我的课表”都要联动刷新否则界面还显示旧状态用户会以为退课失败。刷新时机放在事务提交成功之后不要在回滚路径里刷新。5. MFC 选课系统避坑五个血泪经验换来的排查清单5.1 乱码列表控件里中文显示成问号现象程序运行后课程名称和教师姓名在 CListCtrl 里显示成一串“???”或者乱码。原因项目字符集设置和源文件编码不一致最常见的是工程用了多字节字符集但代码文件里写了带中文的字符串常量或者数据库里的中文字段读取时编码不匹配。解决打开项目属性把“字符集”从“使用多字节字符集”改成“使用 Unicode 字符集”同时把所有 CString 相关的格式化函数统一用 _T() 宏包住字符串。如果已经改了 Unicode 还乱码检查 Access 数据库表字段的类型是不是“文本”有些同学建表时不小心把字段设成了“备注”读取时编码处理会异常。这个坑在课程设计里出现频率极高十个人里至少三个中招。5.2 数据库连接失败驱动名不一致玄学的另一半是位数问题现象程序在开发机上跑得好好的换一台电脑就弹“数据库连接失败”。原因目标机器没有安装对应的 Access 数据库引擎或者 Office 版本从 32 位换成了 64 位导致 ODBC 驱动名对不上。解决程序发布时把 Access 数据库引擎安装包一起带上并且在连接字符串里动态检测驱动。检测方法很简单用 SQLGetInstalledDrivers 遍历本机已安装的 ODBC 驱动选包含“Access Driver”的那个驱动名拼进连接字符串。另一个常见情况是程序编译成 32 位但机器上只有 64 位的 Access 驱动二者不匹配。我一般会把整个解决方案平台改成 x86 发布因为 Access 驱动在 32 位下兼容性最好不要用 Any CPU。5.3 刷新列表后数据“叠罗汉”旧行不消失现象点刷新按钮后列表里新数据下面还留着旧的课程行甚至能看到重复记录。原因LoadCourseList 函数里忘了 DeleteAllItems或者调用时机不对。解决把 DeleteAllItems 放在函数最前面确保数据源重新加载前旧数据已经清空。还有一个隐蔽情况如果 CListCtrl 设置了“整行选中”属性DeleteAllItems 后表头还在但行数据必须重新 InsertItem不要直接在已有行上 SetItemText。另外子项文本的列索引从 0 开始InsertItem 是第一列SetItemText 从第二列开始填充这个顺序别搞反。5.4 选课卡死长时间无响应原因是记录集没关闭现象选课或刷新操作偶发卡死程序转圈只能强制结束进程。原因CRecordset 对象打开后没有及时 Close数据库游标被占用后续的 SQL 操作被阻塞。MFC 的 CRecordset 在析构时会自动关闭但如果你在循环里频繁 Open 而不 Close在 Access 这种文件型数据库上很容易撞上文件锁。解决养成习惯每次用完记录集显式调用 Close把 rs 对象的作用域缩到最小尽量在一个函数内完成打开、读取、关闭的全过程。事务操作也一样BeginTrans 之后一定要有 CommitTrans 或 RollbackTrans 与之配对漏掉一个就会让数据库进入悬挂事务状态。5.5 卸不掉的黑匣子Debug 运行正常Release 发布就报错现象在 Visual Studio 里按 F5 一切正常双击 Release 版 exe 却闪退或者弹错误。原因最常见的是初始化 MFC 控件库的代码被裁剪。对话框程序里的 CListCtrl、CTreeCtrl 属于公共控件MFC 在初始化时需要通过 InitCommonControlsEx 注册这些控件类Debug 模式下库会自动加载Release 下有时会失效。解决在应用程序类的 InitInstance 最前面调用 AfxOleInit 和 InitCommonControlsEx并把 _AFXDLL 和 _UNICODE 等宏的链接选项检查一遍。顺带一提Release 版闪退还有一个高频原因是局部变量没初始化特别是指针类型的控件变量Debug 下内存自动清零掩盖了问题Release 下随机值直接让程序崩溃。代码里所有控件变量的指针声明后立即置 NULL。6. 进阶让老师舍得给分的三个改进点和一个演示技巧改进点一给列表加搜索过滤。在课程列表上方放一个编辑框用户输入课程名关键字LoadCourseList 函数的 lpszFilter 参数传入关键字SQL 里用 LIKE 模糊匹配。这一步改动量不大但把“系统只能看全部课程”升级成了“系统支持检索课程”答辩话术立刻多了一个可讲点。改进点二做一份选课统计报表。用一个独立的对话框列出每门课的选课人数、余量、选课率数据从 enroll 表 GROUP BY 后 JOIN course 表获取展示方式可以用另一个 CListCtrl 或者简单画一个柱状图——MFC 里用 CStatic 控件自绘也不难。改进点三把密码改成 MD5 存储。数据库里 admin_pwd 和 student 表的 stu_pwd 不存明文登录时对输入密码做 MD5 后比对。MFC 里没有现成的 MD5 封装写一个几十行的工具函数即可。三个点做完系统的完整度和专业感会明显上一个台阶。演示技巧讲一个最实用的提前准备一份“压力测试”脚本。答辩时老师通常会问“多人同时选课会怎样”你不能真拿两台电脑演示但可以在数据库里预先造几百条学生数据再用一个循环快速执行选课操作。写一个 Debug 用的按钮批量给同一名学生选 50 门课故意触发学分上限和重复选课的拦截提示。现场演示时点这个按钮程序连续弹出“重复选课”“学分超限”“课程已满”三个对话框比口头解释有说服力得多。我在某次答辩前灵机一动加了这个小功能当场演示后老师只追问了一个问题“这个事务隔离级别是怎么设的”——能问到这个问题就说明他已经默认这套系统是完整可用的了。希望这篇拆解能帮你把 MFC 选课系统做扎实少踩几个我当年踩过的坑祝答辩顺利。本文还有配套的精品资源点击获取