如何使ncurses输出星位平面Unicode字符

31
我有一段非常简单的代码,应该输出(除其他内容外)三个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 输出了所有字符,如我所期望的 "<☃⛄>"(因为我使用了正确配置的语言环境、终端仿真器和适当的字体组合)。然而,应该使用 ncurses 函数输出文本的第一部分无法正常工作。您只能看到第一个字符(雪人),而其他两个字符只会被渲染为空格。"<☃ >"。

我阅读了许多谷歌帖子,说我还需要包含

#define _XOPEN_SOURCE_EXTENDED 1

我在源代码中进行了更改,但对我来说这并没有改变输出结果。

那么,我是在做一些极其愚蠢的事情吗?还是当使用Unicode空间的某些部分时,ncurses出现了故障?

1个回答

60

这并不是说 ncurses 出了问题,更像是 glibc 出了问题。或者说,无论您使用的是哪种 libc 实现,我只是假设它是 glibc

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

原来有一个标准的C库函数wcwidth,它接受类型的字符,并返回0、1或2(理论上可以是任何整数,但据我所知这些是唯一实现的宽度),如果字符是“可打印的”,则返回相应的值;如果字符无效或为控制字符,则返回-1。启用宽字符的ncurses版本使用wcwidth来预测字符打印后光标移动的距离。如果wcwidth返回错误指示,则ncurses会替换为空格。 wcwidth从区域设置的charmapWIDTH部分读取宽度,但该定义仅提供异常情况;假定任何没有定义宽度的可打印字符具有宽度1。因此,wcwidth还需要检查字符是否可打印,这在LC_CTYPE区域设置中定义。这是驱动iswprint库函数的相同数据。

很不幸,终端模拟器与C库函数对Unicode字符数据的视图可能不同。对于那些实际显示宽度与本地配置宽度不同的字符,ncurses会产生意外行为。

在这种情况下,宽度没有问题(所有字符都是1个单位宽,因此默认值是正确的);问题在于这些字符实际上存在于您的控制台字体中,您想要使用它们,但它们不存在于glibc的字符数据库中,因为该数据库仍然基于Unicode 5.0。(实际上,该错误本身应该更新,因为Unicode现在已经到6.3而不是6.1。)

为了帮助您查看,这里有一个小程序,它可以转储配置的unicode代码点的ctype信息[注2]:

#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 

所以,该怎么办呢?你可以等待数据库得到更新,但我怀疑这不会很快发生。因此,如果您真的想使用那些字符,您需要修改自己的区域设置定义。
如果您和我一样拥有相同的安装(并且区域设置文件已经有一段时间没有更改了,所以您可能也是),那么您会在/usr/share/i18n/locales找到您的区域设置文件,在实际的区域设置文件中,LC_CTYPE部分将包括指令copy "i18n",这意味着实际的ctype配置在文件/usr/share/i18n/locales/i18n中。然后,您可以编辑该文件以进行适当的更改。(在更改文件之前,请备份副本。当然,您需要使用sudo编辑器,因为该文件只能由root写入。)
首先找到以graph开头的那一行[注3],然后向前搜索U26(在我的配置中是第716行)。你会发现一行条目看起来像这样:<U26A0>..<U26C3>;,这意味着代码点26A026C3是图形(可见印刷)字符。根据需要扩展该范围。(我将26C3更改为26C4进行最小测试,但您可能需要包含更多字符。)再往下几行,您将看到第二个平面graph范围;添加适当的条目。(同样,作为极简主义者,我添加了一个新行:)
   <U0001F638>;/

但您可能需要包含一个范围。(顺便说一下,尾随的/是续行标记。)
接下来,向下再走几行,您会找到print部分。进行完全相同的更改
然后,您可以通过运行以下命令重新生成您的语言环境信息:
$ sudo locale-gen

然后您可以进行测试:

$ ./wcinfo 2603 26c4 1f638
Code 2603: width 1 punct graph print 
Code 26C4: width 1 graph print 
Code 1F638: width 1 graph print 

一旦您这样做了,您的原始ncurses程序应该会产生预期的输出。
顺便说一下,您可以使用宽字符字符串与ncurses,您不必手动生成UTF-8编码:
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;
}

注意事项

  1. 如需更多信息,请参阅维基百科半角和全角字符

  2. 这是一个快速且简单的程序,没有错误检查,但对于我们在这里所需要的足够好了。为了生产目的,需要写入更多代码 :)

  3. 您可能不需要修复graph wctype; print可能已经足够了。我没有检查。我两个都做了,因为ncurses有时也需要知道字符是否透明,而将字符标记为可见似乎更安全,因为它是可见的。


7
这是一个非常全面的回答。非常感谢你! - GodEater
1
真是令人惊讶!有个好消息:那个漏洞最近已经修复了,而且glibc现在已经更新到Unicode 7.0 :) - MestreLion
2
终于到了血腥的时候;) 仍然希望我能够给rici超过一个赞。这是我在Stackoverflow上得到的最好的答案。它真的让我震惊了。 - GodEater
好问题;杰出的答案。 - ulidtko
这是我在SO上找到的最完整和高质量的答案之一。感谢您的时间。 - eepp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接