使用Python进行Curses编程

作者: A.M。Kuchling,Eric S. Raymond
发布: 2.04

Abstract

本文档描述了如何使用curses扩展模块来控制文本模式显示

什么是curses

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

在图形显示的世界中,人们可能会问“为什么要打扰”?字符单元显示终端是一种过时的技术,但是有一些利基可以做出奇特的东西仍然很有价值。一个利基是小型或嵌入式Unix,不运行X服务器。另一个是OSinstallers和内核配置器等工具,可能必须在任何图形支持可用之前运行.

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

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

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

Python curses模块

Python模块是一个相当简单的包装器,它包含了由cucu提供的C函数;如果您已经熟悉C语言中的curses编程,那么将这些知识转移到Python非常容易。最大的区别是,通过合并不同的C函数,例如addstr(), mvaddstr()mvwaddstr()成一个addstr()方法。稍后你会看到这个更详细.

HOWTO介绍了使用curses和Python编写文本模式程序。它并不试图成为curses API的完整指南;请参阅有关ncurses的Python库指南部分和ncurses的C手册页。但是,它会给你基本的想法.

启动和结束curses应用程序

在执行任何操作之前,必须初始化curses。这是通过调用initscr()函数,它将确定终端类型,将任何所需的设置代码发送到终端,并创建各种内部数据结构。如果成功,initscr()返回表示entirescreen的窗口对象;这通常被称为stdscr在相应的C变量的名称之后

import curses
stdscr = curses.initscr()

通常,curses应用程序关闭自动回显键到屏幕,以便能够读取键并仅在某些情况下显示它们。这需要拨打noecho()function.

curses.noecho()

应用程序通常也需要立即对键做出反应,而不需要按下Enter键;这被称为cbreakmode,而不是通常的缓冲输入模式.

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()函数接受一个可调用的对象并进行上述初始化,如果存在colorsupport则也初始化颜色。wrapper()然后运行你提供的callable。一旦可调用的返回,wrapper()将恢复终端的原始状态。可调用的内容叫tryexcept捕获异常,恢复终端状态,然后重新引发异常。因此,您的终端在异常时不会处于有趣的状态,您将能够阅读异常的消息和追溯.

Windows和垫

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

stdscr返回的对象initscr()function是覆盖整个屏幕的awindow对象。许多程序可能只需要这个单一窗口,但您可能希望将屏幕划分为较小的窗口,以便分别重绘或清除它们。该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()窗口对象的方法来更新屏幕.

这是因为诅咒最初是用缓慢的300-baudterminal连接写的;使用这些终端,最小化重绘屏幕所需的时间非常重要。相反,curses会累积对屏幕的更改,并在您调用refresh()。例如,如果你的程序在窗口中显示一些文本然后清除窗口,就不需要发送原始文本,因为它们从来不可见.

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

垫是窗口的特例;它可以比实际的显示屏大,并且一次只显示一部分打击垫。创建一个垫子需要垫子的高度和宽度,同时刷新垫子需要给屏幕区域的坐标,垫子的一个部分将被显示.

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()call显示矩形中垫的一部分,从坐标(5,5)到屏幕上的坐标(20,75)。显示部分的左上角是垫上的坐标(0,0)。除了差别之外,打击垫与普通窗口完全相同,支持相同方法.

如果屏幕上有多个窗口和打击垫,则可以通过更有效的方式更新屏幕并防止屏幕上的每个部分都出现恼人的屏幕闪烁。refresh()实际上有两件事:

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

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

显示文字

从C程序员的角度来看,诅咒有时可能看起来像迷失的功能迷宫,完全不同。例如,addstr()stdscr窗口,而mvaddstr()在显示字符串之前首先移动到给定的y,xcoordinate。waddstr()就像addstr(),但是允许指定一个窗口而不是默认使用stdscrmvwaddstr()允许指定botha窗口和坐标.

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

形成 描述
str要么 ch 显示字符串str或者字符ch在当前位置
str要么 ch, attr 显示字符串str字符ch,使用属性attr在当前位置
y, x, strch 移动到位置y,x在窗口内,显示strch
y, x, strch, attr 移动到位置y,x在窗口内,并显示strch,使用属性attr

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

addstr()方法将Python字符串orbytestring作为要显示的值。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为真时,thecurses库将尝试抑制闪烁的光标,你不必担心将它留在奇数位置.

属性和颜色

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

属性是一个整数,每个位代表一个不同的属性。您可以尝试显示具有多个属性bitsset的文本,但curses不保证所有可能的组合都可用,或者它们在视觉上都是不同的。这取决于终端的使用能力,所以最安全的是坚持这里列出的最常用的属性.

Attribute 描述
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控制台,其次是colorxterms.

要使用颜色,你必须调用start_color()函数,然后调用initscr()来初始化默认颜色集(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,toforeground color f和背景颜色b。颜色对0硬连接到白色黑色,不能更改.

颜色编号,并且start_color()在激活颜色模式时初始化8个基本颜色。它们是:0:黑色,1:红色,2:绿色,3:黄色,4:蓝色,5:品红色,6:青色,7:白色。curses模块为每种颜色定义命名常量:curses.COLOR_BLACK, curses.COLOR_RED,依此类推.

把所有这些放在一起。要在whitebackground上将颜色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()上设置一个计时器;如果在指定的延迟(以十分之一秒为单位)内没有输入可用,则诅咒会引发异常.

getch()方法返回一个整数;如果它在0到255之间,则表示按下的键的ASCII码。大于255的值是特殊键,例如Page Up,Home或光标键。您可以将返回的值与curses.KEY_PPAGE,curses.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模块提供了一个文本框,支持类似于Ecs的键绑定。Textbox类的各种方法支持使用inputvalidation进行编辑并使用或不使用空格来收集编辑结果。这是一个例子:

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未涵盖一些高级主题,例如,读取屏幕的内容或从xterminstance捕获鼠标事件,但curses模块的Python库页面现在已经完成了。你应该再浏览一下

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

由于curses API太大,因此Python界面中不支持某些函数。通常这不是因为它们很难实现,而是因为还没有人需要它们。此外,Pythondoes尚不支持与ncurses相关的菜单库.Patches添加对这些的支持将是受欢迎的;“Python开发人员指南”允许更多关于向Python提交补丁的信息.

  • 使用NCURSES编写程序:C程序员的冗长教程.
  • ncurses手册页
  • ncurses FAQ
  • “使用诅咒……不要发誓”:使用诅咒或Urwid控制终端的PyCon 2013谈话的视频.
  • “使用Urwid进行控制台应用程序”:PyCon CA 2012的视频演示了一些使用Urwid编写的应用程序