简介:一套基于Visual C++ 6.0与MFC类库开发的桌面版考试系统源码,适合Windows应用开发初学者、MFC爱好者及相关课程设计人员参考。系统覆盖用户注册登录、试题管理、在线答题和成绩统计等功能,使用Access数据库保存试题和用户数据,完整呈现MFC对话框、文档视图、ADO/ODBC数据库访问、消息映射等关键技术的实际用法。资源包为ZIP格式,共57个文件、约356KB,含19个C++源文件、19个头文件,以及工程配置、资源脚本、图标位图和界面预览图;内置Access数据库文件可直接查看数据表结构,工程在VC6.0环境打开后即可编译运行。当前已有210人学习下载。阅读源码可以快速理清各功能模块的界面与逻辑组织方式,是适合课程设计、毕业设计及MFC进阶学习的完整参考,尤其便于按模块进行二次开发与调试学习。
1. 为什么 2025 年还有人翻出 Visual C++ 6.0 写考试系统
机房考试、局域网部署、Windows XP 老机器——这几个词凑在一起,基本就是这套「基于 Visual C++ 6.0 MFC 开发的桌面版考试系统」的典型生存环境。一个十几年前编译出来的 exe,双击直接进登录框,不装运行时、不依赖浏览器、断网也能考,这种存量系统在高校和中小企业里远比想象中多。这篇文章想解决的就是三件事:这类系统内部到底怎么组织、用 MFC 手动搭一个最小可用版本要写哪些代码、以及哪些坑会让它在交付当天翻车。适合接手老项目的维护者、要给单位做内部考试工具的开发,以及想用 MFC 练手的初学者——新项目我一般劝你用 Qt,但读懂这套老框架,仍然是不少人的现实需求。
2. 读懂这个项目的骨架:MFC 程序模型和考试系统的模块划分
Visual C++ 6.0 里的 MFC 项目,核心不是你现在熟悉的 C++ 标准库和 STL 风格,而是一套基于 HWND 和消息映射的框架。考试系统这种业务,在 MFC 里最常见的落点不是文档视图架构,而是「对话框程序」。整个程序的生命周期非常直白:应用类 CWinApp 的 InitInstance 里弹登录框,登录框 DoModal 返回 IDOK 之后再弹主考试框,主框负责加载题目、计时、判分、写成绩。理解这条主链,比纠结某个控件怎么用重要得多,因为所有考试系统的窗口流转都逃不出这个模式。
2.1 对话框框架还是单文档框架:考试系统通常怎么选
MFC 的文档视图架构(CWinApp + CFrameWnd + CView + CDocument)适合做文档编辑器,比如文本编辑器、画图工具这类「打开一个文件、编辑、保存」的场景。考试系统的交互是表单式的:一个登录页、一个答题页、一个结果页,页面之间没有文档概念,用 CDialog 派生类反而最直接。对话框程序的资源文件里放好控件模板,运行时用 DDX 或 GetDlgItem 取数据,DoModal 启动模态循环,返回 IDOK 时带走结果——这套模型和考试流程完全对得上。
我见过有人非要用单文档框架做考试系统,结果遇到两个麻烦:菜单和工具栏占了界面空间,考试界面要隐藏一堆东西;答题数据不知道该放 Document 还是 View,最后全塞在全局变量里。对话框方案没有这些问题,而且模态对话框天然阻止了用户切到别的窗口,很适合考试这种需要锁屏的场景。如果你要同时做考生端和管理端,对话框程序也能拆成多个独立对话框,比单文档框架省心得多。
2.2 题库、成绩与数据库:Access + ODBC 是最常见的落点
考试系统的数据量不大,题库几百道题、成绩几千条,用 Access 数据库加 ODBC 是这类老项目的标配。SQL Server 和 MySQL 在当时部署成本太高,机房老师不愿意装客户端和配置服务,Access 只要一个 .mdb 文件,放在共享目录或本机就能跑。即使到现在,接手这类项目时看到 .mdb 文件,第一反应也应该是「ADO 或 ODBC 连接,不是嵌入式 SQL」。
题库表和数据表的设计,一般逃不出下面这个样子:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| tb_user | user_id, user_name, password, role | 考生和管理员,role 区分身份 |
| tb_question | q_id, q_type, content, opt_a, opt_b, opt_c, opt_d, answer, score | 单选为主,answer 存 A/B/C/D |
| tb_score | exam_id, user_id, score, exam_date | 成绩汇总,按场次区分 |
字段类型是这里最常见的坑源头:q_id 用自动编号,answer 用文本类型别用数字,score 用单精度或整型。老手还会在 tb_question 里加一个 field_id 或 chapter 字段,方便组卷时按章节抽题。连接字符串有两种写法:传统 ODBC 数据源需要在控制面板里配置 DSN,麻烦且换机器就失效;后来普遍改用 ADO 无 DSN 连接,代码里直接写 ACE 或 Jet 的 Provider。后者更推荐,因为不用动系统配置,把 .mdb 路径写在配置里就能用。
2.3 消息映射与控件变量:MFC 事件模型的基础
MFC 的事件处理靠消息映射表,就是类定义里的 DECLARE_MESSAGE_MAP 和实现文件里的 BEGIN_MESSAGE_MAP。考试系统里你至少会用到三类消息:按键或点击触发的命令消息(BN_CLICKED)、定时器触发的 WM_TIMER、以及窗口关闭的 WM_CLOSE。初学者最容易犯的错是直接在头文件里写了个 OnSubmitExam 函数,忘了加 afx_msg 声明和消息映射条目,结果编译通过但按钮怎么点都没反应。
对话框控件和变量的绑定,用 ClassWizard 自动生成的 DDX_Text 和 DDV_MaxChars 最常见。DDV_MaxChars 会给输入框限长,这在考试系统登录框里很实用,防止考生粘一长串字符串进去。但注意:DDX 的关联是双向的,DoModal 返回后从控件取数据是可行的,可如果你在对话框运行中途想读取控件值,更稳妥的是 GetDlgItemText,避免 DDX 还没同步导致读到旧值。这个区别我在给老系统加「实时保存答案」功能时踩过,后面避坑章会展开。
3. 从零跑通一个最小考试系统:登录、加载题目、倒计时与自动判分
这一章直接给可复现的代码。我的目标不是给你一份完整工程,而是把考试系统最核心的四段逻辑讲明白:登录校验、动态控件加载题目、倒计时和判分。你照着这四段拼出一个能跑的对话框程序,剩下的管理端和美化都是体力活。
3.1 登录校验与界面流转
登录对话框用资源编辑器拖一个 CStatic、两个 CEdit、一个 CButton 就能完事。关键在 OnOK 的覆写——用户在密码框按回车会触发 OnOK,所以校验逻辑必须写在这里,不能写在按钮点击响应里,否则回车就绕过校验了。
// LoginDlg.cpp —— 登录校验核心 void CLoginDlg::OnOK() { CString strUser, strPwd; GetDlgItemText(IDC_EDIT_USER, strUser); GetDlgItemText(IDC_EDIT_PWD, strPwd); if (strUser.IsEmpty() || strPwd.IsEmpty()) { AfxMessageBox("学号和密码不能为空"); return; // 不关窗,让用户重新输入 } // 实际项目这里查 tb_user 表,比对 user_id 和 password if (strUser != "admin" || strPwd != "123456") { AfxMessageBox("学号或密码错误"); return; } CDialog::OnOK(); // 校验通过才结束模态循环 }这段代码的逻辑链是:取控件文本 → 判空 → 校验 → 通过才调用基类 OnOK。注意那个 return,很多人写成 if 不通过就弹窗但忘了 return,结果弹完窗继续往下走,基类 OnOK 也被执行,登录框莫名其妙关掉了。
取控件文本用 GetDlgItemText 而不是成员变量,是因为登录框只用一次,不值得为两个编辑框维护 DDX 绑定。代码里 IDC_EDIT_USER 这类 ID 由资源编辑器自动生成,你在项目里要替换成自己的 ID。如果校验通过,对话框以 IDOK 结束,把流程交还给 InitInstance:
// ExamApp.cpp —— 程序入口的主流程 BOOL CExamApp::InitInstance() { CLoginDlg dlgLogin; if (dlgLogin.DoModal() != IDOK) return FALSE; // 未登录直接退出 CExamDlg dlgMain; // 主考试对话框 m_pMainWnd = &dlgMain; dlgMain.DoModal(); // 考试结束后程序退出 return TRUE; }m_pMainWnd 赋给主对话框是 MFC 的要求,这样程序收到 WM_QUERYENDSESSION 或系统关机时能正常走清理流程。如果你跳过这一步,系统关机时 exe 可能被强制结束,答题数据来不及保存。真实项目里这里还会加载配置、初始化 ADO、连接数据库,我习惯把数据库初始化放在登录之前,这样登录查询不会卡顿。
3.2 动态加载题目:从题库表到界面控件
题库题目数量是变量,不可能在资源文件里预置几百个控件。常见做法是程序启动时读题库表,按题目逐行创建静态文本和单选按钮。下面是核心代码,每道题包含一个 CStatic 题目文本和四个单选按钮,单选按钮的 ID 用偏移量计算。
// ExamDlg.cpp —— 加载题目并动态创建控件 void CExamDlg::LoadQuestions() { // 假设 m_pRs 是已打开的记录集,包含题库表全部字段 int nCount = 0; while (!m_pRs->IsEOF()) { CString strContent = (LPCTSTR)(_bstr_t)m_pRs->Fields->GetItem("content")->Value; CStatic* pStc = new CStatic(); CString strQ; strQ.Format("%d. %s", nCount + 1, strContent); CRect rcQ(40, 60 + nCount * 80, 640, 80 + nCount * 80); pStc->Create(strQ, WS_CHILD | WS_VISIBLE, rcQ, this); m_stcList.push_back(pStc); for (int j = 0; j < 4; j++) { CButton* pBtn = new CButton(); CString strOpt; strOpt.Format(" %c. %s", 'A' + j, (LPCTSTR)(_bstr_t)m_pRs->Fields->GetItem("opt_" + CString((char)('a' + j)))->Value); DWORD dwStyle = BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE; if (j == 0) dwStyle |= WS_GROUP; // 每题 A 选项作为单选分组起点 CRect rcOpt(80, 100 + nCount * 80 + j * 24, 600, 122 + nCount * 80 + j * 24); pBtn->Create(strOpt, dwStyle, rcOpt, this, IDC_OPT_BASE + nCount * 4 + j); m_btnList.push_back(pBtn); } nCount++; m_pRs->MoveNext(); } m_nTotal = nCount; }这一段有两个必须说明的参数。第一个是 BS_AUTORADIOBUTTON,它让单选按钮在被点击时自动取消同组内其他按钮的选中状态,不需要你写代码维护互斥。第二个是 WS_GROUP,它决定了「同组」的边界——每道题的 A 选项带上这个样式,四个选项构成一组;如果所有选项都不带 WS_GROUP,几十道题的单选按钮会全部互斥,考生点第 3 题的答案,第 1 题就自动取消。我见过不止一个翻车现场是这个原因。
代码里用 CStatic* 和 CButton* 的 vector 保存动态对象,是为了对话框销毁时统一 delete,否则内存泄漏。题目区域的坐标用的是硬编码 CRect,真实项目里你应该根据题目文本长度动态计算高度,或者把题目放到 CScrollView 里处理滚动。动态控件还有一个隐藏问题:IDC_OPT_BASE 必须和资源文件里已有控件 ID 不冲突,一般用 2000 以上的基数,避开资源编辑器自动分配的 1000 以下区间。
3.3 考试倒计时与交卷判分
倒计时用 SetTimer 实现,每秒触发一次 WM_TIMER。考试时间从配置文件或数据库读取,这里假设 m_nRemain 已经初始化为考试总秒数。
// ExamDlg.cpp —— 倒计时与强制交卷 void CExamDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == TIMER_EXAM) { m_nRemain--; if (m_nRemain <= 0) { KillTimer(TIMER_EXAM); // 时间到,先停表再交卷 OnSubmitExam(); return; } CString strTip; strTip.Format("剩余时间:%02d:%02d", m_nRemain / 60, m_nRemain % 60); SetDlgItemText(IDC_STC_TIME, strTip); // 刷界面 } CDialog::OnTimer(nIDEvent); // 记得调基类,别漏 }SetTimer 只在初始化时调用一次,不要在 OnTimer 里反复 SetTimer,这是新手最常见的重复创建定时器问题。时间显示先用 SetDlgItemText 写到静态文本,后面第 5 章会讲怎么搬到状态栏。强制交卷路径和主动交卷路径共用 OnSubmitExam,保证逻辑只有一份。
// ExamDlg.cpp —— 判分并保存成绩 void CExamDlg::OnSubmitExam() { if (m_bSubmitted) return; // 防止二次交卷 m_bSubmitted = TRUE; KillTimer(TIMER_EXAM); int nScore = 0, nAnswered = 0; for (int i = 0; i < m_nTotal; i++) { int nFirst = IDC_OPT_BASE + i * 4; int nSel = GetCheckedRadioButton(nFirst, nFirst + 3); CString strUserAns; if (nSel == 0) strUserAns = _T(""); else { strUserAns.Format("%c", 'A' + (nSel - nFirst)); nAnswered++; } // 与题库 answer 字段比对 CString strCorrect = GetCorrectAnswer(i); if (strUserAns == strCorrect) nScore += GetScorePerQuestion(i); } SaveScore(m_strUserId, nScore, nAnswered); CString strMsg; strMsg.Format("答题 %d 题,得分 %d", nAnswered, nScore); AfxMessageBox(strMsg); CDialog::OnOK(); // 结束考试窗口 }m_bSubmitted 这个标志很关键。主动交卷按钮和倒计时归零都可能触发 OnSubmitExam,如果没有这个标志,交卷弹窗还没关闭又触发一次,成绩可能写两遍。GetCheckedRadioButton 的两个参数是检查范围的起始和结束 ID,它返回被选中按钮的 ID,返回 0 表示该范围没选中任何按钮——这正好用来区分未答题。判分逻辑完全靠 ID 区间映射题目序号,所以动态创建控件时 ID 必须严格按 i * 4 + j 排列,任何错位都会导致判分错乱。
4. 考试系统 MFC 开发避坑:兼容性、数据库与界面泄漏的 5 个真实坑
这一章全是血泪经验。VC6 的老项目和新系统之间的摩擦,比业务逻辑本身更容易让你加班到深夜。以下五条按出现频率排序,每一条都是「现象 → 原因 → 解决」的结构。
4.1 VC6 编译的 exe 在新电脑上启动没反应或报 MFC42.dll 缺失
现象:把 exe 拷到 Windows 10/11 机器上,双击没反应,或者弹「无法启动此程序,因为计算机中丢失 MFC42.dll」。原因:VC6 的 MFC 默认动态链接到 MFC42.dll,而新系统不一定自带这个运行库。解决:最简单的是把 MFC 使用方式改成静态链接,工程设置 → General → Microsoft Foundation Classes 选「Use MFC in a Static Library」,重新编译后 exe 体积变大但不再依赖 mfc42.dll。如果手里只有现成 exe 没有源码,另一个办法是把 MFC42.dll 和 MSVCRT.dll 一起放到 exe 目录,但要注意 32 位版本,别拿 64 位 dll 硬凑。还有一个容易被忽略的是 VC6 的编译器在 Win11 上跑,安装都要用兼容模式,生成的 exe 本身一般没问题,但 IDE 启动会闪退,这个用属性兼容性改成 Win7 模式能解决。
4.2 界面中文正常,数据库里的中文变成问号
现象:登录窗口中文显示正常,但从 Access 题库读出来的题目中文全部变成「??」,或者写入成绩表的名字变成乱码。原因:VC6 的 MFC 默认是 ANSI 字符集,数据库表字段如果是 Unicode 类型,ADO 读出来再转 CString 就会丢字符。解决:打开 .mdb 检查字段类型,文本类型选「文本」别选「备注」,排序规则保持常规;连接字符串里可以加 CharacterSet=UTF8,但对 Access 并不是灵丹妙药,真正靠谱的是统一用 ANSI 存。另一个隐蔽原因是代码里用了 (LPCTSTR)(_bstr_t) 这种强制转换,VC6 下 _bstr_t 转出的是宽字符,直接赋给 CString 在 ANSI 工程里会丢内容,正确的中间转换是用 CW2A 或先转成 char* 再赋。
4.3 ADO 初始化失败,CoCreateInstance 返回 0x800401f0
现象:程序在连接数据库那一步崩溃,日志或调试器显示 CoCreateInstance 返回 0x800401f0,这个错误码的意思是「尚未调用 CoInitialize」。原因:ADO 是 COM 组件,使用前必须初始化 COM 单元。很多教学代码省略了这一行,你抄过来在自己机器上可能碰巧能跑,但在不同系统上就会翻车。解决:在 InitInstance 或主对话框 OnInitDialog 里加::CoInitialize(NULL);,程序退出时对应::CoUninitialize();。如果用了 MFC 的 OleInit,那会叫你直接调 AfxOleInit(),它内部做了 COM 初始化,两选一就行,别两个都调。还有个相关坑是 #import 指令写死了 msado15.dll 的绝对路径,换机器编译找不到文件,建议写成#import "msado15.dll" no_namespace rename("EOF","rsEOF"),让编译器和 Windows 自己搜路径。
4.4 计时器导致的内存泄漏与界面卡顿
现象:考试系统挂机一小时,内存占用持续上涨,界面响应变慢,倒计时显示延迟。原因:最常见的是 OnTimer 里又调 SetTimer,或者 OnTimer 里做了数据库查询——每秒钟读一次表,连接不关闭,内存涨是必然的。解决:SetTimer 在 OnInitDialog 调一次,OnTimer 里只做减法、格式化字符串、刷新显示;数据库操作只在加载题目和交卷时做,绝不放计时器里。如果确实需要周期刷数据,用 PostMessage 或自定义消息把任务丢到空闲时段,不要在定时器回调里等查询返回。老项目还有一种低级泄漏:每次 OnTimer 里 new 了一个 CString 或控件没释放,VC6 的调试器看不出来,用 Process Explorer 看 GDI 句数能快速定位。
4.5 窗口能拖动、误关窗口导致答题数据丢失
现象:考生拖动考试窗口边框,控件布局变得乱七八糟;或者误点右上角 X,没交卷就直接退出,成绩没保存。原因:对话框默认带 WS_THICKFRAME 和 WS_MAXIMIZEBOX 样式,允许调整大小和最大化;而 OnClose 默认行为是直接 EndDialog。解决:在 OnInitDialog 里加一行ModifyStyle(WS_THICKFRAME | WS_MAXIMIZEBOX, 0);锁死窗口大小,如果用了系统菜单,还要 RemoveMenu 去掉 SC_MAXIMIZE。重写 OnClose 弹确认框:如果有未交卷标记,提示「当前未交卷,确定退出?」,确定后调用基类 OnClose;如果已经交卷,直接放行。这条坑最常见的原因是开发者以为老师不会去拖窗口,事实是在机房里总有人会手滑。
5. 把系统做扎实:随机抽题策略、状态栏剩余时间与成绩导出
最小系统能跑之后,往里面加三个高频需求:随机抽题、状态栏显示考试信息、成绩导出。这三个功能几乎每个考试系统都会被要求,并且是检索里「mfc状态栏怎么显示」「考试系统 mfc」背后最真实的诉求点。
5.1 随机抽题:洗牌算法在试题序号上的落地
随机抽题最容易犯的错是用 SQL 的 ORDER BY RND(),Access 里这样写的随机顺序不稳定,而且题库加了条件筛选后可能抽出重复题。我在老项目里一般先把符合条件的全部题号读进数组,再做 Fisher-Yates 洗牌,取前 N 道。代码很短,但边界要注意:
// ExamDlg.cpp —— 从题库中随机抽取 N 道题 void CExamDlg::ShuffleQuestions(int arrIds[], int nTotal, int nNeed) { srand((unsigned)time(NULL)); // 只调用一次,放 OnInitDialog 更合适 for (int i = nTotal - 1; i > 0; i--) { int j = rand() % (i + 1); // 区间 [0, i],保证每次交换范围正确 int tmp = arrIds[i]; arrIds[i] = arrIds[j]; arrIds[j] = tmp; } // 取前 nNeed 道作为本场考题 m_nTotal = (nNeed < nTotal) ? nNeed : nTotal; memcpy(m_arrExamIds, arrIds, m_nTotal * sizeof(int)); }srand 放循环外是关键,放循环里会导致一秒内多次调用生成相同序列,出题重复。rand() % (i + 1) 这个写法保证每次随机交换的下标不越界。取前 nNeed 之前记得判断 nNeed 不能大于 nTotal,否则越界拷贝会直接内存错误。抽完题后,后面的 LoadQuestions 按 m_arrExamIds 里的顺序查题库表,而不是按全表顺序,这样每场考试题目顺序都不一样。
5.2 状态栏显示考生信息和剩余时间:CDialog 里的 CStatusBar 用法
MFC 对话框默认不带状态栏,这是很多人搜「mfc状态栏怎么显示」的原因。在 CDialog 里用 CStatusBar 要自己创建、自己定位,比文档视图里自动 dock 麻烦一步。标准流程分三小段:头文件加成员变量,OnInitDialog 里创建和 SetIndicators,再在 OnSize 里调整位置。
// ExamDlg.h —— 类内声明 CStatusBar m_wndStatusBar; // ExamDlg.cpp —— OnInitDialog 中创建 UINT nIndicators[] = { ID_SEPARATOR, ID_SEPARATOR }; m_wndStatusBar.Create(this); m_wndStatusBar.SetIndicators(nIndicators, 2); m_wndStatusBar.SetPaneInfo(0, ID_SEPARATOR, SBPS_STRETCH, 200); // 第一格弹性拉伸 m_wndStatusBar.SetPaneInfo(1, ID_SEPARATOR, 0, 120); // 第二格固定宽度 m_wndStatusBar.SetPaneText(0, "考生:张三 考号:001"); m_wndStatusBar.SetPaneText(1, "剩余 30:00");ID_SEPARATOR 是 MFC 预定义的分隔栏标识,SetPaneInfo 第一个参数是索引,第三个参数是样式,SBPS_STRETCH 让第一格随窗口宽度变化,第四个参数是面板宽度像素。注意 CDialog 不会自动把状态栏放在底部,你要在 OnSize 里手动 MoveWindow:
// ExamDlg.cpp —— 调整状态栏位置 void CExamDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if (m_wndStatusBar.GetSafeHwnd()) m_wndStatusBar.MoveWindow(0, cy - 28, cx, 28); }状态栏的常见翻车点是 GetSafeHwnd 判断——OnSize 可能在 OnInitDialog 创建状态栏之前被触发一次,不判空直接用会崩溃。高度 28 是按默认状态栏字体估算的,如果改了字体大小要同步调整。这个方案比用静态文本显示倒计时的好处是:第一格可以顺手显示考生信息,第二格滚动显示剩余时间,视觉上和主窗口融为一体。
5.3 成绩导出到 CSV:让我避开 Excel 组件依赖的笨办法
考试成绩要发给老师存档,最正式的方案是用 Excel 对象,但 VC6 工程引 Excel 的 COM 类型库是个大坑,版本兼容问题一堆。我一般用 CSV 文件兜底,老师双击用 Excel 打开,格式通用,代码也简单。关键是编码——VC6 的 CStdioFile 以 typeText 模式写入时按 ANSI 编码,Excel 直接打开 GBK 的 CSV 不乱码,如果你用记事本另存成 UTF-8,Excel 打开反而乱码,别多此一举。
// ExamDlg.cpp —— 导出成绩到 CSV void CExamDlg::ExportScore(const CString& strPath) { CStdioFile file; if (!file.Open(strPath, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { AfxMessageBox("无法创建成绩文件"); return; } file.WriteString("学号,姓名,成绩\r\n"); // m_rsScore 是成绩记录集,这里遍历写每一行 while (!m_rsScore.IsEOF()) { CString strLine; strLine.Format("%s,%s,%d\r\n", (LPCTSTR)(_bstr_t)m_rsScore.Fields->GetItem("user_id")->Value, (LPCTSTR)(_bstr_t)m_rsScore.Fields->GetItem("user_name")->Value, (int)m_rsScore.Fields->GetItem("score")->Value); file.WriteString(strLine); m_rsScore.MoveNext(); } file.Close(); ShellExecute(NULL, "open", strPath, NULL, NULL, SW_SHOWNORMAL); }CSV 的三处细节:每一行用 \r\n 结尾,Excel 能正确换行;字段值里如果包含逗号或换行,需要给字段加双引号包裹,姓名里出现逗号的情况极少,但你要较真就别省;成绩用 %d 输出而不是 %f,避免 85.000000 这种一串小数点。写完后用 ShellExecute 自动打开文件,既省了老师找文件的操作,也算给程序一个可见的反馈。如果你要更严谨,可以在文件名里带上考试时间,比如 scores_20250115.csv,避免覆盖上次的成绩记录。
6. 交付前怎么验证:一个考试系统上线前的检查清单
开发完别急着拷到机房,先花半小时按下面这张清单走一遍,能拦下八成的交付事故。功能层面:登录框空密码、错误密码、正确密码三条路径分别验证;题目加载后逐题确认互斥正常,重点看跨题目之间选中状态是否互相干扰;倒计时归零强制交卷后,再点交卷按钮,确认不能重复提交。数据层面:交卷后查成绩表,确认成绩写入且没有重复记录;随机抽题跑三次,对比三次的题号和顺序是否真的不同。兼容性层面:在 Win7、Win10、Win11 各跑一遍,静态链接的 exe 通常没大问题,但高 DPI 缩放会造成界面模糊,右键 exe 属性里选「替代高 DPI 缩放行为」能缓解;屏幕分辨率低的机器要确认底部状态栏没被遮住。异常场景:考到一半拔掉数据库文件,程序应该弹出友好提示而不是崩溃;断电后重启机器,已经答完的题如果没有实时保存策略,至少要保证不会把零分写入成绩表。
我自己的教训是:有次交付前只测了快速交卷,没测长时间挂机,结果考生考到第十分钟,答题界面的滚动条消失了——原因是最初的版本在 OnTimer 里反复重绘题目区域,资源泄漏到一定数量就开始失控,吞掉滚动条。自从那次之后,我养成了一个习惯:任何涉及动态创建控件的 MFC 程序,交付前必定用 GDI 句数监控工具跑一次完整考试流程,确认进程的句柄数曲线平稳,而不是一路向上。MFC 的界面代码不比其他框架,静态分析看不出资源泄漏,只有跑完整流程才能暴露。这套最小考试的骨架你照着搭出来后,把题库表、成绩表、随机抽题和状态栏四项粘进去,就是一个能用的考试系统——剩下的美化、打印、断点续考,都是在这个骨架上加肉的问题。希望帮到你。
本文还有配套的精品资源,点击获取