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

God*_*ter 29 c unicode ncurses utf-8

我有以下一段非常简单的代码,它应该输出(除其他外)三个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;
}
Run Code Online (Sandbox Code Playgroud)

最终的printf输出所有字符,因为我期望"<☃⛄>"(因为我使用的是正确配置的区域设置,终端模拟器和适当的字体组合) - 但是第一部分,应该使用输出文本ncurses函数无法正常工作.您只能看到第一个字符(雪人),而其他两个字符只是呈现为空格."<☃>".

我已经阅读了很多谷歌帖子,说我还需要包括

#define _XOPEN_SOURCE_EXTENDED 1
Run Code Online (Sandbox Code Playgroud)

在源头 - 但这样做并没有改变我的输出.

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

ric*_*ici 55

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

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

事实证明,有一个标准的C库函数,如果字符是"可打印的",则wcwidth取一个wchar_t和返回0,1或2(或理论上任何整数,但是afaik那些是唯一实现的宽度),并且-1如果字符无效或控制字符.支持广泛字符的ncurses用途,wcwidth用于预测在打印字符后光标移动的距离.如果wcwidth返回错误指示,则ncurses替换空格.

wcwidthWIDTH区域设置的部分读取宽度charmap,但该定义仅提供异常; 假定任何没有定义宽度的可打印字符的宽度为1.因此wcwidth 需要检查字符是否可打印,这是在LC_CTYPE语言环境规范中定义的.这与驱动iswprint库函数的数据相同.

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

在这种情况下,宽度没有问题(字符都是1个单位宽,所以默认是正确的); 问题是这些字符实际存在于您的控制台字体中并且您想要使用它们,但它们不存在于glibc字符数据库中,因为该数据库仍然基于Unicode 5.0.(实际上,应该更新该bug本身,因为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;
}
Run Code Online (Sandbox Code Playgroud)

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

$ 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 
Run Code Online (Sandbox Code Playgroud)

那么该怎么办?您可以等待glibc数据库更新,但我怀疑这不会很快发生.因此,如果您确实想要使用这些字符,则需要修改自己的区域设置定义.

如果你有glibc和我一样的安装(并且locale文件暂时没有改变,你可能会这样做),那么你将/usr/share/i18n/locales在实际的语言环境文件中找到你的语言环境文件,该LC_CTYPE部分将包含指令copy "i18n",这意味着实际的ctype配置在文件中/usr/share/i18n/locales/i18n.然后,您可以编辑该文件以进行适当的更改.(当然,在更改文件之前制作备份副本.并且您需要sudo编辑器,因为该文件只能由root写入.)

首先找到开始的行graph,[注3]然后向前搜索U26(我的配置中的第716行,fwiw.)你会找到一个带有条目的行<U26A0>..<U26C3>;,这意味着代码点26A0通过26C3图形(可见打印)字符.根据需要扩展该范围.(我将其26C3更改26C4为最小化测试,但您可能希望包含更多字符.)再向下几行,您将看到第二个平面graph范围; 添加适当的条目.(同样,极简主义,我添加了一个新行:

   <U0001F638>;/
Run Code Online (Sandbox Code Playgroud)

但你可能想要包括一个范围.(/顺便说一句,尾随是延续标记.)

接下来,再往下走几行,你就会找到这个print部分.进行完全相同的更改.

然后,您可以通过运行以下命令重新生成区域设置信息

$ sudo locale-gen
Run Code Online (Sandbox Code Playgroud)

然后你可以测试:

$ ./wcinfo 2603 26c4 1f638
Code 2603: width 1 punct graph print 
Code 26C4: width 1 graph print 
Code 1F638: width 1 graph print 
Run Code Online (Sandbox Code Playgroud)

一旦你这样做,你的原始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;
}
Run Code Online (Sandbox Code Playgroud)

笔记

  1. 有关更多信息,请参阅有关半宽和全宽表单的 Wikipedia .

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

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

  • 这只是一个非常全面的答案.非常感谢! (6认同)
  • 关于流血的时间;)仍然希望我有一个以上的投票给rici。这是我收到过关于Stackoverflow问题的最佳答案。这确实让我失望了。 (2认同)