如何让ncurses输出星体平面unicode字符

时间:2014-05-07 19:14:47

标签: c unicode utf-8 ncurses

我有以下非常简单的代码,它应该输出(除其他外)三个unicode字符:

/*
 * To build:
 *   gcc -o curses curses.c -lncursesw
 *
 * Expected result: display these chars:
 *   http://www.fileformat.info/info/unicode/char/2603/index.htm  (snowman)
 *   http://www.fileformat.info/info/unicode/char/26c4/index.htm  (snowman without snow)
 *   http://www.fileformat.info/info/unicode/char/1f638/index.htm (grinning cat face with smiling eyes)
 *
 * Looks like ncurses is NOT able to display second and third char
 * (only the first one is OK...)
 */

#include <ncurses.h>
#include <stdio.h>
#include <locale.h>

int
main (int argc, char *argv[])
{
    WINDOW *stdscr;
    char buffer[] = {
        '<',
        0xE2, 0x98, 0x83,       // U+2603 : snowman: OK
        0xE2, 0x9B, 0x84,       // U+26C4 : snowman without snow: ERROR (space displayed)
        0xF0, 0x9F, 0x98, 0xB8, // U+1F638: grinning cat face: ERROR (space displayed)
        '>',
        '\0' };

    setlocale (LC_ALL, "");

    stdscr = initscr ();
    mvwprintw (stdscr, 0, 0, buffer);
    getch ();
    endwin ();

    /* output the buffer outside of ncurses */
    printf("%s\n",buffer);
    return 0;
}

最终的printf会输出我所期望的所有字符“&lt;☃⛄&gt;” (因为我正在使用正确配置的语言环境,终端模拟器和适当的字体组合) - 但是第一部分,它应该使用ncurses函数输出文本无法正常工作。您只能看到第一个字符(雪人),而其他两个字符只是呈现为空格。 “&lt;☃&gt;”。

我看过很多谷歌帖子,说我还需要加入

#define _XOPEN_SOURCE_EXTENDED 1

在源代码中 - 但这样做并没有改变我的输出。

所以 - 我在这里做了一些极其愚蠢的事情,或者在使用unicode空间的某些部分时是不是已经破坏了?

1 个答案:

答案 0 :(得分:51)

并不完全是ncurses被打破了。更像是,glibc被打破了。或者您正在使用的libc的任何实现;我只是假设它是glibc

与简单的控制台输出(即printf)不同,ncurses需要知道每个字符在打印时的宽度,因为它需要维护自己的屏幕外观模型,以及光标所在的位置。并非所有Unicode代码点都是1个单位宽,即使使用比例字体:许多代码点为零单位宽(例如组合重音),而且相当多的是两个单位宽(汉字表意文字)[注1]。

事实证明,有一个标准的C库函数wcwidth,它接受​​wchar_t并返回0,1或2(或理论上任何整数,但是afaik那些是唯一实现的widths)如果字符是&#34; printable&#34;,如果字符无效或控制字符,则为-1。支持宽字符的ncurses版本使用wcwidth来预测在打印字符后光标移动的距离。如果wcwidth返回错误指示,则ncurses会替换空格。

wcwidth从区域设置WIDTH的{​​{1}}部分读取宽度,但该定义仅提供例外情况;假定任何没有定义宽度的可打印字符的宽度为1.因此charmap 还需要检查该字符是否可打印,这是在{{1}中定义的语言环境规范。这与驱动wcwidth库函数的数据相同。

不幸的是,无法保证终端仿真器与C库函数共享相同的Unicode字符数据视图。对于实际显示宽度与区域设置配置宽度不同的字符,LC_CTYPE将产生意外行为。

在这种情况下,宽度没有问题(字符都是1个单位宽,所以默认是正确的);问题是这些字符实际存在于您的控制台字体中并且您想要使用它们,但它们不存在于iswprint的字符数据库中,因为该数据库是still based on Unicode 5.0 。 (事实上​​,应该更新该bug本身,因为Unicode现在是6.3,而不是6.1。)

为了帮助您了解这里,这是一个小型程序,它为unicode代码点转储已配置的ctype信息[注2]:

ncurses

编译它可以查看你的角色数据。它可能看起来像这样:

glibc

那么,该怎么办?您可以等待#define _XOPEN_SOURCE 600 #include <locale.h> #include <stdio.h> #include <stdlib.h> #include <wctype.h> #include <wchar.h> #define CONC_(x,y) x##y #define IS(x) (CONC_(isw,x)(c)?#x" ":"") int main(int argc, char** argv) { setlocale(LC_CTYPE,""); for (int i = 1; i < argc; ++i) { wint_t c = strtoul(argv[i], NULL, 16); printf("Code %04X: width %d %s%s%s%s%s%s%s%s%s%s%s%s\n", c, wcwidth(c), IS(alpha),IS(lower),IS(upper),IS(digit),IS(xdigit),IS(alnum), IS(punct),IS(graph),IS(blank),IS(space),IS(print),IS(cntrl)); } return 0; } 数据库更新,但我怀疑它不会很快发生。因此,如果您真的想要使用这些字符,则需要修改自己的区域设置定义。

如果您安装的$ gcc -std=c11 -Wall -o wcinfo wcinfo.c $ ./wcinfo 2603 26c4 1f638 Code 2603: width 1 punct graph print Code 26C4: width -1 Code 1F638: width -1 安装与我一样(并且区域设置文件暂时没有更改,那么您可能会这样做),那么您将在{{}找到您的区域设置文件{1}}在实际的语言环境文件中,glibc部分将包含指令glibc,这意味着实际的ctype配置位于文件/usr/share/i18n/locales中。然后,您可以编辑该文件以进行适当的更改。 (当然,在更改文件之前制作备份副本。并且您需要LC_CTYPE您的编辑器,因为该文件只能由root写入。)

首先找到开始copy "i18n",[注3]然后再搜索/usr/share/i18n/locales/i18n的行(我的配置中的第716行,fwiw。)您将找到一个带有条目的行看起来像sudo,这意味着代码点graphU26是图形(可见打印)字符。根据需要扩展该范围。 (我将<U26A0>..<U26C3>;更改为26A0进行最低限度的测试,但您可能希望包含更多字符。)再过几行,您将看到第二个平面26C3范围;添加适当的条目。 (同样,极简主义,我添加了一个新的一行:

26C3

但您可能想要包含一个范围。 (顺便说一句,尾随26C4是延续标记。)

接下来,再往下走几行,您就会找到graph部分。使完全相同的更改

然后,您可以通过运行:

重新生成您的区域设置信息
   <U0001F638>;/

然后你可以测试:

/

一旦你这样做,原来的ncurses程序应该产生预期的输出。

顺便说一下,你可以使用带有ncurses的宽字符串;你不必手动生成UTF-8编码:

print

备注

  1. 有关详情,请参阅halfwidth and fullwidth forms上的维基百科。

  2. 这是一个快速而肮脏的无错误检查程序,但它足以满足我们的需求。出于生产目的,人们可能需要更多代码行:)

  3. 您可能不需要修复$ sudo locale-gen wctype; $ ./wcinfo 2603 26c4 1f638 Code 2603: width 1 punct graph print Code 26C4: width 1 graph print Code 1F638: width 1 graph print 可能就足够了。我没有检查。我之所以这样做是因为int main (int argc, char *argv[]) { WINDOW *stdscr; setlocale (LC_ALL, ""); const wchar_t* wstr = L"<\u2603\u26c4\U0001F638>"; stdscr = initscr (); mvwaddwstr(stdscr, 0, 0, wstr); getch (); endwin (); return 0; } 有时也需要知道字符是否透明,将字符标记为可见似乎更安全,因为它是。