使用 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 取代,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() 返回一個表示整個螢幕的視窗物件;這通常被稱為 stdscr,因為它對應於 C 變數的名稱。

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() 將恢復終端的原始狀態。可呼叫物件在捕獲異常、恢復終端狀態,然後重新引發異常的 try...except 內部被呼叫。因此,您的終端在發生異常時不會處於奇怪的狀態,您將能夠讀取異常訊息和回溯。

視窗和 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.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

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

y, x, strch

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

y, x, strch, attr

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

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

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 為真時,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_BLACKcurses.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 模組提供了接受整數或 1 字元字串引數的 ASCII 類成員函式;這些函式可能有助於編寫此類迴圈中更易讀的測試。它還提供接受整數或 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 例項捕獲滑鼠事件,但 Python 庫頁面的 curses 模組現在相當完整。您應該接下來瀏覽它。

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

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