尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

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

VC6+MFC开发桌面考试系统:从题库设计到部署避坑全指南

VC6+MFC开发桌面考试系统:从题库设计到部署避坑全指南 简介在职业教育、政企内训和传统制造业的考核场景中桌面级考试系统凭借离线可用、环境可控、部署简单的特性依然是刚需。这类系统无需复杂架构核心在于稳定的单机运行与可靠的数据处理。C与MFC作为经典桌面开发组合通过合理设计题库表结构、实现基于随机数与难度权重的抽题算法并利用SQLite进行本地持久化即可高效支撑考试全流程。同时老项目的工程化实践至关重要ANSI编码处理、静态链接运行库、兼容性manifest配置以及基于spdlog的日志模块都能显著提升软件在老旧Windows环境下的稳定性与可维护性。本文以VC6MFC实现的考试系统为例拆解从需求分析、界面交互到自动判分、打包交付的关键技术细节为同类单机信息管理系统提供可直接复用的开发经验。 每个老程序员手里多少都留着几个 VC6 时代的“老古董”项目。说“古董”不是嫌弃恰恰相反像 Visual C 6.0 配合 MFC 这套组合在今天的教育行业、政企内训、传统制造业的培训考核场景里依然有大把系统在跑而且跑得稳的很。我自己就接过好几个类似的需求——客户明确要求必须是桌面版、必须离线可用、最好能直接在老旧的 Windows XP 或者 Win7 工控机上跑起来。这时候VC6 MFC 几乎是最不需要犹豫的选型。这篇文章我想拿一个典型的“桌面版考试系统”作为例子完整拆一遍这类项目从接到需求到最终打包交付的整个思考过程。核心内容涵盖题库表结构怎么设计、抽题算法怎么写、MFC 里倒计时和交卷逻辑怎么处理、答题卡界面用什么控件最省事、成绩怎么离线保存和导出最后还会补充几个我实际开发中踩过的坑包括 ANSI 编码和中文乱码的雷区、VC6 在 Win7/10 上运行时的兼容性问题以及那些年我们绕不开的 DLL 依赖。如果你正准备用 MFC 做一个类似的信息管理系统、培训考核软件或者只是想把老代码重新跑起来这篇应该能帮你省不少时间。1. 为什么 2024 年还要用 VC6 写考试系统选型背后的真实原因先说个可能让新人意外的事实在很多内部培训场景里考试系统的“考核严肃性”和“环境可控性”重要性远高于“界面美观度”和“技术先进性”。1.1 谁还在用这类桌面考试系统我梳理过自己经手的项目主要集中在这几类单位使用场景典型特征为什么不用 B/S 架构职业院校机房考试50-100 台无盘工作站浏览器版本老旧Web 部署受服务器性能限制离线考试不稳定企业内部年度安规考试工控机/老电脑部分没有联网条件涉密内网隔离不允许数据外传驾校/培训机构理论模拟单机练习模式为主学员需要无网环境反复刷题国企/事业单位技能鉴定电脑配置低Windows XP 系统B/S 架构对老系统兼容性差这类客户有个共同点他们不关心你用的是不是微服务架构也不关心前端是不是 Vue3他们只关心三件事——能不能装得上、考试过程中会不会死机、成绩会不会算错。而 VC6 编译出来的 MFC 程序恰好完美满足这三点绿色免安装、运行稳定、逻辑简单直接。1.2 MFC 这类“老技术”的真实优势网上关于“MFC 已死”的说法喊了快二十年但实际项目里它还是有不可替代的位置。我给几个客观的判断第一部署成本极低。一个 Release 版本编译出来主程序 exe 加一个数据库文件总共不到 10MB。不需要装 .NET Framework不需要装 Java 虚拟机不需要配 IIS。用 U 盘拷贝到任何一台 Windows 电脑上双击就能跑。考试系统最怕的就是“考前半小时发现某台机器环境有问题”用 MFC 写这套系统几乎不会出这种状况。第二开发思路直观。考试系统的核心逻辑其实就三块维护题库、生成试卷、判分。用 MFC 的 Dialog 界面加上 CRecordset/ADO 数据库操作半小时就能把主框架搭出来。比起现在动辄要配前后端分离、数据库连接池、Redis 缓存的项目MFC 的逻辑对单机工具型软件来说反而更匹配。第三VC6 编译器有不可替代的兼容性。这一点是重点。VC6 生成的程序默认是 GBK 编码依赖的是 msvcp60.dll、mfc42.dll 这些老版本运行库。对于 Windows XP 甚至 Windows 2000 系统来说这些 DLL 系统自带。如果你用 VS2019 重新编译生成的是 Universal CRT 依赖的版本在没打补丁的 Win7 上反而可能跑不起来。现实中很多考试机就是老系统所以“新编译器”不等于“更好用”。1.3 什么时候不建议用这套方案倒也不是一味鼓吹 VC6。如果你遇到下面这些需求我会直接劝退考试需要联网实时上传成绩云端统一分析需要支持手机端答题、远程监考、人脸识别业务方明确要求前后端分离架构后续有迭代扩展计划题目包含大量视频、富文本、数学公式等复杂题型马老师说过抛开剂量谈毒性就是耍流氓。技术选型也一样抛开场景谈框架全是空谈。桌面版考试系统这个需求本身和 MFC 的特性是精准匹配的。2. 题库表结构设计抽题算法能不能写好全看这张表很多人做考试系统第一件事就急着画界面这是典型的本末倒置。对于考试系统来说题库表结构设计直接决定后期抽题逻辑的灵活性和成绩统计的准确性。我早期做废过一个版本就是栽在表结构上。2.1 基础表结构拆解一个完善的单机考试系统最少需要这么几张表-- 题库表 CREATE TABLE tb_question ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 题目ID question_type INTEGER NOT NULL DEFAULT 1, -- 题目类型1单选 2多选 3判断 4填空 5问答 question_content TEXT NOT NULL, -- 题干 option_a TEXT, -- 选项A option_b TEXT, -- 选项B option_c TEXT, -- 选项C option_d TEXT, -- 选项D correct_answer TEXT NOT NULL, -- 正确答案 difficulty INTEGER DEFAULT 1, -- 难度系数 1-5 chapter_id INTEGER, -- 所属章节 score REAL DEFAULT 1, -- 每题分值 create_time DATETIME ); -- 考试记录表 CREATE TABLE tb_exam_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 记录ID student_name VARCHAR(50), -- 考生姓名 student_no VARCHAR(30), -- 学号/工号 exam_time DATETIME, -- 考试时间 total_score REAL, -- 总分 duration VARCHAR(20), -- 用时 result_status INTEGER DEFAULT 0 -- 0未提交 1已提交 2已批阅 ); -- 答卷明细表 CREATE TABLE tb_answer_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 明细ID record_id INTEGER, -- 关联考试记录ID question_id INTEGER, -- 关联题目ID user_answer TEXT, -- 考生答案 is_correct INTEGER DEFAULT 0, -- 是否正确 0/1 get_score REAL DEFAULT 0 -- 本题得分 );用三张表的原因很简单题库管理、考试记录、答卷明细分开互不干扰。后期做错题统计、通过率分析、平均分计算时SQL 写起来很顺手。2.2 为什么正确答案字段用字符串而不是布尔值这里有个容易被忽略的设计细节。很多人设计选择题表时喜欢给每个选项单独建一个布尔字段比如is_a_correct、is_b_correct但是这种设计的扩展性太差了。今天你只考单选明天要加多选后天要加判断表结构就废了。我的做法是统一用correct_answer字符串字段单选题存A或B多选题存ABD按字母顺序拼接判断题存T或F填空题存标准答案字符串问答题存几个关键采分点用分号分隔后端判分的逻辑就可以统一处理// 判分核心函数 - 根据题目类型选择判分逻辑 double JudgeSingleQuestion(CQuestion q, CString userAnswer) { // 统一转成大写去掉空格避免大小写和空格导致的误判 userAnswer.TrimLeft(); userAnswer.TrimRight(); userAnswer.MakeUpper(); CString correctAnswer q.m_correctAnswer; correctAnswer.TrimLeft(); correctAnswer.TrimRight(); correctAnswer.MakeUpper(); if (q.m_questionType 1) // 单选题 { if (userAnswer correctAnswer) return q.m_score; else return 0; } else if (q.m_questionType 2) // 多选题 { // 多选题判分策略 // 策略一答案完全一致才得分 // 策略二少选得一半分多选错选不得分 // 按客户要求配置这里用策略二举例 if (userAnswer correctAnswer) return q.m_score; else { BOOL bWrong FALSE; for (int i 0; i userAnswer.GetLength(); i) { if (correctAnswer.Find(userAnswer.GetAt(i)) -1) { bWrong TRUE; // 选了错误选项 break; } } if (!bWrong) return q.m_score * 0.5; // 漏选得一半分 else return 0; } } else if (q.m_questionType 3) // 判断题 { if (userAnswer correctAnswer) return q.m_score; else return 0; } return 0; }把正确答案统一存成字符串自动判分的难度直接降一个量级。2.3 抽题策略随机数 章节权重 难度梯度考试系统一个很核心的体验点就是试卷到底“像不像一份正规考试的卷子”。如果直接ORDER BY RAND()随机出题你会发现卷子难度分布很离谱——可能前面全是难题后面的题全是送分题。正规考试的卷子讲究“先易后难、按章节分布、难度有梯度”。我的做法是这样的// 抽题结构体 struct PaperConfig { int totalQuestions; // 总题数 CArrayint, int chapterWeights; // 各章节题数分配 int difficulty[3]; // 难度比例简单/中等/困难 的百分比 }; // 按章节和难度双重筛选随机抽题 void GeneratePaper(CString strChapter, int nDifficulty, int nCount, CArrayint, int questionIds) { // 构造 SQL先从题库中筛选出符合条件的题目 CString strSQL; strSQL.Format(SELECT id FROM tb_question WHERE chapter_id%s AND difficulty%d ORDER BY RAND() LIMIT %d, strChapter, nDifficulty, nCount); // 执行查询取出题目ID放入数组 // ... // 如果该难度题目不够则从相邻难度中补充 // 这一句是很容易漏的实际处理逻辑 }抽题算法建议按这个顺序做先按章节分配题目数量比如第 1 章出 5 题第 2 章出 8 题再按难度比例分配30% 简单、50% 中等、20% 困难每个“章节 难度”的组合里用随机数抽取对应数量的题目如果抽出来的题量不足从同章节其他难度中补足保证总题数正确这里用ORDER BY RAND()在小数据量几千条以内时性能完全没问题不需要为了这点量级去搞复杂的洗牌算法。搞个 Fisher-Yates 洗牌反而有点杀鸡用牛刀了。2.4 题库导入Excel 模板 批量校验题库录入如果靠手工一条条在软件界面里敲效率极低。客户给我的题库经常是 Excel 文件几百道题一次发过来。所以我专门做了一个“Excel 题库导入”的功能模块。大致逻辑是用 ODBC 读取 Excel 文件注意 VC6 里的 ODBC 驱动支持 .xls 老格式读取每一行按模板列顺序解析题干、选项、答案数据校验题干不能为空、选项不能全部为空、答案必须匹配已有选项校验通过的批量插入数据库校验失败的记录到错误列表方便用户定位修改提示VC6 读取 Excel 尽量不要用 MFC 的 CDatabase 直接连 Excel编码问题会让人头疼。更省心的方案是把 Excel 另存为 CSV 格式GBK 编码然后程序一行行解析 CSV可控性和出错排查的便利性都高很多。3. MFC 界面与核心交互答题卡控件、倒计时与交卷防误触界面部分是考试系统最直观的门面也是考生体验最敏感的部分。MFC 做界面虽然不是强项但考试系统这种“功能明确、结构固定”的界面反而是 MFC 的舒适区。3.1 主界面布局左侧答题卡 右侧题目区考试系统的主界面我的排版思路基本是固定的区域占比内容顶部60px考生信息、考试科目、剩余时间左侧220px答题卡题号网格答过的题变绿色右侧剩余题干区 选项区透明背景、上下滚动答题卡控件是考试系统最核心的自定义控件。MFC 里没有现成的“答题卡控件”常见的做法有两个一是用CListCtrl的图标模式LVS_ICON每个题目一个 item答过的题通过改变 item 的背景色区分。优点是实现简单缺点是绘制样式不够灵活。二是自绘控件Owner Draw在OnPaint里自己画。根据题目状态绘制不同颜色的方块或圆形按钮。优点是完全可控可以实现鼠标悬停变色、点击切换选中状态等视觉反馈。我推荐用自绘方式原因很简单答题卡的交互体验直接决定考生对软件专业度的感知一个能自动滚到当前题号的高亮答题卡比普通的列表控件体验好太多。3.2 答题状态跟踪一个数组解决的事别搞复杂答题状态的跟踪在网上看到了很多种方案有人用链表有人搞了个自定义类集合。其实考试系统的题目数量通常是固定的比如 100 题考试过程中题量不会动态变化所以最简单可靠的方式就是一个CArrayint数组下标对应题号值保存考生选择的选项索引。// 答题状态数组 // -1: 未作答 // 0: 选择了A // 1: 选择了B // 2: 选择了C // 3: 选择了D CArrayint, int m_answerStatus; // 初始化全部置为-1 m_answerStatus.SetSize(nTotalQuestions); for (int i 0; i nTotalQuestions; i) m_answerStatus[i] -1; // 考生点击某个选项时更新状态 void CExamDlg::OnRadioClicked(int nQuestionIndex, int nOptionIndex) { m_answerStatus[nQuestionIndex] nOptionIndex; // 通知答题卡控件刷新该题状态 m_answerCard.InvalidateRect(NULL, FALSE); }有了这个数组下面这些功能全部可以基于它轻松实现答题卡上哪些题已答状态 0绿色显示哪些题未答状态 -1白底黑字显示交卷时统计未答题数弹窗提示“你还有 5 题未作答确定要交卷吗”点击答题卡上的题号右侧题目区切换显示对应题目3.3 倒计时逻辑SetTimer 的精度陷阱与交卷兜底MFC 的定时器函数SetTimer精度只能保证在几百毫秒级别不是严格精确的。如果考试时长的精确度要求很高建议用较细粒度的定时器比如 1000ms加上系统时钟校准在实际执行时不要直接在OnTimer里累加变量那个误差会随着时间累积。我的建议是存储“考试结束时刻”这个时间戳然后在每次OnTimer触发时用当前系统时间跟结束时刻做差// 设定考试结束时间考试时长 60 分钟 m_timeEnd CTime::GetCurrentTime() CTimeSpan(0, 1, 0, 0); void CExamDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_EXAM) { CTime timeNow CTime::GetCurrentTime(); CTimeSpan span m_timeEnd - timeNow; if (span.GetTotalSeconds() 0) { // 时间到强制交卷 OnSubmitPaper(); return; } int nMinutes span.GetTotalSeconds() / 60; int nSeconds span.GetTotalSeconds() % 60; CString strTime; strTime.Format(剩余时间%02d:%02d, nMinutes, nSeconds); SetDlgItemText(IDC_STATIC_TIME, strTime); // 最后 5 分钟提示一次 if (span.GetTotalSeconds() 300 !m_bWarned) { MessageBox(考试还剩 5 分钟请注意把握时间, 时间提醒, MB_OK | MB_ICONWARNING); m_bWarned TRUE; } } CDialog::OnTimer(nIDEvent); }注意这里最后 5 分钟提示只弹一次通过m_bWarned标志位控制避免定时器每秒钟触发弹窗把考生烦死。另外当倒计时归零时直接调用交卷程序而不是把控制权留给考生。所有已答的题目都会自动提交不会因为时间到了导致考生成绩丢失。3.4 交卷防误触二次确认 未答题强制检查交卷是最需要防呆设计的地方。每年都有考生因为手滑误点交卷如果系统没有二次确认那投诉就来了。我做的防误触设计分三层第一层交卷按钮不在答题区域内放在顶部栏右侧远离考生频繁操作的选项区域。第二层点击交卷按钮后弹出一个MessageBox提示“你还有 N 题未作答确认要交卷吗”并且把“确定”按钮设为默认焦点。第三层一旦交卷完成将所有按钮置灰禁止任何操作防止重复交卷或者交卷后恶意修改答题数据。注意这里有个反直觉的细节——未答题检查的弹窗文案一定要写清楚具体有多少题未答而不是笼统一句“有题目未完成”。因为有具体数字时考生能感知到系统是认真统计过的对软件的信任感会明显提升。4. 成绩判分、数据持久化与EXCEL导出收尾阶段的核心细节考完试考生交了卷考官最关心的是什么成绩准确、数据能存档、能导出来分析。这个阶段看似简单实际暗坑也不少。4.1 交卷后的判分流程先算客观题再留给问答题人工批阅判分逻辑要区分题型。对于单选题、多选题、判断题交卷时可以直接自动判分成绩基本秒出。对于填空题如果答案完全匹配可以自动判分但存在同义词、相近表述的情况建议先自动判一遍再人工复核。问答题则只能人工批阅。所以成绩有两种状态自动判分状态交卷后客观题分数立即计算完成状态显示“待人工阅卷”人工批阅状态管理员在后台打开未批阅的试卷针对问答题逐题打分最终确认总分// 交卷后自动判分并保存 void CExamDlg::OnSubmitPaper() { // 更新答题状态数组把当前题目控件中的值同步到数组 SyncAnswerFromUI(); double totalScore 0; // 遍历每一题判断对错 for (int i 0; i m_questionCount; i) { double score JudgeSingleQuestion(m_questionArray[i], m_answerStatus[i]); totalScore score; // 保存到答卷明细表包括题目ID、考生答案、是否正确、得分 } // 保存考试记录 SaveExamRecord(m_strStudentName, m_strStudentNo, totalScore); // 显示成绩 ShowResultDialog(totalScore); }这里有个细节值得提一下判分应该在后台一次性完成不要在界面上逐题演示“对了/错了”的动画。考试系统讲究效率考生更关心结果而不是看过程。4.2 数据存储方案SQLite vs Access 的取舍MFC 传统上搭配 Access 数据库通过 ODBC/ADO比较多。但在实际使用中Access 有一个缺点每个 MDB 文件随着数据量增大会越来越慢而且并发能力很差。单机版考试系统虽然没并发问题但 MDB 文件在 U 盘或者网络共享目录上操作时偶尔会出现“文件被占用”的报错。我后来更多用 SQLite原因很简单对比项Access (MDB)SQLite单文件分发可以可以无需安装驱动需要 ODBC 驱动只需一个 dll并发写入差相对较好VC6 兼容性好需要用 C 接口封装跨平台差好VC6 下用 SQLite 的官方 C 接口很老但足够用了。把 sqlite3.dll 和头文件拷进工程封装一层CDatabaseHelper类所有数据库操作统一走这个类以后换数据库也方便。4.3 成绩导出到 ExcelCSV 格式是最稳定的方案导出成绩时如果用 MFC 直接操作 Excel COM 组件在装有 Office 的机器上没问题但遇到精简版 Office 或者 WPSCOM 接口可能就失灵了。我实际测试中最稳的方案是生成 CSV 文件Excel 和 WPS 都能直接打开。// 导出一个成绩单 CSV 文件 void CExamDlg::ExportGradeToCSV(CString strFilePath) { CStdioFile file; if (!file.Open(strFilePath, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { AfxMessageBox(文件创建失败请检查路径或权限); return; } // 写表头注意CSV 用逗号分隔中文用 GBK 编码 file.WriteString(学号,姓名,考试时间,总分,用时,状态\n); // 逐行查询考试记录并写入 CString strSQL SELECT student_no, student_name, exam_time, total_score, duration, result_status FROM tb_exam_record ORDER BY id DESC; // 执行查询逐行拼接字符串写入文件... file.Close(); CString strMsg; strMsg.Format(成绩已导出到%s\n可以直接用 Excel 打开。, strFilePath); AfxMessageBox(strMsg); }提醒如果项目要求导出为真正的 .xlsx 格式带格式、带合并单元格就别用 VC6 折腾了建议用 C 配合 libxlsxwriter 库或者干脆生成 XML Spreadsheet 2003 格式的 .xls 文件VC6 也能处理只是需要额外引一个 XML 库。大部分客户最终都接受 CSV因为他们的目的只是拿去做统计报表。4.4 成绩查询与补考名单一个简单的维度分析开发考试系统时考官经常还需要一些附加功能比如按班级、按部门统计平均分、及格率自动生成补考名单。这些在写 SQL 时可以顺手做了。-- 统计每个班级的平均分和及格率 SELECT class_name, COUNT(*) AS total_students, ROUND(AVG(total_score), 2) AS avg_score, SUM(CASE WHEN total_score 60 THEN 1 ELSE 0 END) AS pass_count, ROUND(SUM(CASE WHEN total_score 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS pass_rate FROM tb_exam_record GROUP BY class_name;实际项目里我建议在开发前先跟客户确认清楚他们是要“成绩单式报表”还是“统计式报表”。很多人默认以为考试系统只要把成绩显示出来就行但实际上“按班级排名”、“按分数段统计人数”这种维度分析功能才是他们真正在年会汇报、考核总结时用的东西。5. 编译打包与兼容性避坑VC6 项目的安装部署全流程写代码只是项目一半的功夫另一半在编译打包和部署环节。MFC 老项目的部署坑比想象中多我按实际踩坑经验把这一步拆成几个关键点。5.1 VC6 的 ANSI 编码与中文乱码问题VC6 工程默认使用 ANSI 编码如果程序里硬编码了中文字符串在简体中文系统上没问题但一旦把程序拷贝到繁体系统或英文系统上所有中文全部变乱码。考试系统离不开中文题目和考生姓名这个问题必须处理。最稳妥的办法是在代码里统一使用 Unicode 字符串处理。虽然 VC6 对 Unicode 支持不算好但可以做到在stdafx.h中定义_UNICODE和UNICODE所有字符串操作统一用CString的_T()宏数据库访问层统一使用CString而不是char*拼接如果工程已经写了一大半才发现编码问题工作量会很大。所以做项目前先问自己一句这个系统将来要不要部署到非简体中文环境如果要编码方案从第一天就该定下来。5.2 依赖 DLL 处理方式静态链接 MFC 与运行库VC6 程序默认动态链接 MFC 库和 C 运行库MSVCRT。目标机器如果没有安装 VC6 运行库程序会直接报找不到mfc42.dll或者msvcp60.dll。解决方法是把项目设置为“在静态库中使用 MFC”即使用/MT编译选项。这样编译出的 exe 体积会大一些通常 2-3MB但不再依赖系统 DLL拷到任何 Windows 机器上都能直接跑。具体设置在 VC6 的菜单路径Project - Settings - C/C 选项卡 - Code Generation - Use run-time library选择Debug Multithreaded或Release Multithreaded。同时在 General 选项卡下Microsoft Foundation Classes 选择Use MFC in a Static Library。5.3 Win7/Win10 兼容性设置manifest 文件与管理员权限VC6 编写的程序在 Win7 以上系统运行时还有两个常见问题一个是界面字体模糊、控件大小异常。原因是系统没有为程序指定 DPI 感知级别默认按 96 DPI 缩放在高分屏下会模糊。解决办法是给 exe 加一个 manifest 文件声明系统 DPI 感知?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 assemblyIdentity version1.0.0.0 processorArchitecture* nameExamSystem typewin32/ descriptionExam System/description trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelasInvoker uiAccessfalse/ /requestedPrivileges /security /trustInfo application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /windowsSettings /application /assembly另一个是权限问题。考试系统要往安装目录写入数据库文件如果装在C:\Program Files下普通用户没有写入权限数据库就写不进去。处理方案有两种一是把数据库文件重定向到%APPDATA%目录二是在 manifest 中声明 requireAdministrator 权限但这样每次运行都会弹 UAC 提示考试场景下体验不好。我通常推荐第一种数据库文件放在C:\ExamData\这样的独立数据目录程序启动时自动检测并创建该目录。5.4 安装包生成Inno Setup 是 VC6 项目的好搭档打包工具我用的是 Inno Setup对老项目兼容性非常好脚本简单生成的安装包体积小。示例脚本[Setup] AppName考试系统 V3.2 AppVersion3.2.0 DefaultDirName{pf}\ExamSystem OutputDir.\Output OutputBaseFilenameExamSystem_Setup_v3.2 [Files] Source: .\Release\ExamSystem.exe; DestDir: {app} Source: .\Release\sqlite3.dll; DestDir: {app} Source: .\Release\template.xlsx; DestDir: {app}; Flags: ignoreversion [Icons] Name: {group}\考试系统; Filename: {app}\ExamSystem.exe Name: {commondesktop}\考试系统; Filename: {app}\ExamSystem.exe [Run] Filename: {app}\ExamSystem.exe; Description: 立即运行考试系统; Flags: nowait postinstall skipifsilent安装包的逻辑就三步释放文件到安装目录、创建桌面和开始菜单快捷方式、最后提示运行。考试系统的数据目录不在{app}下面单独建在C:\ExamData这样以后升级版本只需要覆盖安装目录里的文件考生数据不会丢。6. 考前测试与应急方案一个老项目的“终极验收标准”项目开发完了不代表可以交付。考试系统这种“一年用不了几次、但每次用到都不能出问题”的软件最怕的是极端情况。这部分我总结了一些实际的应急方案。6.1 三种必须覆盖的模拟测试场景正式的考试场景远比平时测试复杂我在交付前会强制自己过一遍这些场景测试场景操作步骤预期结果断电恢复考试进行到一半直接强制关进程再重新打开考生重新进入系统时之前的答题记录还在可以继续答题时间耗尽自动交卷将考试时长临时改成 30 秒等待倒计时归零系统自动提交并保存所有已有答案海量数据提交导入 2000 道题模拟一次考试全部答完交卷判分在 2 秒内完成且最终成绩无误第 1 个“断电恢复”是很多考试系统的致命伤。单机版如果答题数据只存在内存里断电后全丢这时候考生心态直接崩了。所以每答一题都要把答题状态实时写回数据库而不是等到交卷时才一次性保存。void CExamDlg::OnRadioClicked(int nQuestionIndex, int nOptionIndex) { // 更新内存数组 m_answerStatus[nQuestionIndex] nOptionIndex; // 实时保存到数据库中防止断电丢数据 SaveAnswerToDB(nQuestionIndex, nOptionIndex); }保存的频率不用太在意性能单机版写数据库就算是 2000 题也只是一瞬间的事别学 Web 端的防抖思路。6.2 考前巡检脚本自动检查每台机器如果考试是在机房进行的我会额外写一个小的巡检工具考前花 5 分钟跑一遍所有机器检查程序是否能在该机器上正常启动数据库文件能否正常创建和写入屏幕分辨率是否足够显示完整界面是否存在占用 8080 端口或其他冲突的进程这个巡检脚本本质上就是调用考试系统主程序传入一个--check参数程序在检查模式下跑自检逻辑把结果写入一个 txt 文件。考场管理员扫一眼所有机器的结果文件心里就有数了。6.3 数据备份双重保险考试结束后考官最关心的就是成绩别丢。我的方案是三重备份数据库文件原始存储考试结束时自动将数据库文件复制一份到C:\ExamBackup\2024-05-12_14-30.db成绩导出 CSV 时同时生成 MD5 校验文件防止传输过程中被篡改对于单机版软件来说这已经算比较严格的备份策略了实际用的过程中也从来没出过问题。7. 关于使用 spdlog 做日志给老项目加一个现代模块最后我想聊一个跟老 MFC 项目改造相关的话题。网络热词里有人提到“mfc 使用 spdlog”刚好我最近在一个老项目里把日志这条线彻底理清楚了算是给老系统“嫁接现代组件”的一个实战案例。7.1 为什么要在 MFC 项目里加日志模块MFC 老项目很多都是通过OutputDebugString或者MessageBox来排查问题的这在开发和测试阶段还算凑合但程序交付到客户现场出了问题你手边没有 VS 开发环境看不到调试输出只能靠客户描述“它崩了”。这时候一个完整的运行日志文件就成了你最好的远程debug工具。我选择 spdlog 而不是自研日志类的理由很简单支持多级别日志debug/info/warn/error/critical支持滚动文件输出不会让日志文件无限膨胀支持异步写入不影响主线程性能API 设计干净几十行代码就能集成7.2 VC6 集成 spdlog 的注意点spdlog 是 C11 写的模板库VC6 对 C11 的支持等于零直接照搬源码显然不现实。我的做法是两个方案二选一方案一换用老版本的 spdlog比如 v0.x 版本早期版本对 C98 的兼容性稍微好一点但依然需要改一大堆语法不推荐。方案二更推荐用 C/CLI 包装层。把 spdlog 编译成一个单独的 DLL用 VS2015 编译然后 MFC 程序通过一个简单的 C 接口调用日志功能。// spdlog_wrapper.h - MFC 程序引入这个头文件 #ifdef __cplusplus extern C { #endif // 初始化日志系统logFilePath 形如 C:\\logs\\exam.log void LogInit(const char* logFilePath, int logLevel); // 不同级别的日志输出 void LogDebug(const char* msg); void LogInfo(const char* msg); void LogWarn(const char* msg); void LogError(const char* msg); #ifdef __cplusplus } #endifMFC 程序调用的方式LogInit(C:\\logs\\exam.log, 2); // 0debug 1info 2warn 3error LogInfo(考试开始考生张三试卷ID1024); // ...业务逻辑... LogError(数据库写入失败错误码%s, strError);这种方式的好处是把复杂依赖隔离在 DLL 内部了MFC 主程序不需要重新编译只需要在工程链接设置里加上spdlog_wrapper.lib就行。经验VS2015 编译一个 DLL导出 C 接口供 VC6 程序调用是让老项目使用现代 C 库最稳妥的“桥接”模式。这个方法不仅适用于 spdlog也适用于 jsoncpp、libcurl、yaml-cpp 等常见库。核心原则就是——老程序只需要知道函数的 C 签名不需要理解背后的 C 实现。我在实际集成 spdlog 后的效果很直观客户的现场问题从“只能靠猜”变成了“直接翻日志定位”。比如之前遇到过“某台机器考场中弹出了异常窗口”通过日志看到是数据库查询超时进一步定位到是那台机器杀毒软件实时扫描导致磁盘 I/O 暴增。这类问题如果没有日志几乎不可能远程定位。最后多提一句考试系统这类看起来“不够时髦”的项目恰恰是检验一个程序员工程能力的好地方。界面不难、算法不难难的是把每个细枝末节都想清楚——断电了怎么办、答到一半误点交卷怎么办、客户的机器是 XP 怎么办、成绩丢了怎么追溯。这些东西学校里不会教但每一个都是在真实交付中一点点磨出来的。希望这篇关于 VC6 MFC 考试系统的开发实录能让你少走一些我已经走弯过的路。本文还有配套的精品资源点击获取
返回列表