Windows中控制台应用的Unicode支持调查

创建时间:2017/3/11 下午5:29:55
编辑时间:2017/10/15 下午9:01:02
作者: huww98@163.com (huww98@163.com)
分类:Windows

先说结果,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输出。

    这种方式直接使用一般使用的charstring类型来存储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 - codecvtReading 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操作的代码都差不多,依然有几种方法:

  • 首先,可以直接像普通的文本一样使用ifstreamfscanf读取,存入char或类似类型的变量,然后使用上节介绍的直接输出UTF-8的方法。
  • 可以使用wifstream读取,存入wchar_t或类似的变量,然后使用上一节介绍的另外两种方式输出。注意,在从流读取之前也需要调用fin.imbue(locale(wcout.getloc(), new codecvt_utf8_utf16<wchar_t>));finwifstream的实例)关于这其中的原理,上面引用的Reading UTF-8 with C++ streams讲得十分具体。

关于在这两种方式中如何抉择,请见std::wstring VS std::string - StackOverflow和此问题的另一个回答

关于如何在这两种方式中转换,有几种方式:

总结

虽然Windows控制台对Unicode的支持不完整,但是也已经有了基础的支持,可以应付大多数情况了,而且,在Windows中控制台登场的频率并不是很高,所以当前的状况还是可以接受的。

测试平台:

  • Windows 10
  • Visual Studio Community 2017

返回文章列表

评论

登录 / 注册 后发布评论