
简介这是一份基于 Visual C 6.0 MFC 框架开发的桌面版考试系统完整工程包面向有 C 基础、想在 Windows 桌面应用与 Access 数据库结合场景中进阶的读者也可直接用于课程设计或项目参考。系统覆盖用户注册登录、试题维护、答题管理、成绩管理与统计分析等模块后台使用 Access 数据库并通过 ADO 方式连接代码中包含对话框控件与数据库操作之间的完整调用链。压缩包共 57 个文件以 19 个 .cpp 与 19 个 .h 源码文件为主辅以 .rc 界面资源、.bmp/.ico 图标素材、项目工程配置文件和 exam.mdb 示例数据库整体仅约 356KB。已有 210 人学习适合对照 MFC 对话框、列表控件、文档/视图架构与 ADO 数据访问代码梳理一套小型考试系统的完整开发思路。资源内用户管理、试题管理、成绩管理均包含可运行的界面与后端交互逻辑可直接打开工程阅读或二次扩展。1. 还在用 Visual C 6.0 写考试系统先别急着换框架如果你接手过一个老单位的教务系统大概率见过这种场景一台 Windows XP 工控机装了 Visual C 6.0跑着一个界面灰扑扑但稳定运行了十来年的考试程序。别笑在很多高校机房、职业技能鉴定站、企业内部考核场景里这种基于 Visual C 6.0 MFC 开发的桌面版考试系统至今仍在服役。原因很实际硬件老、系统旧、换框架成本高而 VC6.0 MFC 编译出来的原生 Win32 程序在低配机器上跑得比任何带运行时依赖的新框架都稳。这篇笔记要讲的就是如何用 MFC 的文档/视图架构、ODBC 数据库访问和定时器机制从零搭一个能真正投入使用的桌面考试系统——包含题库管理、随机抽题、答题计时、自动评分和成绩留存并附上我在实际部署中踩过的坑。适合谁看两类人。一类是刚被分配去维护老考试系统的工程师需要快速看懂代码结构并改需求另一类是学校或企业内部想自建无纸化考试平台但不想上 Web 端、不想引入 Electron 之类重依赖的技术选型者。读完你会知道这套方案能做什么、怎么做、哪些地方容易翻车。2. 考试系统的骨架MFC 文档/视图架构与类拆分2.1 为什么考试系统适合用文档/视图架构而不是对话框很多人写 MFC 程序习惯直接拖一个 Dialog考试系统也照搬。但实际做过就知道对话框适合表单类交互而考试系统是一个典型的「数据 展示分离」场景题库数据要在多个视图之间共享答题状态要在切换题型时保持一致考试倒计时要全局生效——这些用 CDocument 持有数据、CView 负责渲染的模式会省力很多。MFC 的文档/视图架构Doc/View本质上是 MVC 的变体CDocument 负责管理数据模型CView 负责把模型画到窗口上。考试系统里我把 CDocument 子类命名为 CExamDoc它负责三件事加载题库文件、保存考生作答记录、计算最终得分。窗口上所有控件下一题按钮、答案选项、状态栏倒计时都归 CExamView 管它只做一件事从 CExamDoc 拿当前题目显示给用户再把用户的选择写回文档。这种拆分带来的直接好处是如果某天你想从单机版升级成局域网版只需要改 CExamDoc 的数据加载部分——从读本地文件换成读共享数据库——视图层完全不用动。我第一次没这么做把所有逻辑堆在 View 里后来加一个「按题型顺序出题」的需求改了三天血泪教训。2.2 关键类的职责划分与消息映射设计下面是我会推荐的类结构清单。注意每个类的职责边界要清晰否则后面加功能一定会乱。// ExamDoc.h —— 数据模型题库、考生、作答记录 class CExamDoc : public CDocument { public: // 题库每道题是一个结构体见下方 ExamQuestion CArrayExamQuestion, ExamQuestion m_questionBank; // 本次考试实际抽到的题号序列保证随机但可追溯 CArrayint, int m_paperIndexList; // 考生答案数组下标与 m_paperIndexList 一一对应 CArrayint, int m_userAnswerList; BOOL LoadQuestionBank(LPCTSTR lpszFilePath); // 从题库文件读取 BOOL GeneratePaper(int nQuestionCount, BOOL bRandom); // 抽题 int CalcScore(); // 自动评分 BOOL SaveResult(LPCTSTR lpszFilePath); // 写成绩文件 }; // ExamView.h —— 界面交互显示题目、接收点击、驱动计时器 class CExamView : public CView { protected: CExamDoc* GetDocument(); // MFC 机制从 View 拿 Doc 指针 afx_msg void OnBnClickedPrevQuestion(); // 上一题 afx_msg void OnBnClickedNextQuestion(); // 下一题 afx_msg void OnBnClickedOptionA(); // 选项 A~D 四个按钮 afx_msg void OnTimer(UINT_PTR nIDEvent); // 考试倒计时驱动 };ExamQuestion 结构体这样定义覆盖单选、多选、判断三种常见题型用 m_nType 区分答案用 4 位二进制位图存——第 0 位代表 A第 1 位代表 B以此类推。位图的好处是单选和多选都能表达判断题可以把 A 映射为「正确」、B 映射为「错误」。// ExamQuestion 结构体定义 typedef struct tagExamQuestion { int m_nType; // 1单选2多选3判断 CString m_strQuestion; // 题干文本 CString m_strOptionA; // 选项文本判断题为正确 CString m_strOptionB; // 选项文本判断题为错误 CString m_strOptionC; CString m_strOptionD; int m_nAnswer; // 位图110(A)211(B)412(C)813(D) int m_nScore; // 本题分值 } ExamQuestion;这种拆法的核心思路是把「知识」放在 Doc 里把「交互」放在 View 里。CExamView 有一个指向 CExamDoc 的指针MFC 会自动维护每次点击下一题View 只做两件事把上一题的用户答案写回 doc 的 m_userAnswerList然后根据当前题号从 doc 读出下一题的内容刷新界面。状态栏的倒计时则由 View 的 OnTimer 统一处理时间归零时调用 doc 的 CalcScore 并弹窗提示交卷。2.3 按需抽题的随机算法与试卷结构生成抽题逻辑是考试系统的核心之一。如果只是用 rand() 随机挑 N 道题会连续抽到同一章节的题目考完学生要骂娘。我一般这样做先把题库按题型分组再从每组按比例抽取指定数量最后组内用洗牌算法打乱。这里用 Fisher-Yates 洗牌避免 rand() % n 带来的分布偏差。// 从题库生成一份试卷按题型比例抽取组内随机打乱 BOOL CExamDoc::GeneratePaper(int nSingleCount, int nMultiCount, int nJudgeCount) { m_paperIndexList.RemoveAll(); // 分组索引先把各类题目的下标分别收集 CArrayint, int arrSingle, arrMulti, arrJudge; for (int i 0; i m_questionBank.GetSize(); i) { switch (m_questionBank[i].m_nType) { case 1: arrSingle.Add(i); break; case 2: arrMulti.Add(i); break; case 3: arrJudge.Add(i); break; } } // 按所需数量随机抽取组内洗牌取前 N 个 srand((unsigned)time(NULL)); ShuffleArray(arrSingle); ShuffleArray(arrMulti); ShuffleArray(arrJudge); for (int i 0; i nSingleCount i arrSingle.GetSize(); i) m_paperIndexList.Add(arrSingle[i]); // 多选、判断题同样处理代码省略 return TRUE; }ShuffleArray 是一个自己写的静态工具函数从数组尾部往前遍历每次把当前位置和随机位置的元素交换。注意要在每次生成试卷前重新 srand且种子用 time(NULL) 加当前进程 ID 混合否则同一秒内启动的多个考试进程生成的试卷会完全一样。这个细节当初没注意一次考试 30 台机器开考前 10 台抽到的第一题完全相同被监考老师追着问了半天。3. 考试流程与界面交互从答题到交卷的完整闭环3.1 题目切换与选项状态的正确刷新界面交互的坑比想象中多。先说题目切换。CExamView 里有一个当前题号成员 m_nCurrentIndex点击「下一题」时要先把当前界面上用户勾选的答案通过 GetCheck / GetCheck 读出来写入 doc 的 m_userAnswerList然后 m_nCurrentIndex再根据新题号刷新界面。很多人忘了第一步导致用户做完第 5 题切到第 6 题再返回第 5 题时答案已经丢了。void CExamView::OnBnClickedNextQuestion() { CExamDoc* pDoc GetDocument(); if (pDoc NULL) return; // 1. 先把当前屏幕上勾选的答案保存 SaveCurrentAnswer(); // 2. 如果已经是最后一题提示交卷 if (m_nCurrentIndex pDoc-m_paperIndexList.GetSize() - 1) { // 交卷前弹出确认框避免误触 if (AfxMessageBox(_T(已是最后一题确定交卷), MB_YESNO) IDYES) { SubmitPaper(); } return; } // 3. 题号递增并刷新界面 m_nCurrentIndex; DisplayQuestion(m_nCurrentIndex); }DisplayQuestion 要处理几件事根据题号找到题库里的原题把题干和四个选项填到静态文本控件上根据题型决定四个选项按钮的可见性——判断题隐藏 C 和 D 选项如果是多选选项按钮要允许同时选中两项以上最后把 m_userAnswerList 里已存过的答案恢复到控件勾选状态。这一步用「先取消所有勾选再按位图恢复」实现比「判断当前状态是否需要翻转」要可靠得多。这里最容易翻车的是多选和单选共用的选项按钮。我用四个 CButton 控件风格设为 BS_AUTOCHECKBOX。在单选模式下每次用户点击某个选项要把其余三个选项的 SetCheck(0) 全部清掉。多选模式下则互不干扰。判断当前模式的依据就是题目类型在 DisplayQuestion 里一次性配置好。3.2 考试倒计时的 Timer 与状态栏联动倒计时用 SetTimer 实现每 1 秒触发一次 WM_TIMER 消息。为什么不用 while Sleep因为考试系统界面必须保持响应——用户在答题时如果有个 MessageBox 弹出来while 循环会把界面卡死倒计时反而走得比实际慢。MFC 的 SetTimer 是基于窗口消息的在消息循环空闲时触发不会阻塞用户操作。// 在 View 的 OnInitialUpdate 中启动考试计时器 void CExamView::OnInitialUpdate() { CView::OnInitialUpdate(); // 从文档中读取考试时长分钟换算成秒 m_nRemainSeconds GetDocument()-m_nExamMinutes * 60; // 设置 1 秒间隔的计时器 SetTimer(TIMER_EXAM_CLOCK, 1000, NULL); } // 每秒触发一次 void CExamView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_EXAM_CLOCK) { m_nRemainSeconds--; if (m_nRemainSeconds 0) { // 时间到强制交卷 KillTimer(TIMER_EXAM_CLOCK); AfxMessageBox(_T(考试时间到系统将自动交卷)); SubmitPaper(); return; } UpdateStatusBarClock(); // 刷新状态栏时间显示 } CView::OnTimer(nIDEvent); }状态栏显示倒计时我用的办法是给状态栏设置一个固定窗格不随鼠标移动变化。默认的状态栏第一个窗格会实时显示鼠标所在位置的菜单提示不适合放倒计时。做法是在 MainFrame 的 OnCreate 里找到状态栏的 Indicators 数组把某个索引处的字符串 ID 替换成一个自定义的静态字符串资源然后定时调用 SetPaneText 更新文本。注意要先把该窗格的 ID 设成 ID_SEPARATOR否则 SetPaneText 会因为找不到 ID 对应的字符串资源而失败。窗口大小要锁定防止用户在考试中途拉大窗口导致布局错乱或误点最大化。在 MainFrame 的 PreCreateWindow 里改 cs.style 去掉 WS_THICKFRAME 和 WS_MAXIMIZEBOX 就行比在 OnSize 里强制 SetWindowPos 更干净——后者会造成窗口闪烁看起来非常业余。这个做法在 MFC 里有一个专门的名称叫「禁止拖动窗口大小」改成样式即可一劳永逸。3.3 交卷处理与评分回写的完整链路交卷分主动交卷和超时自动交卷两条路径但最终都汇聚到 SubmitPaper 里。这个函数要做的顺序很关键先把当前屏幕上的答案保存再调用 doc 的 CalcScore 计算得分然后把成绩追加写入成绩文件最后弹窗告知考生得分并关闭窗口。关闭窗口时 MFC 会触发 OnClose需要在里面拦截一次如果还没交卷就强制走交卷流程防止考生直接点右上角 X 逃掉。void CExamView::SubmitPaper() { CExamDoc* pDoc GetDocument(); // 最后一道题的答案可能还留在界面上先存 SaveCurrentAnswer(); // 计算总分 int nTotalScore pDoc-CalcScore(); // 记录成绩文件名带上时间戳避免多人并发写入同一文件 CString strTime COleDateTime::GetCurrentTime().Format(_T(%Y%m%d_%H%M%S)); CString strFileName; strFileName.Format(_T(exam_result_%s.txt), strTime); pDoc-SaveResult(strFileName); // 展示成绩弹窗同时给提示 CString strMsg; strMsg.Format(_T(考试完成\r\n得分%d 分), nTotalScore); AfxMessageBox(strMsg, MB_OK | MB_ICONINFORMATION); // 关闭窗口 PostMessage(WM_CLOSE); }CalcScore 的评分逻辑要区分单选、多选和判断。单选和判断直接比较 m_userAnswerList 里的值与标准答案是否相等多选则要检查「用户所选的所有位」是否和「标准答案的所有位」完全一致——注意不能只按位与结果非零判断那样选错一个选项也算半对很多系统在这里出了 bug。严格模式下只有用户答案和标准答案的位图完全相等才给分。SaveResult 的写入格式我建议用 CSV 或固定长度的文本字段包括考号、姓名、每题答案序列、每题得分、总分。这样后续即使数据库没接通也能在 Excel 里直接看到成绩明细。如果有人想核对某道题的标准答案和考生答案一行数据全都有了。4. 题库与成绩数据的落点ODBC 访问 Access 的工程化配置4.1 为什么 ADO 比 ODBC API 更适合这个场景桌面考试系统的数据量不大——题库几千道题、成绩几千行——用 Access 数据库足够。访问 Access 的常见方式有两条DAO老掉牙VC6 时代就已经是遗留物、ODBC、ADO。我推荐 ADO理由是它可以复用同一个连接对象操作数据库和读取文本文件而且代码量和 DAO 相比少了一半错误处理也更清晰。ADO 在 VC6.0 里的使用方式比较特殊需要先用#import C:\Program Files\Common Files\System\ado\msado15.dll no_namespace rename(EOF, EndOfFile)导入类型库。注意最后的 rename 很关键ADO 的 EOF 属性和 MFC 里 CFile 的 EOF 宏重名不重命名就直接编译报错。这个坑我第一次用时整整查了一上午最后靠啃 MSDN 才找到答案。// ExamDoc.cpp 中加载题库从 Access 的 QuestionBank 表读取 #import C:\Program Files\Common Files\System\ado\msado15.dll no_namespace rename(EOF, EndOfFile) BOOL CExamDoc::LoadQuestionBank(LPCTSTR lpszDbPath) { _ConnectionPtr pConn; _RecordsetPtr pRs; try { // 使用 Microsoft Jet OLE DB Provider 访问 Access 2003 格式 pConn.CreateInstance(__uuidof(Connection)); CString strConn; strConn.Format( _T(ProviderMicrosoft.Jet.OLEDB.4.0;Data Source%s;), lpszDbPath); pConn-Open((_bstr_t)strConn, , , adConnectUnspecified); // 执行查询 pRs.CreateInstance(__uuidof(Recordset)); pRs-Open(SELECT * FROM QuestionBank, pConn.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText); while (!pRs-EndOfFile) { ExamQuestion q; q.m_nType pRs-Fields-GetItem(QType)-Value; q.m_strQuestion (char*)(_bstr_t)pRs-Fields-GetItem(QText)-Value; // 选项和答案的读取代码类似省略 m_questionBank.Add(q); pRs-MoveNext(); } pRs-Close(); pConn-Close(); return TRUE; } catch (_com_error e) { // 常见失败原因数据库文件被占用、连接串格式错误 AfxMessageBox(e.Description()); return FALSE; } }这段代码里_bstr_t转char*是 VC6.0 时代的标准写法。现在用高版本 VS 编译时从_bstr_t到char*的隐式转换已经被移除需要手动(char*)_bstr_t(...)或者用CW2A宏转换。如果你的机器上装了 Office 2016 及以上Jet 4.0 提供程序可能不再被支持这时候要换成Microsoft.ACE.OLEDB.12.0这是一个在部署机上最常见的坑后文单独说。4.2 三个最实用的题库导入脚本SQL预置题库用 SQL 脚本批量插入比在程序里写死要方便得多——出题老师通常用 Excel 整理题目导出成 CSV 后再灌进 Access 即可。我用过的、最简单可靠的三个脚本如下。第一段创建题库表。字段要预留扩展空间题干最长设为 500选项文本 200因为有些题目带图片路径或较长公式。CREATE TABLE QuestionBank ( QID COUNTER PRIMARY KEY, -- 自增主键 QType LONG, -- 1单选2多选3判断 QText MEMO, -- 题干 QOptionA VARCHAR(200), QOptionB VARCHAR(200), QOptionC VARCHAR(200), QOptionD VARCHAR(200), QAnswer LONG, -- 答案位图A1B2C4D8 QScore LONG -- 分值 );第二段从 CSV 文件批量导入。Access 的IN子句可以直接读取外部文本文件注意 CSV 的列顺序要与 INSERT 语句一致且文件第一行不要带表头。INSERT INTO QuestionBank (QType, QText, QOptionA, QOptionB, QOptionC, QOptionD, QAnswer, QScore) SELECT * FROM [Text;FMTCSV;HDRNO;IMEX1;DATABASEC:\examdata].QuestionImport.csv;第三段成绩表的建表语句。成绩表不设计外键原因是 Access 的 Jet 引擎在处理级联删除时性能极差而考试系统的成绩需要长期保留不适合随题库删除而联动删除。CREATE TABLE ExamResult ( RID COUNTER PRIMARY KEY, StudentNo VARCHAR(20), StudentName VARCHAR(50), AnswerSequence MEMO, -- 用户作答序列如 A,C#B,D#A ScoreSequence MEMO, -- 每题的得分序列 TotalScore LONG, ExamDate DATETIME );这些表结构虽然简单但在实际项目中够用且稳定。出题老师在 Excel 里改完题存成 CSV跑一遍第二段 SQL 就能把新题灌进库里不需要专门做维护界面——考试系统的精力应该花在答题流程和稳定性上而不是做一个费时费力的题库管理后台。4.3 数据库连接失败时的降级策略一定要给题库加载失败留降级方案。真实考场里Windows 防火墙可能拦截 Access 的共享连接、U 盘拔太快导致数据库文件未写完、杀毒软件把 Access 的临时文件锁掉——任何一条都能让程序启动时弹出「无法打开题库」然后退出。我的做法是程序启动时先尝试从指定路径加载失败后弹窗让监考老师手动浏览选择题库文件同时内置一份文本格式的紧急题库保证在没有数据库环境的机器上也能开考。// 启动时加载题库数据库优先文件降级 BOOL CExamDoc::LoadQuestionBankSmart() { // 1. 尝试标准路径的 Access 库 if (LoadQuestionBank(_T(C:\\ExamData\\questions.mdb))) return TRUE; // 2. 失败后弹窗让用户手动选择题库文件 CFileDialog dlg(TRUE, _T(mdb), NULL, OFN_FILEMUSTEXIST, _T(考试题库 (*.mdb)|*.mdb||), NULL); if (dlg.DoModal() IDOK) { return LoadQuestionBank(dlg.GetPathName()); } // 3. 最后尝试文本格式紧急题库 return LoadQuestionBankFromText(_T(C:\\ExamData\\backup_questions.txt)); }文本格式紧急题库的格式很简单每行一道题字段用竖线|分隔。判断题型、题干、选项、答案、分值解析出来填进 ExamQuestion 结构体即可。这个降级文件建议打印一份放在考场信封里万一所有电子设备出问题至少还能靠文本文件保证开考。5. 桌面考试系统的避坑笔记五条用时间换来的经验5.1 现象在 Windows 10 上启动时崩溃报错内容指向 MFC DLL 缺失原因VC6.0 的工程默认使用共享 MFC DLLMFCD42.DLL / MFC42U.DLL而 Windows 10 及之后版本不带这些老运行库。新版系统只自带 VC2005 之后的运行库。解决项目属性里把「使用 MFC」改成「在静态库中使用 MFC」或在部署机上把 VC6.0 的 redist 目录下的 MFC42.DLL、MSVCRT.DLL 拷到程序同目录。前者一劳永逸但生成的 exe 会大一倍后者文件数量少适合做绿色版考试系统。我一般两个方案都做自己的开发机用共享 DLL 加快编译交付给考场时用静态链接重新编一版。5.2 现象VC6.0 的编译器不认 CString::Format 传入中文输出乱码原因VC6.0 默认的字符集是 MBCS多字节而 CString 内部存的是窄字符Windows 的 ANSI 编码取决于系统区域设置。在简体中文系统上没问题但如果考试系统要部署到繁体中文或英文 Windows 上ANSI 编码不对就是乱码。解决所有界面字符串统一用_T()宏包起来文件读写用宽字节版本CStdioFile的ReadString可以自动处理。如果实在要用 UTF-8 保存题库文本LoadQuestionBankFromText 里要手动把读进来的字节流做一次 CP_ACP 到 CP_UTF8 的转换而不能直接赋给 CString。这个问题在考试系统里常见于「考生姓名输入框」——某些生僻字在 GBK 里没有映射存进去再读出来直接变成了问号。5.3 现象多选题目用户选了两个正确答案之一评分时却得了零分原因评分代码用的是按位与判断(userAnswer correctAnswer) ! 0这会把「选对一半」判定为正确。更隐蔽的是如果用户选了 A 和 C标准答案是 A 和 D按位与后结果是 A 的位非零也会误给分。解决严格比较位图值而不是做交集判断。if (userAnswer correctAnswer)才是唯一正确的标准。如果题目声明「答对但不全对给一半分」则先计算交集点数再乘分值但绝大多数考试系统为了公平多选必须全对才给分。我在 CalcScore 里同时保留了两种评分模式用文档类的成员布尔变量切换——默认严格模式需要半对模式时由监考老师在界面勾选。5.4 现象考试过程中点击右上角 X 不弹确认框程序直接退出原因CView 的OnClose被默认的窗口销毁流程绕过了。VC6.0 里点击 X 触发的是DefWindowProc的WM_CLOSE如果没有重写OnClose并拦截框架就直接DestroyWindow了。解决在 CExamView 里重写OnClose先检查考试是否已交卷。没交卷就弹确认框——「确定要终止考试吗未交卷本次成绩作废」——再决定是否调用基类的OnClose。注意里面不能直接调用CDocument::OnCloseDocument否则会触发文档关闭把未保存的答案也清掉了。void CExamView::OnClose() { if (!m_bSubmitted) // 尚未交卷需确认 { if (AfxMessageBox(_T(确定要终止考试吗未交卷本次成绩作废), MB_YESNO | MB_ICONWARNING) IDYES) { m_bTerminated TRUE; // 标记非正常结束成绩文件写 0 分 SubmitPaper(); return; // 由 SubmitPaper 里 PostMessage(WM_CLOSE) 再走一次本函数 } return; // 用户取消关闭什么都不做 } CView::OnClose(); // 已交卷正常关闭 }这里还有一个细节PostMessage(WM_CLOSE) 之后 OnClose 会被再次触发此时 m_bSubmitted 已经是 TRUE会直接走基类关闭。这种二次进入的流程比在 OnClose 里直接调 DestroyWindow 要安全。5.5 现象状态栏倒计时不刷新只有鼠标移动时才变原因状态栏第一个窗格默认是ID_INDICATOR_xxx这类的命令路由窗格它有自动更新机制但只响应 WM_IDLEUPDATECMDUI。如果在 OnTimer 里SetPaneText(1, ...)更新的是错误的索引或者没有使能窗格的SetPaneStyle文字就不会即时绘制等鼠标触发界面刷新才显示新值。解决给倒计时用一个专用的ID_SEPARATOR窗格并把它的宽度固定。在 OnCreate 里m_wndStatusBar.SetPaneInfo(1, ID_SEPARATOR, SBPS_STRETCH | SBPS_NOBORDERS, 120);。SetPaneText 之前先m_wndStatusBar.InvalidateRect(NULL, TRUE)强制重绘。考试系统的倒计时必须做到一秒变一次任何「延迟显示」都会让考生和监考老师认为系统卡死。6. 从单机到考场级部署验证清单与打包技巧单机跑通只是第一步真正常见的落点是一间机房 40 台机器同时开考。验证环节我一般分三步。第一步是功能回归写一个自动化测试脚本用脚本模拟 100 次完整答题流程——启动、随机抽题、下一题、勾选答案、交卷每次随机选择不同答案组合最后核对成绩文件的正确性。这套脚本我用 Python 写通过 Win32 API 发送鼠标键盘消息驱动程序跑一夜能找出不少偶现的界面刷新问题。第二步是并发验证。40 台机器同时交卷写入成绩文件时Access 的 Jet 引擎在同一秒内多个进程写同一 MDB 文件会报「文件正被另一用户使用」。我的解法是每台客户端独立写本地成绩文件文件名带机器名和时间戳考试结束后监考老师用一个合并工具批量导入服务器而不是让所有机器直连数据库写成绩。这个合并工具单独写只做一件事遍历目录下所有exam_result_*.txt按学号去重后入 Excel。第三步是打包部署。VC6.0 的安装程序我一般用 Inno Setup 做因为它可以预设静默安装 MFC 静态链接库和 ODBC 驱动注册。部署机上如果没装 Access需要随安装包带上Microsoft.Jet.OLEDB.4.0提供程序的注册命令通常在系统盘 Windows\System32\msjetoledb40.dll用 regsvr32 注册一次即可。安装界面我只放三个选项安装目录、数据库路径、是否创建桌面快捷方式其他全部走默认监考老师不需要动任何配置。最后分享一个我自己的习惯VC6.0 工程有一个.dsp/.dsw工作区文件升级到新版 Visual Studio 时转换向导偶尔会把资源文件搞坏。所以我建议考试系统的源码文件全部用纯文本管理.cpp/.h/.rc 正常文本不要存成 UTF-8 带 BOM万一哪天要从 VC6.0 迁到 VS2022改动量能降到最小。这也是为什么我坚持用_T()而不是裸字符串、用 SDK 风格函数而不是各种向导生成的依赖代码——为的就是今天写完五年后有人接手还能编译得起来。希望这些踩坑经验帮到你哪怕只躲掉其中一两个坑也算没白写。本文还有配套的精品资源点击获取