用 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.LINEScurses.COLS 分別代表了 yx 方向上的尺寸。合理的坐標(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í)際上做了兩件事:

  1. 調(diào)用每個窗口的 noutrefresh() 方法來更新一個表達(dá)屏幕期望狀態(tài)的底層的數(shù)據(jù)結(jié)構(gòu)。

  2. 調(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)的 stdscrmvwaddstr() 允許同時(shí)指定一個窗口和一個坐標(biāo)。

幸運(yùn)的是,Python 接口隱藏了所有這些細(xì)節(jié)。stdscr 和其他任何窗口一樣是一個窗口對象,并且諸如 addstr() 之類的方法接受多種參數(shù)形式。通常有四種形式。

形式

描述

strch

在當(dāng)前位置顯示字符串 str 或字符 ch

strch, attr

在當(dāng)前位置使用 attr 屬性顯示字符串 str 或字符 ch

y, x, strch

移動到窗口內(nèi)的 y,x 位置,并顯示 strch

y, x, strch, attr

移至窗口內(nèi)的 y,x 位置,并使用 attr 屬性顯示 strch

屬性允許以突出顯示形態(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)妥的方式是只采用最常見的有效屬性,見下表。

屬性

描述

A_BLINK

閃爍文本

A_BOLD

超亮或粗體文本

A_DIM

半明亮文本

A_REVERSE

反相顯示文本

A_STANDOUT

可用的最佳突出顯示模式

A_UNDERLINE

帶下劃線的文本

所以,為了在屏幕頂部顯示一個反相的狀態(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_HOMEcurses.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ǔ)丁的詳情。