用 Python 進行 Curses 編程?

作者

A.M. Kuchling, Eric S. Raymond

發(fā)布版本

2.04

摘要

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

curses 是什么??

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

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

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

curses 庫最初是為BSD Unix 編寫的。 后來 AT&T 的Unix System V 版本加入了許多增強功能和新功能。如今BSD curses已不再維護,被ncurses取代,ncurses是 AT&T 接口的開源實現(xiàn)。如果使用的是 Linux 或 FreeBSD 等開源Unix系統(tǒng),則幾乎肯定會使用ncurses。由于大多數(shù)當前的商業(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 模塊相當簡單地封裝了 curses 提供的 C 函數(shù);如果你已經(jīng)熟悉在 C 語言中使用 curses 編程,把這些知識轉(zhuǎn)移的 Python 是非常容易的。最大的差異在于 Python 中的接口通過把不同的 C 函數(shù)合并來讓事情變得更簡單,比如 addstr()、mvaddstr()mvwaddstr() 三個 C 函數(shù)被并入 addstr() 這一個方法。下文中會描述更多的細節(jié)。

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

開始和結(jié)束 curses 應(yīng)用程序?

在做任何事情之前,curses 必須先被初始化。可以通過調(diào)用函數(shù) initscr() 來實現(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)義序列的形式返回特殊按鍵,比如光標鍵和導航鍵比如 Page Up 鍵和 Home 鍵。盡管你可以編寫你的程序來應(yīng)對這些序列,curses 能夠代替你做到這件事,返回一個特殊值比如 curses.KEY_LEFT。為了讓 curses 做這項工作,你需要啟用 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)用程序時常會發(fā)生,一個應(yīng)用程序還未能還原終端到原本的狀態(tài)就意外退出了,這會攪亂你的終端。在 Python 中這常常會發(fā)生在你的代碼中有 bug 并引發(fā)了一個未捕獲的異常。當你嘗試輸入時按鍵不會上屏,這使得使用終端變得困難。

在 Python 中你可以避免這些復雜問題并讓調(dià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)用對象并首先進行上述初始化過程,在終端支持著色時還會初始化顏色。接著 wrapper() 運行你提供的可調(diào)用對象。當該可調(diào)用對象返回時,wrapper() 會還原終端到初始狀態(tài)。該可調(diào)用對象會在 try...except 這樣的結(jié)構(gòu)內(nèi)被調(diào)用,當它捕獲到異常時,會先還原終端再重新引發(fā)這個異常。所以你的終端不會因為異常而被留在一個搞笑的狀態(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 使用的坐標系統(tǒng)與尋常的不同。坐標始終是以 y,x 的順序傳遞,并且左上角是坐標 (0,0)。這打破了正常的坐標處理約定,即 x 坐標在前。這是一個與其他計算機應(yīng)用程序糟糕的差異,但這從 curses 最初被編寫出來就已是它的一部分,現(xiàn)在想要修改它已為時已晚。

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

當你調(diào)用一個方法來顯示或擦除文本時,效果并不會立即顯示。相反,你必須調(diào)用窗口對象的 refresh() 方法來更新屏幕。

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

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

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

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

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

  1. 調(diào)用每個窗口的 noutrefresh() 方法來更新一個表達屏幕期望狀態(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ù)組成的扭曲迷宮。舉個例子,addstr()stdscr 窗口的當前光標位置顯示一個字符串,而 mvaddstr() 則是先移動到一個給定的 y,x 坐標再顯示字符串。waddstr()addstr() 類似,但允許指定一個窗口而非默認的 stdscr。mvwaddstr() 允許同時指定一個窗口和一個坐標。

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

形式

描述

strch

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

strch, attr

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

y, x, strch

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

y, x, strch, attr

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

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

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ù)。

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

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

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

屬性和顏色?

字符可以以不同的方式顯示?;谖谋镜膽?yīng)用程序常常以反相顯示狀態(tài)行,一個文本查看器可能需要突出顯示某些單詞。為了支持這種用法,curses 允許你為屏幕上的每個單元指定一個屬性值。

屬性值是一個整數(shù),它的每一個二進制位代表一個不同的屬性。你可以嘗試以多種不屬性位組合來顯示文本,但 curses 不保證所有的組合都是有效的,或者看上去有明顯不同。這一點取決于用戶終端的能力,所以最穩(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(),來初始化默認顏色集 (curses.wrapper() 函數(shù)自動完成了這一點)。 當它完成后,如果使用中的終端支持顯示顏色, has_colors() 會返回真值。 (注意:curses 使用美式拼寫 “color”,而不是英式/加拿大拼寫 “colour”。如果你習慣了英式拼寫,你需要避免自己在這些函數(shù)上拼寫錯誤。)

curses 庫維護一個有限數(shù)量的顏色對,包括一個前景(文本)色和一個背景色。你可以使用函數(shù) color_pair() 獲得一個顏色對對應(yīng)的屬性值。它可以通過按位或運算與其他屬性,比如 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)被編號,并且當其激活 color 模式時 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 等等。

讓我們來做個綜合練習。 要將顏色 1 改為紅色文本白色背景,你應(yīng)當調(diào)用:

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

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

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

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

用戶輸入?

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

有兩個方法可以從窗口獲取輸入:

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

  • getkey() 將做同樣的事但是會把整數(shù)轉(zhuǎn)換為字符串。 每個字符將返回為長度為 1 個字符的字符串,特殊鍵例如函數(shù)鍵將返回包含鍵名的較長字符串例如 KEY_UP^G。

使用 nodelay() 窗口方法可以做到不等待用戶。 在 nodelay(True) 之后,窗口的 getch()getkey() 將成為非阻塞的。 為表明輸入未就緒,getch() 會返回 curses.ERR (值為 -1) 而 getkey() 會引發(fā)異常。 此外還有 halfdelay() 函數(shù),它可被用來(實際地)在每個 getch() 上設(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)編寫更具可讀性的測試時可能會很有用。 它還提供了一些轉(zhuǎn)換函數(shù),它們接受整數(shù)或長度為 1 個字符的字符串參數(shù)并返回同樣的類型。 例如,curses.ascii.ctrl() 返回與其參數(shù)相對應(yīng)的控制字符。

還有一個可以提取整個字符串的方法 getstr()。 它并不經(jī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 類的各種方法支持帶輸入驗證的編輯及包含或不包含末尾空格地收集編輯結(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 的庫文檔了解更多細節(jié)。

更多的信息?

本 HOWTO 沒有涵蓋一些進階主題,例如讀取屏幕的內(nèi)容或從 xterm 實例捕獲鼠標事件等,但是 curses 模塊的 Python 庫文檔頁面現(xiàn)在已相當完善。 接下來你應(yīng)當去瀏覽一下其中的內(nèi)容。

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

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