Curses Programming with Python

作者:上午。 Kuchling,Eric S. Raymond
发布:2.04

抽象

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

What is curses?

curses库为基于文本的终端提供终端独立的屏幕绘制和键盘处理设施;这样的终端包括VT100,Linux控制台和由各种程序提供的模拟终端。显示终端支持各种控制代码,以执行常用操作,如移动光标,滚动屏幕和擦除区域。不同的终端使用广泛不同的代码,并且常常具有它们自己的轻微怪癖。

在图形显示的世界中,人们可能会问“为什么要麻烦”?这是真的,字符单元显示终端是一个过时的技术,但有一些壁龛,能够做与他们的奇怪的东西仍然是有价值的。一个利基在于没有运行X服务器的小占用或嵌入式Unix。另一种是诸如OS安装程序和内核配置程序之类的工具,可能必须在任何图形支持可用之前运行。

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

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

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

The Python curses module

你的Python模块是由curses提供的C函数的一个相当简单的包装;如果你已经熟悉C语言中的curses编程,那么很容易将这些知识传递给Python。The biggest difference is that the Python interface makes things simpler by merging different C functions such as addstr(), mvaddstr(), and mvwaddstr() into a single addstr() method. 稍后将更详细地介绍这一点。

这个HOWTO是用curses和Python编写文本模式程序的简介。它不试图成为curses API的完整指南;为此,请参阅Python库指南的ncurses部分,以及ncurses的C手册页。然而,它会给你的基本想法。

Starting and ending a curses application

在做任何事情之前,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要得到curses来做这项工作,你必须启用键盘模式。

stdscr.keypad(True)

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

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

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

curses.endwin()

调试curses应用程序时的一个常见问题是,当应用程序死机时,您的终端无法正常工作,而不会将终端恢复到之前的状态。在Python中,这通常发生在你的代码是buggy和引发未捕获的异常。例如,当您键入时,键不再回显到屏幕,这使得使用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()将恢复终端的原始状态。try ... except捕获异常,恢复终端的状态,然后重新引发异常。因此,您的终端不会在异常时处于滑稽状态,您将能够读取异常的消息和回溯。

Windows and Pads

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的一部分,因为它是第一次写,现在要改变太晚了。

您的应用程序可以使用curses.LINEScurses.COLS变量来确定yx 尺寸。合法坐标将从(0,0)延伸到(curses.LINES - 1, curses.COLS - 1)

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

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

在实践中,明确告诉curses重新绘制窗口并不会使编程更加复杂。大多数程序进入一系列活动,然后暂停等待用户的按键或其他操作。您需要做的是确保屏幕已暂停之前重新绘制,等待用户输入,首先调用stdscr.refresh()refresh()方法等一些其他相关窗口。

Pad是window的特殊情况;它可以大于实际的显示屏幕,并且一次只显示一部分垫。创建垫需要垫的高度和宽度,而刷新垫需要给出屏幕区域的坐标,其中将显示垫的子部分。

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()实际上做了两件事:

  1. 调用每个窗口的noutrefresh()方法来更新表示屏幕所需状态的基础数据结构。
  2. 调用函数doupdate()以更改物理屏幕以匹配数据结构中记录的所需状态。

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

Displaying Text

从C程序员的角度来看,curses有时可能看起来像一个扭曲的函数迷宫,所有的微妙的不同。例如,addstr()stdscr窗口中的当前光标位置显示一个字符串,而mvaddstr()waddstr()就像addstr(),但是允许指定一个窗口,而不是默认使用stdscrmvwaddstr()允许同时指定窗口和坐标。

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

形成描述
strch在当前位置显示字符串str或字符ch
strchattr使用当前位置的属性attr显示字符串str或字符ch
yxstrch移动到窗口中y,x的位置,并显示strch
yxstrchattr使用属性attr移动到窗口中的y,x位置并显示strch

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

addstr()方法使用Python字符串或bytestring作为要显示的值。将bytestrings的内容原样发送到终端。字符串使用窗口的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库将尝试抑制闪烁的光标,您不必担心将其保留在奇怪位置。

Attributes and Color

字符可以以不同的方式显示。基于文本的应用程序中的状态行通常以反向视频显示,或文本查看器可能需要突出显示某些字。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控制台,其次是颜色xterms。

要使用颜色,您必须在调用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)函数更改颜色对t4>,前景颜色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如果你很幸运有这样一个才华横溢的终端,请咨询您的系统的手册页获取更多信息。

User Input

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

None

  • getch() refreshes the screen and then waits for the user to hit a key, displaying the key if echo() has been called earlier. 您可以选择指定在暂停前光标应移动到的坐标。
  • 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()它不经常使用,因为它的功能非常有限;唯一可用的编辑键是退格键和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上的库文档。

For More Information

这个HOWTO不包括一些高级主题,例如读取屏幕的内容或从xterm实例捕获鼠标事件,但是curses模块的Python库页面现在已经相当完整。你应该浏览它下一步。

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

因为curses API如此之大,一些函数在Python接口中不受支持。通常这不是因为它们难以实现,而是因为没有人需要它们。此外,Python还不支持与ncurses相关联的菜单库。欢迎补充支持这些的补丁;有关向Python提交修补程序的详细信息,请参阅Python开发人员指南