使用 Python 進行 Curses 程式設計

作者:

A.M. Kuchling, Eric S. Raymond

釋出:

2.04

什麼是 curses?

curses 庫為基於文字的終端提供了一個與終端無關的螢幕繪製和鍵盤處理工具;這些終端包括 VT100、Linux 控制檯以及各種程式提供的模擬終端。顯示終端支援各種控制程式碼來執行常見操作,例如移動游標、滾動螢幕和擦除區域。不同的終端使用差異很大的程式碼,並且通常有自己的小怪癖。

在圖形顯示的世界中,人們可能會問“為什麼要費心”?確實,字元單元顯示終端是一種過時的技術,但在某些情況下,能夠使用它們進行花哨的操作仍然很有價值。一個用例是在不執行 X 伺服器的小型或嵌入式 Unix 上。另一個用例是作業系統安裝程式和核心配置器等工具,它們可能必須在任何圖形支援可用之前執行。

curses 庫提供了相當基本的功能,為程式設計師提供了一個包含多個不重疊文字視窗的顯示抽象。可以透過多種方式更改視窗的內容——新增文字、擦除文字、更改其外觀——而 curses 庫將計算出需要傳送到終端的控制程式碼以產生正確的輸出。curses 不提供許多使用者介面概念,例如按鈕、複選框或對話方塊;如果您需要此類功能,請考慮使用使用者介面庫,例如 Urwid

curses 庫最初是為 BSD Unix 編寫的;來自 AT&T 的後來的 System V 版本 Unix 添加了許多增強功能和新功能。BSD curses 不再維護,已被 ncurses 取代,後者是 AT&T 介面的開源實現。如果您使用 Linux 或 FreeBSD 等開源 Unix,您的系統幾乎肯定會使用 ncurses。由於當前大多數商業 Unix 版本都基於 System V 程式碼,因此此處描述的所有功能都可能可用。某些專有 Unix 攜帶的舊版本 curses 可能不支援所有功能。

Python 的 Windows 版本不包括 curses 模組。有一個名為 UniCurses 的移植版本可用。

Python curses 模組

Python 模組是 curses 提供的 C 函式的相當簡單的包裝器;如果您已經熟悉 C 語言的 curses 程式設計,那麼將該知識轉移到 Python 非常容易。最大的區別在於 Python 介面透過將不同的 C 函式(例如 addstr()mvaddstr()mvwaddstr())合併為一個 addstr() 方法來簡化操作。稍後您將看到更詳細的介紹。

本 HOWTO 介紹瞭如何使用 curses 和 Python 編寫文字模式程式。它並不試圖成為 curses API 的完整指南;為此,請參閱 Python 庫指南中關於 ncurses 的部分,以及 ncurses 的 C 手冊頁。但是,它將為您提供基本概念。

啟動和結束 curses 應用程式

在執行任何操作之前,必須初始化 curses。這透過呼叫 initscr() 函式來完成,該函式將確定終端型別,向終端傳送任何所需的設定程式碼,並建立各種內部資料結構。如果成功,initscr() 將返回一個表示整個螢幕的視窗物件;這通常在對應的 C 變數名之後稱為 stdscr

import curses
stdscr = curses.initscr()

通常,curses 應用程式會關閉按鍵到螢幕的自動回顯,以便能夠讀取按鍵並僅在某些情況下顯示它們。這需要呼叫 noecho() 函式。

curses.noecho()

應用程式通常還需要立即響應按鍵,而無需按下 Enter 鍵;這稱為 cbreak 模式,而不是通常的緩衝輸入模式。

curses.cbreak()

終端通常會以多位元組轉義序列的形式返回特殊按鍵,例如游標鍵或導航鍵(如 Page Up 和 Home)。雖然您可以編寫應用程式來期望此類序列並相應地處理它們,但 curses 可以為您執行此操作,返回一個特殊值,例如 curses.KEY_LEFT。要讓 curses 完成這項工作,您必須啟用鍵盤模式。

stdscr.keypad(True)

終止 curses 應用程式比啟動它容易得多。您需要呼叫

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

來反轉 curses 友好的終端設定。然後呼叫 endwin() 函式將終端恢復到其原始操作模式。

curses.endwin()

除錯 curses 應用程式時的一個常見問題是,當應用程式在未將終端恢復到先前狀態的情況下終止時,您的終端會變得混亂。在 Python 中,當您的程式碼存在錯誤並引發未捕獲的異常時,通常會發生這種情況。例如,當您鍵入時,鍵不再回顯到螢幕上,這使得使用 shell 變得困難。

在 Python 中,您可以透過匯入 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() 函式接受一個可呼叫物件並執行上述初始化,如果存在顏色支援,還會初始化顏色。wrapper() 然後執行您提供的可呼叫物件。一旦可呼叫物件返回,wrapper() 將恢復終端的原始狀態。可呼叫物件在 tryexcept 中呼叫,該語句會捕獲異常,恢復終端狀態,然後重新引發異常。因此,您的終端不會因異常而處於奇怪的狀態,並且您將能夠讀取異常的訊息和回溯。

視窗和 Pad

視窗是 curses 中的基本抽象。視窗物件表示螢幕的矩形區域,並支援顯示文字、擦除文字、允許使用者輸入字串等方法。

initscr() 函式返回的 stdscr 物件是一個覆蓋整個螢幕的視窗物件。許多程式可能只需要這一個視窗,但您可能希望將螢幕劃分為較小的視窗,以便單獨重繪或清除它們。newwin() 函式建立一個給定大小的新視窗,並返回新的視窗物件。

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 座標排在第一位。這與其他大多數計算機應用程式存在不幸的差異,但它自 curses 最初編寫以來一直是 curses 的一部分,現在更改它為時已晚。

您的應用程式可以透過使用 curses.LINEScurses.COLS 變數來獲取 yx 大小,從而確定螢幕的大小。然後,合法座標將從 (0,0) 擴充套件到 (curses.LINES - 1, curses.COLS - 1)

當你呼叫方法來顯示或擦除文字時,效果不會立即顯示在螢幕上。相反,你必須呼叫視窗物件的 refresh() 方法來更新螢幕。

這是因為 curses 最初是為慢速 300 波特的終端連線而編寫的;對於這些終端,儘量減少重繪螢幕所需的時間非常重要。curses 會累積對螢幕的更改,並在你呼叫 refresh() 時以最有效的方式顯示它們。例如,如果你的程式在一個視窗中顯示一些文字,然後清除該視窗,則無需傳送原始文字,因為它們永遠不可見。

實際上,顯式地告訴 curses 重繪視窗並不會使 curses 的程式設計變得複雜。大多數程式會進行一系列活動,然後暫停等待使用者按下鍵或其他操作。你所要做的就是確保在暫停等待使用者輸入之前,螢幕已經被重繪,首先呼叫 stdscr.refresh() 或其他相關視窗的 refresh() 方法。

pad 是視窗的一種特殊情況;它可以大於實際的顯示螢幕,並且一次只顯示 pad 的一部分。建立 pad 需要 pad 的高度和寬度,而重新整理 pad 需要給出螢幕上要顯示 pad 的子部分的區域的座標。

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() 呼叫會在螢幕上從座標 (5,5) 延伸到座標 (20,75) 的矩形區域中顯示 pad 的一部分;顯示部分的左上角是 pad 上的座標 (0,0)。除此之外,pad 與普通視窗完全相同,並支援相同的方法。

如果螢幕上有多個視窗和 pad,則有一種更有效的方式來更新螢幕,並防止螢幕的每個部分更新時出現惱人的閃爍。refresh() 實際上做了兩件事

  1. 呼叫每個視窗的 noutrefresh() 方法來更新一個表示螢幕所需狀態的底層資料結構。

  2. 呼叫函式 doupdate() 函式來更改物理螢幕,以匹配資料結構中記錄的所需狀態。

相反,你可以在多個視窗上呼叫 noutrefresh() 來更新資料結構,然後呼叫 doupdate() 來更新螢幕。

顯示文字

從 C 程式設計師的角度來看,curses 有時看起來像一個曲折的函式迷宮,所有這些函式都有細微的差別。例如,addstr()stdscr 視窗的當前游標位置顯示一個字串,而 mvaddstr() 先移動到給定的 y,x 座標,然後再顯示字串。waddstr() 就像 addstr() 一樣,但允許指定要使用的視窗,而不是預設使用 stdscrmvwaddstr() 允許指定視窗和座標。

幸運的是,Python 介面隱藏了所有這些細節。stdscr 是一個像其他視窗一樣的視窗物件,並且諸如 addstr() 之類的方法接受多種引數形式。通常有四種不同的形式。

形式

描述

strch

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

strch, attr

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

y, x, strch

移動到視窗中的位置 y,x,並顯示 strch

y, x, strch, attr

移動到視窗中的位置 y,x,並顯示 strch,使用屬性 attr

屬性允許以突出顯示的形式(如粗體、下劃線、反轉程式碼或彩色)顯示文字。它們將在下一小節中更詳細地解釋。

addstr() 方法將 Python 字串或位元組字串作為要顯示的值。位元組字串的內容按原樣傳送到終端。字串使用視窗的 encoding 屬性編碼為位元組;這預設為 locale.getencoding() 返回的預設系統編碼。

addch() 方法接受一個字元,該字元可以是長度為 1 的字串、長度為 1 的位元組字串或整數。

為擴充套件字元提供了常量;這些常量是大於 255 的整數。例如,ACS_PLMINUS 是一個 +/- 符號,而 ACS_ULCORNER 是一個框的左上角(用於繪製邊框)。你還可以使用適當的 Unicode 字元。

視窗會記住上次操作後游標離開的位置,因此如果你省略了 y,x 座標,則字串或字元將顯示在上次操作結束的任何位置。你還可以使用 move(y,x) 方法移動游標。由於某些終端始終顯示閃爍的游標,因此你可能需要確保將游標定位在不會分散注意力的位置;讓游標在某些看似隨機的位置閃爍可能會令人困惑。

如果你的應用程式根本不需要閃爍的游標,你可以呼叫 curs_set(False) 使其不可見。為了與較舊的 curses 版本相容,有一個 leaveok(bool) 函式,它是 curs_set() 的同義詞。當 bool 為 true 時,curses 庫將嘗試抑制閃爍的游標,你無需擔心將其留在奇怪的位置。

屬性和顏色

字元可以以不同的方式顯示。基於文字的應用程式中的狀態行通常以反向影片顯示,或者文字檢視器可能需要突出顯示某些單詞。curses 透過允許你為螢幕上的每個單元格指定一個屬性來支援這一點。

屬性是一個整數,每個位代表不同的屬性。你可以嘗試使用設定的多個屬性位來顯示文字,但是 curses 不能保證所有可能的組合都可用,或者它們在視覺上都是不同的。這取決於所用終端的功能,因此最安全的方法是堅持使用此處列出的最常用的屬性。

屬性

描述

A_BLINK

閃爍的文字

A_BOLD

超亮或粗體文字

A_DIM

半亮文字

A_REVERSE

反向影片文字

A_STANDOUT

可用的最佳突出顯示模式

A_UNDERLINE

帶下劃線的文字

因此,要在螢幕頂行顯示反向影片狀態行,你可以編寫程式碼

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

curses 庫還支援那些提供顏色的終端上的顏色。最常見的此類終端可能是 Linux 控制檯,其次是彩色 xterm。

要使用顏色,你必須在呼叫 initscr() 後立即呼叫 start_color() 函式,以初始化預設顏色集(curses.wrapper() 函式會自動執行此操作)。完成此操作後,如果正在使用的終端實際上可以顯示顏色,則 has_colors() 函式將返回 TRUE。(注意:curses 使用美式拼寫“color”,而不是加拿大/英式拼寫“colour”。如果你習慣了英式拼寫,則為了這些函式,你必須接受拼錯的事實。)

curses 庫維護著有限數量的顏色對,每個顏色對包含一個前景色(或文字顏色)和一個背景色。您可以使用 color_pair() 函式獲取與顏色對對應的屬性值;該值可以與諸如 A_REVERSE 之類的其他屬性進行按位或運算,但同樣地,不能保證這些組合在所有終端上都能正常工作。

一個示例,使用顏色對 1 顯示一行文字

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

正如我之前所說,一個顏色對由前景色和背景色組成。init_pair(n, f, b) 函式將顏色對 *n* 的定義更改為前景色 f 和背景色 b。顏色對 0 硬編碼為黑底白字,且無法更改。

顏色被編號,並且 start_color() 在啟用顏色模式時初始化 8 種基本顏色。它們是:0:黑色,1:紅色,2:綠色,3:黃色,4:藍色,5:品紅色,6:青色和 7:白色。curses 模組為每種顏色定義了命名常量:curses.COLOR_BLACK, curses.COLOR_RED, 等等。

讓我們把這些放在一起。要將顏色 1 更改為白色背景上的紅色文字,您需要呼叫

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。如果您有幸擁有這樣一個功能強大的終端,請查閱系統的手冊頁以獲取更多資訊。

使用者輸入

C curses 庫只提供了非常簡單的輸入機制。Python 的 curses 模組添加了一個基本的文字輸入小部件。(諸如 Urwid 之類的其他庫具有更廣泛的小部件集合。)

有兩種方法可以從視窗獲取輸入

  • getch() 重新整理螢幕,然後等待使用者按下鍵,如果之前呼叫過 echo(),則會顯示該鍵。您可以選擇指定一個座標,在暫停之前游標應移動到該座標。

  • getkey() 執行相同的操作,但將整數轉換為字串。單個字元作為 1 個字元的字串返回,而諸如功能鍵之類的特殊鍵返回包含鍵名稱(例如 KEY_UP^G)的更長字串。

可以使用 nodelay() 視窗方法不等待使用者。在 nodelay(True) 之後,該視窗的 getch()getkey() 將變為非阻塞的。為了表示沒有輸入準備就緒,getch() 返回 curses.ERR(值為 -1),並且 getkey() 引發異常。還有一個 halfdelay() 函式,可用於(實際上)在每個 getch() 上設定一個計時器;如果在指定的延遲(以十分之一秒為單位)內沒有輸入可用,curses 將引發異常。

getch() 方法返回一個整數;如果它介於 0 和 255 之間,則表示所按鍵的 ASCII 碼。大於 255 的值是特殊鍵,例如 Page Up、Home 或游標鍵。您可以將返回值與諸如 curses.KEY_PPAGEcurses.KEY_HOMEcurses.KEY_LEFT 之類的常量進行比較。您程式的主迴圈可能如下所示

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 類成員函式,該函式接受整數或 1 個字元的字串引數;這些函式對於為此類迴圈編寫更具可讀性的測試可能很有用。它還提供轉換函式,該函式接受整數或 1 個字元的字串引數,並返回相同的型別。例如,curses.ascii.ctrl() 返回與其引數對應的控制字元。

還有一種檢索整個字串的方法,getstr()。它不常用,因為它的功能非常有限;唯一可用的編輯鍵是退格鍵和回車鍵,回車鍵終止字串。它可以選擇限制為固定數量的字元。

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 類的各種方法支援使用輸入驗證進行編輯,並收集帶有或不帶有尾隨空格的編輯結果。這是一個示例

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 沒有涵蓋一些高階主題,例如讀取螢幕內容或從 xterm 例項捕獲滑鼠事件,但是現在 curses 模組的 Python 庫頁面已相當完整。接下來您應該瀏覽它。

如果您對 curses 函式的詳細行為有疑問,請查閱 curses 實現的手冊頁,無論是 ncurses 還是專有的 Unix 供應商的手冊頁。手冊頁將記錄任何怪癖,並提供所有可用的函式、屬性和 ACS_* 字元的完整列表。

由於 curses API 非常龐大,因此 Python 介面中不支援某些函式。通常,這不是因為它們難以實現,而是因為還沒有人需要它們。此外,Python 還不支援與 ncurses 關聯的選單庫。歡迎新增對這些功能的支援的補丁;請參閱 Python 開發人員指南,以瞭解有關向 Python 提交補丁的更多資訊。