用 Pythoncurses 编程

  • Author

    • 上午。埃里克·雷蒙德·库奇林
  • Release

    • 2.04

Abstract

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

什么是 curses?

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

在图形显示的世界中,人们可能会问“为什么要打扰”?字符单元显示终端确实是一种过时的技术,但是在某些领域,能够用它们来做花哨的事情仍然很有价值。一个小众市场是在不运行 X 服务器的小型或嵌入式 Unix 上。另一个是可能需要在提供任何图形支持之前运行的工具,例如 OS 安装程序和内核配置程序。

curses 库提供了相当基本的Function,为程序员提供了包含多个非重叠文本窗口的显示的抽象。窗口的内容可以pass多种方式更改-添加文本,擦除文本,更改其外观-并且 curses 库将确定需要向终端发送哪些控制代码以产生正确的输出。 curses 并没有提供许多用户界面概念,例如按钮,复选框或对话框。如果需要这些Function,请考虑用户界面库,例如Urwid

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

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

Python curses 模块

Python 模块是对 curses 提供的 C 函数的相当简单的包装;如果您已经熟悉 C 语言中的 curses 编程,那么将这些知识转移到 Python 上真的很容易。最大的不同是 Python 界面pass将addstr()mvaddstr()mvwaddstr()之类的不同 C 函数合并为一个addstr()方法,使事情变得更简单。稍后,您将看到更详细的内容。

本 HOWTO 是使用 curses 和 Python 编写文本模式程序的简介。它并没有试图成为 curses API 的完整指南。为此,请参见有关 ncurses 的 Python 库指南部分,以及有关 ncurses 的 C 手册页。但是,它将为您提供基本的想法。

开始和结束 curses 申请

在做任何事情之前,必须先初始化 curses。这是pass调用initscr()函数来完成的,该函数将确定终端类型,将所需的所有设置代码发送到终端,并创建各种内部数据结构。如果成功,则initscr()返回代表整个屏幕的窗口对象;通常在相应的 C 变量的名称后称为stdscr

import curses
stdscr = curses.initscr()

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

curses.noecho()

应用程序通常还需要立即对键做出反应,而无需按下 Enter 键。与通常的缓冲 Importing 模式相反,这称为 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 中,您可以pass导入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内部被调用,该捕获异常,恢复终端的状态,然后重新引发异常。因此,您的终端不会在发生异常时处于有趣状态,并且您将能够读取异常的消息和回溯。

Windows 和 Pads

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

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 的 Sequences 传递,并且窗口的左上角为坐标(0,0)。这违反了处理坐标的常规约定,其中 x *坐标排在第一位。不幸的是,这与大多数其他计算机应用程序有所不同,但是自从第一次编写以来,这一直是 curses 的一部分,现在改变现状为时已晚。

您的应用程序可以使用curses.LINEScurses.COLS变量来确定* y x *大小,从而确定屏幕的大小。合法坐标将从(0,0)延伸到(curses.LINES - 1, curses.COLS - 1)

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

这是因为 curses 最初是在考虑 300 波特的慢速终端连接的情况下编写的;使用这些终端,尽量减少重新绘制屏幕所需的时间非常重要。而是 curses 将更改累积到屏幕上,并在您调用refresh()时以最有效的方式显示它们。例如,如果您的程序在窗口中显示一些文本然后清除该窗口,则无需发送原始文本,因为它们从不可见。

在实践中,明确告诉 curses 重绘窗口并不会使 cursestrue 复杂化。大多数程序都会进行一系列活动,然后暂停以 await 用户方面的按键或其他操作。您要做的就是确保pass先调用其他相关窗口的stdscr.refresh()refresh()方法,在暂停 await 用户 Importing 之前重绘了屏幕。

垫子是窗户的特例;它可以大于实际的显示屏,并且一次只显示该键盘的一部分。创建垫板需要垫板的高度和宽度,而刷新垫板则需要提供屏幕区域的坐标,该区域将显示垫板的小部分。

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)。除此以外,垫完全类似于普通的窗户并支持相同的方法。

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

  • 调用每个窗口的noutrefresh()方法以更新表示屏幕所需状态的基础数据结构。

  • 调用函数doupdate()函数来更改物理屏幕以匹配数据结构中记录的所需状态。

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

Displaying Text

从 C 程序员的角度来看,curses 有时看起来像是一堆曲折的函数迷宫,它们之间有着微妙的不同。例如,addstr()stdscr窗口中的当前光标位置显示一个字符串,而mvaddstr()在显示该字符串之前先移至给定的 y,x 坐标。 waddstr()就像addstr()一样,但是允许指定要使用的窗口,而不是默认情况下使用stdscrmvwaddstr()允许同时指定窗口和坐标。

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

FormDescription
* str ch *在当前位置显示字符串* str 或字符 ch *
* str ch attr *在当前位置使用属性* attr 显示字符串 str 或字符 ch *
* y x str ch *移动到窗口内的* y,x 位置,并显示 str ch *
* y x str ch attr *使用属性* attr 移至窗口内的 y,x 位置并显示 str ch *

属性允许以加亮形式显示文本,例如粗体,下划线,反向代码或彩色。下一部分将对它们进行详细说明。

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

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

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

Windows 会记住上次操作后光标留在的位置,因此,如果Ellipsis* y,x *坐标,则在上次操作break的地方都会显示字符串或字符。您也可以使用move(y,x)方法移动光标。由于某些终端始终显示闪烁的光标,因此您可能要确保将光标定位在不会分散注意力的某个位置。使光标在某些明显随机的位置闪烁可能会造成混淆。

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

属性和颜色

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

属性是一个整数,每个位代表一个不同的属性。您可以try显示设置了多个属性位的文本,但是 curses 不能保证所有可能的组合都可用,或者它们在视觉上完全不同。这取决于所用终端的能力,因此坚持此处列出的最常用的属性是最安全的。

AttributeDescription
A_BLINKBlinking text
A_BOLD超亮或粗体 Literals
A_DIM半亮文本
A_REVERSEReverse-video text
A_STANDOUT最好的突出显示模式
A_UNDERLINEUnderlined text

因此,要在屏幕顶部显示反向视频状态行,可以编写以下代码:

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 使用美国拼写“颜色”,而不是加拿大/英国拼写“颜色”.如果您习惯了英国拼写,则由于这些Function,您必须辞职以使其拼写错误.)

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 控制台不支持此Function,因此我无法try它,并且无法提供任何示例。您可以pass调用can_change_color()来检查终端是否可以执行此操作,如果存在该Function,则返回True。如果您有幸拥有如此出色的终端机,请查阅系统的手册页以获取更多信息。

User Input

C curses 库仅提供非常简单的 Importing 机制。 Python 的curses模块添加了一个基本的文本 Importing 小部件。 (其他库(例如Urwid)具有更多小部件集合。)

有两种从窗口获取 Importing 的方法:

  • getch()刷新屏幕,然后 await 用户按下按键,如果echo()较早被调用,则显示该按键。您可以有选择地指定在暂停之前光标应移动到的坐标。

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

可以不使用nodelay()窗口方法来 await 用户。在nodelay(True)之后,窗口的getch()getkey()变为非阻塞。为了表明没有 Importing 准备就绪,getch()返回curses.ERR(值-1),而getkey()引发异常。还有一个halfdelay()函数,可用于(实际上)在每个getch()上设置计时器;如果在指定的延迟(以十分之一秒为单位)内没有 Importing 变为可用,则 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()。它很少使用,因为它的Function非常有限。唯一可用的编辑键是 Backspace 键和 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类的各种方法都支持pass Importing 验证进行编辑,并在有或没有尾随空格的情况下收集编辑结果。这是一个例子:

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 供应商。手册页将记录所有怪癖,并提供所有可用Function,属性和ACS_*字符的完整列表。

由于 curses API 太大,因此 Python 界面不支持某些Function。通常这不是因为它们难以实现,而是因为还没有人需要它们。此外,Python 尚不支持与 ncurses 关联的菜单库。欢迎增加支持这些补丁的补丁;请参阅Python 开发人员指南以了解有关向 Python 提交补丁的更多信息。