简介:这是一份基于MFC框架开发的增进版学生选课系统完整工程文件,主要面向学习Windows桌面应用开发的高校学生、准备课程设计与毕业设计的开发者,以及需要快速构建选课管理平台的教学人员。项目在原版基础上重构了用户管理、课程管理、选课流程、数据统计与消息通知等核心模块,内置学生、教师、管理员三类角色权限,解决了传统选课系统操作繁琐、高峰期响应慢和功能分散等痛点。压缩包共含85个文件,以C++头文件与实现文件(h/cpp)为主体,另有位图/图标界面资源、工程配置文件(dsp/dsw)、Access数据库(mdb)以及编译好的可执行文件(exe),整体仅3.11MB,便于离线研读和直接运行。通过阅读源码可理解MFC文档/视图架构、ODBC数据库连接、菜单工具栏设计与选课算法逻辑,体会从登录、权限控制到选课分配、信息统计的完整业务流程。目前已有255人学习下载,结构清晰、模块分明,适合作为课程设计模板、面试项目素材或二次开发蓝本。
1. 项目概述:为什么贪多求全反而不如一个“增进版”
做MFC课设或者毕业设计选题时,一搜“学生选课系统”,五花八门的版本满天飞。有的用了最新的Web框架,有的搞了Android端,还有的硬生生把C/S架构写成了B/S的样子。但你要是真打算在Windows环境下,用C++老老实实做一个桌面端选课系统,最终还是绕不开MFC这个老牌框架。
我这次做的这个“增进版学生选课系统”,说白了就是在传统课设基础上做了一次功能增强。传统的MFC选课系统,基本就是学生登录、查课程、选课程、退课程,管理员登录、管理课程、查看选课名单,这套流程跑通就能交差了。但实际你去试用一下那些基础版就会发现,做得糙一点的,界面拉伸一下就乱成一团,课程列表不能按学分筛选,选课冲突没有明显提示,甚至数据库连接串直接写死在代码里。这些痛点我全踩过,所以这次干脆做一个“增进版”,主要解决三个核心问题:让界面更像正经软件、让数据操作更稳健、让程序发布出去能直接安装使用。
这篇博客适合谁?如果你是正在做MFC课设的计算机专业学生、需要快速上手MFC做小工具的开发者,或者想了解桌面应用完整开发流程的朋友,这篇内容应该对你有用。我会尽量避开教科书式的废话,直接讲怎么拆代码、怎么设计、怎么避坑。
2. 整体设计思路拆解:选课系统的核心流程与信息模型
2.1 需求分析与角色梳理
无论界面怎么做,选课系统的业务逻辑其实很固定。首先是角色划分,系统里必须有学生和管理员这两种账户。学生能做什么?登录后查看可选课程列表、根据自己的专业和年级筛选课程、选课、退课、查看已选课程。管理员能做什么?维护课程信息(增删改查)、查看每门课的选课名单、统计选课人数、重置学生密码。
这些需求看似简单,但细化到实现层面,就需要规划清楚数据流向。比如学生选课的时候,系统要检查什么?一是这门课是否还存在名额,二是学生是否已经选过这门课,三是选课时间是否冲突。这三个检查点,任何一个遗漏都会造成数据混乱。我在设计时,把这三个检查点全部放到了服务端逻辑里,也就是说MFC客户端只负责发出“选课”这个指令,真正做校验的是数据库的存储过程或者封装好的查询函数。这样即使以后换一个客户端入口,业务规则也不会丢。
2.2 数据库设计:表结构如何组织
选课系统一般至少要有三张核心表:学生表、课程表、选课记录表。我这里稍微扩展了一下,加了管理员表和院系表。
学生表(student)字段我建议这样定:学号作为主键,然后姓名、密码、专业、年级、院系编号。注意密码不要用明文存储,哪怕只是个课设,也建议做一次简单的哈希转换,毕竟写出来会被别人拿去当参考,咱别带坏风气。
课程表(course)字段:课程编号主键、课程名、学分、任课教师、上课时间、上课地点、容量、已选人数。这里有个关键点,已选人数可以通过统计选课记录表得到,但那样每次查询都需要做一次聚合运算,效率低。我选择在课程表里冗余一个已选人数字段,选课成功的逻辑里做“原子更新”,即先判断“已选人数小于容量”再执行更新,SQL里用一条带条件的UPDATE语句完成,避免并发超额。
选课记录表(elective)字段:选课记录编号、学号、课程编号、选课时间。这张表要建联合唯一索引,防止同一个人重复选同一门课,虽然我在代码里做了判断,但数据库层面的约束才是最后的防线。
2.3 功能增强点:从能用变得好用
这一版我增加的第一个增强点是“已选课程与可选课程联动”。学生在选课界面勾选了一门课,下方的已选课程列表马上刷新,同时该课程在可选列表中变成灰色不可重复选中。第二个增强点是“学分累计显示”,选课界面实时统计当前已选总学分,超过上限给出浮动提示。第三个增强点是“管理员端课程冲突检测”,管理员新加一门课时,如果上课时间和已有课程重叠,会在界面上弹出警告,让管理员二次确认而不是直接拒绝,这种处理方式更贴近实际教务系统的人性化设计。
3. MFC界面改造:从默认控件到自定义样式
3.1 自定义按钮的朴实实现方式
MFC框架自带的CButton样式比较朴素,想让界面看起来有点档次,最常见的手段是自定义绘制(Owner-Draw)。具体做法是自定义一个类继承CButton,重写DrawItem函数,在里面用绘图函数绘制按钮的各个状态。
网上很多教程会让你直接贴一段代码,用四五个渐变刷子把按钮画成玻璃质感。但我的实际经验是,课设里的自定义按钮做到“干净整洁”就够了,不要过度追求花哨,否则反而和整体界面风格不协调。
class CCustomButton : public CButton { public: CCustomButton() : m_bHover(FALSE) {} protected: BOOL m_bHover; afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); afx_msg void OnMouseLeave(); DECLARE_MESSAGE_MAP() public: void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) override; };在DrawItem里,先判断按钮是否处于按下状态(ODS_SELECTED)、是否获得焦点(ODS_FOCUS),然后分别绘制不同的背景颜色。文本绘制用DrawText,加上SetBkMode(TRANSPARENT)让文字背景透明。这里有个坑:如果按钮设置了Owner-Draw属性,运行时会发现按钮不出现,原因是DrawItem没有被调用——检查一下控件的Owner Draw属性有没有设为TRUE。
3.2 列表控件的使用与排版优化
MFC里的CListCtrl(列表控件)是选课系统的门面,课程列表用它,已选课程列表也用重。网上搜“mfc列表框控件的使用”能找到很多基础讲解,但实际做选课系统时,推荐直接用报表视图(Report View)模式,这样才能显示多列信息:课程编号、课程名、学分、教师、时间、地点、名额。
初始化代码大致如下:
m_listCourse.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES); m_listCourse.InsertColumn(0, _T("课程编号"), LVCFMT_LEFT, 80); m_listCourse.InsertColumn(1, _T("课程名称"), LVCFMT_LEFT, 160); m_listCourse.InsertColumn(2, _T("学分"), LVCFMT_CENTER, 50); m_listCourse.InsertColumn(3, _T("教师"), LVCFMT_CENTER, 80); m_listCourse.InsertColumn(4, _T("上课时间"), LVCFMT_CENTER, 120); m_listCourse.InsertColumn(5, _T("容量"), LVCFMT_CENTER, 60); m_listCourse.InsertColumn(6, _T("已选"), LVCFMT_CENTER, 60);这里我特别想强调一个细节:给列表设置LVS_EX_CHECKBOXES扩展风格,可以让每一行前面出现一个复选框,学生勾选后再点按钮提交。这种做法比单击整行选中更直观,也更贴近现代软件的操作习惯。不过复选框状态需要程序自己维护,比如学生退掉一门课之后,重新加载列表时需要把对应行的勾选状态恢复为未选中,不能沿用上次的状态。
3.3 静态文本覆盖与透明度处理
搜索热词里有“mfc静态文本 覆盖”,这其实是说静态文本控件之间层级遮挡的问题。做自定义界面时,我习惯把窗口背景换成一个纯色位图,然后在窗口上放文本控件。默认情况下,控件背景是灰色或者其他颜色,会遮挡底图,很难看。
解决办法有两种。一种是重写OnCtlColor,对需要透明背景的静态文本返回透明画刷:
HBRUSH CMyDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr = CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor == CTLCOLOR_STATIC) { pDC->SetBkMode(TRANSPARENT); return (HBRUSH)GetStockObject(NULL_BRUSH); } return hbr; }另一种是直接用CStatic的SetBitmap成员函数,让整个“标签区域”变成一张提前做好的透明底图片,但这种方式不适合需要动态更新内容的文本。实际项目里,我推荐用第一种,因为它可以让任意文本控件融入背景,而且代码量很少。
4. MFC连接MySQL:数据访问的实现细节
4.1 连接方式选型
MFC连数据库有几种路径:ADO、ODBC、MySQL官方C API、或者封装好的第三方库。我这次用的是MySQL官方提供的C API(libmysql.dll),因为不依赖微软的ODBC配置,也不需要额外打包数据源,发布时把DLL带上就行。
代码连接数据库的步骤固定:初始化库、初始化连接句柄、设置连接参数、执行SQL。一个典型的连接函数如下:
bool CDatabaseManager::Connect() { mysql_library_init(0, NULL, NULL); MYSQL* pConn = mysql_init(NULL); if (pConn == NULL) return false; if (mysql_real_connect(pConn, "127.0.0.1", "root", "123456", "course_system", 3306, NULL, CLIENT_MULTI_STATEMENTS) == NULL) { return false; } m_pConn = pConn; return true; }连接参数这里建议做一个小框架:不要直接把IP、用户名、密码写在每个使用数据库的对话框里,而是做一个CDatabaseManager单例类,所有数据库操作都走这个类。这样后续改数据库IP、换账号密码,只需要改一个地方。
4.2 增删改查的实现与预编译语句
查询课程列表是最常用的操作。用mysql_query执行SELECT语句,然后mysql_store_result取结果集,逐行读取。关键点是编码问题,MFC项目里一般用Unicode字符集(VS2013及以后默认就是Unicode),但MySQL默认连接是UTF-8,中文会变成乱码。解决办法是在连接后立刻执行一句SET NAMES utf8mb4,同时确保MFC工程里将项目字符集设置为“使用Unicode字符集”,并在查询结果转为CString时使用多字节转换。
插入选课记录时,强烈建议用mysql_stmt系列预编译语句接口。这样做的好处一是防SQL注入,二是在循环插入大量数据时性能更好。选课系统虽然并发不高,但既然写了就写得规范些。
const char* sql = "INSERT INTO elective(stu_id, course_id, create_time) VALUES(?, ?, NOW())"; MYSQL_STMT* stmt = mysql_stmt_init(m_pConn); mysql_stmt_prepare(stmt, sql, strlen(sql)); MYSQL_BIND bind[2]; memset(bind, 0, sizeof(bind)); char stuId[20]; char courseId[20]; bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = stuId; bind[1].buffer_type = MYSQL_TYPE_STRING; bind[1].buffer = courseId; mysql_stmt_bind_param(stmt, bind); strcpy(stuId, strStuId); strcpy(courseId, strCourseId); mysql_stmt_execute(stmt);4.3 事务处理提升数据一致性
选课这个操作实际上需要两步:插入选课记录、更新课程表已选人数。如果第一步成功了、第二步失败,学生就会碰到“明明选上了课但列表里没显示”的诡异问题。正确做法是开启事务,两个操作一起提交或一起回滚。
mysql_query(m_pConn, "START TRANSACTION"); bool bOk = true; // 执行插入选课记录 if (!InsertElective(stuId, courseId)) bOk = false; // 更新已选人数 if (!UpdateSelectedCount(courseId, 1)) bOk = false; if (bOk) mysql_query(m_pConn, "COMMIT"); else mysql_query(m_pConn, "ROLLBACK");另外,判断课程是否满员时不能只靠客户端查一次已选人数,因为多人同时选课可能同时读到同一个数值。别人做的项目我不评价,我自己这个版本在UPDATE语句里直接加上条件判断:
UPDATE course SET selected_count = selected_count + 1 WHERE course_id = 'C001' AND selected_count < capacity然后检查affected rows是否为1,如果是0就说明这门课已经满了,这种原子操作在多账号同时选课的时候也不会出问题。
5. 界面自适应分辨率与项目打包实战
5.1 控件随窗口大小变化
热词里有“mfc 控件自适应屏幕分辨率”,这个问题绝对是MFC开发中最烦的之一。对话框的大小固定,但是不同用户的屏幕分辨率不同,高DPI缩放下控件还会变形。
便携有效的方案是在对话框的OnSize事件里,计算当前窗口大小与初始设计大小的比例,然后逐一移动和缩放控件。先添加一个OnInitDialog里记录初始大小的逻辑:
CRect rect; GetClientRect(&rect); m_nInitWidth = rect.Width(); m_nInitHeight = rect.Height();然后在OnSize里对每个需要自适应的控件调用SetWindowPos。控件较多时,我封装了一个函数:
void CMainDlg::CaculateControlPos(CWnd* pWnd, CRect initRect) { CRect rtNow; pWnd->GetWindowRect(&rtNow); // 根据比例重新计算位置和大小 int nWidth = initRect.Width() * m_scaleX; int nHeight = initRect.Height() * m_scaleY; int nX = initRect.left * m_scaleX; int nY = initRect.top * m_scaleY; pWnd->SetWindowPos(NULL, nX, nY, nWidth, nHeight, SWP_NOZORDER); }实操中还有一个容易忽略的点:OnSize在窗口最小化、还原时都会触发,如果原来的窗口已经被点击过最小化按钮,再还原时重新计算的比例可能出现异常,所以在OnSize里要加一个判断,如果m_bInit标志为FALSE就直接返回。另外一个细节是字体大小,如果用SetWindowPos改变了控件大小,但字体没变,整个界面看起来还是别扭,建议在主对话框初始化时调用一次EnumChildWindows统一设置一遍字体大小,或者在OnSize里动态创建合适大小的字体。
5.2 发布打包:Release静态编译与部署
MFC项目开发调试时用Debug模式没问题,交作业或者部署到别的机器必须切到Release模式。这里有几个坑。第一个坑是VS2013默认的MFC项目是动态链接到MFC DLL的,目标机器如果没有安装对应运行库,程序根本起不来。解决办法是在项目属性页里,将“MFC的使用”改成“在静态库中使用MFC”,将“运行库”改成“多线程/MT”,这样编译出来的exe体积会大一些,但不需要额外装运行库环境。
第二个坑是MySQL的依赖。程序用到了libmysql.dll,这个DLL体积不大但必须和exe放在同一目录下。如果数据库连接的配置不对,程序崩溃前会弹出一堆警告,为了看起来专业,建议在启动程序时检查DLL是否存在,并给出中文提示而不是系统弹窗。
第三个坑是“mfc项目如何打包”这个热搜词每个人都搜过,我自己第一次做安装包时也走了弯路。最省事的方案是用Inno Setup写一个简单的脚本,把exe和必要的DLL文件打成一个Install.exe,用户双击后自动安装到Program Files目录,并生成桌面快捷方式。脚本里只需指定源文件、输出目录、安装目录即可,十分钟就能搞定。
6. 常见报错与问题排查实录
6.1 运行时报错“无法打开包括文件: afxwin.h”
这个报错最常见的原因是把MFC项目不小心建成了Win32控制台项目,或者Win32项目创建时没有选中“使用MFC”。还有一个隐蔽原因:项目属性里“使用MFC”选项被改成了“使用标准 Windows 库”。解决办法:右键项目,属性 → 常规 → MFC的使用 → 在共享DLL中使用MFC,再把字符集改成“使用Unicode字符集”,重新生成。
6.2 数据库中文乱码
我前面提到,连接后要执行SET NAMES utf8mb4。但很多人还是会乱码,原因是MySQL表结构本身是latin1编码。建表时建议明确指定:
CREATE TABLE student ( stu_id VARCHAR(20) PRIMARY KEY, stu_name VARCHAR(50) NOT NULL, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;如果表已经建好了,就用ALTER TABLE语句转换编码。转换之后,代码里查询结果转CString时再确认一下,MFC工程如果设定了Unicode字符集,直接用mysql_fetch_row拿到的char去构造CString是没问题的,但反过来把CString转成const char要用CT2A或者WideCharToMultiByte,别图省事用(TCHAR*)强转,否则数据全是乱的。
6.3 自定义按钮不显示或者点击无效果
DrawItem不被调用是最常见的情况,确认方法:在DrawItem里用OutputDebugString输出一行日志,如果调试窗口里没打出来,说明控件没有设置Owner-Draw属性。右键按钮控件,属性 → Owner Draw → True,即可解决。
点击无效果则是消息映射的问题。很多新手重写按钮类时,把OnLButtonDown或者OnMouseMove重写了,却忘了调用基类的同名函数,或者没有正确添加ON_MESSAGE映射。记得在按钮类的消息映射里加上:
BEGIN_MESSAGE_MAP(CCustomButton, CButton) ON_WM_MOUSEMOVE() ON_WM_MOUSELEAVE() END_MESSAGE_MAP()6.4 列表复选框状态与数据脱节
这个问题的典型场景:学生勾选了三门课,点完选课按钮,其中两门选成功了,一分失败。然后界面刷新后,列表控件里的复选框乱掉了——原来勾选的行自动被清除,但其他行出现了“残留勾选”。
原因在于复用列表项时,插入新行没有设置初始的复选框状态。每次刷新列表前,先调用一次DeleteAllItems清空所有行,然后在插入每一项时,主动设置CV_CheckBoxes的检查状态为FALSE。如果课程已经被选过,同时也要把对应行显示成灰色,配合SetItemState的LVIS_CUT标记实现视觉区分。
6.5 高DPI环境下字体模糊
很多人在普通分辨率屏幕下开发一切正常,拿到高分屏笔记本上一打开程序,字全糊了或控件重叠了。解决办法是在程序入口处,也就是InitInstance里调用SetProcessDPIAware函数:
#include <windows.h> SetProcessDPIAware();如果项目用的VS2013,可能还需要在项目清单文件里加入DPI awareness配置,直接编辑*.manifest文件加入:
<asmv3:application> <asmv3:windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> </asmv3:windowsSettings> </asmv3:application>设置后,程序会在高分屏上保持清晰,但控件的物理大小也变大了,对自适应逻辑的要求也更高。
7. 实操过程中的几点体会
这个增进版选课系统前后写了将近一周,大部分时间都耗在了界面排布和数据库细节上。我个人最大的体会是:MFC开发虽然老派,但它把Windows窗口机制展现得很完整,理解好消息映射、设备描述表、控件自绘这些东西,对理解整个桌面应用开发的底层逻辑很有帮助。如果你是一位正在做课设的学生,不要在界面上追求酷炫,把选课流程走通、数据不丢、用户体验舒服,就已经超过百分之八十的作业了。
最后分享两个小技巧。第一个,MFC对话框里可以考虑用Tab控件做导航,把“选课”和“已选课程查询”放在同一个窗口的不同页签里,比反复切换对话框更顺手。第二个,开发过程中用SQLite的日志记录关键操作,再也不用猜“程序跑没跑过那条更新语句”了。选课系统不是越复杂越好,条理清楚、逻辑严谨,才是“增进版”的核心价值。
本文还有配套的精品资源,点击获取