先说结果,Windows控制台中的Unicode支持和想象中还有一些距离,我研究了一天半时间,目前还没能在控制台中获得完整的Unicode支持,在UCS-4中超过两个字节编码的字符我还没有成功地使其显示在控制台中。幸运的是,这些字符很少用,并且,我成功地输出了UCS-2中用两个字节编码的各种字符。
控制台本身的限制
控制台本身对Unicode的支持不是很彻底,以下是我尝试的方法:将🍌(U+0001F34C)𢒉(U+00022489)
这两个字符以UTF-8(无BOM)编码保存于test.txt
文件中,在控制台中输入chcp 65001
以将当前代码页更改为UTF-8,然后在命令行输入:
type test.txt
bash
然后cat test.txt
(我已安装“适用于Linux的Windows子系统”功能)
我看到的输出都是四个方框。
对于这个结果,我认为有2个原因:
- 我使用的字体不支持这些字符,我在Word中粘贴这两个字符,并不能为他们应用cmd的默认字体“新宋体”,我也没有在cmd可选的字体列表里找到任何一个支持这两个字符的字体。我也很难自己弄一个支持这些字符的字体,原因是能应用于控制台的字体有很严格的要求,详见Necessary criteria for fonts to be available in a command window
- cmd根本不支持这些编码超过两个字节的字符,鉴于我看到了4个方框,而不是2个,cmd很有可能把这2个字符当成4个字符来处理了。但这只是猜测,由于我无法排除第一个原因,我也无法验证这个原因。
使用VC++输出较为困难
虽然较为困难,但是还是有一些办法的。当然,这都无法突破上段所提的控制台本身的限制。
直接使用UTF-8输出。
这种方式直接使用一般使用的
char
或string
类型来存储UTF-8编码的字符,然后使用wprintf(L"%S", ...);
进行输出。但是首先要设置代码页为UTF-8才能正常显示。详情请见Unicode Output to the Windows Console,最简单的实例如下:#include <iostream> #include <Windows.h> using namespace std; int main() { UINT oldcp = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); wprintf(L"%S", u8"汉字한국의🍌\n"); SetConsoleOutputCP(oldcp); return 0; }
使用wcout输出。
这种方式似乎是处理Unicode比较自然的方式,但是也需要设置代码页,还需要一些额外工作,设置locale,实例如下:
#include <iostream> #include <string> #include <locale> #include <codecvt> #include <Windows.h> using namespace std; void printState() { bool good = wcout.good(); bool eof = wcout.eof(); bool fail = wcout.fail(); bool bad = wcout.bad(); wcout.clear(); wcout << "\nGood:" << good << "\nEOF:" << eof << "\nFail:" << fail << "\nBad:" << bad << endl; } int main() { UINT oldcp = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); wcout.imbue(locale(wcout.getloc(), new codecvt_utf8_utf16<wchar_t>)); wcout << L"W汉字ABC" << endl; printState(); wcout << L"W한국의ABC" << endl; printState(); wcout << L"W🍌ABC" << endl; printState(); SetConsoleOutputCP(oldcp); return 0; }
如果没有
wcout.imbue
的调用的话,下面的输出都会只输出一个W,并将wcout
对象置于失败状态。另外,这样输出的🍌
是4个“菱形里面带个问号”,如果把输出重定向到文件,再用type
命令显示,却还是方框,有点奇怪。想了解详情,请参见:C++ Reference - codecvt,Reading UTF-8 with C++ streams
使用Windows的API输出。
直接调用WriteConsoleW函数来输出到控制台。最为简单,但是却不能重定向了,所以这几乎不是可用的方案。或者,可以混合使用这种和上面的方案,重定向时使用上面的方案,输出到控制台时用这个。更详细参见:type命令的行为
DWORD ws; wchar_t* test = L"汉字한국의🍌ABC"; WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), test, 9, &ws, NULL);
注意这里我指定了长度9,但却只输出到了B,更加印证了我认为cmd将
🍌
看作两个字符的猜测。
使用VC++读取UTF-8编码文件
有了上面的经验,到这里我就轻车熟路了,C++中各种IO操作的代码都差不多,依然有几种方法:
- 首先,可以直接像普通的文本一样使用
ifstream
或fscanf
读取,存入char
或类似类型的变量,然后使用上节介绍的直接输出UTF-8的方法。 - 可以使用
wifstream
读取,存入wchar_t
或类似的变量,然后使用上一节介绍的另外两种方式输出。注意,在从流读取之前也需要调用fin.imbue(locale(wcout.getloc(), new codecvt_utf8_utf16<wchar_t>));
(fin
为wifstream
的实例)关于这其中的原理,上面引用的Reading UTF-8 with C++ streams讲得十分具体。
关于在这两种方式中如何抉择,请见std::wstring VS std::string - StackOverflow和此问题的另一个回答
关于如何在这两种方式中转换,有几种方式:
- wstring_convert标准库中的类,详见:How to convert UTF-8 std::string to UTF-16 std::wstring?
- MultiByteToWideChar和WideCharToMultiByte Windows API
总结
虽然Windows控制台对Unicode的支持不完整,但是也已经有了基础的支持,可以应付大多数情况了,而且,在Windows中控制台登场的频率并不是很高,所以当前的状况还是可以接受的。
测试平台:
- Windows 10
- Visual Studio Community 2017