用 Python 進(jìn)行 Curses 編程?
- 作者
A.M. Kuchling, Eric S. Raymond
- 發(fā)布版本
2.04
摘要
本文檔介紹了如何使用 curses
擴(kuò)展模塊控制文本模式的顯示。
curses 是什么??
curses 庫為基于文本的終端提供了獨(dú)立于終端的屏幕繪制和鍵盤處理功能;這些終端包括 VT100,Linux 控制臺以及各種程序提供的模擬終端。顯示終端支持各種控制代碼以執(zhí)行常見的操作,例如移動光標(biāo),滾動屏幕和擦除區(qū)域。不同的終端使用相差很大的代碼,并且往往有自己的小怪癖。
在普遍使用圖形顯示的世界中,人們可能會問“為什么自找要麻煩”?畢竟字符單元顯示終端確實(shí)是一種過時(shí)的技術(shù),但是在某些領(lǐng)域中,能夠用它們做花哨的事情仍然很有價(jià)值。一個小眾市場是在不運(yùn)行 X server 的小型或嵌入式 Unix 上。另一個是在提供圖形支持之前,可能需要運(yùn)行的工具,例如操作系統(tǒng)安裝程序和內(nèi)核配置程序。
curses 庫提供了相當(dāng)基礎(chǔ)的功能,為程序員提供了包含多個非重疊文本窗口的顯示的抽象。窗口的內(nèi)容可以通過多種方式更改---添加文本,擦除文本,更改其外觀---以及curses庫將確定需要向終端發(fā)送哪些控制代碼以產(chǎn)生正確的輸出。 curses并沒有提供諸多用戶界面概念,例如按鈕,復(fù)選框或?qū)υ捒?。如果需要這些功能,請考慮用戶界面庫,例如 Urwid 。
curses 庫最初是為BSD Unix 編寫的。 后來 AT&T 的Unix System V 版本加入了許多增強(qiáng)功能和新功能。如今BSD curses已不再維護(hù),被ncurses取代,ncurses是 AT&T 接口的開源實(shí)現(xiàn)。如果使用的是 Linux 或 FreeBSD 等開源Unix系統(tǒng),則幾乎肯定會使用ncurses。由于大多數(shù)當(dāng)前的商業(yè)Unix版本都基于System V代碼,因此這里描述的所有功能可能都可用。但是,某些專有Unix所帶來的較早版本的curses可能無法支持所有功能。
The Windows version of Python doesn't include the curses
module. A ported version called UniCurses is available.
Python 的 curses 模塊?
此 Python 模塊相當(dāng)簡單地封裝了 curses 提供的 C 函數(shù);如果你已經(jīng)熟悉在 C 語言中使用 curses 編程,把這些知識轉(zhuǎn)移的 Python 是非常容易的。最大的差異在于 Python 中的接口通過把不同的 C 函數(shù)合并來讓事情變得更簡單,比如 addstr()
、mvaddstr()
和 mvwaddstr()
三個 C 函數(shù)被并入 addstr()
這一個方法。下文中會描述更多的細(xì)節(jié)。
本 HOWTO 是關(guān)于使用 curses 和 Python 編寫文本模式程序的概述。它并不被設(shè)計(jì)為一個 curses API 的完整指南;如需完整指南,請參見 ncurses 的 Python 庫指南章節(jié)和 ncurses 的 C 手冊頁。相對地,本 HOWTO 將會給你一些基本思路。
開始和結(jié)束 curses 應(yīng)用程序?
在做任何事情之前,curses 必須先被初始化??梢酝ㄟ^調(diào)用函數(shù) initscr()
來實(shí)現(xiàn),它將查明終端的類型,向終端發(fā)送任何必須的設(shè)置代碼,并創(chuàng)建多種內(nèi)部數(shù)據(jù)結(jié)構(gòu)。如果此操作成功,initscr()
將會返回一個代表整個屏幕的窗口對象;它通常會遵循對應(yīng)的 C 變量名被稱作 stdscr
:
import curses
stdscr = curses.initscr()
使用 curses 的應(yīng)用程序通常會關(guān)閉按鍵自動上屏,目的是讀取按鍵并只在特定情況下展示它們。這需要調(diào)用函數(shù) noecho()
:
curses.noecho()
應(yīng)用程序也會廣泛地需要立即響應(yīng)按鍵,而不需要按下回車鍵;這被稱為 “cbreak” 模式,與通常的緩沖輸入模式相對:
curses.cbreak()
終端通常會以多字節(jié)轉(zhuǎn)義序列的形式返回特殊按鍵,比如光標(biāo)鍵和導(dǎo)航鍵比如 Page Up 鍵和 Home 鍵。盡管你可以編寫你的程序來應(yīng)對這些序列,curses 能夠代替你做到這件事,返回一個特殊值比如 curses.KEY_LEFT
。為了讓 curses 做這項(xiàng)工作,你需要啟用 keypad 模式:
stdscr.keypad(True)
終止一個 curses 應(yīng)用程序比建立一個容易得多,你只需要調(diào)用:
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
來還原對終端作出的 curses 友好設(shè)置。然后,調(diào)用函數(shù) endwin()
來將終端還原到它的原始操作模式:
curses.endwin()
調(diào)試一個 curses 應(yīng)用程序時(shí)常會發(fā)生,一個應(yīng)用程序還未能還原終端到原本的狀態(tài)就意外退出了,這會攪亂你的終端。在 Python 中這常常會發(fā)生在你的代碼中有 bug 并引發(fā)了一個未捕獲的異常。當(dāng)你嘗試輸入時(shí)按鍵不會上屏,這使得使用終端變得困難。
在 Python 中你可以避免這些復(fù)雜問題并讓調(diào)試變得更簡單,只需要導(dǎo)入 curses.wrapper()
函數(shù)并像這樣使用它:
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)
函數(shù) wrapper()
接受一個可調(diào)用對象并首先進(jìn)行上述初始化過程,在終端支持著色時(shí)還會初始化顏色。接著 wrapper()
運(yùn)行你提供的可調(diào)用對象。當(dāng)該可調(diào)用對象返回時(shí),wrapper()
會還原終端到初始狀態(tài)。該可調(diào)用對象會在 try
...except
這樣的結(jié)構(gòu)內(nèi)被調(diào)用,當(dāng)它捕獲到異常時(shí),會先還原終端再重新引發(fā)這個異常。所以你的終端不會因?yàn)楫惓6涣粼谝粋€搞笑的狀態(tài),你也可以正常閱讀異常消息和回溯信息。
窗口和面板?
窗口是 curses 中的基本抽象。一個窗口對象表示了屏幕上的一個矩形區(qū)域,并且提供方法來顯示文本、擦除文本、允許用戶輸入字符串等等。
函數(shù) initscr()
返回的 stdscr
對象覆蓋整個屏幕。許多程序可能只需要這一個窗口,但你可能希望把屏幕分割為多個更小的窗口,來分別重繪或者清除它們。函數(shù) newwin()
根據(jù)給定的尺寸創(chuàng)建一個新窗口,并返回這個新的窗口對象:
begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)
注意 curses 使用的坐標(biāo)系統(tǒng)與尋常的不同。坐標(biāo)始終是以 y,x 的順序傳遞,并且左上角是坐標(biāo) (0,0)。這打破了正常的坐標(biāo)處理約定,即 x 坐標(biāo)在前。這是一個與其他計(jì)算機(jī)應(yīng)用程序糟糕的差異,但這從 curses 最初被編寫出來就已是它的一部分,現(xiàn)在想要修改它已為時(shí)已晚。
你的應(yīng)用程序能夠查明屏幕的尺寸,curses.LINES
和 curses.COLS
分別代表了 y 和 x 方向上的尺寸。合理的坐標(biāo)應(yīng)位于 (0,0)
到 (curses.LINES - 1, curses.COLS - 1)
范圍內(nèi)。
當(dāng)你調(diào)用一個方法來顯示或擦除文本時(shí),效果并不會立即顯示。相反,你必須調(diào)用窗口對象的 refresh()
方法來更新屏幕。
這是因?yàn)?curses 最初是為 300 波特的龜速終端連接編寫的;在這些終端上,壓制重繪屏幕的時(shí)間就非常重要。相對地,當(dāng)你調(diào)用 refresh()
時(shí),curses 會累積屏幕的修改并以效率最高的方式顯示它們。打個比方,如果你的程序在一個窗口內(nèi)顯示一些文本然后清楚了這個窗口,那么這些原始文本不需要被發(fā)送,因?yàn)樗鼈兩踔敛辉鼙豢匆姟?/p>
在實(shí)踐中,顯式地告訴 curses 來重繪一個窗口并不會太復(fù)雜化 curses 編程。大部分程序會顯示一堆內(nèi)容然后等待按鍵或者其他某些用戶側(cè)動作。你要做的事情就是,保證屏幕在暫停并等待用戶輸入前被重繪,只需要先調(diào)用 stdscr.refresh()
或者其他相關(guān)窗口的 refresh()
方法。
一個面板是一種特殊的窗口,它可以比實(shí)際的顯示屏幕更大,并且能只顯示它的一部分。創(chuàng)建面板需要指定面板的高度和寬度,但刷新一個面板需要給出屏幕坐標(biāo)和面板的需要顯示的局部。
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()
調(diào)用會在屏幕坐標(biāo) (5,5) 到坐標(biāo) (20,75) 的矩形范圍內(nèi)顯示面板的一個部分,被顯示的部分在面板上的坐標(biāo)是 (0,0)。除了上述差異,面板與常規(guī)的窗口相同,也支持相同的方法。
如果你在屏幕上有多個窗口和面板,有一個更有效率的方法來更新窗口,避免每個部分單獨(dú)更新時(shí)煩人的屏幕閃爍。refresh()
實(shí)際上做了兩件事:
調(diào)用每個窗口的
noutrefresh()
方法來更新一個表達(dá)屏幕期望狀態(tài)的底層的數(shù)據(jù)結(jié)構(gòu)。調(diào)用函數(shù)
doupdate()
來改變物理屏幕來符合這個數(shù)據(jù)結(jié)構(gòu)中記錄的期望狀態(tài)。
你可以改為調(diào)用在多個窗口上 noutrefresh()
方法來更新該數(shù)據(jù)結(jié)構(gòu),然后調(diào)用函數(shù) doupdate()
來更新屏幕。
顯示文字?
從一名 C 語言程序員的視角來看,curses 有時(shí)看起來就像是一堆略有差異的函數(shù)組成的扭曲迷宮。舉個例子,addstr()
在 stdscr
窗口的當(dāng)前光標(biāo)位置顯示一個字符串,而 mvaddstr()
則是先移動到一個給定的 y,x 坐標(biāo)再顯示字符串。waddstr()
與 addstr()
類似,但允許指定一個窗口而非默認(rèn)的 stdscr
。mvwaddstr()
允許同時(shí)指定一個窗口和一個坐標(biāo)。
幸運(yùn)的是,Python 接口隱藏了所有這些細(xì)節(jié)。stdscr
和其他任何窗口一樣是一個窗口對象,并且諸如 addstr()
之類的方法接受多種參數(shù)形式。通常有四種形式。
形式 |
描述 |
---|---|
str 或 ch |
在當(dāng)前位置顯示字符串 str 或字符 ch |
str 或 ch, attr |
在當(dāng)前位置使用 attr 屬性顯示字符串 str 或字符 ch |
y, x, str 或 ch |
移動到窗口內(nèi)的 y,x 位置,并顯示 str 或 ch |
y, x, str 或 ch, attr |
移至窗口內(nèi)的 y,x 位置,并使用 attr 屬性顯示 str 或 ch |
屬性允許以突出顯示形態(tài)顯示文本,比如加粗、下劃線、反相或添加顏色。這些屬性將來下一小節(jié)細(xì)說。
The addstr()
method takes a Python string or
bytestring as the value to be displayed. The contents of bytestrings
are sent to the terminal as-is. Strings are encoded to bytes using
the value of the window's encoding
attribute; this defaults to
the default system encoding as returned by locale.getencoding()
.
方法 addch()
接受一個字符,可以是長度為 1 的字符串,長度為 1 的字節(jié)串或者一個整數(shù)。
對于特殊擴(kuò)展字符有一些常量,這些常量是大于 255 的整數(shù)。比如,ACS_PLMINUS
是一個 “加減” 符號,ACS_ULCORNER
是一個框的左上角(方便繪制邊界)。你也可以使用正確的 Unicode 字符。
窗口會記住上次操作之后光標(biāo)所在位置,所以如果你忽略 y,x 坐標(biāo),字符串和字符會出現(xiàn)在上次操作結(jié)束的位置。你也可以通過 move(y,x)
的方法來移動光標(biāo)。因?yàn)橐恍┙K端始終會顯示一個閃爍的光標(biāo),你可能會想要保證光標(biāo)處于一些不會讓人感到分心的位置。在看似隨機(jī)的位置出現(xiàn)一個閃爍的光標(biāo)會令人非常迷惑。
如果你的應(yīng)用程序完全不需要一個閃爍的光標(biāo),你可以調(diào)用 curs_set(False)
來使它隱形。為與舊版本 curses 的兼容性的關(guān)系,有函數(shù) leaveok(bool)
作為 curs_set()
的等價(jià)替換。如果 bool 是真值,curses 庫會嘗試移除閃爍光標(biāo),并且你也不必?fù)?dān)心它會留在一些奇怪的位置。
屬性和顏色?
字符可以以不同的方式顯示?;谖谋镜膽?yīng)用程序常常以反相顯示狀態(tài)行,一個文本查看器可能需要突出顯示某些單詞。為了支持這種用法,curses 允許你為屏幕上的每個單元指定一個屬性值。
屬性值是一個整數(shù),它的每一個二進(jìn)制位代表一個不同的屬性。你可以嘗試以多種不屬性位組合來顯示文本,但 curses 不保證所有的組合都是有效的,或者看上去有明顯不同。這一點(diǎn)取決于用戶終端的能力,所以最穩(wěn)妥的方式是只采用最常見的有效屬性,見下表。
屬性 |
描述 |
---|---|
|
閃爍文本 |
|
超亮或粗體文本 |
|
半明亮文本 |
|
反相顯示文本 |
|
可用的最佳突出顯示模式 |
|
帶下劃線的文本 |
所以,為了在屏幕頂部顯示一個反相的狀態(tài)行,你可以這么編寫:
stdscr.addstr(0, 0, "Current mode: Typing mode",
curses.A_REVERSE)
stdscr.refresh()
curses 庫還支持在提供了顏色功能的終端上顯示顏色的功能。最常見的提供顏色的終端很可能是 Linux 控制臺,采用了 xterms 配色方案。
為了使用顏色,你必須在調(diào)用完函數(shù) initscr()
后盡快調(diào)用函數(shù) start_color()
,來初始化默認(rèn)顏色集 (curses.wrapper()
函數(shù)自動完成了這一點(diǎn))。 當(dāng)它完成后,如果使用中的終端支持顯示顏色, has_colors()
會返回真值。 (注意:curses 使用美式拼寫 “color”,而不是英式/加拿大拼寫 “colour”。如果你習(xí)慣了英式拼寫,你需要避免自己在這些函數(shù)上拼寫錯誤。)
curses 庫維護(hù)一個有限數(shù)量的顏色對,包括一個前景(文本)色和一個背景色。你可以使用函數(shù) color_pair()
獲得一個顏色對對應(yīng)的屬性值。它可以通過按位或運(yùn)算與其他屬性,比如 A_REVERSE
組合。但再說明一遍,這種組合并不保證在所有終端上都有效。
一個樣例,用 1 號顏色對顯示一行文本:
stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()
如前所述, 顏色對由前景色和背景色組成。 init_pair(n, f, b)
函數(shù)可改變顏色對 n 的定義 為前景色 f 和背景色 b。 顏色對 0 硬編碼為黑底白字,不能改變。
顏色已經(jīng)被編號,并且當(dāng)其激活 color 模式時(shí) start_color()
會初始化 8 種基本顏色。 它們是: 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan 和 7:white。 curses
模塊為這些顏色定義了相應(yīng)的名稱常量: curses.COLOR_BLACK
, curses.COLOR_RED
等等。
讓我們來做個綜合練習(xí)。 要將顏色 1 改為紅色文本白色背景,你應(yīng)當(dāng)調(diào)用:
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)
當(dāng)你改變一個顏色對時(shí),任何已經(jīng)使用該顏色對來顯示的文本將會更改為新的顏色。 你還可以這樣來顯示新顏色的文本:
stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))
某些非?;ㄉ诘慕K端可以將實(shí)際顏色定義修改為給定的 RGB 值。 這允許你將通常為紅色的 1 號顏色改成紫色或藍(lán)色或者任何你喜歡的顏色。 不幸的是,Linux 控制臺不支持此特性,所以我無法嘗試它,也無法提供任何示例。 想要檢查你的終端是否能做到你可以調(diào)用 can_change_color()
,如果有此功能則它將返回 True
。 如果你幸運(yùn)地?fù)碛幸粋€如此優(yōu)秀的終端,請查詢你的系統(tǒng)的幫助頁面來了解詳情。
用戶輸入?
C curses 庫提供了非常簡單的輸入機(jī)制。 Python 的 curses
模塊添加了一個基本的文本輸入控件。 (其他的庫例如 Urwid 擁有更豐富的控件集。)
有兩個方法可以從窗口獲取輸入:
getch()
會刷新屏幕然后等待用戶按鍵,如果之前調(diào)用過echo()
還會顯示所按的鍵。 你還可以選擇指定一個坐標(biāo)以便在暫停之前讓光標(biāo)移動到那里。getkey()
將做同樣的事但是會把整數(shù)轉(zhuǎn)換為字符串。 每個字符將返回為長度為 1 個字符的字符串,特殊鍵例如函數(shù)鍵將返回包含鍵名的較長字符串例如KEY_UP
或^G
。
使用 nodelay()
窗口方法可以做到不等待用戶。 在 nodelay(True)
之后,窗口的 getch()
和 getkey()
將成為非阻塞的。 為表明輸入未就緒,getch()
會返回 curses.ERR
(值為 -1) 而 getkey()
會引發(fā)異常。 此外還有 halfdelay()
函數(shù),它可被用來(實(shí)際地)在每個 getch()
上設(shè)置一個計(jì)時(shí)器;如果在指定的延遲內(nèi)沒有輸入可用(以十分之一秒為單位),curses 將引發(fā)異常。
getch()
方法返回一個整數(shù);如果數(shù)值在 0 到 255 之間,它代表所按下鍵的 ASCII 碼。 大于 255 的值為特殊鍵例如 Page Up, Home 或方向鍵等。 你可以將返回的值與 curses.KEY_PPAGE
, curses.KEY_HOME
或 curses.KEY_LEFT
等常量做比較。 你的程序主循環(huán)看起來可能是這樣:
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 類成員函數(shù),它們接受整數(shù)或長度為 1 個字符的字符串參數(shù);這些函數(shù)在為這樣的循環(huán)編寫更具可讀性的測試時(shí)可能會很有用。 它還提供了一些轉(zhuǎn)換函數(shù),它們接受整數(shù)或長度為 1 個字符的字符串參數(shù)并返回同樣的類型。 例如,curses.ascii.ctrl()
返回與其參數(shù)相對應(yīng)的控制字符。
還有一個可以提取整個字符串的方法 getstr()
。 它并不經(jīng)常被使用,因?yàn)樗墓δ芟喈?dāng)受限;可用的編輯鍵只有 Backspace 和 Enter 鍵,它們會結(jié)束字符串。 也可以選擇限制為固定數(shù)量的字符。
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
類的各種方法支持帶輸入驗(yàn)證的編輯及包含或不包含末尾空格地收集編輯結(jié)果。 下面是一個例子:
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
的庫文檔了解更多細(xì)節(jié)。
更多的信息?
本 HOWTO 沒有涵蓋一些進(jìn)階主題,例如讀取屏幕的內(nèi)容或從 xterm 實(shí)例捕獲鼠標(biāo)事件等,但是 curses
模塊的 Python 庫文檔頁面現(xiàn)在已相當(dāng)完善。 接下來你應(yīng)當(dāng)去瀏覽一下其中的內(nèi)容。
如果你對 curses 函數(shù)的細(xì)節(jié)行為有疑問,請查看你的 curses 實(shí)現(xiàn)版本的說明頁面,不論它是 ncurses 還是特定 Unix 廠商的版本。 說明頁面將記錄任何具體問題,并提供所有函數(shù)、屬性以及可用 ACS_*
字符的完整列表。
由于 curses API 是如此的龐大,某些函數(shù)并不被 Python 接口所支持。 這往往不是因?yàn)樗鼈冸y以實(shí)現(xiàn),而是因?yàn)檫€沒有人需要它們。 此外,Python 尚不支持與 ncurses 相關(guān)聯(lián)的菜單庫。 歡迎提供添加這些功能的補(bǔ)??;請參閱 Python 開發(fā)者指南 了解有關(guān)為 Python 提交補(bǔ)丁的詳情。
使用 NCURSES 編寫程序: 一篇面向 C 程序員的詳細(xì)教程。
ncurses 手冊主頁 <https://linux.die.net/man/3/ncurses>`_
ncurses 常見問題 <http://invisible-island.net/ncurses/ncurses.faq.html>`_
"使用 curses... 請勿爆粗": 一場有關(guān)使用 curses 或 Urwid 來控制終端的 PyCon 2013 演講的視頻。
"使用 Urwid 的控制臺應(yīng)用程序": 一場演示使用 Urwid 編寫應(yīng)用程序的 PyCon CA 2012 演講的視頻。