《curses模块官方教程》
作者:A.M Kuching, Eric S. Raymond
版本:2.04
摘要:本文档描述了如何使用curses扩展模块来控制文本模式的显示。
前言:什么是curses?
curses库提供了终端方式的屏幕显示和键盘处理能力,这是为了文本终端而建造的,例如包括,VT100s、Linux终端、和各种模拟终端的程序。在终端里显示要支持各种控制代码来执行共性的操作,例如移动光标、滚动屏幕、和清除区域显示。不同的终端会使用不一样的代号,并且常常具有它们自己的书写方式。
在图形化显示世界里,会给人带来一种无聊的感受。以字符为单位的终端显示确实是一项看似过时的技术,但终端显示能够带来许多许多有价值的好玩的事情。其中一点是在低功耗或嵌入式Unix环境中,我们不需要运行X服务器来消耗资源。另外一点是有许多工具要在显卡可用之前需要运行,例如系统的安装和内核的配置都是比显卡还要先具备运行能力。
curses库提供了非常基础的功能,给程序员提供了一种抽象显示技术,包括多个文本窗口不会重叠。一个窗口的内容可以有许多种方法来进行变化,例如,增加文本内容、擦除文本内容、改变文本显示。并且curses库会搞清楚需要发送什么样的控制代码给终端,从而产生正确的输出结果。curses不提供太多的用户接口概念,例如,按钮、勾选项、或对话框,如果你需要这些特性,可以考虑使用Urwid这种用户接口库。
curses库最初是为BSD Unix系统而写的,后来又为AT&T的Unix V版本系统而写,增加了许多强化和新功能。BSD的curses已经终止了维护,也被ncurses所代替,这是一个开源的AT&T接口部署库。如果你使用了一个开源Unix系统的话,例如Linux或FreeBSD系统,你的系统基本上都是使用ncurses库。由于最近的Unix商业版都是基于System V代码建立的,这里我们讲的所有函数都可以使用。比较老的curses版本会有一些不能使用的功能。
Python的Windows系统版本没有curses模块,而是使用一个名叫UniCurses的移植版本。也许你可以尝试一下由Fredrik Lundh所写的一个终端模块用在Windows系统的CMD上,该模块与curses使用的API不一样,但提供了光标地址文本输出,以及能够全面支持鼠标和键盘的输入。
简介:Python的curses模块
Python模块是非常容易打包C语言函数,如果你已经熟悉C语言中的curses编程的话,你会很容易把C语言的光标知识融汇到Python模块里。最不同的地方就是Python接口让你做事情时非常有效率,例如把C语言种的addstr()、mvaddstr()、和mvwaddstr()函数合并成一个Python的addstr()方法后,就变得使用简单许多了。在课程里你会看到更多细节。
本课程就是介绍如何使用curses和Python来书写文本模式的程序。我们不会介绍全部的curses的API内容,如果你想了解全部功能,请阅读Python官方文档,这样你会在ncurses部分和C手册部分了解到基本思想。
第一节课:启动和结束一个curses应用
在做任何事情之前,curses必须要完成初始化过程。通过调用initscr()函数来做这件事,该函数会确定终端的类型,发送任何需要的配置代码给终端,然后建立各种内部数据结构。如果初始化成功了,initscr()函数会返回一个窗口对象,是一种全屏模式,我们常常叫它stdscr,这个名字也是来自C语言种的变量名。
import curses
stdscr = curses.initscr()
通常curses应用都会关闭自动回应按键到屏幕上的功能,要想读取按键后只显示在某些情况下,那就需要调用noecho()函数来完成关闭功能。
curses.noecho()
许多应用也会共同采用对按键的立即响应,这样就不需要通过输入回车键来作为一个动作的终止符,这种工作方式名叫cbreak模式,这与缓存的输入模式正好相反。
curses.cbreak()
终端常常返回具体的按键,例如光标按键或方向键,例如上下左右、上一页、主页等按键,这些与多字节转义序列是一样的。在你写你的应用的同时,你会期望这类序列能够正确地处理,curses可以为你做这件事,返回一个具体的值,例如,curses.KEY_LEFT。要想让curses来做这件事,你就要开启键盘模式。
stdscr.keypad(True)
终止一个curses应用要比启动来说更简单,你只需要调用:
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
这样的逆向设置即可。然后再调用endwin()函数来把终端恢复成操作系统打开时的最初模式。
curses.endwin()
一个共性的问题是在调试curses应用时发生,那就是你的curses程序崩溃时没有恢复终端的初始状态,那么你的终端操作就无法如往常一样,你会感到终端出了问题,乱作一团。当你的代码中有bug时,以及抛出一个无法捕获的例外时,这种问题常常会出现在Python程序中。因为按键都无法再回应给屏幕了,当你按任何按键时屏幕都没有反应,那么你使用Shell时就会觉得痛苦。
在Python中要避免这些症状,并且让调试更容易,那就需要导入curses.wrapper()函数后使用它:
from curses import wrapper
def main(stdscr):
#清屏
stdscr.clear()
#当i是10的时候抛出ZeroDivisionError例外错误
for i in range(0, 11):
v = i-10
stdscr.addstr(i, 0, f'10 divided by {v} is {10/v}')
stdscr.refresh()
stdscr.getkey()
wrapper(main)
这里的warpper()函数得到一个可调用的对象后做了我们上面所描述过的初始化工作,如果支持颜色呈现的话,也会对颜色进行初始化。wrapper()函数负责运行提所提供的可调用对象。一旦调用对象执行了return语句,wrapper()函数会恢复终端的最初状态。可调用对象是在try/except语句结构中被调用的,所以确保了捕获例外,恢复终端初始状态,以及二次抛出例外的能力。因此你的终端不会进入一种你所不期望的状态中,并且你能够读取例外消息和回溯信息。
第二节课:窗口和面板
窗口在curses模块中都是基础的抽象对象。一个窗口对象代表了屏幕中的一个四边形区域,并且支持许多方法来显示文本、擦除文本,允许用户输入字符串。
stdscr对象是initscr()函数返回的一个结果,也就是一个窗口对象,涉及整个屏幕区域。许多程序也许只需要单个窗口,但你希望能够对一个窗口进行分屏,要想能够分别重新绘制或清屏,那就需要newwin()函数来建立一个新尺寸的窗口,返回一个新的窗口对象。
begin_x, bengin_y = 20, 7
height, width = 5, 40
win = curses.newwin(height, width, begin_y, begin_x)
注意curses库中使用的坐标系统与往常的不一样。对屏幕来说坐标一直都是按照y,x的顺序来代入的,一个窗口的左上角的坐标就是(0,0)。处理x作为第一项就不那么方便了。curses采用x作为第一项是因为第一次开发该库时就这样写的代码,现在再改已经太晚了,所以会与其它的计算机应用坐标表述循序不同。
你的应用通过使用curses.LINES和curses.COLS变量来确定屏幕的尺寸,对应的代入值也是y和x的尺寸。那么坐标(0,0)的合法坐标就要变成(curses.LINES - 1, cursesCOLS - 1)
当你调用一个方法去显示或清除文本时,效果不会立即显示。相反你必须调用窗口对象的refresh()方法来更新屏幕显示效果。
这是因为curses最初使用了慢速300波特终端连接方式来写作,这一点要记载你的脑子里。使用这些终端刷新速度能够让重新绘制屏幕所需的时间最小化,那么等待的时间也就最少,这一点是非常重要的。所以使用curses窗口对象的refresh()方法对屏幕和显示的累积变化是最有效率的一种方式。例如,如果你的程序在一个窗口中显示一些文本之后清除的话,就不再需要发送原来的文本内容了,因为原来的文本已经不会再显示了。
在实际中,明确地告诉curses来重新绘制一个窗口不是那么复杂的编程,因为curses会为你做许多工作。大多数程序在这方面都会遇到许多麻烦,然后会暂停等待一个按键或其它用户的动作。而你所需要做的就是确保屏幕在暂停等待之前完成所有的重新绘制工作,只需要先调用stdscr.refresh()或相关窗口的refresh()方法即可。
面板是一种特殊的窗口情况,它可以比实际的显示屏幕区域更大,并且一次只能显示面板的一个部分。建立一个面板需要面板的高和宽尺寸,同时刷新一个面板需要提供屏幕区域上的坐标,这样才会作为面板中的一个子区域显示出来。
pad = curses.newpad(100, 100)
#下面的循环会用字母来填充面板区域。
# addch()方法会稍后解释。
for y in range(0, 99):
for x in range(0, 99):
pad.addch(y,x, ord('a')+(x*x + y*y)% 26)
#会在屏幕的中间显示一个面板区域。
#(0,0)是面板的左上角坐标。
#(5,5)是窗口的左上角坐标,面板的内容会填充的起始位。
#(20, 75)是窗口的右下角坐标,面板的内容会填充的结束位。
pad.refresh( 0,0, 5,5, 20,75)
面板对象使用refresh()方法来显示内容时,可以提供额外的6个坐标参数。内容会显示在从(5,5)到(20,75)这个四边形中。左上角的面板坐标(0,0)与(5,5)之间形成一个边界。另外面板与窗口对象类似,并且支持与窗口同样的方法。
如果在屏幕上你有多个窗口和面板的话,会有一个更有效率的方法来更新屏幕的显示内容,而且防止屏幕烦人的闪烁情况。实际上refresh()做了两件事:
1.调用了每个窗口的noutrefresh()方法来更新数据结构呈现在期望的屏幕状态中。
2.调用了doupdate()函数来改变物理屏幕,从而与数据结构中期望的记录状态相匹配。
在一些窗口上来调用noutrefresh()会更新数据结构,然后调用doupdate()来更新屏幕显示内容。
程序示例:
win = curses.newwin(17, 85)
def newpad(win):
pad = curses.newpad(15, 79)
for y in range(0, 14):
for x in range(0, 79):
pad.addch(y,x, ord('a')+(x*x + y*y)% 26)
pad.refresh(0,0, 1,5, 16,79)
win.refresh()
win.getkey()
wrapper(newpad)
第三节课:显示文本内容
从C语言编程者的角度来看,curses库有时候看起来像微调过的迷人函数组,所有微妙的不同之处都可以体现出来。例如:
addstr()会把一个字符串内容显示在stdscr窗口中的当前光标位置上,
mvaddstr()会根据提供的y,x坐标来移动光标,之后再显示字符串内容。
waddstr()就像addstr()一样,不同之处在于允许描述一个窗口,而不是使用默认的stdscr窗口对象。
mvwaddstr()允许描述窗口对象和一个坐标。
Python接口的好处是隐藏了所有细节。stdscr是一个窗口对象,并且许多对象的方法例如addstr()都可以接收多个参数。通常采用四种形式:
1. str 或 ch 形式是用来在当前光标位置上显示字符串str或字符ch
2. str 或 ch 和 attr 形式是使用attr属性在当前光标位置上显示字符串str或字符ch
3. y,x 和 str 或 ch 形式是在窗口里移动到光标的y,x位置上来显示字符串str或字符ch
4. y,x 和 str 或 ch 和 attr 形式是使用属性attr在窗口里移动到光标到y,x位置后,显示字符串str或字符ch
对象的许多属性允许在高亮形式中来显示文本,例如粗体字、下划线、反差或字体颜色。这个内容会在后面来详细解释。
其中addstr()方法得到一个Python字符串或字节字符串作为参数值来显示成文本内容。字节字符串的内容都是原文发送给终端。字符串都是经过编码成字节,使用窗口的encoding属性值进行编码,这个编码默认值会采用操作系统的编码locale.getpreferredencoding()
其中addch()方法得到一个字符,就是长度为1的一个字符串,或长度为1的字节字符串,又或是一个整数。
常数作为额外的字符时,这些常数都必须是大于255的整数。例如,ACS_PLMINUS 是一个正负号,ACS_ULCORNER 是绘制边界区域的左上角。你也可以使用对应的unicode字符。
窗口对象会记住光标上一次操作之后的光标位置,所以如果你离开了y,x坐标的话,字符串或字符会显示在上一次操作的位置的左边。你也可以用move(y,x)方法来移动光标位置。因为有些终端会一直显示一个闪烁着的光标,你要想确保光标放在某个位置上来保持不会分散注意力,毕竟不想让闪烁的光标到处乱跑让你感到困惑。
如果你的应用不需要一种闪烁着的光标的话,你可以调用curs_set(Fasle)来不显示光标。对于那些比较老旧的curses库版本来说需要保持一种兼容能力,此时会有一个leaveok(bool)函数,它与curs_set()是一样的作用。当参数bool的值是True的时候,curses库会故意压制闪烁着的光标,然后你就不需要担心光标跑到一个古怪的位置上了。
第四节课:属性与颜色
字符的显示有许多种不同的形式。在基于文本的应用中,状态行都通常显示成一种反差视觉形式,或者在一个文本阅读器中需要高亮某些单词。curses库支持这种功能,让你来对屏幕上的每个单元格描述一个属性。
一个属性就是一个整数,每个都代表了一个不同的属性。你可以试着用多个属性设置来显示文本,但curses库不能保证所有的组合都是可用的,也不能保证这些组合形式都能够具有不同的显示效果。那是因为使用的终端自身具备哪些能力也是不一样的,所以最安全地就是采用最共性的可用属性,那就是:
A_BLINK 属性让文本内容闪烁。
A_BOLD 属性提供额外的亮度或粗体文本内容。
A_DIM 属性是一半亮度的文本内容。
A_REVERSE 属性是反差视觉文本内容。
A_STATNDOUT 属性是最亮的高亮模式。
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()函数来获得一个颜色对儿的相应属性值,属性值可以与其它属性值做比特智能OR操作,例如,A_REVERSE 这个属性,但注意这种组合在所有终端上都不能保证一定有效。
显示一行文本内容使用了颜色对儿1的一个例子:
stdscr.addstr(“Pretty text“, curses.color_pair(1))
stdscr.refresh()
如同前面所讲的,一个颜色对是由前景色和背景色组成的。其中init_pair(n, f, b)函数是改变颜色对儿n、前景色f和背景色b参数值定义用的。颜色对儿0是黑底白字的硬编码,是无法改变的。
颜色对儿n参数值都是数字,并且当激活了颜色模式的时候,start_color()会初始化8个基础色,它们分别是:
0 黑色black
1 红色red
2 绿色green
3 黄色yellow
4 蓝色blue
5 品红magenta
6 青色cyan
7 白色white
在curses模块中定义了每个颜色的常量,例如,curses.COLOR_BLACK、curses.COLOR_RED,以此类推。
那么把这些放在一起来用的话,在白底上变成红色字体,你可以调用:
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值的话,你可以尝试这种功能。同时你也要参考一下系统中的man手册信息。
第五节课:用户输入
在C语言的curses库中提供了非常简单的输入机制。Python的curses模块增加了基础的文本输入组件。例如Urwid库提供了更多的扩展组件。
从一个窗口对象中来获得输入内容有两种方法:
1. getch()是刷新屏幕后等待用户来敲击键盘,如果前面调用了echo()的话,会显示按键的内容。你可以有选择地描述一个坐标位置后再执行等待输入。
2. getkey()是做同样的事情,但会把整数转化成字符串。每个字符都要返回成一个字符长度的字符串,并且特殊的按键,例如功能按键会返回更长的字符串内容,因为里面包含了一个按键的名字,例如,KEY_UP 或^G
使用窗口对象nodelay()方法时不会提供用户等待。在nodelay(True)调用之后,getch()和getkey()都会让窗口对象变成无阻碍形式。要发送没有输入的信号,getch()返回curses.ERR就是一种-1值,然后getkey()抛出一个例外。这里也有一个halfdelay()函数,它可以用来在每个getch()上设置一个延迟时间,如果没有输入行为的话,在给出的延迟内是可以让无输入状态保留一段时间,时间单位是0.1秒,之后curses再抛出一个例外。也就是说可以让无输入信号抛出例外的现象增加一定的时间延迟。
其中getch()方法返回一个整数值,如果值介于0到255之间的话,那就代表了按键的ASCII代号。返回值大于255的时候,说明特殊的按键被按下了,例如,PAGEUP、HOME、或光标方向键。你可以比较一下返回的值与常量curses.KEY_PPAGE、curses.KEY_HOME、或curses.KEY_LEFT,程序的main循环中的代码可以像下面的一样:
while True:
c = stdscr.getch()
if c == ord('p'):
PrintDocument()
elif c == ord('q'):
break #退出while循环
elif c == curses.KEY_HOME:
x = y = 0
在curses.ascii模块中提供了ASCII类成员关系函数,这些函数既可以接收整数,也可以接收1个字符串长的字符串参数值,这些函数在为这类循环书写更具有可读性的测试时是有帮助的。同时也提供了许多方便的函数,例如,curses.ascii.ctrl()返回CTRL按键的字符和相关组合键。
这里也有一个方法是获得一整个字符串内容,那就是getstr(),它并不常用,因为本身的功能是非常有限的。只会在编辑可用的后退按键和回车键时使用,这两个按键都是用来终止输入字符串用的。它也可以有选择地来限制一个固定数量的字符。
curses.echo()# 开启字符回应功能
#得到首行光标位上15个字符长的字符串
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+31+1)
stdscr.refresh()
box = Textbox(editwin)
#用户进入编辑模式直到按下Ctrl-G结束编辑状态
box.edit()
#获得文本框的内容结果
message = box.gather()
对于更多细节阅读curses.textpad模块的文档内容。
第六节课:更多的curses模块信息
我们的课程中没有涉及一些高级话题,例如从一个xterm实例读取屏幕的内容、或捕获鼠标操作事件,不过Python的curses模块文档内容现在已经相当完整。完成本课程你应该具备自己学会阅读的能力。
如果你还有对细节上的疑问,那么可以咨询一些手册来实现你想要的curses部署,虽然手册都是一种罗列形式,有的也许看起来很不容易理解,但都可以提供比较完整的函数、属性、ASC_*可用的内容给你。
由于curses库的API非常大,一些函数没有部署到Python接口中。因为部署到Python确实有一些困难,而且也很少有任需要使用到它们。同时Python也不支持与menu库相关的ncurses库。同时也欢迎有能力的人来补充这些库,如果你是C语言的专家或志愿者可以查看Python开发者指导来学习更多关于给Python提交补充库的内容。