手记

使用 ncurses 进行颜色编程

Jim 给他的终端冒险游戏添加了颜色演示了如何用 curses 操纵颜色。


在我的使用 ncurses 库进行编程的系列文章的第一篇和第二篇中我已经介绍了一些 curses 函数来在屏幕上作画、从屏幕上查询和从键盘读取字符。为了搞清楚这些函数我使用 curses 来利用简单字符绘制游戏地图和玩家角色创建了一个简单的冒险游戏。在这篇紧接着的文章里我展示了如何为你的 curses 程序添加颜色。


在屏幕上绘图一切都挺好的但是如果只有黑底白字的文本你的程序可能看起来很无趣。颜色可以帮助传递更多的信息。举个例子如果你的程序需要报告执行成功或者执行失败时。在这样的情况下你可以使用绿色或者红色来帮助强调输出。或者你只是简单地想要“潮艺”一下给你的程序来让它看起来更美观。


在这篇文章中我用一个简单的例子来展示通过 curses 函数进行颜色操作。在我先前的文章中我写了一个可以让你在一个粗糙绘制的地图上移动玩家角色的初级冒险类游戏。但是那里面的地图完全是白色和黑色的文本通过形状来表明是水或者山^。所以让我们将游戏更新到使用颜色的版本吧。


颜色要素

在你可以使用颜色之前你的程序需要知道它是否可以依靠终端正确地显示颜色。在现代操作系统上此处应该永远为true。但是在经典的计算机上一些终端是单色的例如古老的 VT52 和 VT100 终端一般它们提供黑底白色或者黑底绿色的文本。


可以使用 has_colors() 函数查询终端的颜色功能。这个函数将会在终端可以显示颜色的时候返回 true否则将会返回 false。这个函数一般用于 if 块的开头就像这样


if (has_colors() == FALSE) {

    endwin();

    printf("Your terminal does not support color\n");

    exit(1);

}

在知道终端可以显示颜色之后你可以使用 start_color() 函数来设置 curses 使用颜色。现在是时候定义程序将要使用的颜色了。


在 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

应用颜色

在我的冒险游戏中我想要让草地呈现绿色而玩家的足迹变成不易察觉的绿底黄色点迹。水应该是蓝色那些表示波浪的 ~ 符号应该是近似青色的。我想让山^是灰色的但是我可以用白底黑色文本做一个可用的折中方案。LCTT 译注意为终端预设的颜色没有灰色使用白底黑色文本做一个折中方案为了让玩家的角色更易见我想要使用一个刺目的品红底红色设计。我可以像这样定义这些颜色对


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;

    /* 初始化curses */

    initscr();

    keypad(stdscr, TRUE);

    cbreak();

    noecho();

    /* 初始化颜色 */

    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();

    /* 初始化探索地图 */

    draw_map();

    /* 在左下角创建新角色 */

    y = LINES - 1;

    x = 0;

    do {

        /* 默认情况下你获得了一个闪烁的光标--用来指明玩家 * */

        attron(COLOR_PAIR(PLAYER_PAIR));

        mvaddch(y, x, PLAYER);

        attroff(COLOR_PAIR(PLAYER_PAIR));

        move(y, x);

        refresh();

        ch = getch();

        /* 测试输入键值并获取方向 */

        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;

    /* 当空白处可以进入的时候返回true */

    testch = mvinch(y, x);

    return (((testch & A_CHARTEXT) == GRASS)

            || ((testch & A_CHARTEXT) == EMPTY));

}

void draw_map(void)

{

    int y, x;

    /* 绘制探索地图 */

    /* 背景 */

    attron(COLOR_PAIR(GRASS_PAIR));

    for (y = 0; y < LINES; y++) {

        mvhline(y, 0, GRASS, COLS);

    }

    attroff(COLOR_PAIR(GRASS_PAIR));

    /* 山峰和山路 */

    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));

    /* 湖 */

    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 做得更多。

编译自http://www.linuxjournal.com/content/programming-color-ncurses作者 Jim Hall
原创LCTT https://linux.cn/article-9546-1.html译者 leemeans

0人推荐
随时随地看视频
慕课网APP