使用 Python 进行 Curses 编程 — Python 文档

来自菜鸟教程
Python/docs/2.7/howto/curses
跳转至:导航、​搜索

使用 Python 进行 Curses 编程

作者
是 库克林,埃里克 S. 雷蒙德
发布
2.03

摘要

本文档介绍如何使用 Python 2.x 编写文本模式程序,使用 curses 扩展模块来控制显示。


什么是诅咒?

curses 库为基于文本的终端提供独立于终端的屏幕绘制和键盘处理设施; 此类终端包括VT100s、Linux 控制台以及xterm 和rxvt 等X11 程序提供的模拟终端。 显示终端支持各种控制码来执行常见的操作,如移动光标、滚动屏幕和擦除区域。 不同的终端使用大不相同的代码,并且通常有自己的小怪癖。

在 X 显示器的世界中,人们可能会问“为什么要麻烦”? 字符单元显示终端确实是一种过时的技术,但在某些领域,能够用它们做一些奇特的事情仍然很有价值。 一种是在不携带 X 服务器的小尺寸或嵌入式 Unix 上。 另一个是操作系统安装程序和内核配置程序等工具,它们可能必须在 X 可用之前运行。

curses 库隐藏了不同终端的所有细节,并为程序员提供了一个显示抽象,包含多个不重叠的窗口。 窗口的内容可以通过各种方式改变——添加文本、删除它、改变它的外观——curses 库会自动找出需要发送到终端的控制代码以产生正确的输出。

curses 库最初是为 BSD Unix 编写的; AT&T 的后来的 System V 版本的 Unix 添加了许多增强功能和新功能。 BSD curses 不再维护,取而代之的是 ncurses,它是 AT&T 接口的开源实现。 如果您使用的是开源 Unix,例如 Linux 或 FreeBSD,那么您的系统几乎肯定会使用 ncurses。 由于当前大多数商业 Unix 版本都基于 System V 代码,因此这里描述的所有功能可能都可用。 但是,某些专有 Unix 所携带的旧版本的 curses 可能不支持所有内容。

没有人制作过curses 模块的Windows 端口。 在 Windows 平台上,尝试 Fredrik Lundh 编写的 Console 模块。 控制台模块提供光标可寻址的文本输出,以及对鼠标和键盘输入的完全支持,可从 http://effbot.org/zone/console-index.htm 获得。

Python 诅咒模块

你的 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() 返回一个代表整个屏幕的窗口对象; 这通常称为 stdscr,在相应 C 变量的名称之后。

import curses
stdscr = curses.initscr()

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

curses.noecho()

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

curses.cbreak()

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

stdscr.keypad(1)

终止一个 curses 应用程序比启动一个要容易得多。 你需要打电话

curses.nocbreak(); stdscr.keypad(0); curses.echo()

反转对诅咒友好的终端设置。 然后调用endwin()函数将终端恢复到原来的工作模式。

curses.endwin()

调试 curses 应用程序时的一个常见问题是,当应用程序死掉而不将终端恢复到以前的状态时,终端就会变得一团糟。 在 Python 中,当您的代码有问题并引发未捕获的异常时,通常会发生这种情况。 例如,当您键入时,键不再显示在屏幕上,这使得使用 shell 变得困难。

在 Python 中,您可以通过导入 curses.wrapper() 函数来避免这些复杂情况并使调试更加容易。 它需要一个可调用对象并进行上述初始化,如果存在颜色支持,还会初始化颜色。 然后它运行您提供的可调用对象,最后适当地取消初始化。 可调用对象在 try-catch 子句中调用,该子句捕获异常,执行curses 取消初始化,然后向上传递异常。 因此,您的终端不会因异常而处于有趣的状态。


窗户和垫子

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

initscr()函数返回的stdscr对象是一个覆盖整个屏幕的窗口对象。 许多程序可能只需要这个单一窗口,但您可能希望将屏幕分成更小的窗口,以便分别重绘或清除它们。 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 最初是为慢速300 波特终端连接编写的; 对于这些终端,最小化重绘屏幕所需的时间非常重要。 这让curses 累积对屏幕的更改,并以最有效的方式显示它们。 例如,如果您的程序在窗口中显示一些字符,然后清除窗口,则无需发送原始字符,因为它们永远不可见。

因此,curses 要求您使用窗口对象的 refresh() 方法明确告诉它重绘窗口。 在实践中,这并没有真正使带有 curses 的编程复杂化。 大多数程序进入一系列活动,然后暂停等待用户按下按键或其他一些动作。 您所要做的就是在暂停等待用户输入之前确保屏幕已经重绘,只需调用 stdscr.refresh() 或其他相关窗口的 refresh() 方法。

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

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

#  Displays a section of the pad in the middle of the screen
pad.refresh(0,0, 5,5, 20,75)

refresh() 调用在屏幕上从坐标 (5,5) 延伸到坐标 (20,75) 的矩形中显示焊盘的一部分; 显示部分的左上角是焊盘上的坐标 (0,0)。 除此之外,pads 和普通窗口完全一样,支持相同的方法。

如果屏幕上有多个窗口和垫,则有一种更有效的方法,这将防止刷新时烦人的屏幕闪烁。 使用每个窗口的noutrefresh()方法更新代表屏幕所需状态的数据结构; 再用【X202X】【X206X】功能,一键更换物理屏以匹配想要的状态。 正常的 refresh() 方法调用 doupdate() 作为它的最后一个动作。


显示文本

从 C 程序员的角度来看,curses 有时可能看起来像一个曲折的函数迷宫,每个函数都略有不同。 例如,addstr() 在 stdscr 窗口的当前光标位置显示一个字符串,而 mvaddstr() 在显示字符串之前首先移动到给定的 y,x 坐标。 waddstr() 就像 addstr(),但允许指定要使用的窗口,而不是默认使用 stdscr。 mvwaddstr() 类似。

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

表格 说明
str 或 ch 在当前位置显示字符串str或字符ch
str 或 ch, attr 在当前位置使用属性 attr 显示字符串 str 或字符 ch
y、x、str 或 ch 移动到窗口内的位置 y,x,并显示 str 或 ch
y、x、str 或 ch、attr 移动到窗口内的位置 y,x,并显示 str 或 ch,使用属性 attr

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

addstr() 函数将 Python 字符串作为要显示的值,而 addch() 函数则是一个字符,它可以是长度为 1 的 Python 字符串或整数。 如果是字符串,则只能显示 0 到 255 之间的字符。 SVr4 curses 为扩展字符提供常量; 这些常量是大于 255 的整数。 例如,ACS_PLMINUS 是一个 +/- 符号,而 ACS_ULCORNER 是一个框的左上角(便于绘制边框)。

Windows 会记住上次操作后光标离开的位置,因此如果您省略 y,x 坐标,则字符串或字符将显示在上次操作停止的位置。 您也可以使用 move(y,x) 方法移动光标。 由于某些终端总是显示闪烁的光标,您可能需要确保光标位于不会分散注意力的位置; 让光标在某个明显随机的位置闪烁可能会令人困惑。

如果您的应用程序根本不需要闪烁的光标,您可以调用 curs_set(0) 使其不可见。 同样,为了与旧版本的 curses 兼容,有一个 leaveok(bool) 函数。 当 bool 为 true 时,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 控制台,其次是 color xterms。

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

curses 库维护有限数量的颜色对,包含前景(或文本)颜色和背景颜色。 可以通过color_pair()函数获取颜色对对应的属性值; 这可以与其他属性(例如 A_REVERSE)进行按位或运算,但同样,这种组合不能保证在所有终端上都有效。

使用颜色对 1 显示一行文本的示例:

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

正如我之前所说,颜色对由前景色和背景色组成。 start_color() 激活颜色模式时初始化 8 种基本颜色。 它们是:0:黑色、1:红色、2:绿色、3:黄色、4:蓝色、5:品红色、6:青色和 7:白色。 curses 模块为以下每种颜色定义命名常量:curses.COLOR_BLACK、curses.COLOR_RED 等等。

init_pair(n, f, b) 函数将颜色对 n 的定义更改为前景色 f 和背景色 b。 颜色对 0 硬接线为黑底白字,不能更改。

让我们把这一切放在一起。 要将颜色 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。 如果您有幸拥有如此出色的终端,请查阅您系统的手册页以获取更多信息。


用户输入

curses 库本身只提供非常简单的输入机制。 Python 的支持添加了一个文本输入小部件,弥补了一些不足。

获取窗口输入的最常见方法是使用其 getch() 方法。 getch() 暂停并等待用户敲击一个键,如果 echo() 之前已经被调用,则显示它。 您可以选择指定在暂停之前光标应移动到的坐标。

可以使用 nodelay() 方法更改此行为。 在 nodelay(1) 之后,窗口的 getch() 变为非阻塞并在没有输入就绪时返回 curses.ERR(值 -1)。 还有一个halfdelay()函数,可以用来(实际上)在每个getch()上设置一个定时器; 如果在指定的延迟(以十分之一秒为单位)内没有输入可用,curses 会引发异常。

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

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

curses.ascii 模块提供 ASCII 类成员函数,这些函数采用整数或 1 个字符的字符串参数; 这些可能有助于为您的命令解释器编写更具可读性的测试。 它还提供采用整数或 1 个字符的字符串参数并返回相同类型的转换函数。 例如,curses.ascii.ctrl() 返回与其参数对应的控制字符。

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

curses.echo()            # Enable echoing of characters

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

Python curses.textpad 模块提供了更好的东西。 有了它,您可以将窗口变成支持类似 Emacs 的一组键绑定的文本框。 Textbox 类的各种方法支持使用输入验证进行编辑并收集带有或不带有尾随空格的编辑结果。 有关详细信息,请参阅 curses.textpad 上的库文档。


更多信息

本 HOWTO 没有涵盖一些高级主题,例如屏幕抓取或从 xterm 实例捕获鼠标事件。 但是curses 模块的Python 库页面现在非常完整。 你应该接下来浏览它。

如果您对任何 ncurses 入口点的详细行为有疑问,请查阅 Curses 实现的手册页,无论是 ncurses 还是专有 Unix 供应商的。 手册页将记录任何怪癖,并提供您可用的所有功能、属性和 ACS_* 字符的完整列表。

因为curses API 太大,Python 接口不支持某些函数,不是因为它们难以实现,而是因为还没有人需要它们。 随意添加它们,然后提交补丁。 此外,我们还不支持与 ncurses 相关的菜单库; 随意添加。

如果您编写了一个有趣的小程序,请随时将其作为另一个演示进行贡献。 我们总是可以使用更多!

ncurses 常见问题:http://invisible-island.net/ncurses/ncurses.faq.html