如何使用ncurses進(jìn)行顏色編程
Jim 給他的終端冒險游戲添加了顏色,演示了如何用 curses 操縱顏色。
在我的使用 ncurses 庫進(jìn)行編程的系列文章的第一篇和第二篇中,我已經(jīng)介紹了一些 curses 函數(shù)來在屏幕上作畫、從屏幕上查詢和從鍵盤讀取字符。為了搞清楚這些函數(shù),我使用 curses 來利用簡單字符繪制游戲地圖和玩家角色,創(chuàng)建了一個簡單的冒險游戲。在這篇緊接著的文章里,我展示了如何為你的 curses 程序添加顏色。
在屏幕上繪圖一切都挺好的,但是如果只有黑底白字的文本,你的程序可能看起來很無趣。顏色可以幫助傳遞更多的信息。舉個例子,如果你的程序需要報告執(zhí)行成功或者執(zhí)行失敗時。在這樣的情況下你可以使用綠色或者紅色來幫助強調(diào)輸出。或者,你只是簡單地想要“潮藝”一下給你的程序來讓它看起來更美觀。
在這篇文章中,我用一個簡單的例子來展示通過 curses 函數(shù)進(jìn)行顏色操作。在我先前的文章中,我寫了一個可以讓你在一個粗糙繪制的地圖上移動玩家角色的初級冒險類游戲。但是那里面的地圖完全是白色和黑色的文本,通過形狀來表明是水(~)或者山(^)。所以,讓我們將游戲更新到使用顏色的版本吧。
顏色要素
在你可以使用顏色之前,你的程序需要知道它是否可以依靠終端正確地顯示顏色。在現(xiàn)代操作系統(tǒng)上,此處應(yīng)該永遠(yuǎn)為true。但是在經(jīng)典的計算機上,一些終端是單色的,例如古老的 VT52 和 VT100 終端,一般它們提供黑底白色或者黑底綠色的文本。
可以使用 has_colors() 函數(shù)查詢終端的顏色功能。這個函數(shù)將會在終端可以顯示顏色的時候返回 true,否則將會返回 false。這個函數(shù)一般用于 if 塊的開頭,就像這樣:
if (has_colors() == FALSE) {endwin();printf("Your terminal does not support color\n");exit(1);}
在知道終端可以顯示顏色之后,你可以使用 start_color() 函數(shù)來設(shè)置 curses 使用顏色。現(xiàn)在是時候定義程序?qū)⒁褂玫念伾恕?/p>
在 curses 中,你應(yīng)該按對定義顏色:一個前景色放在一個背景色上。這樣允許 curses 一次性設(shè)置兩個顏色屬性,這也是一般你想要使用的方式。通過 init_pair() 函數(shù)可以定義一個前景色和背景色并關(guān)聯(lián)到索引數(shù)字來設(shè)置顏色對。大致語法如下:
init_pair(index, foreground, background);
控制臺支持八種基礎(chǔ)的顏色:黑色、紅色、綠色、黃色、藍(lán)色、品紅色、青色和白色。這些顏色通過下面的名稱為你定義好了:
COLOR_BLACKCOLOR_REDCOLOR_GREENCOLOR_YELLOWCOLOR_BLUECOLOR_MAGENTACOLOR_CYANCOLOR_WHITE
應(yīng)用顏色
在我的冒險游戲中,我想要讓草地呈現(xiàn)綠色而玩家的足跡變成不易察覺的綠底黃色點跡。水應(yīng)該是藍(lán)色,那些表示波浪的 ~ 符號應(yīng)該是近似青色的。我想讓山(^)是灰色的,但是我可以用白底黑色文本做一個可用的折中方案。(LCTT 譯注:意為終端預(yù)設(shè)的顏色沒有灰色,使用白底黑色文本做一個折中方案)為了讓玩家的角色更易見,我想要使用一個刺目的品紅底紅色設(shè)計。我可以像這樣定義這些顏色對:
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 設(shè)置哪種顏色屬性。為了更好的編程實踐,你同樣應(yīng)該在你完成了顏色使用的時候告訴 curses 取消顏色組合。為了設(shè)置顏色,應(yīng)該在調(diào)用像 mvaddch() 這樣的函數(shù)之前使用attron(),然后通過 attroff() 關(guān)閉顏色屬性。例如,在我繪制玩家角色的時候,我應(yīng)該這樣做:
attron(COLOR_PAIR(PLAYER_PAIR));mvaddch(y, x, PLAYER);attroff(COLOR_PAIR(PLAYER_PAIR));
記住將顏色應(yīng)用到你的程序?qū)δ闳绾尾樵兤聊挥幸恍┪⒚畹挠绊憽R话銇碇v,由 mvinch() 函數(shù)返回的值是沒有帶顏色屬性的類型 chtype,這個值基本上是一個整型值,也可以當(dāng)作整型值來用。但是,由于使用顏色添加了額外的屬性到屏幕上的字符上,所以 chtype 按照擴展的位模式攜帶了額外的顏色信息。一旦你使用 mvinch(),返回值將會包含這些額外的顏色值。為了只提取文本值,例如在 is_move_okay() 函數(shù)中,你需要和 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 4int 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();/* 在左下角創(chuàng)建新角色 */y = LINES - 1;x = 0;do {/* 默認(rèn)情況下,你獲得了一個閃爍的光標(biāo)--用來指明玩家 * */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;/* 當(dāng)空白處可以進(jìn)入的時候返回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));}
你可能不能認(rèn)出所有為了在冒險游戲里面支持顏色需要的修改,除非你目光敏銳。diff 工具展示了所有為了支持顏色而添加的函數(shù)或者修改的代碼:
$ diff quest-color/quest.c quest/quest.c12,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));
開始玩吧--現(xiàn)在有顏色了
程序現(xiàn)在有了更舒服的顏色設(shè)計了,更匹配原來的桌游地圖,有綠色的地、藍(lán)色的湖和壯觀的灰色山峰。英雄穿著紅色的制服十分奪目。

圖 1. 一個簡單的帶湖和山的桌游地圖

圖 2. 玩家站在左下角

圖 3. 玩家可以在游戲區(qū)域移動,比如圍繞湖,通過山的通道到達(dá)未知的區(qū)域。
通過顏色,你可以更清楚地展示信息。這個例子使用顏色指出可游戲的區(qū)域(綠色)相對著不可通過的區(qū)域(藍(lán)色或者灰色)。我希望你可以使用這個示例游戲作為你自己的程序的一個起點或者參照。這取決于你需要你的程序做什么,你可以通過 curses 做得更多。
在下一篇文章,我計劃展示 ncurses 庫的其它特性,比如怎樣創(chuàng)建窗口和邊框。同時,如果你對于學(xué)習(xí) curses 有興趣,我建議你去讀位于 Linux 文檔計劃 的 Pradeep Padala 寫的 NCURSES Programming HOWTO。
























