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

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

使用 Python 进行 Curses 编程

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

抽象的

本文档介绍如何使用 curses 扩展模块来控制文本模式显示。


什么是诅咒?

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

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

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

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

Python 的 Windows 版本不包括 curses 模块。 一个名为 UniCurses 的移植版本可用。 您还可以尝试 Fredrik Lundh 编写的控制台模块 ,它不使用与 curses 相同的 API,但提供光标可寻址的文本输出并完全支持鼠标和键盘输入。

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

import curses
stdscr = curses.initscr()

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

curses.noecho()

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

curses.cbreak()

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

stdscr.keypad(True)

终止一个 curses 应用程序比启动一个要容易得多。 您需要致电:

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

反转对诅咒友好的终端设置。 然后调用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() 然后运行您提供的可调用对象。 一旦 callable 返回,wrapper() 将恢复终端的原始状态。 callable 在 tryexcept 中被调用,它捕获异常,恢复终端状态,然后重新引发异常。 因此,您的终端不会在异常时处于有趣的状态,您将能够读取异常的消息和回溯。


窗户和垫子

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.LINEScurses.COLS 变量来确定屏幕的尺寸,以获得 yx 尺寸。 合法坐标将从 (0,0) 扩展到 (curses.LINES - 1, curses.COLS - 1)

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

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

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

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) 的矩形中显示焊盘的一部分; 显示部分的左上角是焊盘上的坐标 (0,0)。 除此之外,pads 和普通窗口完全一样,支持相同的方法。

如果屏幕上有多个窗口和面板,则有一种更有效的方法来更新屏幕并防止在屏幕的每个部分都更新时出现烦人的屏幕闪烁。 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 在当前位置使用属性 attr 显示字符串 str 或字符 ch
yxstrch 移动到窗口内的位置 y,x,并显示 strch
yxstrchattr 移动到窗口内的位置 y,x,并显示 strch,使用属性 attr

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

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

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

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

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

如果您的应用程序根本不需要闪烁的光标,您可以调用 curs_set(False) 使其不可见。 为了与旧的curses 版本兼容,有一个leaveok(bool) 函数,它是curs_set() 的同义词。 当 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()函数自动执行此操作)。 一旦完成,has_colors() 函数返回 TRUE,如果使用的终端实际上可以显示颜色。 (注意:curses 使用美国拼写“颜色”,而不是加拿大/英国拼写“颜色”。 如果您习惯了英式拼写,那么为了这些功能,您将不得不放弃拼写错误。)

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 模块提供采用整数或 1 个字符的字符串参数的 ASCII 类成员函数; 这些可能有助于为此类循环编写更具可读性的测试。 它还提供采用整数或 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)

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 提交补丁的更多信息。