Windows C++ MFC 多字节与 Unicode
一个外包项目,客户的软件是 Windows 下的桌面应用程序,基于 C++ / MFC 开发。客户的需求是:目前软件在日文 Windows 系统上只能打开英文路径和日文路径下的文件,文件内容需要显示在界面上,如果文件内容中有非英文和日文的文字,如中文、韩文、泰文等,显示结果为码乱。要做成可以在任意一个语言版本的 Windows 系统上处理非当前系统语言的文件,包括文件路径和文件内容。本文以在日文 Windows 上打开中文路径上的文件、文件内容有中文为例进行讲解。项目涉及字符编码,不明白的同学可以先学习这方面知识,这里不展开讨论。
MFC 项目可以设置字符集,如图1所示。

图1
看似只要将字符集设置成”使用 Unicode 字符集“就能解决。但是使用这个选项的前提是代码里与路径相关的变量必须使用 TCHAR 数组、LPTSTR(本质是 TCHAR)、LPCTSTR(本质是 const TCHAR)才行,而客户代码里使用的全是 char 数组和 char、const char,所以改字符集是不行的(行的话也不会当一期项目发包过来)。顺便科普一下 TCHAR,它是一个宏,定义如下:
ifdef UNICODE
typedef WCHAR TCHAR, *PTCHAR;
else
typedef char TCHAR, *PTCHAR;
endif
当项目属性的字符集设置为”使用 Unicode 字符集“时,#ifdef UNICODE 为 true,TCHAR 在编译时就被替换成 WCHAR,否则被替换成 char。当程序里使用 WCHAR 时,可以打开任意语言路径下的文件;如果使用 char,就只能打开英文和当前系统语言路径下的文件。所以本项目要么将 char 改成 TCHAR,再将字符集改为 Unicode,要么直接将 char 改成 WCHAR,最终客户选择了后者。除了要将 char 改成 WCHAR,使用 char 的相关 API 也要改成 W 版本,而且要小心,有些 char 数组是做为缓存的,不是路径,不需要修改。
客户代码这里就不展示了,下面以一个测试程序来讲解本项目的技术点。
1.打开文件
比如在日文系统上,要打开文件”C:\这是中文文档.txt",文件内容为“你好,欢迎来到 unicode 的世界",编码为 UTF8。虽然日本也使用汉字,但只使用了一部分,像”这“、”你“、”欢“不在日文字符集中,使用 char 作为路径的程序无法正常处理。比如:
FILE* file;
char buffer[100] = {};
fopen_s(&file, "C:\这是中文文档.txt", "r");
fread_s(buffer, sizeof(buffer), sizeof(char), 100, file);
fclose(file);
在调用到 fopen_s 的时候程序就会报错退出。所以要改成:
FILE* file;
char buffer[100] = {};
_wfopen_s(&file, L"C:\这是中文文档.txt", L"r");
fread_s(buffer, sizeof(buffer), sizeof(char), 100, file);
fclose(file);
这样才能正常打开文件。
2.文件编码转换
虽然正常打开了文件,但是文件中的内容显示在界面上却是乱码,如图2所示。

图2
原因是,文件内容的编码是 UTF8,而 MFC 只支持 ANSI 和 UTF16。上面那个设置字符集的选项里,”使用多字节字符集“对应的就是 ANSI,而”使用 Unicode 字符集“对应的则是 UTF16,所以还要将 UTF8 转换成 UTF16才行,转换代码如下:
// 计算转换时需要的缓存长度
int len = MultiByteToWideChar(CP_ACP, 0, string_ANSI, -1, NULL, 0);
// 创建 UTF16 字符串
WCHAR* string_UTF16 = new WCHAR[len];
// 转换成 UTF16 字符串
MultiByteToWideChar(CP_ACP, 0, string_ANSI, -1, string_UTF16, len);
// 使用 UTF16 字符串
...
// 释放 UTF16 字符串
delete[] string_UTF16;
3.创建支持 UTF16 的 MFC 控件
经过上面两步,虽然成功拿到了 UTF16 字符串,但在使用它的时候还是有问题。图2的程序是一个对话框,上面有一个 CStatic 控件,要把 UTF16 字符串显示在上面,就要调用 CStatic::SetWindowText 方法。然而直接调用无法通过编译,因为 CStatic::SetWindowText 调用的是 Windows API 的 SetWindowText,这也是个宏,它的定义如下:
ifdef UNICODE
typedef SetWindowText SetWindowTextW
else
typedef SetWindowText SetWindowTextA
endif
也就是说,最终调用的是 SetWindowTextA,而 SetWindowTextA 只接受 char 类型参数,传 WCHAR 当然编译不过。这里有两种解决方法,一是直接调用 SetWindowTextW,将 CStatic 的 HWND 和 UTF16 字符串传入,但是这样不符合面象对象编程的原则,而且后面遇到复杂控件如 CListCtrl 时就行不通了,所以第二种方法是写一个类继承自 CStatic,在类中提供 SetWindowText 方法并在该方法中调用 SetWindowTextW,如下所示:
void CStaticW::SetWindowText(LPCWSTR lpszString) {
SetWindowTextW(CStatic::GetSafeHwnd(), lpszString);
}
这样就通过了编译。但是运行时的效果却不理想,那些不在日文字符集里的汉字被显示成“?”,如图3所示:

图3
原因是:控件本身也是分为 A 和 W 的,在多字节字符集项目下的 CStatic 是 A 的,要替换成 W 的才行。因为 CStatic 的对象是在 Dialog::OnInitDialog 就去里调用 SubclassDlgItem 进行子类化创建的,所以要在 CStaticW 里重写 SubclassDlgItem 方法,如下所示:
BOOL CStaticW::SubclassDlgItem(UINT nID, CWnd* pParent) {
BOOL result = CStatic::SubclassDlgItem(nID, pParent);
// 获取风格
DWORD style = CStatic::GetStyle();
// 获取位置和尺寸
CRect rect;
CStatic::GetWindowRect(rect);
pParent->ScreenToClient(rect);
// 获取字体
CFont* font = CStatic::GetFont();
// 解除 HWND 和 CStatic 的绑定
CStatic::Detach();
// 创建支持 Unicode 的 Static
HWND hWnd = CreateWindowExW(0, L"Static", L"", style, 0, 0, 0, 0, pParent->GetSafeHwnd(), NULL, NULL, NULL);
// 绑定新的 HWND 到 CStatic
CStatic::Attach(hWnd);
// 设置位置和尺寸
CStatic::MoveWindow(rect);
// 设置字体
CStatic::SetFont(font);
return result;
}
上面代码中最关键的就是解除绑定、创建支持 Unicode 的 Static (即 W 的控件)和绑定新的 HWND 这三部分。经过以上处理,界面上终于能正常显示中文内容了,如图4所示。

图4
总结一下,一共3点:
1.用 _wfopen_s 打开 Unicode 路径下的文件。
2.用 MultiByteToWideChar 函数将文件里的 UTF8 字符串转换成 UTF16 字符串。
3.创建继承自 CStatic 的类 CStaticW,在里面重写 SetWindowText 方法 和 SubclassDlgItem 方法使控件支持显示 UTF16 字符串(如果控件不是用 SubclassDlgItem 创建的而是用 Create 创建的,那就重写 Create 方法)。
全文完。

浙公网安备 33010602011771号