用 Python 進(jìn)行 Curses 編程?

作者

A.M. Kuchling, Eric S. Raymond

發(fā)布版本

2.04

摘要

本文檔介紹了如何使用 curses 擴展模塊控制文本模式的顯示。

curses 是什么??

curses 庫為基于文本的終端提供了獨立于終端的屏幕繪制和鍵盤(pán)處理功能;這些終端包括 VT100,Linux 控制臺以及各種程序提供的模擬終端。顯示終端支持各種控制代碼以執行常見(jiàn)的操作,例如移動(dòng)光標,滾動(dòng)屏幕和擦除區域。不同的終端使用相差很大的代碼,并且往往有自己的小怪癖。

在普遍使用圖形顯示的世界中,人們可能會(huì )問(wèn)“為什么自找要麻煩”?畢竟字符單元顯示終端確實(shí)是一種過(guò)時(shí)的技術(shù),但是在某些領(lǐng)域中,能夠用它們做花哨的事情仍然很有價(jià)值。一個(gè)小眾市場(chǎng)是在不運行 X server 的小型或嵌入式 Unix 上。另一個(gè)是在提供圖形支持之前,可能需要運行的工具,例如操作系統安裝程序和內核配置程序。

curses 庫提供了相當基礎的功能,為程序員提供了包含多個(gè)非重疊文本窗口的顯示的抽象。窗口的內容可以通過(guò)多種方式更改---添加文本,擦除文本,更改其外觀(guān)---以及curses庫將確定需要向終端發(fā)送哪些控制代碼以產(chǎn)生正確的輸出。 curses并沒(méi)有提供諸多用戶(hù)界面概念,例如按鈕,復選框或對話(huà)框。如果需要這些功能,請考慮用戶(hù)界面庫,例如 Urwid 。

curses 庫最初是為BSD Unix 編寫(xiě)的。 后來(lái) AT&T 的Unix System V 版本加入了許多增強功能和新功能。如今BSD curses已不再維護,被ncurses取代,ncurses是 AT&T 接口的開(kāi)源實(shí)現。如果使用的是 Linux 或 FreeBSD 等開(kāi)源Unix系統,則幾乎肯定會(huì )使用ncurses。由于大多數當前的商業(yè)Unix版本都基于System V代碼,因此這里描述的所有功能可能都可用。但是,某些專(zhuān)有Unix所帶來(lái)的較早版本的curses可能無(wú)法支持所有功能。

The Windows version of Python doesn't include the curses module. A ported version called UniCurses is available.

Python 的 curses 模塊?

此 Python 模塊相當簡(jiǎn)單地封裝了 curses 提供的 C 函數;如果你已經(jīng)熟悉在 C 語(yǔ)言中使用 curses 編程,把這些知識轉移的 Python 是非常容易的。最大的差異在于 Python 中的接口通過(guò)把不同的 C 函數合并來(lái)讓事情變得更簡(jiǎn)單,比如 addstr()、mvaddstr()mvwaddstr() 三個(gè) C 函數被并入 addstr() 這一個(gè)方法。下文中會(huì )描述更多的細節。

本 HOWTO 是關(guān)于使用 curses 和 Python 編寫(xiě)文本模式程序的概述。它并不被設計為一個(gè) curses API 的完整指南;如需完整指南,請參見(jiàn) ncurses 的 Python 庫指南章節和 ncurses 的 C 手冊頁(yè)。相對地,本 HOWTO 將會(huì )給你一些基本思路。

開(kāi)始和結束 curses 應用程序?

在做任何事情之前,curses 必須先被初始化??梢酝ㄟ^(guò)調用函數 initscr() 來(lái)實(shí)現,它將查明終端的類(lèi)型,向終端發(fā)送任何必須的設置代碼,并創(chuàng )建多種內部數據結構。如果此操作成功,initscr() 將會(huì )返回一個(gè)代表整個(gè)屏幕的窗口對象;它通常會(huì )遵循對應的 C 變量名被稱(chēng)作 stdscr

import curses
stdscr = curses.initscr()

使用 curses 的應用程序通常會(huì )關(guān)閉按鍵自動(dòng)上屏,目的是讀取按鍵并只在特定情況下展示它們。這需要調用函數 noecho()

curses.noecho()

應用程序也會(huì )廣泛地需要立即響應按鍵,而不需要按下回車(chē)鍵;這被稱(chēng)為 “cbreak” 模式,與通常的緩沖輸入模式相對:

curses.cbreak()

終端通常會(huì )以多字節轉義序列的形式返回特殊按鍵,比如光標鍵和導航鍵比如 Page Up 鍵和 Home 鍵。盡管你可以編寫(xiě)你的程序來(lái)應對這些序列,curses 能夠代替你做到這件事,返回一個(gè)特殊值比如 curses.KEY_LEFT。為了讓 curses 做這項工作,你需要啟用 keypad 模式:

stdscr.keypad(True)

終止一個(gè) curses 應用程序比建立一個(gè)容易得多,你只需要調用:

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

來(lái)還原對終端作出的 curses 友好設置。然后,調用函數 endwin() 來(lái)將終端還原到它的原始操作模式:

curses.endwin()

調試一個(gè) curses 應用程序時(shí)常會(huì )發(fā)生,一個(gè)應用程序還未能還原終端到原本的狀態(tài)就意外退出了,這會(huì )攪亂你的終端。在 Python 中這常常會(huì )發(fā)生在你的代碼中有 bug 并引發(fā)了一個(gè)未捕獲的異常。當你嘗試輸入時(shí)按鍵不會(huì )上屏,這使得使用終端變得困難。

在 Python 中你可以避免這些復雜問(wèn)題并讓調試變得更簡(jiǎn)單,只需要導入 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() 接受一個(gè)可調用對象并首先進(jìn)行上述初始化過(guò)程,在終端支持著(zhù)色時(shí)還會(huì )初始化顏色。接著(zhù) wrapper() 運行你提供的可調用對象。當該可調用對象返回時(shí),wrapper() 會(huì )還原終端到初始狀態(tài)。該可調用對象會(huì )在 try...except 這樣的結構內被調用,當它捕獲到異常時(shí),會(huì )先還原終端再重新引發(fā)這個(gè)異常。所以你的終端不會(huì )因為異常而被留在一個(gè)搞笑的狀態(tài),你也可以正常閱讀異常消息和回溯信息。

窗口和面板?

窗口是 curses 中的基本抽象。一個(gè)窗口對象表示了屏幕上的一個(gè)矩形區域,并且提供方法來(lái)顯示文本、擦除文本、允許用戶(hù)輸入字符串等等。

函數 initscr() 返回的 stdscr 對象覆蓋整個(gè)屏幕。許多程序可能只需要這一個(gè)窗口,但你可能希望把屏幕分割為多個(gè)更小的窗口,來(lái)分別重繪或者清除它們。函數 newwin() 根據給定的尺寸創(chuàng )建一個(gè)新窗口,并返回這個(gè)新的窗口對象:

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 坐標在前。這是一個(gè)與其他計算機應用程序糟糕的差異,但這從 curses 最初被編寫(xiě)出來(lái)就已是它的一部分,現在想要修改它已為時(shí)已晚。

你的應用程序能夠查明屏幕的尺寸,curses.LINEScurses.COLS 分別代表了 yx 方向上的尺寸。合理的坐標應位于 (0,0)(curses.LINES - 1, curses.COLS - 1) 范圍內。

當你調用一個(gè)方法來(lái)顯示或擦除文本時(shí),效果并不會(huì )立即顯示。相反,你必須調用窗口對象的 refresh() 方法來(lái)更新屏幕。

這是因為 curses 最初是為 300 波特的龜速終端連接編寫(xiě)的;在這些終端上,壓制重繪屏幕的時(shí)間就非常重要。相對地,當你調用 refresh() 時(shí),curses 會(huì )累積屏幕的修改并以效率最高的方式顯示它們。打個(gè)比方,如果你的程序在一個(gè)窗口內顯示一些文本然后清楚了這個(gè)窗口,那么這些原始文本不需要被發(fā)送,因為它們甚至不曾能被看見(jiàn)。

在實(shí)踐中,顯式地告訴 curses 來(lái)重繪一個(gè)窗口并不會(huì )太復雜化 curses 編程。大部分程序會(huì )顯示一堆內容然后等待按鍵或者其他某些用戶(hù)側動(dòng)作。你要做的事情就是,保證屏幕在暫停并等待用戶(hù)輸入前被重繪,只需要先調用 stdscr.refresh() 或者其他相關(guān)窗口的 refresh() 方法。

一個(gè)面板是一種特殊的窗口,它可以比實(shí)際的顯示屏幕更大,并且能只顯示它的一部分。創(chuàng )建面板需要指定面板的高度和寬度,但刷新一個(gè)面板需要給出屏幕坐標和面板的需要顯示的局部。

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() 調用會(huì )在屏幕坐標 (5,5) 到坐標 (20,75) 的矩形范圍內顯示面板的一個(gè)部分,被顯示的部分在面板上的坐標是 (0,0)。除了上述差異,面板與常規的窗口相同,也支持相同的方法。

如果你在屏幕上有多個(gè)窗口和面板,有一個(gè)更有效率的方法來(lái)更新窗口,避免每個(gè)部分單獨更新時(shí)煩人的屏幕閃爍。refresh() 實(shí)際上做了兩件事:

  1. 調用每個(gè)窗口的 noutrefresh() 方法來(lái)更新一個(gè)表達屏幕期望狀態(tài)的底層的數據結構。

  2. 調用函數 doupdate() 來(lái)改變物理屏幕來(lái)符合這個(gè)數據結構中記錄的期望狀態(tài)。

你可以改為調用在多個(gè)窗口上 noutrefresh() 方法來(lái)更新該數據結構,然后調用函數 doupdate() 來(lái)更新屏幕。

顯示文字?

從一名 C 語(yǔ)言程序員的視角來(lái)看,curses 有時(shí)看起來(lái)就像是一堆略有差異的函數組成的扭曲迷宮。舉個(gè)例子,addstr()stdscr 窗口的當前光標位置顯示一個(gè)字符串,而 mvaddstr() 則是先移動(dòng)到一個(gè)給定的 y,x 坐標再顯示字符串。waddstr()addstr() 類(lèi)似,但允許指定一個(gè)窗口而非默認的 stdscr。mvwaddstr() 允許同時(shí)指定一個(gè)窗口和一個(gè)坐標。

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

形式

描述

strch

在當前位置顯示字符串 str 或字符 ch

strch, attr

在當前位置使用 attr 屬性顯示字符串 str 或字符 ch

y, x, strch

移動(dòng)到窗口內的 y,x 位置,并顯示 strch

y, x, strch, attr

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

屬性允許以突出顯示形態(tài)顯示文本,比如加粗、下劃線(xiàn)、反相或添加顏色。這些屬性將來(lái)下一小節細說(shuō)。

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() 接受一個(gè)字符,可以是長(cháng)度為 1 的字符串,長(cháng)度為 1 的字節串或者一個(gè)整數。

對于特殊擴展字符有一些常量,這些常量是大于 255 的整數。比如,ACS_PLMINUS 是一個(gè) “加減” 符號,ACS_ULCORNER 是一個(gè)框的左上角(方便繪制邊界)。你也可以使用正確的 Unicode 字符。

窗口會(huì )記住上次操作之后光標所在位置,所以如果你忽略 y,x 坐標,字符串和字符會(huì )出現在上次操作結束的位置。你也可以通過(guò) move(y,x) 的方法來(lái)移動(dòng)光標。因為一些終端始終會(huì )顯示一個(gè)閃爍的光標,你可能會(huì )想要保證光標處于一些不會(huì )讓人感到分心的位置。在看似隨機的位置出現一個(gè)閃爍的光標會(huì )令人非常迷惑。

如果你的應用程序完全不需要一個(gè)閃爍的光標,你可以調用 curs_set(False) 來(lái)使它隱形。為與舊版本 curses 的兼容性的關(guān)系,有函數 leaveok(bool) 作為 curs_set() 的等價(jià)替換。如果 bool 是真值,curses 庫會(huì )嘗試移除閃爍光標,并且你也不必擔心它會(huì )留在一些奇怪的位置。

屬性和顏色?

字符可以以不同的方式顯示?;谖谋镜膽贸绦虺3R苑聪囡@示狀態(tài)行,一個(gè)文本查看器可能需要突出顯示某些單詞。為了支持這種用法,curses 允許你為屏幕上的每個(gè)單元指定一個(gè)屬性值。

屬性值是一個(gè)整數,它的每一個(gè)二進(jìn)制位代表一個(gè)不同的屬性。你可以嘗試以多種不屬性位組合來(lái)顯示文本,但 curses 不保證所有的組合都是有效的,或者看上去有明顯不同。這一點(diǎn)取決于用戶(hù)終端的能力,所以最穩妥的方式是只采用最常見(jiàn)的有效屬性,見(jiàn)下表。

屬性

描述

A_BLINK

閃爍文本

A_BOLD

超亮或粗體文本

A_DIM

半明亮文本

A_REVERSE

反相顯示文本

A_STANDOUT

可用的最佳突出顯示模式

A_UNDERLINE

帶下劃線(xiàn)的文本

所以,為了在屏幕頂部顯示一個(gè)反相的狀態(tài)行,你可以這么編寫(xiě):

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

curses 庫還支持在提供了顏色功能的終端上顯示顏色的功能。最常見(jiàn)的提供顏色的終端很可能是 Linux 控制臺,采用了 xterms 配色方案。

為了使用顏色,你必須在調用完函數 initscr() 后盡快調用函數 start_color(),來(lái)初始化默認顏色集 (curses.wrapper() 函數自動(dòng)完成了這一點(diǎn))。 當它完成后,如果使用中的終端支持顯示顏色, has_colors() 會(huì )返回真值。 (注意:curses 使用美式拼寫(xiě) “color”,而不是英式/加拿大拼寫(xiě) “colour”。如果你習慣了英式拼寫(xiě),你需要避免自己在這些函數上拼寫(xiě)錯誤。)

curses 庫維護一個(gè)有限數量的顏色對,包括一個(gè)前景(文本)色和一個(gè)背景色。你可以使用函數 color_pair() 獲得一個(gè)顏色對對應的屬性值。它可以通過(guò)按位或運算與其他屬性,比如 A_REVERSE 組合。但再說(shuō)明一遍,這種組合并不保證在所有終端上都有效。

一個(gè)樣例,用 1 號顏色對顯示一行文本:

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

如前所述, 顏色對由前景色和背景色組成。 init_pair(n, f, b) 函數可改變顏色對 n 的定義 為前景色 f 和背景色 b。 顏色對 0 硬編碼為黑底白字,不能改變。

顏色已經(jīng)被編號,并且當其激活 color 模式時(shí) start_color() 會(huì )初始化 8 種基本顏色。 它們是: 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan 和 7:white。 curses 模塊為這些顏色定義了相應的名稱(chēng)常量: curses.COLOR_BLACK, curses.COLOR_RED 等等。

讓我們來(lái)做個(gè)綜合練習。 要將顏色 1 改為紅色文本白色背景,你應當調用:

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

當你改變一個(gè)顏色對時(shí),任何已經(jīng)使用該顏色對來(lái)顯示的文本將會(huì )更改為新的顏色。 你還可以這樣來(lái)顯示新顏色的文本:

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

某些非?;ㄉ诘慕K端可以將實(shí)際顏色定義修改為給定的 RGB 值。 這允許你將通常為紅色的 1 號顏色改成紫色或藍色或者任何你喜歡的顏色。 不幸的是,Linux 控制臺不支持此特性,所以我無(wú)法嘗試它,也無(wú)法提供任何示例。 想要檢查你的終端是否能做到你可以調用 can_change_color(),如果有此功能則它將返回 True。 如果你幸運地擁有一個(gè)如此優(yōu)秀的終端,請查詢(xún)你的系統的幫助頁(yè)面來(lái)了解詳情。

用戶(hù)輸入?

C curses 庫提供了非常簡(jiǎn)單的輸入機制。 Python 的 curses 模塊添加了一個(gè)基本的文本輸入控件。 (其他的庫例如 Urwid 擁有更豐富的控件集。)

有兩個(gè)方法可以從窗口獲取輸入:

  • getch() 會(huì )刷新屏幕然后等待用戶(hù)按鍵,如果之前調用過(guò) echo() 還會(huì )顯示所按的鍵。 你還可以選擇指定一個(gè)坐標以便在暫停之前讓光標移動(dòng)到那里。

  • getkey() 將做同樣的事但是會(huì )把整數轉換為字符串。 每個(gè)字符將返回為長(cháng)度為 1 個(gè)字符的字符串,特殊鍵例如函數鍵將返回包含鍵名的較長(cháng)字符串例如 KEY_UP^G。

使用 nodelay() 窗口方法可以做到不等待用戶(hù)。 在 nodelay(True) 之后,窗口的 getch()getkey() 將成為非阻塞的。 為表明輸入未就緒,getch() 會(huì )返回 curses.ERR (值為 -1) 而 getkey() 會(huì )引發(fā)異常。 此外還有 halfdelay() 函數,它可被用來(lái)(實(shí)際地)在每個(gè) getch() 上設置一個(gè)計時(shí)器;如果在指定的延遲內沒(méi)有輸入可用(以十分之一秒為單位),curses 將引發(fā)異常。

getch() 方法返回一個(gè)整數;如果數值在 0 到 255 之間,它代表所按下鍵的 ASCII 碼。 大于 255 的值為特殊鍵例如 Page Up, Home 或方向鍵等。 你可以將返回的值與 curses.KEY_PPAGE, curses.KEY_HOMEcurses.KEY_LEFT 等常量做比較。 你的程序主循環(huán)看起來(lái)可能是這樣:

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 類(lèi)成員函數,它們接受整數或長(cháng)度為 1 個(gè)字符的字符串參數;這些函數在為這樣的循環(huán)編寫(xiě)更具可讀性的測試時(shí)可能會(huì )很有用。 它還提供了一些轉換函數,它們接受整數或長(cháng)度為 1 個(gè)字符的字符串參數并返回同樣的類(lèi)型。 例如,curses.ascii.ctrl() 返回與其參數相對應的控制字符。

還有一個(gè)可以提取整個(gè)字符串的方法 getstr()。 它并不經(jīng)常被使用,因為它的功能相當受限;可用的編輯鍵只有 Backspace 和 Enter 鍵,它們會(huì )結束字符串。 也可以選擇限制為固定數量的字符。

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 模塊提供了一個(gè)文本框,它支持類(lèi)似 Emacs 的鍵綁定集。 Textbox 類(lèi)的各種方法支持帶輸入驗證的編輯及包含或不包含末尾空格地收集編輯結果。 下面是一個(gè)例子:

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 沒(méi)有涵蓋一些進(jìn)階主題,例如讀取屏幕的內容或從 xterm 實(shí)例捕獲鼠標事件等,但是 curses 模塊的 Python 庫文檔頁(yè)面現在已相當完善。 接下來(lái)你應當去瀏覽一下其中的內容。

如果你對 curses 函數的細節行為有疑問(wèn),請查看你的 curses 實(shí)現版本的說(shuō)明頁(yè)面,不論它是 ncurses 還是特定 Unix 廠(chǎng)商的版本。 說(shuō)明頁(yè)面將記錄任何具體問(wèn)題,并提供所有函數、屬性以及可用 ACS_* 字符的完整列表。

由于 curses API 是如此的龐大,某些函數并不被 Python 接口所支持。 這往往不是因為它們難以實(shí)現,而是因為還沒(méi)有人需要它們。 此外,Python 尚不支持與 ncurses 相關(guān)聯(lián)的菜單庫。 歡迎提供添加這些功能的補??;請參閱 Python 開(kāi)發(fā)者指南 了解有關(guān)為 Python 提交補丁的詳情。