ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MFC增进版学生选课系统开发实战:从界面优化到数据库连接与打包

MFC增进版学生选课系统开发实战:从界面优化到数据库连接与打包 简介这是一份基于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 APIlibmysql.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, ... ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果表已经建好了就用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 xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /asmv3:windowsSettings /asmv3:application设置后程序会在高分屏上保持清晰但控件的物理大小也变大了对自适应逻辑的要求也更高。7. 实操过程中的几点体会这个增进版选课系统前后写了将近一周大部分时间都耗在了界面排布和数据库细节上。我个人最大的体会是MFC开发虽然老派但它把Windows窗口机制展现得很完整理解好消息映射、设备描述表、控件自绘这些东西对理解整个桌面应用开发的底层逻辑很有帮助。如果你是一位正在做课设的学生不要在界面上追求酷炫把选课流程走通、数据不丢、用户体验舒服就已经超过百分之八十的作业了。最后分享两个小技巧。第一个MFC对话框里可以考虑用Tab控件做导航把“选课”和“已选课程查询”放在同一个窗口的不同页签里比反复切换对话框更顺手。第二个开发过程中用SQLite的日志记录关键操作再也不用猜“程序跑没跑过那条更新语句”了。选课系统不是越复杂越好条理清楚、逻辑严谨才是“增进版”的核心价值。本文还有配套的精品资源点击获取
返回列表