ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC实现文件校验和工具:CRC32与MD5计算实战

MFC实现文件校验和工具:CRC32与MD5计算实战 简介这是一份基于MFC与VC开发的校验和计算小工具在Visual Studio 2015环境中采用对话框界面实现面向学习MFC编程、数据通信校验及Windows桌面应用开发的读者。工具支持累加和与异或两种常见校验方式可对输入数据进行快速校验计算辅助数据解析与调试工作。压缩包共41个文件大小约63.52MB其中包含cpp/h头文件与源文件、rc资源脚本、sln/vcxproj工程配置、exe可执行程序以及pdb/ilk等调试辅助文件结构完整便于直接编译运行与二次修改。目前已有988人学习下载。通过阅读源码可以深入理解MFC对话框程序的消息映射与控件交互方式掌握累加和与异或校验的算法实现细节并学习如何在VS2015环境中组织和管理一个完整的MFC工程适合作为入门到进阶的参考范例。 做 Windows 桌面小工具MFC 这个框架放到今天确实显得有点老派但当你需要在 Windows 上快速交付一个“选一个文件算一下校验值”的本地工具时MFC 的对话框工程依然是最省事的选择。我最近整理了手头一个名为 MFCApplicationCheckSum 的 MFC 校验和计算小工具项目把从界面搭建、控件消息处理、算法实现到最终打包发布的完整路径又走了一遍。这个工具解决的是非常具体的问题拿到一个文件之后快速计算它的 CRC32 / MD5 校验值用来确认文件在传输或复制过程中是否完整。适合三类人参考正在学 MFC 的 C 开发者、需要给同事或客户做内部工具的桌面程序员以及想动手验证“校验和”到底是什么的爱好者。按这个思路走能少踩不少坑。1. 方案选型与功能边界为什么校验和工具用 MFC 最省事1.1 校验和到底在解决什么问题校验和Checksum是一种验证数据完整性的手段用一段固定长度的数值来尽量唯一地代表一份数据。数据发生任何一点变化校验值大概率都会变。最常见的场景就是文件下载下载完一个压缩包计算它的 CRC32 或 MD5跟发布方给出的值对比如果一致说明文件在传输过程中没有被破坏如果不一致那基本可以判定文件损坏了直接重新下载比继续解压试错明智得多。在 Windows 平台上“校验和”这个词涵盖的范围其实比较宽累加和Sum、CRC16/CRC32、MD5、SHA 系列哈希都能起到完整性校验的作用。实际使用中CRC32 速度快、实现简单很适合做传输校验MD5 和 SHA 系列则更常用于安全场景比如验证软件镜像是否被篡改。小工具产品里最常见的做法是把 CRC32 和 MD5 放进同一个界面按需切换。这也是我最初做这个项目时的核心想法。1.2 为什么选 MFC 而不是 C# / Qt / Win32这种小工具的核心交互就三步选文件、点计算、看结果。用一个对话框窗口完全够了不需要文档/视图结构更不需要 Ribbon 这类复杂框架。MFC 的 CDialog 工程在 Visual Studio 里几秒钟就能创建出来CFileDialog、CEdit、CComboBox、CProgressCtrl 这些控件都已经封装好了拖拽控件、双击绑定事件开发效率很高。有人可能会问为什么不用 C# WinForms 或者 QtC# 做这类工具确实更快但很多老项目环境里并没有 .NET 运行时或者团队对 C 依赖库有硬性要求这时候 MFC 反而是最自然的选项。跟纯 Win32 SDK 比MFC 又省去了大量手动创建窗口、写消息循环的重复劳动属于“够用、可交付、不啰嗦”的中间状态。如果你只做内部工具、不追求跨平台MFC 至今仍有不可替代的价值。1.3 功能边界第一版做到什么程度这个项目最初我只想算 CRC32后来加上了 MD5顺便补了进度条。最终的功能清单是选择文件、选择算法CRC32 / MD5、点击开始计算、显示结果和进度。没有做拖拽、批量、文件夹递归因为这些功能会引入额外复杂度比如多线程队列、List Control 的状态管理对第一版来说收益不高。在动手写代码之前先把边界想清楚是这类小项目最值钱的一步。窄而扎实的功能比一堆半成品功能强得多。第一版把“选文件-算校验值-显示结果”这串主链路跑通后续再扩展批量、拖拽、多算法路都是现成的。我见过不少项目一上来就想做全套结果连文件的 CRC 都对不上标准值这种教训很常见。2. 界面搭建与控件绑定MFC 对话框实操细节2.1 控件清单与布局静态文本、编辑框、按钮、进度条怎么排界面上放了一组很常规的控件一个静态文本显示“文件路径”下面是一个只读编辑框用来显示选中的文件路径旁边是“浏览”按钮弹出文件选择对话框再往下是算法选择的组合框列出 CRC32 和 MD5“开始计算”按钮排在之后紧接着是用于显示计算结果的静态文本底部放一个进度条。布局上我建议所有控件都基于对话框客户区做相对定位也就是在 OnSize 里根据 GetClientRect 重新计算各控件坐标而不是写死绝对位置。虽然这个小工具窗口默认尺寸基本不变但养成相对定位的习惯之后以后做可缩放窗口会轻松很多。Tab 顺序也值得注意在资源编辑器里按 CtrlD 就能调整应该符合“浏览-算法-开始计算”的自然操作顺序。还碰到过一个细节静态文本默认会吃掉鼠标事件如果需要让文本被拖动或者响应点击要记得给控件加 SS_NOTIFY 样式否则事件传不到父窗口。2.2 文件选择CFileDialog 关键参数与写法MFC 里选文件最标准的做法是 CFileDialog。第一个参数 bOpenFileDialog 传入 TRUE 表示打开文件第二个是默认扩展名第三个是默认文件名。过滤器用双竖线 || 分隔多个类型注意结尾要有两个连续的双竖线作为结束标记。下面这段就是我在“浏览”按钮事件里用的写法void CCheckSumDlg::OnBnClickedBtnBrowse() { CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, _T(所有文件 (*.*)|*.*||), this); if (dlg.DoModal() IDOK) { m_strFilePath dlg.GetPathName(); SetDlgItemText(IDC_EDIT_FILE, m_strFilePath); } }OFN_FILEMUSTEXIST 会强制用户只能选择真实存在的文件OFN_HIDEREADONLY 则隐藏掉文件对话框中那个“以只读方式打开”的复选项。这两个 flag 对工具类程序很合适。GetPathName() 返回的是完整路径直接丢进编辑框展示就行。需要注意如果工程是 Unicode 字符集这里的 CString 就是宽字符路径里带中文不会有问题。2.3 防止界面卡死工作线程与消息投递我第一次实现时直接在按钮点击的消息处理函数里读取文件、算 CRC32结果选了一个 2GB 的 ISO 文件界面立刻变成“未响应”标题栏上出现“正在运行”却半天不动看起来像死机。原因很简单UI 消息循环被计算任务阻塞了。MFC 里按钮点击事件全部运行在主线程只要计算函数不返回窗口就无法处理重绘和用户点击。解决办法是开一个工作线程。最省事的方式是用 AfxBeginThread它底层调用 _beginthreadex并且做好了 CRT 初始化比直接 CreateThread 安全得多启动代码就一行AfxBeginThread(CheckSumThreadProc, this, THREAD_PRIORITY_NORMAL);线程函数里调用对话框对象上的计算逻辑计算过程中通过 PostMessage 把进度消息发回主线程。这里必须用 PostMessage不能是 SendMessageSendMessage 会等接收方处理完才返回如果主线程忙于 UI 操作进度刷新反而会阻塞工作线程。计算完成后发送一个自定义消息 WM_CALC_DONE主线程收到后在消息处理函数里更新最终结果。这样界面永远能保持响应。3. 校验和算法实现CRC32 查表法与模块封装3.1 累加和、CRC32、MD5 怎么选选算法的时候我做了一个简单对比算法输出长度速度适用场景实现难度累加和2/4 字节最快简单通信协议校验极低CRC324 字节很快文件传输完整性校验低查表法十几行MD516 字节较快文件指纹、镜像比对中可用现成库累加和的碰撞率太高文件数据稍有规律就很容易误判我直接排除了。CRC32 是性价比最高的选择4 字节长度就能覆盖绝大多数传输错误查表法实现起来逻辑也不复杂。MD5 虽然密码学上已经不适合做安全对抗但用来做完整性比对、跟发布方提供的 MD5 值做对照至今依然是非常普遍的场景所以把它作为第二个算法。3.2 CRC32 查表法核心代码与标准细节CRC32 有逐位计算和查表两种方式。逐位计算逻辑直观但速度慢一个字节要处理 8 次位运算查表法则把 8 位二进制对应的 CRC 值预先算成 256 项的表计算时每读一个字节只需要一次查表和几次异或、移位性能提升非常明显。下面这段代码可以直接用DWORD g_crc32Table[256]; BOOL g_bTableReady FALSE; void InitCRC32Table() { for (DWORD i 0; i 256; i) { DWORD crc i; for (int j 0; j 8; j) { if (crc 1) crc (crc 1) ^ 0xEDB88320; else crc 1; } g_crc32Table[i] crc; } g_bTableReady TRUE; } DWORD CalcCRC32(const BYTE* pData, DWORD dwLen) { if (!g_bTableReady) InitCRC32Table(); DWORD crc 0xFFFFFFFF; for (DWORD i 0; i dwLen; i) crc (crc 8) ^ g_crc32Table[(crc ^ pData[i]) 0xFF]; return crc ^ 0xFFFFFFFF; }有几个细节需要强调0xEDB88320 是 CRC32 标准生成多项式的反转形态初始值必须用 0xFFFFFFFF并且最后要跟 0xFFFFFFFF 异或一次这样算出来的结果才跟 WinRAR、7-Zip 等工具显示的值一致。我最初把初值写成了 0x00000000结果算出的 CRC 跟压缩软件对不上排查了很久才发现是标准没看完。这种“玄学 bug”其实一点都不玄纯粹是细节问题。3.3 算法模块封装统一接口与分块读取虽然是小工具我还是把算法单独拆了一个文件 CheckSumLib.cpp对外只暴露统一接口大体类似BOOL CalcFileCheckSum(LPCTSTR lpszFilePath, int nAlgorithm, DWORD* pdwResult, UINT* pnProgressPercent);对话框代码只关心界面不关心算法细节以后加 SHA-256 只需要在库内部扩展不用动 UI。文件读取用 CFile 分块进行缓冲区设成 1MB。为什么不一次性读入内存因为要处理大文件一次性读入既占内存又有风险。分块读取时每读一块调用一次进度回调把百分比通过 PostMessage 发到主线程。计算时始终用 unsigned char 处理缓冲区这点非常重要。如果缓冲区类型是 char右移运算时可能发生符号扩展导致计算结果错误。这个坑很难排查因为小文件可能碰巧对大文件就随机乱一旦出现优先检查缓冲区类型和移位方式。4. 从建工程到编译通过MFC 小工具完整流程4.1 VS2013 新建 MFC 对话框工程的关键选项我这边用的是 VS2013创建流程是新建项目 - Visual C - MFC - MFC 应用程序。向导里“应用程序类型”选“基于对话框”其他选项保持默认点完成。MFC 向导会自动生成一个带“确定/取消”按钮的对话框模板直接删掉这两个按钮再从工具箱里拖入前面规划的控件。需要提醒一句如果安装 VS 时没有勾选 MFC 相关组件新建项目列表里根本看不到 MFC 模板需要先到安装器中补装“适用于 C 的 MFC”组件。字符集这里要提前想好。项目默认是 Unicode 字符集CString 内部是宽字符。如果你在项目属性 - 常规 - 字符集改成多字节字符集后面路径处理、宽窄字符转换会有不少麻烦。我建议直接保持 Unicode因为 Windows 路径经常带中文Unicode 下处理最省心。网上大量“中文路径乱码”问题多半就是多字节字符集加本地代码页不匹配造成的。4.2 控件变量绑定与按钮事件处理逻辑在资源编辑器里双击“浏览”按钮会自动生成 BN_CLICKED 消息处理函数。然后给关键控件添加成员变量文件路径编辑框对应 CString m_strFilePath“开始计算”按钮对应 CButton m_btnCalc进度条对应 CProgressCtrl m_progress。组合框用 AddString 把 “CRC32” 和 “MD5” 加进去默认选中 CRC32。“开始计算”按钮的处理逻辑分成几步先校验文件路径非空然后把按钮禁用防止用户重复点击导致多个线程同时计算再初始化进度条范围 0-100设置位置为 0最后调用 AfxBeginThread 启动工作线程。工作线程结束后通过 WM_CALC_DONE 携带结果回到主线程在消息处理函数中格式化结果、更新显示、恢复按钮。这里有一个细节线程里读取 m_strFilePath 时如果主线程同时修改了它会产生读写竞争。稳妥的做法是线程启动前把路径拷贝到一个独立的 CString 成员变量计算全程只读这份副本。4.3 编译期常见错误C4996 和 MFC 链接方式编译过程中最常遇到的是 C4996 警告/错误比如 strcpy、fopen 这些函数在 VS2013 下会被标记为 deprecated。解决方法有两种一是代码里改用带 _s 后缀的安全版本二是如果只是快速验证逻辑可以在预编译头文件里加一句 #pragma warning(disable: 4996)。我处理旧代码时通常选第二种新写代码尽量用安全版本。另一个容易踩的坑是 MFC 库的链接方式。项目属性 - 常规 - MFC 的使用里可以选“使用标准 Windows 库”“在静态库中使用 MFC”“在共享 DLL 中使用 MFC”。开发调试阶段用共享 DLL 编译速度更快生成文件也更小正式发布时切到“在静态库中使用 MFC”生成的 EXE 会变大但目标机器上不需要额外安装运行库省去一堆环境问题。小工具我倾向于发布版用静态链接省心最重要。5. 打包发布与问题排查实际踩过的坑5.1 发布时该带哪些依赖动态链接与静态链接的取舍如果项目保持默认的“在共享 DLL 中使用 MFC”发布时需要把 MFC 运行库一起分发。以 VS2013 为例关键文件包括 mfc120u.dll、msvcr120.dll、msvcp120.dll放错、漏放都会导致程序双击报错 “无法启动此程序因为计算机中丢失 xxx.dll”。另一个办法就是前面说的发布配置改成“在静态库中使用 MFC”直接把这几个 DLL 的代码链进 EXE不依赖外部运行库。个人做内部工具我最常用的交付形态是Release 静态链接 一个 ZIP 压缩包里面放 EXE 和一份简短的使用说明。不要做成安装包这种小工具还需要装一遍实在没必要。如果公司内网有多台机器环境未知静态链接是所有方案里容错率最高的。文件十几 MB 不算什么用户跑来跑去“缺 DLL”的沟通成本才高。5.2 实测中遇到的四个典型问题实际试跑的时候我遇到的事还真不少逐个记录一下。大文件计算界面卡顿第一次没有开工作线程选了一个 2GB 文件界面直接假死。后来加上 AfxBeginThread进度条和按钮都正常了。这是所有桌面工具都要提前考虑的问题别等用户抱怨“卡死”再改。中文文件名显示异常工程是 MBCS 字符集时CFileDialog 返回的路径在宽窄字符转换时出现乱码。切到 Unicode 字符集之后彻底消失。CRC 结果对不上标准值最初 CRC32 初值写成了 0算出来的结果和压缩软件不一致。改成标准初值 0xFFFFFFFF最后补一次异或结果完全一致。进度条偶尔回跳工作线程发送的进度消息在消息队列里排队旧消息还没处理完新消息又到了导致进度条从 90 回到 60。UI 端设置进度条位置之前加了一个 max 判断只增不减问题解决。第四个问题尤其典型。PostMessage 是异步的不能假设消息到达的时序。进度显示这类场景宁可 UI 端多做一次判断也不要直接信任消息携带的数据。5.3 后续扩展拖拽、批量、多算法第一版做扎实以后扩展空间很大。支持把文件拖到窗口上就自动获取路径需要处理 WM_DROPFILES 消息支持多算法同时计算可以在工作线程里依次跑 CRC32 和 MD5一次给出多个结果支持批量选择文件用 CListCtrl 列出所有文件逐项计算并标记结果。再高级一点可以递归遍历文件夹生成类似 md5sum 的清单文件方便归档。做扩展的时候注意守住“算法库 界面层”的分离原则。算法库里只接收文件路径、算法类型、进度回调界面层只负责收集用户输入和展示结果。只要这条边界不破加功能基本就是加按钮、加线程分支不会把代码搅成一团。另外MFC 里如果想把按钮做得更好看一点可以自绘按钮也就是派生 CButton 的子类重写 DrawItem 或 OnPaint这也是网上搜“mfc 自定义按钮”最常见的需求。对话框里的静态文本覆盖问题则多半是 Z 序或者 WS_CLIPSIBLINGS 样式没处理好。这些都是后续美化时再考虑的事第一版先把功能跑通最重要。把这个 MFCApplicationCheckSum 项目从头到尾走一遍我最深的体会是MFC 里很多机制比如消息映射、工作线程、控件变量绑定单独看文档总觉得抽象但放进一个“选文件-算校验值-看结果”的真实小工具里一下子就通了。如果你正在学 MFC或者只是需要给同事交一个小工具练手我强烈建议从这种单窗口工具开始。它足够小小到能完整写完、编译、打包、交付又足够真真能解决工作中文件完整性校验的问题。本文还有配套的精品资源点击获取
返回列表