使用 ncurses 对文本窗口进行编程
如何使用 ncurses 来操作您的终端屏幕。
在我关于使用 ncurses 库为文本控制台编程的文章系列中,我向您展示了如何在屏幕上绘制文本并使用基本的文本属性。我的 Sierpinski 三角形的例子(参见“ncurses 入门”)和一个简单的 Quest 冒险游戏(参见“使用 ncurses 在终端中创建冒险游戏”)一次使用了整个屏幕。
但是,如果将屏幕分成几部分更有意义呢? 例如,冒险游戏可能会划分屏幕,一部分用于游戏地图,另一部分用于玩家的状态。许多程序将屏幕组织成多个部分——例如,Emacs 编辑器使用编辑窗格、状态栏和命令栏。您可能需要以类似的方式划分程序的显示区域。有一种简单的方法可以做到这一点,那就是使用 ncurses 中的窗口函数。这是任何 curses 兼容库的标准部分。
简易赛尼特您可能会将“窗口”与图形环境联系起来,但这里的情况并非如此。在 ncurses 中,“窗口”是将屏幕划分为逻辑区域的一种手段。一旦您定义了一个窗口,您就不需要跟踪它在屏幕上的位置;您只需使用一组 ncurses 函数绘制到您的窗口即可。
为了演示,让我用一种意想不到的方式定义一个游戏板。 古埃及游戏赛尼特使用一个由 30 个方格组成的棋盘,排列成三行十列。两位玩家以倒“S”形移动他们的棋子绕棋盘移动,因此棋盘看起来像这样
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
如果没有窗口函数,您将不得不跟踪每个棋子的行和列,并分别绘制它们。由于棋盘以倒“S”形图案排列,因此每次更新棋盘上的每个方格时,您总是需要进行奇怪的数学运算来正确地定位行和列。但是使用窗口函数,ncurses 允许您一次定义方格,包括它们的位置,然后在以后通过逻辑标识符引用这些窗口。
ncurses 函数 newwin()
允许您在屏幕上的特定位置定义具有一定尺寸的文本窗口
WINDOW *newwin(int nlines, int ncols, int begin_y,
↪int begin_x);
newwin()
函数返回类型为 WINDOW*
的指针,您可以将其存储在数组中以供以后引用。要创建一个赛尼特棋盘,您可以使用全局数组 BOARD[30]
,并编写一个函数来使用窗口定义赛尼特棋盘的 30 个方格
#define SQ_HEIGHT 5
#define SQ_WIDTH 8
WINDOW *BOARD[30];
void create_board(void)
{
int i;
int starty, startx;
starty = 0;
for (i = 0; i < 10; i++) {
startx = i * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
starty = SQ_HEIGHT;
for (i = 10; i < 20; i++) {
startx = (19 - i) * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
starty = 2 * SQ_HEIGHT;
for (i = 20; i < 30; i++) {
startx = (i - 20) * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
/* put border on each window and refresh */
for (i = 0; i < 30; i++) {
box(BOARD[sq], '2', '2');
wrefresh(BOARD[sq]);
}
}
此函数的第一个部分使用 newwin()
来定义 30 个方格。我将其分为三个部分,以使其明显可见,第二行实际上是倒着计数的。
ncurses 中的文本窗口不会创建“框架”来在屏幕上显示窗口。如果您想绘制框架,可以使用两个函数之一来完成。在这里,在定义窗口之后,该函数然后调用 ncurses 函数 box()
在屏幕上绘制正方形。通常,box()
函数接受用于垂直和水平边框的字符的参数;如果您将零作为任一或两个参数传递,ncurses 将使用默认的线条绘制字符。
请注意,您需要使用窗口特定的 wrefresh()
函数来刷新窗口,而不是通常的 refresh()
函数来更新屏幕。
在赛尼特游戏中,几个方格具有一定的含义。根据常见的赛尼特规则,玩家必须停在第 26 格(窗口数组元素 BOARD[25]
)才能移出棋盘。第 27 格(数组元素 BOARD[26]
)是一个陷阱,会将玩家送回第 15 格(BOARD[14]
)。第 28 格和第 29 格(分别为 BOARD[27] 和 BOARD[28])要求玩家掷出正好“3”或“2”才能将其棋子移出棋盘。
为了表示这些特殊方格,我将 draw_board()
末尾的 box()
和 wrefresh()
函数替换为对我自己的函数 draw_square()
的调用。此函数为特殊方格绘制不同的边框
/* put border on each window and refresh */
for (i = 0; i < 30; i++) {
draw_square(i);
}
}
void draw_square(int sq)
{
switch (sq) {
case 14: /* revive square */
wborder(BOARD[sq], '#', '#', '#', '#', '#', '#',
↪'#', '#');
break;
case 25: /* stop square */
box(BOARD[sq], 'X', 'x');
break;
case 26: /* water square */
box(BOARD[sq], 'O', 'o');
break;
case 27: /* 3-move square */
box(BOARD[sq], '3', '3');
break;
case 28: /* 2-move square */
box(BOARD[sq], '2', '2');
break;
default:
box(BOARD[sq], 0, 0);
}
wrefresh(BOARD[sq]);
}
draw_square()
函数显示了在文本窗口上绘制框架的两种方法。我之前介绍了 box()
函数。另一种方法是使用 wborder()
函数,该函数为窗口的左、右、上和下边缘以及左上角、右上角、左下角和右下角采用单独的参数。与 box()
一样,如果您将零作为任何或所有参数传递,ncurses 将使用默认的线条绘制字符。
查看示例输出,了解使用不同参数调用 box()
和 wborder()
之间的区别。我在我的赛尼特程序中有意使用了不同的方法来展示一些组合。例如,“停止”方格的顶部和底部边框使用小写字母“x”,左侧和右侧边框使用大写字母“X”。“复活”方格使用 wborder()
填充角,而其他方格则使用 box()
更简单地绘制。请注意,使用字符参数调用 box()
仍然会绘制角部的线条图形字符。

图 1. 突出显示第一个方格的赛尼特棋盘

图 2. 突出显示第 29 个方格的赛尼特棋盘
由于这使用了窗口函数,draw_square()
函数不需要知道每个方格的位置。所有这些都由 ncurses 作为构成棋盘上 30 个方格的 30 个窗口的属性来跟踪。一旦程序定义了窗口(包括它们的位置),绘制到每个方格就变成了简单地调用关联的“w”函数,引用要绘制到的窗口。
例如,要在窗口中绘制字符,您可以使用 waddch()
函数,或者如果您想在窗口中的特定位置绘制,则可以使用 mvwaddch()
函数。大多数 curses 函数都有一个在特定窗口上操作的“w”伙伴函数,例如这些
int move(int y, int x);
int wmove(WINDOW *win, int y, int x);
int addch(const chtype ch);
int waddch(WINDOW *win, const chtype ch);
int mvaddch(int y, int x, const chtype ch);
int mvwaddch(WINDOW *win, int y, int x, const chtype ch);
完整程序
现在您已经了解了如何使用窗口创建 30 个独立的绘图区域,让我们来看一个简单的程序,该程序绘制一个赛尼特棋盘并允许用户使用加号和减号键浏览方格。在较高的层次上,该程序遵循以下步骤
- 初始化 curses 环境。
- 定义并在赛尼特棋盘上绘制 30 个方格。
- 循环:1) 从键盘获取一个键;2) 相应地将玩家的位置调整到上一个或下一个方格;以及 3) 重复。
- 完成后,关闭 curses 环境并退出。
这是程序
/* senet.c */
#include <stdlib.h>
#include <ncurses.h>
#define SQ_HEIGHT 5
#define SQ_WIDTH 8
void create_board(void);
void destroy_board(void);
void draw_square(int sq);
void highlight_square(int sq);
WINDOW *BOARD[30];
int main(int argc, char **argv)
{
int key;
int sq;
/* initialize curses */
initscr();
noecho();
cbreak();
if ((LINES < 24) || (COLS < 80)) {
endwin();
puts("Your terminal needs to be at least 80x24");
exit(2);
}
/* print welcome text */
clear();
mvprintw(LINES - 1, (COLS - 5) / 2, "Senet");
refresh();
/* draw board */
create_board();
/* loop: '+' to increment squares, '-'
to decrement squares */
sq = 0;
highlight_square(sq);
do {
key = getch();
switch (key) {
case '+':
case '=':
if (sq < 29) {
draw_square(sq);
highlight_square(++sq);
}
break;
case '-':
case '_':
if (sq > 0) {
draw_square(sq);
highlight_square(--sq);
}
}
} while ((key != 'q') && (key != 'Q'));
/* when done, free up the board, and exit */
destroy_board();
endwin();
exit(0);
}
void create_board(void)
{
int i;
int starty, startx;
starty = 0;
for (i = 0; i < 10; i++) {
startx = i * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
starty = SQ_HEIGHT;
for (i = 10; i < 20; i++) {
startx = (19 - i) * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
starty = 2 * SQ_HEIGHT;
for (i = 20; i < 30; i++) {
startx = (i - 20) * SQ_WIDTH;
BOARD[i] = newwin(SQ_HEIGHT, SQ_WIDTH, starty,
↪startx);
}
/* put border on each window and refresh */
for (i = 0; i < 30; i++) {
draw_square(i);
}
}
void destroy_board(void)
{
int i;
/* erase every box and delete each window */
for (i = 0; i < 30; i++) {
wborder(BOARD[i], ' ', ' ', ' ', ' ', ' ', ' ', '
↪', ' ');
wrefresh(BOARD[i]);
delwin(BOARD[i]);
}
}
void draw_square(int sq)
{
switch (sq) {
case 14: /* revive square */
wborder(BOARD[sq], '#', '#', '#', '#', '#', '#',
↪'#', '#');
break;
case 25: /* stop square */
box(BOARD[sq], 'X', 'x');
break;
case 26: /* water square */
box(BOARD[sq], 'O', 'o');
break;
case 27: /* 3-move square */
box(BOARD[sq], '3', '3');
break;
case 28: /* 2-move square */
box(BOARD[sq], '2', '2');
break;
default:
box(BOARD[sq], 0, 0);
}
wrefresh(BOARD[sq]);
}
void highlight_square(int sq)
{
wattron(BOARD[sq], A_BOLD);
draw_square(sq);
wattroff(BOARD[sq], A_BOLD);
}
这只是赛尼特游戏的基本框架。它所做的只是生成一个游戏板,并允许用户浏览所有方格。为了使重点放在 ncurses 中的窗口函数上,我省略了所有的游戏玩法和规则。
该程序仅使用几个函数
-
void create_board(void);
— 将 30 个方格定义为文本窗口并在屏幕上绘制它们。 -
void destroy_board(void);
— 擦除 30 个方格并删除窗口。 -
void draw_square(int sq);
— 在棋盘上绘制单个方格。此函数根据赛尼特中使用的特殊方格绘制不同的轮廓。 -
void highlight_square(int sq);
— 在用户浏览棋盘上的每个方格时,突出显示一个方格。为了简单起见,这里使用了A_BOLD
属性而不是颜色。
此程序是如何使用 nurses 窗口函数在屏幕上定义单独区域的简单示例。示例程序是一个游戏,但您可以将其用作您自己的程序的起点。任何需要更新屏幕多个区域的程序都可以使用窗口函数。
ncurses 库提供了一组丰富的功能来更新和访问文本模式下的屏幕。虽然图形用户界面非常酷,但并非每个程序都需要使用点击式界面运行。如果您的程序在纯文本终端中运行,请考虑使用 ncurses 来操作终端屏幕。
资源- 如果您有兴趣了解更多关于 curses 的信息,ncurses 手册页提供了关于不同函数的广泛文档。
- 有关更多信息,包括编程示例,请阅读 Pradeep Padala 在 Linux 文档项目中的“NCURSES 编程 HOWTO”。
- Jim Hall 的“使用 ncurses 在终端中创建冒险游戏”
- Jim Hall 的“ncurses 入门”
- Jim Hall 的“使用 ncurses 进行彩色编程”
- Jim Hall 的“关于 ncurses 颜色”
- 赛尼特 (维基百科)