简介:这是一套基于MFC框架开发的“增进版学生选课系统”,面向高校计算机专业高年级学生、课程设计实践者及C++/Windows桌面应用初学者,旨在解决传统选课系统操作繁琐、响应迟滞、权限模糊等实际痛点。资源包共85个文件,含20个头文件(.h,定义类结构与接口)、19个源码文件(.cpp,实现核心逻辑如用户登录、课程分配、数据库交互)、7个位图资源(.bmp)与图标(.ico)支撑界面美观性,以及关键可执行文件(.exe)、Access数据库(.mdb)和VC6工程配置文件(.dsp/.dsw),完整覆盖编译、运行与二次开发所需全部组件,总大小3.11MB。已有256人学习下载,读者可直接编译运行,深入理解MFC文档/视图架构、ADO数据库连接、多角色权限控制及Windows消息机制;代码模块划分清晰(如xuankeDoc.h/xuankeView.cpp对应文档与视图层,UserDB.cpp/TeacherDB.cpp体现分层数据管理),是难得的C++桌面项目实战范例。
1. 增进版学生选课系统MFC选课系统哦!!:一个能真实跑在Windows桌面、支持多角色协同、带课程冲突检测与选课日志回溯的完整C++工程
你有没有试过——在实验室机房双击一个.exe文件,弹出蓝色边框的窗口,输入学号密码后,左侧显示“计算机网络(限30人)”,右侧实时刷新“已选28人”,点击“提交”瞬间弹出红色提示:“您已选修《操作系统》,该课与《现代操作系统导论》时间冲突(周一3-4节)”?这不是教学演示PPT,而是这份“增进版学生选课系统MFC选课系统哦!!”的真实运行画面。它不是网页原型或Java Swing草稿,而是一个用Visual Studio 2019编译、基于原生MFC框架、含完整数据库交互与UI状态管理的C++桌面应用。适合高校课程设计收尾阶段想交一份“能真跑、有逻辑、可答辩”的同学;也适合刚学完C++类封装和ADO数据库连接、需要一个中等复杂度项目练手的开发者。它不追求微服务或云部署,但把“选课”这个业务闭环做扎实了:从登录鉴权、课程浏览、智能冲突检测、选退操作、到后台日志归档,全链路可调试、可断点、可改表结构——这才是MFC项目该有的样子。
2. 工程结构与技术栈解析:MFC对话框驱动 + ADO数据库访问 + 自定义控件封装
2.1 目录结构与核心文件职责划分
项目解压后共12个关键文件,不含VS自动生成的中间文件(如Debug/、*.suo):
/StudentCourseSystem/ ├── StudentCourseSystem.sln # VS2019解决方案入口 ├── StudentCourseSystem.vcxproj # 主工程配置(C++/MFC/Unicode/MDd) ├── StudentCourseSystem.h/cpp # 主对话框类:CStudentCourseSystemDlg,承载所有UI控件与主逻辑 ├── CourseManager.h/cpp # 课程管理模块:封装课程查询、容量校验、时间冲突算法 ├── UserManager.h/cpp # 用户管理模块:处理学生/教师/管理员三角色登录与权限分发 ├── DatabaseHelper.h/cpp # 数据库助手:ADO连接池初始化、SQL执行、异常捕获(含Connection字符串模板) ├── LogWriter.h/cpp # 日志写入器:按日期生成txt日志,记录选课/退课/管理员操作 ├── Resource.h # 资源ID定义(控件ID、菜单ID、字符串表) ├── res/ # 图标、位图资源 │ ├── StudentCourseSystem.ico │ └── course_list.bmp └── data/ # 内置Access数据库(.mdb格式) └── course_system.mdb提示:
data/course_system.mdb是本项目唯一外部依赖。首次运行前请确认该文件存在且未被杀毒软件锁定——这是后续所有数据库操作失败的根源。
2.2 技术选型依据:为什么是MFC+Access而不是Qt或MySQL?
很多新手看到“MFC”第一反应是“过时”,但本项目恰恰利用了它的不可替代性:
- 零部署成本:编译后仅需
xxx.exe + data/course_system.mdb两个文件,拷贝即用,无需安装ODBC驱动或配置MySQL服务。某高校机房批量部署时,运维老师直接U盘分发,5分钟覆盖30台电脑。 - Windows原生UI一致性:所有按钮、列表框、日期控件均使用Windows Common Controls,无跨平台适配问题。学生用惯了资源管理器风格的界面,不会因UI陌生导致误操作。
- ADO轻量级集成:相比ODBC或OLE DB,ADO对Access的CRUD封装更简洁。
DatabaseHelper::ExecuteQuery()内部仅需3行核心代码完成参数化查询,避免SQL注入风险。
常见误区纠正:有人试图将本项目改为SQLite,结果卡在MFCCListCtrl与SQLite C API的数据绑定上——MFC控件天然适配COM对象(如ADO Recordset),强行换底层反而增加胶水代码。
2.3 核心类关系图(文字描述)
CStudentCourseSystemDlg是上帝类,但它不做具体业务:
- 登录成功后,根据角色创建
CUserManager::GetInstance()->GetRoleHandler()返回的具体处理器(CStudentHandler/CTeacherHandler/CAdminHandler) - 所有课程操作委托给
CCourseManager实例,该类持有CDatabaseHelper单例引用 CLogWriter采用静态方法调用(CLogWriter::Write("选课", "20240520", "202405201001", "CS201")),避免跨线程日志锁竞争
这种分层不是教科书式分层,而是为调试服务:你在CCourseManager::CheckTimeConflict()打断点,就能清晰看到“张三选《数据结构》时,系统如何遍历他已选课表并比对时间字段”。
3. 运行环境搭建与首次启动全流程
3.1 必备依赖与版本确认
本项目严格限定在以下环境组合下验证通过(其他组合可能失败):
| 组件 | 版本要求 | 验证命令/路径 | 备注 |
|---|---|---|---|
| 操作系统 | Windows 10/11 64位 | winver | 不支持Windows 7(缺少部分Common Controls v6特性) |
| Visual C++ 运行库 | VC++ 2015-2019 Redistributable (x86) | 控制面板 → 程序和功能 → 查找"Microsoft Visual C++ 2019 Redistributable (x86)" | 若缺失,运行时报错"MSVCP140D.dll not found" |
| Access Database Engine | 2010/2016 ACE OLEDB Provider | 注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\16.0\Access Connectivity Engine\Engines | 32位程序必须配32位ACE驱动,64位系统需单独安装32位版 |
注意:若你的系统已安装64位Office(如Office 365),默认ACE驱动为64位,会导致MFC程序(x86)连接Access失败。解决方案:下载 Microsoft Access Database Engine 2010 Redistributable (32-bit) ,以管理员身份运行并添加
/passive参数(避免与64位Office冲突)。
3.2 数据库初始化与测试数据注入
data/course_system.mdb已预置基础表结构,但首次运行前需手动验证数据完整性:
双击打开
data/course_system.mdb(需系统已安装Access或Access Runtime)检查以下4张表是否存在且非空:
Users:至少含3条测试数据(学号20240001/密码123456,角色student;教师T001/123456/teacher;管理员admin/admin/admin)Courses:课程ID、名称、学分、限选人数、上课时间(格式:周一3-4节,周三5-6节)StudentCourses:空表(用于记录选课关系)OperationLog:空表(用于日志归档)
关键字段约束检查(在Access设计视图中确认):
Courses.TimeSlot字段类型为Text(非Date),因需存储多时段字符串StudentCourses.StudentID + CourseID设为联合主键,防止重复选课Users.Password为明文存储(教学项目简化设计,生产环境应加盐哈希)
若发现表缺失,可用随包附带的init_db.sql(文本文件)在Access中执行粘贴导入。
3.3 编译与调试启动步骤
- 用Visual Studio 2019(非VS Code或VS2022)打开
StudentCourseSystem.sln - 解决方案配置选择Debug | Win32(必须是Win32,x64会因ACE驱动位数不匹配崩溃)
- 右键项目 → 属性 → 配置属性 → 常规 → 字符集 →使用Unicode字符集
- 链接器 → 输入 → 附加依赖项 → 确认含
ole32.lib oleaut32.lib(ADO必需) - 按
F5启动调试,观察输出窗口:- 正常流程:
[DB] Connected to data\course_system.mdb successfully - 异常提示:
[ERROR] Failed to create COM object: 0x80040154→ ACE驱动未安装或位数错误
- 正常流程:
首次启动后,主窗口标题栏显示“增进版学生选课系统MFC选课系统哦!!”,此时可尝试用学号20240001密码123456登录——若弹出“欢迎,张三同学!”,说明环境完全就绪。
4. 核心业务逻辑拆解:从一次选课请求看MFC消息循环与数据库事务
4.1 选课按钮响应链:从WM_COMMAND到CommitTransaction
当学生点击“选课”按钮(IDC_BTN_SELECT),事件流如下:
- MFC消息映射触发
CStudentCourseSystemDlg::OnBnClickedBtnSelect() - 该函数获取当前选中课程ID(来自
CListCtrl选中项) - 调用
CCourseManager::Instance()->SelectCourse(m_strStudentID, courseID) SelectCourse()内部执行三步原子操作:- 步骤1:检查课程余量(
SELECT Capacity - COUNT(*) FROM StudentCourses WHERE CourseID = ?) - 步骤2:检查时间冲突(
CCourseManager::CheckTimeConflict(m_strStudentID, courseID)) - 步骤3:插入选课记录(
INSERT INTO StudentCourses VALUES (?, ?))
- 步骤1:检查课程余量(
关键代码段(CourseManager.cpp第142行):
bool CCourseManager::SelectCourse(const CString& studentID, const CString& courseID) { // 开启事务(ADO中对应BeginTrans) HRESULT hr = m_pConnection->BeginTrans(); if (FAILED(hr)) { AfxMessageBox(_T("数据库事务开启失败,请检查连接")); return false; } // 步骤1:余量校验 CString strSQL = _T("SELECT Capacity FROM Courses WHERE CourseID = ?"); _variant_t varCourseID = _variant_t(courseID); _RecordsetPtr pRS = ExecuteQuery(strSQL, &varCourseID); if (pRS == NULL || pRS->GetRecordCount() == 0) return false; long capacity = pRS->GetFields()->GetItem(_T("Capacity"))->GetValue().lVal; long enrolled = GetEnrolledCount(courseID); // 另一查询 if (enrolled >= capacity) { AfxMessageBox(_T("该课程已满员!")); m_pConnection->RollbackTrans(); // 回滚 return false; } // 步骤2:冲突检测(见4.2节详解) if (!CheckTimeConflict(studentID, courseID)) { m_pConnection->RollbackTrans(); return false; } // 步骤3:执行插入 strSQL = _T("INSERT INTO StudentCourses (StudentID, CourseID) VALUES (?, ?)"); _variant_t params[2] = { _variant_t(studentID), _variant_t(courseID) }; if (FAILED(ExecuteNonQuery(strSQL, params, 2))) { m_pConnection->RollbackTrans(); return false; } m_pConnection->CommitTrans(); // 提交事务 CLogWriter::Write(_T("选课"), COleDateTime::GetCurrentTime(), studentID, courseID); return true; }逻辑说明:
BeginTrans()和CommitTrans()是ADO事务控制核心。ExecuteQuery()封装了_CommandPtr创建、参数绑定、Execute()调用全过程,避免裸写COM接口。GetEnrolledCount()是优化点:它用单条SQL统计而非先查再计数,减少网络往返。
4.2 时间冲突检测算法:字符串解析与区间重叠判断
CheckTimeConflict()是本项目最具实用价值的算法模块。它不依赖数据库JOIN,而是在内存中解析时间字符串并做数学判断:
bool CCourseManager::CheckTimeConflict(const CString& studentID, const CString& courseID) { // 1. 获取学生已选所有课程的时间字符串(逗号分隔) CString strStudentTime = GetStudentTimeSlots(studentID); // 返回:"周一3-4节,周三5-6节" // 2. 获取目标课程时间字符串 CString strTargetTime = GetCourseTimeSlot(courseID); // 返回:"周一3-4节" // 3. 解析为结构体数组 vector<TimeSlot> vecStudentSlots = ParseTimeSlots(strStudentTime); TimeSlot targetSlot = ParseTimeSlots(strTargetTime)[0]; // 4. 逐一对比是否重叠 for (const auto& slot : vecStudentSlots) { if (IsTimeOverlap(slot, targetSlot)) { CString msg; msg.Format(_T("时间冲突:您已选修【%s】,与【%s】上课时间重叠(%s)"), slot.courseName, targetSlot.courseName, FormatTimeSlot(targetSlot)); AfxMessageBox(msg); return false; } } return true; } // 关键辅助函数:将"周一3-4节"转为{day:1, start:3, end:4} TimeSlot CCourseManager::ParseSingleTimeSlot(const CString& timeStr) { TimeSlot slot; if (timeStr.Find(_T("周一")) >= 0) slot.day = 1; else if (timeStr.Find(_T("周二")) >= 0) slot.day = 2; // ... 其他工作日 // 提取数字:用正则太重,直接Find+Mid int pos = timeStr.Find(_T("节")); if (pos > 0) { CString numPart = timeStr.Mid(0, pos); // 提取"3-4"中的3和4 int dashPos = numPart.Find(_T("-")); if (dashPos > 0) { slot.start = _ttoi(numPart.Mid(0, dashPos)); slot.end = _ttoi(numPart.Mid(dashPos + 1)); } } return slot; }参数说明:
TimeSlot结构体含int day(1=周一)、int start(起始节次)、int end(结束节次)。IsTimeOverlap()判断逻辑为:slot1.day == slot2.day && !(slot1.end < slot2.start || slot2.end < slot1.start)。这种设计让冲突检测脱离数据库,即使网络抖动也能快速反馈。
4.3 日志写入器实现:线程安全的本地文件追加
CLogWriter::Write()采用最简可靠的文件I/O策略:
void CLogWriter::Write(const CString& action, const COleDateTime& dt, const CString& userID, const CString& courseID) { // 生成日志文件名:log_20240520.txt CString logFileName = GetLogFileName(dt); // 使用CStdioFile以追加模式打开(线程安全关键!) CStdioFile file; if (!file.Open(logFileName, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { return; // 失败静默处理,不影响主流程 } file.SeekToEnd(); // 定位到文件末尾 // 格式:2024-05-20 14:23:15 | 选课 | 20240001 | CS201 CString logLine; logLine.Format(_T("%s | %s | %s | %s\r\n"), dt.Format(_T("%Y-%m-%d %H:%M:%S")), action, userID, courseID); file.WriteString(logLine); file.Close(); }为什么不用CFileDialog或注册表存路径?因为日志路径硬编码在
CLogWriter::GetLogFileName()中为./logs/子目录,首次运行自动创建。modeNoTruncate确保不覆盖旧日志,SeekToEnd()避免多线程写入时覆盖——这是Windows桌面程序最稳妥的日志方案。
5. 常见问题排查:5个真实翻车现场与血泪修复方案
5.1 现象:点击登录按钮无响应,输出窗口显示“Failed to initialize COM library”
- 原因:MFC程序未在主线程调用
CoInitialize(NULL)。本项目在CStudentCourseSystemApp::InitInstance()中已添加,但若你修改过入口函数,可能被删。 - 解决:打开
StudentCourseSystem.cpp,定位到BOOL CStudentCourseSystemApp::InitInstance()函数,在CStudentCourseSystemDlg dlg;创建前插入:
并在if (FAILED(CoInitialize(NULL))) { AfxMessageBox(_T("COM初始化失败")); return FALSE; }return FALSE;前添加CoUninitialize();
5.2 现象:登录成功后课程列表为空,调试发现ExecuteQuery()返回空Recordset
- 原因:
data/course_system.mdb被杀毒软件(如Windows Defender)锁定,ADO无法读取。 - 解决:右键该文件 → 属性 → 常规 → “解除锁定”复选框(若存在)→ 确定;或临时关闭实时防护。验证方式:用Access手动打开该文件,若提示“文件被占用”,即为此问题。
5.3 现象:选课时弹出“数据库连接失败”,错误码0x80004005
- 原因:ACE OLEDB Provider未正确安装,或安装了64位版但程序是32位。
- 解决:
- 下载32位ACE驱动(链接见3.1节)
- 以管理员身份运行CMD,执行:
AccessDatabaseEngine.exe /passive - 重启VS并重新编译(Clean Solution后Rebuild)
5.4 现象:时间冲突检测总返回false,实际无冲突
- 原因:
Courses.TimeSlot字段在Access中被设为“备注”类型(Memo),而ADO读取Memo字段需特殊处理(GetChunk()),本项目假设其为Text类型。 - 解决:在Access中打开
course_system.mdb→ 设计视图 → 选中TimeSlot字段 → 字段属性 → 数据类型 → 改为文本→ 字段大小设为255 → 保存。
5.5 现象:管理员登录后“课程管理”按钮点击无效,OnBnClickedBtnManageCourses()未触发
- 原因:MFC Class Wizard未正确关联消息。
Resource.h中IDC_BTN_MANAGE_COURSESID与对话框类中声明不一致。 - 解决:
- 在VS资源视图中右键该按钮 → “类向导”
- 确认“消息”选项卡下
BN_CLICKED已映射到CStudentCourseSystemDlg类 - 若未映射,点击“添加处理程序” → 名称填
OnBnClickedBtnManageCourses→ 确定 - 检查
StudentCourseSystemDlg.h中是否有afx_msg void OnBnClickedBtnManageCourses(); - 检查
StudentCourseSystemDlg.cpp中BEGIN_MESSAGE_MAP是否包含ON_BN_CLICKED(IDC_BTN_MANAGE_COURSES, &CStudentCourseSystemDlg::OnBnClickedBtnManageCourses)
6. 二次开发实战:为系统增加“课程评价”功能的完整改造路径
6.1 需求分析与数据库扩展
新增评价功能需支撑:学生对已选课程打分(1-5星)、填写文字评语、教师查看本课平均分。这要求三处改动:
- 数据库:新增
CourseReviews表 - UI:在课程详情页增加评分控件与文本框
- 逻辑:选课成功后自动跳转至评价页,或提供独立“我的评价”入口
首先扩展Access数据库(data/course_system.mdb):
| 字段名 | 类型 | 说明 |
|---|---|---|
| ReviewID | AutoNumber(主键) | 评价唯一ID |
| StudentID | Text(10) | 外键,关联Users |
| CourseID | Text(10) | 外键,关联Courses |
| Rating | Number(整型) | 1-5分 |
| Comment | Memo | 评语(支持换行) |
| ReviewTime | Date/Time | 评价时间 |
提示:Access中新建表后,务必在“关系”视图中设置
StudentID和CourseID的外键约束,并勾选“实施参照完整性”。
6.2 UI层改造:动态加载评分控件
MFC不自带星级评分控件,我们用5个CButton模拟(更轻量):
- 在课程详情对话框资源中,拖入5个
Button控件,ID分别设为IDC_BTN_STAR1至IDC_BTN_STAR5 - 设置其样式为“Owner Draw”,并在
OnInitDialog()中加载图标:// 加载空心星图标(res/star_empty.bmp)和实心星(res/star_full.bmp) m_btnStar1.LoadBitmaps(IDB_STAR_EMPTY, IDB_STAR_FULL); m_btnStar2.LoadBitmaps(IDB_STAR_EMPTY, IDB_STAR_FULL); // ... 其他 - 为每个按钮添加
BN_CLICKED消息处理,例如OnBnClickedBtnStar3():void CReviewDialog::OnBnClickedBtnStar3() { m_nRating = 3; UpdateStarButtons(); } void CReviewDialog::UpdateStarButtons() { // 根据m_nRating设置按钮状态 m_btnStar1.SetCheck(m_nRating >= 1 ? BST_CHECKED : BST_UNCHECKED); m_btnStar2.SetCheck(m_nRating >= 2 ? BST_CHECKED : BST_UNCHECKED); // ... }
6.3 业务逻辑注入:在选课成功后自动唤起评价页
修改CCourseManager::SelectCourse()的成功分支(原第187行):
// 原有代码:CLogWriter::Write(...) 后添加 if (AfxGetMainWnd()) { // 弹出评价对话框(CReviewDialog) CReviewDialog dlg; dlg.m_strStudentID = studentID; dlg.m_strCourseID = courseID; dlg.DoModal(); // 模态对话框,阻塞直到用户关闭 }关键点:CReviewDialog需在构造函数中接收studentID和courseID,并在DoDataExchange()中绑定编辑框控件。
6.4 数据持久化:评价提交的事务封装
CReviewDialog::OnOK()中执行插入:
void CReviewDialog::OnOK() { if (m_nRating < 1 || m_nRating > 5) { AfxMessageBox(_T("请给出1-5星评价")); return; } CString strSQL = _T("INSERT INTO CourseReviews (StudentID, CourseID, Rating, Comment, ReviewTime) VALUES (?, ?, ?, ?, ?)"); _variant_t params[5] = { _variant_t(m_strStudentID), _variant_t(m_strCourseID), _variant_t((long)m_nRating), _variant_t(m_strComment), _variant_t(COleDateTime::GetCurrentTime()) }; if (SUCCEEDED(CDatabaseHelper::Instance()->ExecuteNonQuery(strSQL, params, 5))) { AfxMessageBox(_T("评价提交成功!")); CDialogEx::OnOK(); } else { AfxMessageBox(_T("评价提交失败,请重试")); } }验证技巧:提交后,直接打开
data/course_system.mdb查看CourseReviews表是否新增记录。若无,检查params数组长度是否为5(易漏写ReviewTime参数)。
从那以后我每次给MFC项目加新功能,都强制走一遍“数据库改表 → UI拖控件 → 类向导绑消息 → 逻辑写死桩 → 调试单步验证”五步法。少跳任何一环,第二天准在ExecuteNonQuery()的HRESULT返回值上卡住两小时。希望帮到你。
本文还有配套的精品资源,点击获取