使用 ncurses 进行彩色编程
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。