使用 ncurses 进行彩色编程

作者:Jim Hall

Jim 通过在他的终端冒险游戏中添加颜色,演示了使用 curses 进行颜色操作。

在我关于使用 ncurses 库进行编程的系列文章的第一部分第二部分中,我介绍了一些 curses 函数,用于在屏幕上绘制文本、查询屏幕上的字符以及从键盘读取输入。为了演示这些函数,我用 curses 创建了一个简单的冒险游戏,该游戏使用简单的字符绘制游戏地图和玩家角色。在本文的后续文章中,我将展示如何在 curses 程序中添加颜色。

在屏幕上绘制内容固然很好,但如果所有内容都是白底黑字,您的程序可能会显得很枯燥。颜色可以帮助传达更多信息——例如,如果您的程序需要指示成功或失败。在这种情况下,您可以将文本显示为绿色或红色,以帮助强调结果。或者,也许您只是想使用颜色来“美化”您的程序,使其看起来更漂亮。

在本文中,我将使用一个简单的示例来演示通过 curses 函数进行颜色操作。在我之前的文章中,我编写了一个基本的冒险风格游戏,让您可以在粗略绘制的地图上移动玩家角色。然而,该地图完全是黑白文本,依靠形状来表示水(~)或山脉(^),所以让我们更新游戏以使用颜色。

颜色基础知识

在您可以使用颜色之前,您的程序需要知道它是否可以依赖终端来正确显示颜色。在现代系统上,这通常应该总是为真。但在经典的计算机时代,一些终端是单色的,例如受人尊敬的 VT52 和 VT100 终端,通常提供白底黑字或绿底黑字文本。

要查询终端的颜色能力,请使用 has_colors() 函数。如果终端可以显示颜色,这将返回真值;如果不能,则返回假值。它通常用于启动一个 if 代码块,如下所示


if (has_colors() == FALSE) {
    endwin();
    printf("Your terminal does not support color\n");
    exit(1);
}

在确定终端可以显示颜色后,您可以设置 curses 以使用 start_color() 函数来使用颜色。现在您可以定义您的程序将要使用的颜色了。

curses 中,您以颜色对的形式定义颜色:前景色和背景色。这允许 curses 一次设置两个颜色属性,这通常是您想要做的。要建立颜色对,请使用 init_pair() 定义前景色和背景色,并将其与索引号关联。通用语法是


init_pair(index, foreground, background);

控制台仅支持八种基本颜色:黑色、红色、绿色、黄色、蓝色、洋红色、青色和白色。这些颜色使用以下名称为您定义

  • COLOR_BLACK
  • COLOR_RED
  • COLOR_GREEN
  • COLOR_YELLOW
  • COLOR_BLUE
  • COLOR_MAGENTA
  • COLOR_CYAN
  • COLOR_WHITE
应用颜色

在我的冒险游戏中,我希望草地是绿色的,玩家的“足迹”是微妙的黄绿色虚线路径。水应该是蓝色的,波浪线(~)是类似的青色。我希望山脉是灰色的,但黑底白字应该是一个合理的折衷方案。为了使玩家角色更显眼,我想使用鲜艳的红底洋红色方案。我可以像这样定义这些颜色对


start_color();
init_pair(1, COLOR_YELLOW, COLOR_GREEN);
init_pair(2, COLOR_CYAN, COLOR_BLUE);
init_pair(3, COLOR_BLACK, COLOR_WHITE);
init_pair(4, COLOR_RED, COLOR_MAGENTA);

为了使我的颜色对易于记忆,我的程序定义了一些符号常量


#define GRASS_PAIR     1
#define EMPTY_PAIR     1
#define WATER_PAIR     2
#define MOUNTAIN_PAIR  3
#define PLAYER_PAIR    4

使用这些常量,我的颜色定义变为


start_color();
init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);

每当您想要使用颜色显示文本时,您只需要告诉 curses 设置该颜色属性即可。为了良好的编程实践,您还应该告诉 curses 在您完成使用颜色后撤消颜色组合。要设置颜色,请在调用诸如 mvaddch() 之类的函数之前使用 attron(),然后在之后使用 attroff() 关闭颜色属性。例如,当我绘制玩家角色时,我可能会这样做


attron(COLOR_PAIR(PLAYER_PAIR));
mvaddch(y, x, PLAYER);
attroff(COLOR_PAIR(PLAYER_PAIR));

请注意,将颜色应用于您的程序会对您查询屏幕的方式进行细微更改。通常,mvinch() 返回的值是 chtype 类型。在没有颜色属性的情况下,这基本上是一个整数,可以这样使用。但是,颜色会向屏幕上的字符添加额外的属性,因此 chtype 在扩展的位模式中携带额外的颜色信息。如果您使用 mvinch(),则返回值将包含此额外的颜色值。要仅提取“文本”值,例如在 is_move_okay() 函数中,您需要使用 A_CHARTEXT 位掩码应用按位与 (&)


int is_move_okay(int y, int x)
{
    int testch;

    /* return true if the space is okay to move into */

    testch = mvinch(y, x);
    return (((testch & A_CHARTEXT) == GRASS)
            || ((testch & A_CHARTEXT) == EMPTY));
}

通过这些更改,我可以更新冒险游戏以使用颜色


/* quest.c */

#include <curses.h>
#include <stdlib.h>

#define GRASS     ' '
#define EMPTY     '.'
#define WATER     '~'
#define MOUNTAIN  '^'
#define PLAYER    '*'

#define GRASS_PAIR     1
#define EMPTY_PAIR     1
#define WATER_PAIR     2
#define MOUNTAIN_PAIR  3
#define PLAYER_PAIR    4

int is_move_okay(int y, int x);
void draw_map(void);

int main(void)
{
    int y, x;
    int ch;

    /* initialize curses */

    initscr();
    keypad(stdscr, TRUE);
    cbreak();
    noecho();

    /* initialize colors */

    if (has_colors() == FALSE) {
        endwin();
        printf("Your terminal does not support color\n");
        exit(1);
    }

    start_color();
    init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
    init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
    init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
    init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);

    clear();

    /* initialize the quest map */

    draw_map();

    /* start player at lower-left */

    y = LINES - 1;
    x = 0;

    do {
                            
        /* by default, you get a blinking cursor - use it to
           indicate player * */

        attron(COLOR_PAIR(PLAYER_PAIR));
        mvaddch(y, x, PLAYER);
        attroff(COLOR_PAIR(PLAYER_PAIR));
        move(y, x);
        refresh();

        ch = getch();

        /* test inputted key and determine direction */

        switch (ch) {
        case KEY_UP:
        case 'w':
        case 'W':
            if ((y > 0) && is_move_okay(y - 1, x)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                y = y - 1;
            }
            break;
        case KEY_DOWN:
        case 's':
        case 'S':
            if ((y < LINES - 1) && is_move_okay(y + 1, x)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                y = y + 1;
            }
            break;
        case KEY_LEFT:
        case 'a':
        case 'A':
            if ((x > 0) && is_move_okay(y, x - 1)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                x = x - 1;
            }
            break;
        case KEY_RIGHT:
        case 'd':
        case 'D':
            if ((x < COLS - 1) && is_move_okay(y, x + 1)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                x = x + 1;
            }
            break;
        }
    }
    while ((ch != 'q') && (ch != 'Q'));

    endwin();

    exit(0);
}

int is_move_okay(int y, int x)
{
    int testch;

    /* return true if the space is okay to move into */

    testch = mvinch(y, x);
    return (((testch & A_CHARTEXT) == GRASS)
            || ((testch & A_CHARTEXT) == EMPTY));
}

void draw_map(void)
{
    int y, x;

    /* draw the quest map */

    /* background */

    attron(COLOR_PAIR(GRASS_PAIR));
    for (y = 0; y < LINES; y++) {
        mvhline(y, 0, GRASS, COLS);
    }
    attroff(COLOR_PAIR(GRASS_PAIR));

    /* mountains, and mountain path */

    attron(COLOR_PAIR(MOUNTAIN_PAIR));
    for (x = COLS / 2; x < COLS * 3 / 4; x++) {
        mvvline(0, x, MOUNTAIN, LINES);
    }
    attroff(COLOR_PAIR(MOUNTAIN_PAIR));

    attron(COLOR_PAIR(GRASS_PAIR));
    mvhline(LINES / 4, 0, GRASS, COLS);
    attroff(COLOR_PAIR(GRASS_PAIR));

    /* lake */

    attron(COLOR_PAIR(WATER_PAIR));
    for (y = 1; y < LINES / 2; y++) {
        mvhline(y, 1, WATER, COLS / 3);
    }
    attroff(COLOR_PAIR(WATER_PAIR));
}

除非您有敏锐的眼力,否则您可能无法发现冒险游戏中支持颜色所需的所有更改。diff 工具显示了为了支持颜色而添加函数或更改代码的所有实例


$ diff quest-color/quest.c quest/quest.c
12,17d11
< #define GRASS_PAIR     1
< #define EMPTY_PAIR     1
< #define WATER_PAIR     2
< #define MOUNTAIN_PAIR  3
< #define PLAYER_PAIR    4
<
33,46d26
<     /* initialize colors */
<
<     if (has_colors() == FALSE) {
<    endwin();
<    printf("Your terminal does not support color\n");
<    exit(1);
<     }
<
<     start_color();
<     init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
<     init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
<     init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
<     init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);
<
61d40
<    attron(COLOR_PAIR(PLAYER_PAIR));
63d41
<    attroff(COLOR_PAIR(PLAYER_PAIR));
76d53
<            attron(COLOR_PAIR(EMPTY_PAIR));
78d54
<            attroff(COLOR_PAIR(EMPTY_PAIR));
86d61
<            attron(COLOR_PAIR(EMPTY_PAIR));
88d62
<            attroff(COLOR_PAIR(EMPTY_PAIR));
96d69
<            attron(COLOR_PAIR(EMPTY_PAIR));
98d70
<            attroff(COLOR_PAIR(EMPTY_PAIR));
106d77
<            attron(COLOR_PAIR(EMPTY_PAIR));
108d78
<            attroff(COLOR_PAIR(EMPTY_PAIR));
128,129c98
<     return (((testch & A_CHARTEXT) == GRASS)
<        || ((testch & A_CHARTEXT) == EMPTY));
---
>     return ((testch == GRASS) || (testch == EMPTY));
140d108
<     attron(COLOR_PAIR(GRASS_PAIR));
144d111
<     attroff(COLOR_PAIR(GRASS_PAIR));
148d114
<     attron(COLOR_PAIR(MOUNTAIN_PAIR));
152d117
<     attroff(COLOR_PAIR(MOUNTAIN_PAIR));
154d118
<     attron(COLOR_PAIR(GRASS_PAIR));
156d119
<     attroff(COLOR_PAIR(GRASS_PAIR));
160d122
<     attron(COLOR_PAIR(WATER_PAIR));
164d125
<     attroff(COLOR_PAIR(WATER_PAIR));

让我们玩吧——现在是彩色的了

该程序现在具有更令人愉悦的配色方案,更接近原始的桌面游戏地图,其中有绿色田野、蓝色湖泊和雄伟的灰色山脉。英雄穿着红色和洋红色制服,清晰地脱颖而出。

图 1. 简单的桌面游戏地图,带有湖泊和山脉

图 2. 玩家在左下角开始游戏。

图 3. 玩家可以在游戏区域内移动,例如绕湖泊、穿过山口并进入未知区域。

使用颜色,您可以更清晰地表示信息。这个简单的示例使用颜色来指示可玩区域(绿色)与无法通行的区域(蓝色或灰色)。我希望您将此示例游戏用作您自己程序的起点或参考。根据您的程序需要执行的操作,您可以使用 curses 做更多的事情。

在后续文章中,我计划演示 ncurses 库的其他功能,例如如何创建窗口和框架。与此同时,如果您有兴趣了解更多关于 curses 的信息,我鼓励您阅读 Pradeep Padala 在 Linux 文档项目中的 NCURSES Programming HOWTO

Jim Hall 是一位开源软件倡导者和开发者,可能最出名的是 FreeDOS 的创始人。Jim 还非常积极地参与 GNOME 等开源软件项目的可用性测试。在工作中,Jim 是 Hallmentum 的 CEO,这是一家 IT 执行咨询公司,帮助 CIO 和 IT 领导者进行战略规划和组织发展。

加载 Disqus 评论