使用 Python 进行 Curses 编程

作者:

A.M. Kuchling, Eric S. Raymond

版本:

2.04

什么是 curses?

curses 库提供了一个与终端无关的屏幕绘制和键盘处理工具,适用于基于文本的终端;此类终端包括 VT100、Linux 控制台以及各种程序提供的模拟终端。显示终端支持各种控制代码来执行常见操作,例如移动光标、滚动屏幕和擦除区域。不同的终端使用截然不同的代码,并且通常有自己的细微差别。

在图形显示的世界中,人们可能会问“为什么要费心”?的确,字符单元显示终端是一种过时的技术,但有一些领域仍然需要使用它们来完成一些复杂的操作。一个领域是小型或嵌入式 Unix 系统,这些系统不运行 X 服务器。另一个领域是操作系统安装程序和内核配置器等工具,这些工具可能需要在任何图形支持可用之前运行。

curses 库提供了相当基本的功能,为程序员提供了一个包含多个不重叠文本窗口的显示抽象。窗口的内容可以通过多种方式更改——添加文本、擦除文本、更改其外观——curses 库将找出需要发送到终端的控制代码以产生正确的输出。curses 没有提供许多用户界面概念,例如按钮、复选框或对话框;如果您需要此类功能,请考虑使用 Urwid 等用户界面库。

curses 库最初是为 BSD Unix 编写的;AT&T 后来的 System V 版本的 Unix 添加了许多增强功能和新函数。BSD curses 已经不再维护,已被 ncurses 取代,ncurses 是 AT&T 接口的开源实现。如果您使用的是 Linux 或 FreeBSD 等开源 Unix 系统,您的系统几乎肯定使用的是 ncurses。由于大多数当前的商业 Unix 版本都是基于 System V 代码的,因此这里描述的所有函数都可能可用。不过,一些专有 Unix 系统提供的旧版本 curses 可能不支持所有功能。

Python 的 Windows 版本不包含 curses 模块。一个名为 UniCurses 的移植版本可用。

Python curses 模块

Python 模块是 curses 提供的 C 函数的简单包装器;如果您已经熟悉 C 中的 curses 编程,那么将这些知识转移到 Python 中非常容易。最大的区别是 Python 接口通过将不同的 C 函数(例如 addstr()mvaddstr()mvwaddstr())合并到单个 addstr() 方法中来简化操作。您将在后面看到对此的更详细介绍。

本 HOWTO 是使用 curses 和 Python 编写文本模式程序的入门介绍。它不试图成为 curses API 的完整指南;有关详细信息,请参阅 Python 库指南中关于 ncurses 的部分,以及 ncurses 的 C 手册页。但是,它将为您提供基本概念。

启动和结束 curses 应用程序

在执行任何操作之前,必须初始化 curses。这可以通过调用 initscr() 函数来完成,该函数将确定终端类型,向终端发送任何必要的设置代码,并创建各种内部数据结构。如果成功,initscr() 将返回一个表示整个屏幕的窗口对象;这通常在对应 C 变量的名称之后被称为 stdscr

import curses
stdscr = curses.initscr()

通常,curses 应用程序会关闭键到屏幕的自动回显,以便能够读取键并在特定情况下才显示它们。这需要调用 noecho() 函数。

curses.noecho()

应用程序通常还需要立即对键做出反应,而无需按 Enter 键;这被称为 cbreak 模式,与通常的缓冲输入模式相反。

curses.cbreak()

终端通常将特殊键(例如光标键或导航键,如 Page Up 和 Home)返回为多字节转义序列。虽然您可以编写应用程序来预期此类序列并相应地处理它们,但 curses 可以为您完成此操作,返回一个特殊值,例如 curses.KEY_LEFT。要让 curses 完成这项工作,您必须启用键盘模式。

stdscr.keypad(True)

终止 curses 应用程序比启动它容易得多。您需要调用

curses.nocbreak()
stdscr.keypad(False)
curses.echo()

以反转 curses 友好的终端设置。然后调用 endwin() 函数将终端恢复到其原始操作模式。

curses.endwin()

调试 curses 应用程序时,一个常见的问题是,当应用程序在未将终端恢复到其先前状态的情况下终止时,终端会变得混乱。在 Python 中,这通常发生在您的代码有错误并引发未捕获的异常时。例如,当您键入时,键不再回显到屏幕上,这使得使用 shell 变得困难。

在 Python 中,您可以避免这些复杂情况并使调试更容易,方法是导入 curses.wrapper() 函数并像这样使用它

from curses import wrapper

def main(stdscr):
    # Clear screen
    stdscr.clear()

    # This raises ZeroDivisionError when i == 10.
    for i in range(0, 11):
        v = i-10
        stdscr.addstr(i, 0, '10 divided by {} is {}'.format(v, 10/v))

    stdscr.refresh()
    stdscr.getkey()

wrapper(main)

wrapper() 函数接受一个可调用对象,并执行上面描述的初始化,如果存在颜色支持,还会初始化颜色。 wrapper() 然后运行您提供的可调用对象。一旦可调用对象返回,wrapper() 将恢复终端的原始状态。可调用对象在 tryexcept 中被调用,该语句捕获异常,恢复终端的状态,然后重新引发异常。因此,您的终端不会在异常时处于奇怪的状态,您将能够阅读异常的消息和回溯。

窗口和填充区

窗口是 curses 中的基本抽象。窗口对象表示屏幕上的一个矩形区域,并支持显示文本、擦除文本、允许用户输入字符串等方法。

stdscrinitscr() 函数返回的对象是一个覆盖整个屏幕的窗口对象。许多程序可能只需要这个单一窗口,但您可能希望将屏幕分成更小的窗口,以便分别重绘或清除它们。该 newwin() 函数创建一个给定大小的新窗口,并返回新的窗口对象。

begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

请注意,curses 中使用的坐标系是不寻常的。坐标始终按 y,x 的顺序传递,窗口的左上角是坐标 (0,0)。这打破了处理坐标的正常约定,其中 x 坐标位于第一位。这与大多数其他计算机应用程序不同,但它一直是 curses 的一部分,因为它最初被编写,现在改变它为时已晚。

您的应用程序可以使用 curses.LINEScurses.COLS 变量来获取 yx 大小,从而确定屏幕的大小。然后,合法坐标将从 (0,0) 扩展到 (curses.LINES - 1, curses.COLS - 1)

当您调用方法来显示或擦除文本时,效果不会立即显示在屏幕上。相反,您必须调用窗口对象的refresh()方法来更新屏幕。

这是因为 curses 最初是针对速度缓慢的 300 波特终端连接而编写的;对于这些终端,最大限度地减少重绘屏幕所需的时间非常重要。相反,curses 会累积对屏幕的更改,并在您调用 refresh() 时以最有效的方式显示它们。例如,如果您的程序在窗口中显示一些文本,然后清除窗口,则无需发送原始文本,因为它们永远不会可见。

在实践中,显式地告诉 curses 重绘窗口并不会真正使使用 curses 的编程变得复杂。大多数程序会进入一阵忙碌,然后暂停等待用户按键或其他操作。您只需确保在暂停等待用户输入之前重绘了屏幕,方法是首先调用 stdscr.refresh() 或其他相关窗口的 refresh() 方法。

一个 pad 是窗口的一种特殊情况;它可以比实际显示屏幕更大,并且一次只显示 pad 的一部分。创建 pad 需要 pad 的高度和宽度,而刷新 pad 需要给出 pad 的子部分将在屏幕上显示的区域的坐标。

pad = curses.newpad(100, 100)
# These loops fill the pad with letters; addch() is
# explained in the next section
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# Displays a section of the pad in the middle of the screen.
# (0,0) : coordinate of upper-left corner of pad area to display.
# (5,5) : coordinate of upper-left corner of window area to be filled
#         with pad content.
# (20, 75) : coordinate of lower-right corner of window area to be
#          : filled with pad content.
pad.refresh( 0,0, 5,5, 20,75)

refresh() 调用在屏幕上从坐标 (5,5) 到坐标 (20,75) 的矩形中显示 pad 的一部分;显示部分的左上角是 pad 上的坐标 (0,0)。除了这种差异之外,pad 与普通窗口完全相同,并支持相同的方法。

如果您在屏幕上有多个窗口和 pad,则有一种更有效的方法来更新屏幕并防止每个屏幕部分更新时出现的烦人屏幕闪烁。 refresh() 实际上执行两件事

  1. 调用每个窗口的 noutrefresh() 方法来更新表示屏幕所需状态的底层数据结构。

  2. 调用函数 doupdate() 函数将物理屏幕更改为与数据结构中记录的所需状态匹配。

相反,您可以对多个窗口调用 noutrefresh() 来更新数据结构,然后调用 doupdate() 来更新屏幕。

显示文本

从 C 程序员的角度来看,curses 有时看起来像一个错综复杂的函数迷宫,所有函数都微妙地不同。例如, addstr()stdscr 窗口的当前光标位置显示字符串,而 mvaddstr() 首先移动到给定的 y,x 坐标,然后显示字符串。 waddstr()addstr() 相同,但允许指定要使用的窗口,而不是默认使用 stdscrmvwaddstr() 允许同时指定窗口和坐标。

幸运的是,Python 接口隐藏了所有这些细节。 stdscr 是一个与其他窗口对象一样的窗口对象,并且 addstr() 等方法接受多种参数形式。通常有四种不同的形式。

形式

描述

strch

在当前位置显示字符串 str 或字符 ch

strch, attr

在当前位置显示字符串 str 或字符 ch,使用属性 attr

y, x, strch

移动到窗口内的位置 y,x,并显示 strch

y, x, strch, attr

移动到窗口内的位置 y,x,并显示 strch,使用属性 attr

属性允许以突出显示的形式显示文本,例如粗体、下划线、反转代码或颜色。它们将在下一小节中详细解释。

The addstr() 方法将 Python 字符串或字节字符串作为要显示的值。字节字符串的内容将按原样发送到终端。字符串使用窗口的 encoding 属性的值编码为字节;默认情况下,此属性设置为 locale.getencoding() 返回的默认系统编码。

The addch() 方法接受一个字符,该字符可以是长度为 1 的字符串、长度为 1 的字节字符串或整数。

为扩展字符提供了常量;这些常量是大于 255 的整数。例如,ACS_PLMINUS 是 +/- 符号,而 ACS_ULCORNER 是框的左上角(便于绘制边框)。您也可以使用相应的 Unicode 字符。

窗口会记住上次操作后光标的位置,因此,如果您省略 y,x 坐标,则字符串或字符将显示在上次操作结束的位置。您也可以使用 move(y,x) 方法移动光标。由于某些终端始终显示闪烁的光标,因此您可能希望确保光标位于不会分散注意力的位置;光标在一些明显随机的位置闪烁可能会令人困惑。

如果您的应用程序根本不需要闪烁的光标,则可以调用 curs_set(False) 使其不可见。为了与旧版本的 curses 保持兼容,有一个 leaveok(bool) 函数,它是 curs_set() 的同义词。当 bool 为真时,curses 库将尝试抑制闪烁的光标,您无需担心将其留在奇怪的位置。

属性和颜色

字符可以以不同的方式显示。基于文本的应用程序中的状态行通常以反转视频显示,或者文本查看器可能需要突出显示某些单词。curses 通过允许您为屏幕上的每个单元格指定一个属性来支持这一点。

属性是一个整数,每个位代表一个不同的属性。您可以尝试以设置了多个属性位的形式显示文本,但 curses 不保证所有可能的组合都可用,也不保证它们在视觉上都不同。这取决于所用终端的功能,因此最安全的方法是坚持使用最常用的属性,如下所示。

属性

描述

A_BLINK

闪烁的文本

A_BOLD

超亮或粗体文本

A_DIM

半亮文本

A_REVERSE

反转视频文本

A_STANDOUT

可用的最佳突出显示模式

A_UNDERLINE

带下划线的文本

因此,要显示屏幕顶部的反转视频状态行,您可以编写以下代码

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

curses 库还支持那些提供颜色的终端。最常见的此类终端可能是 Linux 控制台,其次是彩色 xterm。

要使用颜色,您必须在调用 initscr() 后立即调用 start_color() 函数,以初始化默认颜色集(curses.wrapper() 函数会自动执行此操作)。完成此操作后,has_colors() 函数将在所用终端实际上可以显示颜色时返回 TRUE。(注意:curses 使用美式拼写“color”,而不是加拿大/英国拼写“colour”。如果您习惯于英国拼写,则必须为了这些函数而放弃这种拼写。)

curses 库维护着有限数量的颜色对,每个颜色对包含一个前景色(或文字颜色)和一个背景色。您可以使用 color_pair() 函数获取对应于颜色对的属性值;这可以与其他属性(如 A_REVERSE)进行按位或运算,但同样,这种组合不能保证在所有终端上都能正常工作。

一个例子,它使用颜色对 1 显示一行文本

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

正如我之前所说,一个颜色对包含一个前景色和一个背景色。 init_pair(n, f, b) 函数更改颜色对 n 的定义,使其具有前景色 f 和背景色 b。颜色对 0 被硬编码为黑底白字,无法更改。

颜色是编号的,当 start_color() 激活颜色模式时,它会初始化 8 种基本颜色。它们分别是:0:黑色,1:红色,2:绿色,3:黄色,4:蓝色,5:洋红色,6:青色,以及 7:白色。 curses 模块为每种颜色定义了命名常量:curses.COLOR_BLACKcurses.COLOR_RED,等等。

让我们把这些都整合起来。要将颜色 1 更改为红字白底,您需要调用

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

当您更改颜色对时,任何使用该颜色对显示的文本都会更改为新的颜色。您也可以使用以下方法以这种颜色显示新文本

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

非常高级的终端可以将实际颜色的定义更改为给定的 RGB 值。这使您可以更改颜色 1(通常为红色)为紫色或蓝色或您喜欢的任何其他颜色。不幸的是,Linux 控制台不支持此功能,因此我无法尝试它,也无法提供任何示例。您可以通过调用 can_change_color() 来检查您的终端是否支持此功能,如果支持,它将返回 True。如果您有幸拥有如此强大的终端,请查阅系统的手册页以获取更多信息。

用户输入

C curses 库只提供非常简单的输入机制。Python 的 curses 模块添加了一个基本的文本输入小部件。(其他库,如 Urwid,拥有更广泛的小部件集合。)

从窗口获取输入有两种方法

  • getch() 刷新屏幕,然后等待用户按下键,如果之前调用了 echo(),则显示该键。您可以选择指定一个坐标,将光标移动到该坐标,然后再暂停。

  • getkey() 做同样的事情,但将整数转换为字符串。单个字符将作为 1 个字符的字符串返回,特殊键(如功能键)将返回包含键名(如 KEY_UP^G)的较长字符串。

可以使用 nodelay() 窗口方法来避免等待用户输入。在执行 nodelay(True) 后,窗口的 getch()getkey() 方法将变为非阻塞模式。为了指示没有输入准备就绪,getch() 将返回 curses.ERR(值为 -1),而 getkey() 将引发异常。还有一个 halfdelay() 函数,它可以用来(实际上)为每个 getch() 设置一个计时器;如果在指定延迟(以十分之一秒为单位)内没有输入可用,curses 将引发异常。

getch() 方法返回一个整数;如果该整数在 0 到 255 之间,它表示所按键的 ASCII 码。大于 255 的值是特殊键,例如 Page Up、Home 或光标键。您可以将返回值与常量进行比较,例如 curses.KEY_PPAGEcurses.KEY_HOMEcurses.KEY_LEFT。您的程序主循环可能看起来像这样

while True:
    c = stdscr.getch()
    if c == ord('p'):
        PrintDocument()
    elif c == ord('q'):
        break  # Exit the while loop
    elif c == curses.KEY_HOME:
        x = y = 0

curses.ascii 模块提供 ASCII 类成员函数,这些函数接受整数或 1 个字符的字符串参数;这些函数可能在编写更易读的循环测试时很有用。它还提供转换函数,这些函数接受整数或 1 个字符的字符串参数,并返回相同类型的参数。例如,curses.ascii.ctrl() 返回与其参数相对应的控制字符。

还有一个方法可以检索整个字符串,即 getstr()。它并不经常使用,因为它的功能非常有限;唯一可用的编辑键是退格键和回车键,它们会终止字符串。它可以选择限制为固定数量的字符。

curses.echo()            # Enable echoing of characters

# Get a 15-character string, with the cursor on the top line
s = stdscr.getstr(0,0, 15)

curses.textpad 模块提供了一个文本框,它支持类似 Emacs 的一组键绑定。Textbox 类的各种方法支持使用输入验证进行编辑,并使用或不使用尾随空格收集编辑结果。以下是一个示例

import curses
from curses.textpad import Textbox, rectangle

def main(stdscr):
    stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")

    editwin = curses.newwin(5,30, 2,1)
    rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
    stdscr.refresh()

    box = Textbox(editwin)

    # Let the user edit until Ctrl-G is struck.
    box.edit()

    # Get resulting contents
    message = box.gather()

有关更多详细信息,请参阅 curses.textpad 的库文档。

更多信息

本 HOWTO 未涵盖一些高级主题,例如读取屏幕内容或从 xterm 实例捕获鼠标事件,但 curses 模块的 Python 库页面现在已相当完整。您应该接下来浏览它。

如果您对 curses 函数的详细行为有任何疑问,请查阅您 curses 实现的联机帮助页,无论它是 ncurses 还是专有的 Unix 供应商。联机帮助页将记录任何怪癖,并提供所有函数、属性和 ACS_* 字符的完整列表。

由于 curses API 非常庞大,因此 Python 接口不支持某些函数。这通常不是因为它们难以实现,而是因为还没有人需要它们。此外,Python 尚未支持与 ncurses 相关的菜单库。欢迎添加对这些功能的支持的补丁;请参阅 Python 开发者指南,了解有关向 Python 提交补丁的更多信息。