使用 NCURSES 编写程序

使用 NCURSES 编写程序

作者:Eric S. Raymond 和 Zeyd M. Ben-Halim,1.9.9e 版本之后的更新由 Thomas Dickey 完成

目录

简介

本文档是使用 curses 编程的入门指南。 它不是 curses 应用程序编程接口 (API) 的详尽参考; 该角色由 curses 手册页承担。 而是旨在帮助 C 程序员轻松使用该软件包。 本文档面向尚未特别熟悉 ncurses 的 C 应用程序程序员。 如果您已经是经验丰富的 curses 程序员,您仍然应该阅读有关 鼠标接口调试与旧版本的兼容性提示、技巧和诀窍 的部分。 这些将使您了解 ncurses 实现的特殊功能和怪癖。 如果您没有那么多经验,请继续阅读。 curses 软件包是一个用于与终端无关的屏幕绘制和输入事件处理的子例程库,它向程序员呈现了一个高级屏幕模型,隐藏了终端类型之间的差异,并自动优化输出以将一个屏幕的文本更改为另一个屏幕。 Curses 使用 terminfo,这是一种数据库格式,可以描述数千种不同终端的功能。 在 UNIX 桌面越来越受 X、Motif 和 Tcl/Tk 主导的情况下,curses API 可能显得有些过时。 然而,UNIX 仍然支持 tty 行,X 支持 xterm(1)curses API 具有 (a) 向后移植到字符单元终端,以及 (b) 简单性的优点。 对于不需要位图图形和多种字体的应用程序,使用 curses 的接口实现通常比使用 X 工具包的实现简单得多且成本更低。

Curses 简史

从历史上看,curses 的第一个祖先是为 vi 编辑器提供屏幕处理而编写的例程; 这些使用了 termcap 数据库工具(均在 3BSD 中发布)来描述终端功能。 这些例程被抽象成一个有文档记录的库,并首次与早期的 BSD UNIX 版本一起发布。 所有这些工作都是由加州大学(伯克利分校)的学生完成的。 curses 库首次发布于 4.0BSD,比 3BSD 晚一年(即 1980 年末)。 毕业后,其中一位学生进入 AT&T Bell Labs 工作,并制作了一个改进的 termcap 库,称为 terminfo(即“libterm”),并使 curses 库适应使用它。 随后在 System V Release 2(1984 年初)中发布。 此后,其他开发人员添加了 curses 和 terminfo 库。 例如,康奈尔大学的一名学生编写了一个改进的 terminfo 库以及一个编译终端描述的工具 (tic)。 一般来说,AT&T 没有在源代码或文档中识别开发人员; ticinfocmp 程序是例外。 来自 Bell Labs 的 System V Release 3(System III UNIX)包含一个重写且大大改进的 curses 库以及 tic 程序(1986 年末)。 总而言之,terminfo 基于 Berkeley 的 termcap 数据库,但包含许多改进和扩展。 引入了参数化 Capability 字符串,使得可以描述多个视频属性、颜色以及处理比 termcap 可能处理的更为不寻常的终端。 在后来的 AT&T System V 版本中,curses 不断发展以使用更多功能并提供更多 Capability,在功能和灵活性方面远远超出 BSD curses。

本文档的范围

本文档描述了 ncurses,它是 System V curses API 的免费实现,具有一些明确标记的扩展。 它包括以下 System V curses 功能:

此外,此软件包利用了配备终端的插入和删除行和字符功能,并确定如何最佳地使用这些功能,而无需程序员的帮助。 即使在屏幕上留下“魔法 Cookie”以标记属性更改的终端上,它也允许显示视频属性的任意组合。 ncurses 软件包还可以捕获和使用来自某些环境中的鼠标的事件报告(特别是,X 窗口系统下的 xterm)。 本文档包括使用鼠标的技巧。 ncurses 软件包由 Pavel Curtis 发起。 该软件包的原始维护者是 Zeyd Ben-Halim zmbenhal@netcom.com。 Eric S. Raymond esr@snark.thyrsus.com 在 1.8.1 之后的版本中编写了许多新功能,并编写了大部分介绍。 Jürgen Pfeifer 编写了所有菜单和 Forms 代码以及 Ada95 绑定。 正在进行的工作由 Thomas Dickey(维护者)完成。 请通过 bug-ncurses@gnu.org 联系当前的维护者。 本文档还描述了 panels 扩展库,其模型与 SVr4 panels 功能类似。 此库允许您将后备存储与堆栈或重叠窗口中的每个窗口相关联,并提供用于在堆栈中移动窗口的操作,这些操作以自然的方式更改其可见性(处理窗口重叠)。 最后,本文档详细描述了 menusforms 扩展库,它们也从 System V 克隆而来,支持轻松构建和菜单和填写 Forms 的序列。

术语

在本文档中,以下术语以合理的连贯性使用: window 一种数据结构,描述了屏幕的子矩形(可能是整个屏幕)。 您可以像将其视为微型屏幕一样写入窗口,该窗口可以独立于物理屏幕上的其他窗口滚动。 screens 作为窗口子集,其大小与终端屏幕一样大,即它们从左上角开始并包含右下角。 其中一个 stdscr 是自动为程序员提供的。 terminal screen 软件包对终端显示器当前外观的想法,即用户现在看到的内容。 这是一个特殊的屏幕。

Curses 库

Curses 概述

使用 Curses 编译程序

为了使用该库,必须定义某些类型和变量。 因此,程序员必须有一行:

     #include <curses.h>

在程序源代码的顶部。 屏幕软件包使用标准 I/O 库,因此 <curses.h> 包括 <stdio.h><curses.h> 还包括 <termios.h><termio.h><sgtty.h>,具体取决于您的系统。 程序员也进行这些包含是多余的(但无害的)。 在与 curses 链接时,您需要在 LDFLAGS 中或命令行上使用 -lncurses。 不需要任何其他库。

更新屏幕

为了最佳地更新屏幕,例程必须知道屏幕当前的外观以及程序员希望它的下一个外观。 为此,定义了一个名为 WINDOW 的数据类型(结构),该类型描述了窗口的图像给例程,包括其在屏幕上的起始位置(左上角的 (y, x) 坐标)及其大小。 其中一个(称为 curscr,表示当前屏幕)是终端当前外观的屏幕图像。 默认情况下,提供了另一个屏幕(称为 stdscr,表示标准屏幕)以进行更改。 窗口是纯粹的内部表示。 它用于构建和存储终端一部分的潜在图像。 它与终端屏幕上真正的内容没有任何必要的关联; 它更像是一个草稿纸或写缓冲区。 为了使与窗口相对应的物理屏幕部分反映窗口结构的内容,调用例程 refresh()(如果窗口不是 stdscr,则调用 wrefresh())。 给定的物理屏幕部分可能在任意数量的重叠窗口的范围内。 此外,可以以任何顺序更改窗口,而无需考虑运动效率。 然后,程序员可以随意有效地说“使其看起来像这样”,并让包实现确定重新绘制屏幕的最有效方法。

标准窗口和函数命名约定

如上所述,例程可以使用多个窗口,但是会自动给出两个:curscr,它知道终端的样子,而 stdscr,这是程序员希望终端的下一个样子。 用户实际上不应该直接访问 curscr。 应该通过 API 进行更改,然后调用例程 refresh()(或 wrefresh())。 定义了许多函数来使用 stdscr 作为默认屏幕。 例如,要向 stdscr 添加字符,可以调用 addch() 并将所需的字符作为参数。 要写入其他窗口。 提供了例程 waddch()(对于 w indow-specific addch())。 这种在函数名称前加上“w”的约定在将其应用于特定窗口时是一致的。 唯一不遵循它的例程是那些必须始终指定窗口的例程。 为了将当前的 (y, x) 坐标从一个点移动到另一个点,提供了例程 move()wmove()。 但是,通常希望先移动然后再执行某些 I/O 操作。 为了避免笨拙,大多数 I/O 例程可以在前面加上前缀“mv”,并将所需的 (y, x) 坐标添加到函数的参数中。 例如,调用

     move(y, x);
     addch(ch);

可以替换为

     mvaddch(y, x, ch);

     wmove(win, y, x);
     waddch(win, ch);

可以替换为

     mvwaddch(win, y, x, ch);

请注意,窗口描述指针 (win) 在添加的 (y, x) 坐标之前。 如果函数需要窗口指针,则它始终是传递的第一个参数。

变量

curses 库设置了一些描述终端功能的变量。

   type  name   description
   ------------------------------------------------------------------
   int  LINES   终端上的行数
   int  COLS   终端上的列数

curses.h 还引入了一些 #define 常量和具有通用性的类型: bool 布尔类型,实际上是“char”(例如,bool doneit;) TRUE 布尔“true”标志 (1)。 FALSE 布尔“false”标志 (0)。 ERR 例程失败时返回的错误标志 (-1)。 OK 事情进展顺利时例程返回的错误标志。

使用库

现在我们描述如何实际使用屏幕软件包。 在其中,我们假设所有更新、读取等都应用于 stdscr。 这些说明适用于任何窗口,前提是您如上所述更改函数名称和参数。 这是一个示例程序来激发讨论:

#include <stdlib.h>
#include <curses.h>
#include <signal.h>
static void finish(int sig);
int
main(int argc, char *argv[])
{
  int num = 0;
  /* initialize your non-curses data structures here */
  (void) signal(SIGINT, finish);   /* arrange interrupts to terminate */
  (void) initscr();   /* initialize the curses library */
  keypad(stdscr, TRUE); /* enable keyboard mapping */
  (void) nonl();     /* tell curses not to do NL->CR/NL on output */
  (void) cbreak();    /* take input chars one at a time, no wait for \n */
  (void) echo();     /* echo input - in color */
  if (has_colors())
  {
    start_color();
    /*
     * Simple color assignment, often all we need. Color pair 0 cannot
     * be redefined. This example uses the same value for the color
     * pair as for the foreground color, though of course that is not
     * necessary:
     */
    init_pair(1, COLOR_RED,   COLOR_BLACK);
    init_pair(2, COLOR_GREEN,  COLOR_BLACK);
    init_pair(3, COLOR_YELLOW, COLOR_BLACK);
    init_pair(4, COLOR_BLUE,  COLOR_BLACK);
    init_pair(5, COLOR_CYAN,  COLOR_BLACK);
    init_pair(6, COLOR_MAGENTA, COLOR_BLACK);
    init_pair(7, COLOR_WHITE,  COLOR_BLACK);
  }
  for (;;)
  {
    int c = getch();   /* refresh, accept single keystroke of input */
    attrset(COLOR_PAIR(num % 8));
    num++;
    /* process the command keystroke */
  }
  finish(0);        /* we are done */
}
static void finish(int sig)
{
  endwin();
  /* do your non-curses wrapup here */
  exit(0);
}

启动

为了使用屏幕软件包,例程必须了解终端特性,并且必须分配 curscrstdscr 的空间。 这些函数 initscr() 可以同时执行这两项操作。 由于它必须为窗口分配空间,因此在尝试这样做时会溢出内存。 在极少数情况下发生这种情况时,initscr() 将终止程序并显示错误消息。 必须始终在使用任何影响窗口的例程之前调用 initscr()。 如果不是,程序将在引用 curscrstdscr 时立即核心转储。 但是,通常最好等到确定需要它之后再调用它,例如在检查启动错误之后。 像 nl()cbreak() 这样的终端状态更改例程应在 initscr() 之后调用。 分配屏幕窗口后,您可以为程序设置它们。 如果您想允许屏幕滚动,请使用 scrollok()。 如果希望在上次更改后将光标留在原处,请使用 leaveok()。 如果不这样做,refresh() 将在更新后将光标移动到窗口的当前 (y, x) 坐标。 您可以使用函数 newwin()derwin()subwin() 创建自己的新窗口。 例程 delwin() 将允许您摆脱旧窗口。 上述所有选项都可以应用于任何窗口。

输出

现在我们已经设置好了,我们将要实际更新终端。 用于更改窗口上内容的基函数是 addch()move()addch() 在当前的 (y, x) 坐标处添加一个字符。 move() 将当前的 (y, x) 坐标更改为您想要的任何坐标。 如果您尝试移出窗口,它将返回 ERR。 如上所述,您可以将两者组合成 mvaddch() 以一次完成两件事。 其他输出函数(例如 addstr()printw())都调用 addch() 以向窗口添加字符。 在您将想要的内容放入窗口后,当您希望窗口覆盖的终端部分看起来像它时,您必须调用 refresh()。 为了优化查找更改,refresh() 假设自上次 refresh() 该窗口以来未更改的窗口的任何部分都未在终端上更改,即您尚未使用重叠窗口刷新终端的一部分。 如果不是这种情况,则提供例程 touchwin() 以使其看起来好像整个窗口都已更改,从而使 refresh() 检查终端的整个子部分以进行更改。 如果使用 curscr 作为参数调用 wrefresh(),它将使屏幕看起来像 curscr 认为的样子。 这对于实现一个命令很有用,该命令会在屏幕混乱的情况下重新绘制屏幕。

输入

addch() 互补的函数是 getch(),如果设置了 echo,它将调用 addch() 以回显字符。 由于屏幕软件包需要始终知道终端上的内容,如果要回显字符,则 tty 必须处于 raw 或 cbreak 模式。 由于最初终端已启用回显并且处于普通的“cooked”模式,因此必须在调用 getch() 之前更改其中一个; 否则,程序的输出将是不可预测的。 当您需要在窗口中接受面向行的输入时,可以使用函数 wgetstr() 及其变体。 甚至还有一个 wscanw() 函数,可以在窗口输入上执行 scanf()(3) 样式的多字段解析。 这些伪面向行的函数在执行时会启用回显。 上面的示例代码使用调用 keypad(stdscr, TRUE) 来启用对功能键映射的支持。 借助此功能,getch() 代码会监视输入流中与箭头键和功能键相对应的字符序列。 这些序列作为伪字符值返回。 返回的 #define 值在 curses.h 中列出。 从序列到 #define 值的映射由终端的 terminfo 条目中的 key_ 功能确定。

使用 Forms 字符

addch() 函数(以及其他一些函数,包括 box()border())可以接受一些由 ncurses 特别定义的伪字符参数。 这些是在 curses.h 标头中设置的 #define 值; 有关完整列表,请参见此处(查找前缀 ACS_)。 ACS 定义中最有用的是 Forms 绘制字符。 您可以使用这些在屏幕上绘制框和简单的图形。 如果终端没有这样的字符,curses.h 会将它们映射到一组可识别的(但丑陋的)ASCII 默认值。

字符属性和颜色

ncurses 软件包支持屏幕高亮,包括突出显示、反向视频、下划线和闪烁。 它还支持颜色,颜色被视为另一种高亮显示。 在内部,高亮显示被编码为伪字符类型 (chtype) 的高位,curses.h 使用该类型来表示屏幕单元格的内容。 有关高亮显示掩码值的完整列表,请参见 curses.h 标头文件(查找前缀 A_)。 有两种制作高亮显示的方法。 一种是将您想要的高亮显示的值进行逻辑或运算到 addch() 调用的字符参数中,或任何其他采用 chtype 参数的输出调用中。 另一种是设置当前高亮显示值。 这将与您以第一种方式指定的任何高亮显示进行 逻辑或 运算。 您可以使用函数 attron()attroff()attrset() 来执行此操作; 有关详细信息,请参见手册页。 颜色是一种特殊的高亮显示。 该软件包实际上考虑的是颜色对,即前景色和背景色的组合。 上面的示例代码设置了八个颜色对,所有保证可用的颜色都在黑色上。 请注意,实际上为每个颜色对都指定了其前景色名称。 任何其他范围的八个非冲突值都可以用作 init_pair() 值的第一个参数。 完成创建颜色对 N 的 init_pair() 后,您可以将 COLOR_PAIR(N) 用作调用该特定颜色组合的高亮显示。 请注意,对于常量 N,COLOR_PAIR(N) 本身就是一个编译时常量,可以在初始化程序中使用。

鼠标接口

ncurses 库还提供了一个鼠标接口。

**注意:**此功能特定于 ncurses,它既不是 XSI Curses 标准的一部分,也不是 System V Release 4 或 BSD curses 的一部分。 System V Release 4 curses 包含具有类似接口定义的代码,但没有文档记录。 除了反汇编库之外,我们没有办法确定该鼠标代码的确切工作方式。 因此,我们建议您使用功能宏 NCURSES_MOUSE_VERSION 将鼠标相关代码包装在 #ifdef 中,以便它不会在非 ncurses 系统上编译和链接。 目前,鼠标事件报告在以下环境中有效:

鼠标界面非常简单。 要激活它,您可以使用函数 mousemask(),将一个位掩码作为第一个参数传递给它,该位掩码指定您希望程序能够看到哪些类型的事件。 它将返回实际变为可见的事件的位掩码,如果鼠标设备无法报告您指定的某些事件类型,则该位掩码可能与参数不同。 激活鼠标后,您的应用程序的命令循环应监视来自 wgetch()KEY_MOUSE 返回值。 当您看到此返回值时,表示已将鼠标事件报告排队。 要从队列中取出它,请使用函数 getmouse()(您必须在下一个 wgetch() 之前执行此操作,否则可能会有另一个鼠标事件进入并使第一个事件无法访问)。 每次调用 getmouse() 都会使用鼠标事件数据填充一个结构(您将传递给它的地址)。 事件数据包括鼠标指针的零原点、屏幕相关的字符单元坐标。 它还包括一个事件掩码。 将设置此掩码中的位,对应于正在报告的事件类型。 鼠标结构包含两个附加字段,当 ncurses 连接到新型指针设备时,这些字段将来可能会变得重要。 除了 x 和 y 坐标之外,还有一个 z 坐标的插槽; 这对于可以返回压力或持续时间参数的触摸屏可能很有用。 还有一个设备 ID 字段,可用于区分多个指针设备。 可见事件的类别可以随时通过 mousemask() 更改。 可以报告的事件包括按下、释放、单击、双击和三击(您可以设置单击的最大按下时间)。 如果您不使单击可见,它们将被报告为按下-释放对。 在某些环境中,事件掩码可能包括在事件期间报告键盘上 shift、alt 和 ctrl 键状态的位。 还提供了一个函数来检查鼠标事件是否落在给定的窗口内。 您可以使用它来查看给定的窗口是否应认为鼠标事件与其相关。 由于并非在所有环境中都可以使用鼠标事件报告,因此构建 需要 使用鼠标的 ncurses 应用程序是不明智的。 相反,您应该使用鼠标作为应用程序通常会从键盘接受的点击命令的快捷方式。 ncurses 发行版中的两个测试游戏(bsknight) 包含说明如何执行此操作的代码。 有关鼠标接口函数的完整详细信息,请参见手册页 curs_mouse(3X)

完成

为了在 ncurses 例程之后进行清理,提供了例程 endwin()。 它将 tty 模式恢复为首次调用 initscr() 时的状态,并将光标向下移动到左下角。 因此,在调用 initscr 之后的任何时候,都应在退出之前调用 endwin()

函数描述

我们在此处描述一些重要 curses 函数的详细行为,作为手册页描述的补充。

初始化和清理

initscr() 调用的第一个函数几乎总是应该是 initscr()。 这将确定终端类型并初始化 curses 数据结构。 initscr() 还安排第一次调用 refresh() 会清除屏幕。 如果发生错误,则将消息写入标准错误,并且程序退出。 否则,它将返回指向 stdscr 的指针。 可以在 initscr 之前调用一些函数(slk_init()filter()ripoffline()use_env(),以及如果您使用多个终端,则调用 newterm()。) endwin() 您的程序应始终在退出或从程序中跳出之前调用 endwin()。 此函数将恢复 tty 模式,将光标移动到屏幕的左下角,并将终端重置为正确的非可视模式。 在临时退出程序后调用 refresh()doupdate() 会从退出之前恢复 ncurses 屏幕。 newterm(type, ofp, ifp) 输出到多个终端的程序应使用 newterm() 而不是 initscr()。 应该为每个终端调用一次 newterm()。 它返回一个 SCREEN * 类型的变量,该变量应保存为对该终端的引用。 (注意:SCREEN 变量不是本简介中描述的 屏幕,而是用于帮助优化显示的一组参数。)这些参数是终端的类型(一个字符串)和终端的输出和输入的 FILE 指针。 如果类型为 NULL,则使用环境变量 $TERM。 应该在使用此函数打开的每个终端的清理时调用一次 endwin()set_term(new) 此函数用于切换到先前由 newterm() 打开的不同终端。 新终端的屏幕引用作为参数传递。 前一个终端由函数返回。 所有其他调用仅影响当前终端。 delscreen(sp)newterm() 相反; 释放与给定 SCREEN 引用关联的数据结构。

将输出发送到终端

refresh()wrefresh(win) 必须调用这些函数才能实际在终端上获得任何输出,因为其他例程仅操作数据结构。 wrefresh() 将命名的窗口复制到物理终端屏幕,同时考虑到已经存在的内容以进行优化。 refresh()stdscr 进行刷新。 除非已启用 leaveok(),否则终端的物理光标将保留在窗口光标的位置。 doupdate()wnoutrefresh(win) 这两个函数允许比 wrefresh 更有效率地进行多次更新。 要使用它们,重要的是要了解 curses 的工作方式。 除了所有窗口结构之外,curses 还保留两个数据结构来表示终端屏幕:一个物理屏幕,描述屏幕上实际的内容;一个虚拟屏幕,描述程序员想要在屏幕上显示的内容。 wrefresh 的工作方式是首先将命名的窗口复制到虚拟屏幕 (wnoutrefresh()),然后调用例程来更新屏幕 (doupdate())。 如果程序员希望一次输出多个窗口,则对 wrefresh 的一系列调用将导致交替调用 wnoutrefresh()doupdate(),从而导致多次突发输出到屏幕。 通过为每个窗口调用 wnoutrefresh(),然后可以调用一次 doupdate(),从而导致只有一次突发输出,传输的总字符数更少(这也避免了每次更新时视觉上令人讨厌的闪烁)。

底层 Capability 访问

setupterm(term, filenum, errret) 调用此例程以初始化终端的描述,而无需设置 curses 屏幕结构或更改 tty 驱动程序模式位。 term 是表示所用终端名称的字符串。 filenum 是用于输出的终端的 UNIX 文件描述符。 errret 是指向整数的指针,其中返回成功或失败指示。 返回的值可以是 1(一切正常)、0(没有这样的终端)或 -1(定位 terminfo 数据库时出现问题)。 term 的值可以指定为 NULL,这将导致使用环境变量 TERM 中的值。 errret 指针也可以指定为 NULL,这意味着不需要错误代码。 如果 errret 是默认的,并且出现问题