
简介基于MFC编写的汉字编码转换工具完整源代码工程面向需要深入理解中文编码机制的C学习者和Windows桌面应用开发者。工程围绕国标码、区位码与机内码的相互转换清晰演示了字节拆分、偏移量计算和ASCII扩展处理等核心算法同时展示CDialog、CEdit、CButton等MFC控件与消息映射的配合方式。压缩包共38个文件以h头文件、cpp源文件、obj中间文件、exe可执行程序及rc资源脚本为主工程配置齐全整体仅3.69MB可直接在Visual C 6.0环境下打开编译调试。已有474人学习下载。对于想结合理论解决编码转换实际问题、提升C编程与MFC开发能力的读者这是一份难得的参考代码既可直接运行查看效果也可逐行分析转换流程快速应用到自己的项目中。 做MFC开发的朋友十有八九都跟汉字编码转换打过交道。自己写的小工具、老项目改造、或者从别处拷贝的源码一编译运行满屏乱码源头基本都是GBK、UTF-8、Unicode这些编码在互相打架。这篇博文我把一个基于MFC对话框的汉字编码转换工具从设计到实现完整拆开讲包含核心源码和调试经验适合正在用VS做MFC开发、又需要处理中文文本编码的开发者参考。你可以直接拿代码去改也可以把它当成一个学习编码转换原理的入门案例。先说明白一点这个工具解决的是字节层面的编码互转问题不是简单的字符替换。它是把一串汉字字符从GBK转成UTF-8或者从UTF-8转回GBK/GB18030同时支持从剪贴板和文件读入、转换后输出到界面和文件。核心代码用了Windows平台最稳定的MultiByteToWideChar和WideCharToMultiByte这两个API组合不依赖第三方库MFC工程配置好就能编译。1. 编码转换这件事先理清概念再动手1.1 汉字编码的三大家族很多新手容易把GBK、UTF-8、Unicode混为一谈其实它们完全不是一个层面的东西。Unicode是字符集它给每个字符分配一个唯一的码点比如汉的Unicode码点是U6C49。GBK、UTF-8、UTF-16这些是编码方案它们决定码点怎么用字节表示。GB2312/GBK/GB18030这是国内用的中文字符集编码。GB2312收字较少后来GBK扩展了GB18030更是用四字节支持了全字符集。它们有一个共同特点中文字符通常占2字节GB18030部分冷僻字占4字节。在Windows中文系统里ANSI代码页就是936对应GBK。UTF-8变长编码兼容ASCII中文一般占3字节。现在网页、接口、数据库绝大多数都默认UTF-8所以它成了国际普通话。UTF-16LE/BEWindows系统内部UTF-16宽字符的字节序大多数Windows API内部处理的就是这个。转换的本质很简单先把源编码的字节序列解码成Unicode码点再用目标编码重新编码字节序列。无论GBK转UTF-8、UTF-8转GB18030中间都必须经过Unicode码点这一步不存在直接从GBK映射到UTF-8的捷径。1.2 为什么MFC项目里更容易出现编码问题MFC工程有个罪魁祸首项目属性里字符集选项分为使用Unicode字符集和使用多字节字符集。选Unicode时CString是CStringW宽字符选多字节时它是CStringA窄字符。如果你的代码写死了CString但不区分两种模式换到另一个工程就乱码。此外MFC编辑框控件CEdit本身使用Unicode字符集当我们往里面SetWindowText一个多字节字符串时MFC内部会做一次隐式转换。如果转换时判断错了源编码界面显示就是乱码。所以做编码转换工具我强烈建议把工程设置为Unicode字符集然后所有读入的字节流统一用CStringA或char*保存转换完成后再转成宽字符送入控件。2. 工具的整体设计与界面规划2.1 为什么用MFC而不是控制台或Qt控制台程序也能做编码转换但处理中文时有几个痛点控制台编码混乱、不方便复制粘贴大段文本、也没法做文件拖拽。MFC对话框程序刚好弥补这些界面直观、操作顺手。Qt当然也可以但如果你本身就在维护老MFC项目用MFC写这个工具无需引入额外依赖编译体积小分发给同事也方便。这个方案选MFC的另一个好处是Windows API对编码转换的原生支持到位。MultiByteToWideChar和WideCharToMultiByte能覆盖所有常见代码页连繁体中文的BIG5、日文的Shift-JIS都能处理不需要自己维护映射表。2.2 界面控件安排界面我设计得比较克制目标是把功能做清楚上方是源文本编辑框多行、带滚动条用户粘入要转换的内容。中间两排下拉框和按钮源编码选择、目标编码选择、转换按钮、清空按钮。下方是结果编辑框显示转换后的文本。底部还有两个按钮读取文件、保存文件方便处理批量文本。编码下拉框的选项包括ANSI系统当前代码页、GBK、GB2312、GB18030、UTF-8、UTF-8 BOM、UTF-16LE、UNICODE。虽然GB2312、GBK、GB18030在大多数中文系统上代码页都是936但为了真实性我还是把它们列为独立选项后面在转换逻辑里做区分。2.3 核心API选型与理解转换的基石是Windows的这两个APIint MultiByteToWideChar( UINT uCodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar );MultiByteToWideChar做的就是把多字节编码GBK、UTF-8等转成UTF-16宽字符。WideCharToMultiByte则反过来。用法上有固定的两步套路调用一次lpWideCharStr传NULL、cchWideChar传0API返回需要的缓冲区大小。按返回大小分配缓冲区再调用一次真正转换。这个套路我建议你死记硬背因为太常用了。注意第二个参数的dwFlags做GBK/UTF-8转换时直接传0即可WC_NO_BEST_FIT_CHARS这类标志通常用不上。3. 核心源代码解析逐个函数讲清楚3.1 编码转换的总闸这里是我写的编码转换入口函数。它接收源字符串、源代码页、目标代码页内部先统一转成宽字符再输出为目标编码字符串。由于我们用的是Unicode字符集工程这个函数返回std::string多字节字符串如果需要显示到CEdit外部再转成CStringW。std::string ConvertEncoding( const std::string input, UINT uSrcCodePage, UINT uDstCodePage) { if (input.empty()) { return std::string(); } // 第一步源编码 - UTF-16 int nWideLen MultiByteToWideChar( uSrcCodePage, 0, input.c_str(), (int)input.size(), NULL, 0); if (nWideLen 0) { return std::string(); } std::wstring wstr(nWideLen, L\0); MultiByteToWideChar( uSrcCodePage, 0, input.c_str(), (int)input.size(), wstr[0], nWideLen); // 第二步UTF-16 - 目标编码 int nDstLen WideCharToMultiByte( uDstCodePage, 0, wstr.c_str(), (int)wstr.size(), NULL, 0, NULL, NULL); if (nDstLen 0) { return std::string(); } std::string output(nDstLen, \0); WideCharToMultiByte( uDstCodePage, 0, wstr.c_str(), (int)wstr.size(), output[0], nDstLen, NULL, NULL); return output; }这里有一个非常关键的细节MultiByteToWideChar的cbMultiByte参数我传的是(int)input.size()而不是-1。传-1表示输入以空字符结尾会连带结尾的空字符一起转换返回的长度包含null。如果input内存中本身包含\0比如从文件读入的二进制数据传-1会截断。所以从文件读入数据时务必按实际字节数传入。3.2 如何精准处理UTF-8 BOMUTF-8文件常见的坑是文件头部带BOMEF BB BF也可能是无BOM。转换之前必须检测BOM否则BOM会被当作普通字符显示成乱码。BOM检测逻辑不复杂bool HasUtf8Bom(const std::string data) { if (data.size() 3) { return false; } return (unsigned char)data[0] 0xEF (unsigned char)data[1] 0xBB (unsigned char)data[2] 0xBF; } // 使用示例 std::string fileData ReadAllBytes(filePath); UINT srcCp CP_UTF8; if (HasUtf8Bom(fileData)) { fileData fileData.substr(3); // 去掉 BOM }这个函数虽然短但实用。我的工具里读取文件后第一步就是检测BOM自动跳过这样转出来的第一行不会多出锟斤拷。3.3 界面转换按钮的完整逻辑转换按钮的动作不是简单调一次ConvertEncoding就完事了还要处理用户选择的编码、带不带BOM、以及空输入。我写的处理函数大致如下void CEncodingConvertDlg::OnBnClickedBtnConvert() { CStringW strSource; m_editSource.GetWindowTextW(strSource); // 将宽字符文本还原成字节流 // 这里以 UTF-16LE 作为中转 int nSrcLen WideCharToMultiByte( CP_UTF8, 0, strSource.GetString(), strSource.GetLength(), NULL, 0, NULL, NULL); std::string utf8Input(nSrcLen, \0); WideCharToMultiByte( CP_UTF8, 0, strSource.GetString(), strSource.GetLength(), utf8Input[0], nSrcLen, NULL, NULL); // 根据下拉框确定源编码 UINT uSrcCp GetSelectedCodePage(m_cmbSource, utf8Input); // 执行转换 UINT uDstCp GetSelectedCodePage(m_cmbDest, std::string()); std::string result ConvertEncoding(utf8Input, uSrcCp, uDstCp); // 如果目标是 UTF-8 BOM则手动添加 BOM if (m_cmbDest.GetCurSel() IDX_UTF8_BOM) { result.insert(0, \xEF\xBB\xBF); } // 显示结果先转成宽字符 int nWideLen MultiByteToWideChar( uDstCp, 0, result.c_str(), (int)result.size(), NULL, 0); std::wstring wstrResult(nWideLen, L\0); MultiByteToWideChar( uDstCp, 0, result.c_str(), (int)result.size(), wstrResult[0], nWideLen); m_editResult.SetWindowTextW(wstrResult.c_str()); }有的读者会问源编辑框里已经是宽字符文本为什么还要先转成UTF-8字节流再转换原因是我需要模拟真实场景——从文件读入时数据就是裸字节界面编辑框只是展示用。如果你只做界面文本互转完全可以跳过中间这一步直接用宽字符转换。但为了代码统一我坚持所有数据先还原成UTF-8字节串再按用户选择的源编码解析。3.4 文件读写与文件拖拽支持处理文件是整个工具最有价值的部分。我封装了ReadAllBytes和WriteAllBytes两个函数原生使用CFile避免ifstream在Unicode路径下的麻烦。bool ReadAllBytes(const CStringW strFilePath, std::string data) { CFile file; if (!file.Open(strFilePath, CFile::modeRead | CFile::shareDenyWrite)) { return false; } ULONGLONG len file.GetLength(); if (len 1024 * 1024 * 100) { // 限制 100MB AfxMessageBox(L文件太大请使用更专业的工具); return false; } data.resize((size_t)len); if (len 0) { file.Read(data[0], (UINT)len); } file.Close(); return true; } bool WriteAllBytes(const CStringW strFilePath, const std::string data) { CFile file; if (!file.Open(strFilePath, CFile::modeCreate | CFile::modeWrite)) { return false; } if (!data.empty()) { file.Write(data.c_str(), (UINT)data.size()); } file.Close(); return true; }文件拖拽支持不用自己处理WM_DROPFILESMFC可以直接接受文件拖放。在对话框初始化时调用DragAcceptFiles(TRUE)然后重写OnDropFiles提取文件路径后自动读取并转换。这个体验比手动选文件好太多同事拿过来一个乱码txt直接拖进窗口就搞定了。注意文件大小限制我设了100MB上限防止拖入一个超大文件导致内存暴涨毕竟这只是个轻量转换工具不是专业数据处理器。4. 实操从零创建一个MFC对话框工程4.1 VS工程配置我用Visual Studio 2019/2022版本做演示。新建项目时选择MFC应用应用程序类型选基于对话框然后在向导里高级功能保持默认但生成的类那里注意基类选CDialogEx。创建完成后最重要的一步右键项目属性 - 配置属性 - 常规 - 字符集选择使用Unicode字符集。这步如果在创建向导时没选后面也可以改。字符集设置影响CString的宽窄行为我们全程使用CStringW或std::string混搭必须把工程统一到Unicode模式否则CString的隐式转换会坑死人。然后我在stdafx.h新版可能叫pch.h里加上了#include string和#include vector因为核心代码用了STL。如果你的工程是VS2022创建的新MFC项目默认已经有pch.h了。4.2 控件布局与变量绑定在资源编辑器里拖控件ID命名我习惯用有意义的名称源文本编辑框IDC_EDIT_SOURCE多行、垂直滚动条、水平滚动条、WantReturn。结果编辑框IDC_EDIT_RESULT属性同上。源编码组合框IDC_COMBO_SOURCE类型为Drop List禁止用户手动输入。目标编码组合框IDC_COMBO_DEST同样Drop List。转换按钮IDC_BTN_CONVERT。控件绑定变量用向导或手动写我这里列出我绑定的成员变量CEdit m_editSource; CEdit m_editResult; CComboBox m_cmbSource; CComboBox m_cmbDest; CButton m_btnConvert;在OnInitDialog里初始化下拉框内容m_cmbSource.AddString(LANSI); m_cmbSource.AddString(LGBK); m_cmbSource.AddString(LGB2312); m_cmbSource.AddString(LGB18030); m_cmbSource.AddString(LUTF-8); m_cmbSource.AddString(LUTF-8 BOM); m_cmbSource.AddString(LUTF-16LE); m_cmbSource.AddString(LUNICODE(UTF-16LE)); m_cmbDest.AddString(LANSI); // ... 同样的选项 m_cmbSource.SetCurSel(0); // 默认选中ANSI m_cmbDest.SetCurSel(4); // 默认UTF-84.3 编译与测试编译运行后的常规测试路径在源编辑框粘入汉字编码转换测试123abc。源编码选ANSI即当前系统代码页GBK目标选UTF-8。点击转换结果应该是没有乱码的UTF-8文本但界面显示会正常因为MFC最终显示到CEdit时又转成了宽字符。用读取文件按钮打开一个UTF-8文件源编码选择UTF-8目标选择ANSI转换后另存再用记事本打开确认不乱码。我测试时遇到最典型的情况从网页复制的文本是UTF-8无BOM直接贴进编辑框选择ANSI转换结果出现锟斤拷。这是因为编辑框内部的宽字符文本已经被Windows用系统代码页解释了一遍再去主动转换一次相当于双重转换。遇到这种情况正确姿势是从文件读取原始字节流或者先让工具读取剪贴板中的字节数据。5. 常见问题与排查技巧实录5.1 界面显示锟斤拷和烫烫烫锟斤拷是UTF-8字节被当作GBK解释后产生的经典乱码也是编码转换最典型的错误。排查思路很简单先确定原始字节流到底是什么编码再去选择源编码。不要靠猜用工具自带的读取文件功能最准确。烫烫烫则是MSVC调试模式下未初始化内存的填充值看到这个说明代码里用了未初始化的缓冲区。我的代码里std::string和std::wstring通过resize预留空间后再调用API不会有这个问题。如果你用new char[]记得清零或者用vector别裸用堆缓冲区。5.2 从文件读入时编码识别不准我没有做自动检测编码核心原因是自动检测本身就是个概率问题很容易误判。一款好用的工具应该把选择权交给用户但可以通过BOM做初步判断。工具逻辑是如果文件带UTF-8 BOM自动把源编码切到UTF-8 BOM否则保持用户选择。GBK和UTF-8在没有BOM时很难100%区分遇到这种情况我建议干脆把源编码设置为ANSI因为中文Windows系统下的文本多半是ANSIGBK生成的老文件。如果你的工作环境经常遇到UTF-8无BOM文件就选择UTF-8选项多试一次也没什么成本。5.3 转换前后字节数不一致是不是bug不是。GBK中文字符占2字节UTF-8中文字符占3字节所以汉字两个字从GBK转UTF-8后字节数必然变多。看到输出字节数增长不要慌这是编码正常的膨胀过程。但如果你发现英文字母和数字的字节数也变了那就要查查是不是把UTF-8转GBK时英文字符被错误处理了。正常情况ASCII字符在UTF-8和GBK里都占1字节不改变数量。5.4 关于UNICODE选项与UTF-16LE的重复我在下拉框里同时放了UNICODE和UTF-16LE实际转换时代码页都是1200。之所以分两个选项是为了照顾不同用户的习惯叫法。代码里映射到同一个值即可转换结果没有区别。类似地ANSI在当前中文系统上其实就是CP_ACP936也就是GBKGBK选项也映射到936功能相同但语义上给人选择空间。6. 可扩展的几个方向小程序做完了后续还能加不少实用功能。我给自己列了几个待做清单也供你参考剪贴板监视用户复制文本后自动检测并转换这需要定时器或热键MFC里用SetTimer就能实现。批量文件转换现在的拖拽只支持单文件改成支持多文件路径拖入循环处理即可。十六进制预览在转换前显示原始字节的十六进制方便技术人员确认编码类型排查乱码会更省事。加密壳集成工具本质是字节流处理不限于编码转换以后还可以加载自定义转换插件。我在实际使用中还有一个体会不要过度依赖万能自动检测。真正稳定的处理流程一定是明确字节流来源 用户手动指定源编码 转换 校验。小工具的定位是解决单次、批量的确定性转换把选择权留给用户反而少出问题。最后分享一个排查编码问题的老经验拿到一段乱码不要急着转换先看它长什么样。锟斤拷指向UTF-8被当GBK读鈥这类符号通常指向UTF-8被当ANSI读乱码问号往往说明目标编码无法表达某些字符。多看几眼乱码基本就能猜出源编码方向再用这个小工具一转换问题就落地了。本文还有配套的精品资源点击获取