curses 庫(kù) ( ncurses ) 提供了控制字符屏幕的獨(dú)立于終端的方法。curses 是大多數(shù)類似于 UNIX 的系統(tǒng)(包括 Linux)的標(biāo)準(zhǔn)部分,而且它已經(jīng)移植到 Windows 和其它系統(tǒng)。curses 程序?qū)⒃诩兾谋鞠到y(tǒng)上、xterm 和其它窗口化控制臺(tái)會(huì)話中運(yùn)行,這使這些應(yīng)用程序具有良好的可移植性。
介紹 curses
Python 的標(biāo)準(zhǔn) curses 提供了“玻璃電傳”(glass teletype)(在 20 世紀(jì) 70 年代,原始 curses 庫(kù)剛創(chuàng)建時(shí),它叫做 CRT)的公共特性的基本接口。有許多方法可以讓用 Python 編寫的交互式文本模式程序變得更巧妙。這些方法分成兩類。
一方面,有些Python 模塊支持 ncurses (curses 的超集)或 slang (相似卻獨(dú)立的控制臺(tái)庫(kù))的全部功能集合。最值得注意的是,這當(dāng)中有一個(gè)增強(qiáng)庫(kù)(由適當(dāng)?shù)?Python 模塊封裝)可以讓您將顏色添加到界面上。
另一方面,許多構(gòu)建在curses(或 ncurses / slang )上的高級(jí)窗口小部件庫(kù)添加了諸如按鈕、菜單、滾動(dòng)欄和各種公共界面設(shè)備之類的特性。如果您看到過(guò)用諸如 Borland's TurboWindows(DOS 版)之類的庫(kù)開(kāi)發(fā)的應(yīng)用程序,您就知道在文本模式控制臺(tái)中,這些特性是多么吸引人。窗口小部件庫(kù)中的功能單單使用 curses 都可以達(dá)到,但是還可以利用其它程序員在高級(jí)界面上取得的成果。請(qǐng)參閱 參考資料,以尋找所提到的模塊的鏈接。
本文只涉及 curses 自身的特性。由于 curses 模塊是標(biāo)準(zhǔn)發(fā)行版的一部分,您不必下載支持庫(kù)或其它 Python 模塊就可以找到并使用它(至少在 Linux 或 UNIX 系統(tǒng)中是這樣)。理解 curses 提供的基本支持很有用,即使只是作為理解高級(jí)模塊的基礎(chǔ)。即使不使用其它模塊,單獨(dú)使用 curses 構(gòu)建漂亮且實(shí)用的 Python 文本模式應(yīng)用程序也很簡(jiǎn)單。預(yù)先發(fā)行的說(shuō)明提到 Python 2.0 將包括 curses 的增強(qiáng)版本,但不管怎樣,它應(yīng)該兼容此處說(shuō)明的版本。
應(yīng)用程序
我將討論為 Txt2Html(在 “可愛(ài)的 Python:我的第一個(gè)基于 Web 的過(guò)濾代理” 中介紹的文本到 HTML 轉(zhuǎn)換程序)編寫的封裝器,作為本文的測(cè)試應(yīng)用程序。Txt2Html 有幾種運(yùn)行方式。但為了與本文的目的保持一致,我們將研究從命令行運(yùn)行的 Txt2Html。操作 Txt2Html 的一種方式是向它提供一組命令行變量(它們說(shuō)明要執(zhí)行的轉(zhuǎn)換的各方面),然后將應(yīng)用程序當(dāng)作批處理運(yùn)行。對(duì)于偶爾使用的用戶,一個(gè)更友好的用戶界面提供了一個(gè)交互式選擇屏幕,它可以在執(zhí)行實(shí)際轉(zhuǎn)換之前,引導(dǎo)用戶遍歷轉(zhuǎn)換選項(xiàng)(提供選中選項(xiàng)的視覺(jué)反饋)。
curses_txt2html 的界面基于常見(jiàn)的頂欄菜單,它帶有下拉和嵌套子菜單。所有菜單相關(guān)的功能都在 curses 上“從頭”開(kāi)始設(shè)計(jì)。雖然這些菜單缺少更復(fù)雜的 curses 封裝器的一些特性,但它們的基本功能是由幾行只使用 curses 的代碼實(shí)現(xiàn)的。這個(gè)界面還帶有一個(gè)簡(jiǎn)單的卷動(dòng)幫助框和幾個(gè)用戶輸入字段。以下是顯示常規(guī)布局和樣式的應(yīng)用程序的屏幕快照。
X終端上的應(yīng)用程序
2015411164836286.gif (486×262)
Linux終端上的應(yīng)用程序
2015411164913129.gif (667×430)
封裝 curses 應(yīng)用程序
curses 編程的基本元素是窗口對(duì)象。窗口是帶有一個(gè)可尋址光標(biāo)的實(shí)際物理屏幕的區(qū)域,光標(biāo)的坐標(biāo)與窗口相關(guān)??梢缘教幰苿?dòng)窗口,并且可以創(chuàng)建和刪除窗口而不影響其它窗口。在窗口對(duì)象中,輸入或輸出操作發(fā)生在光標(biāo)上,這通常由輸入或輸出方法明確設(shè)置,但也可以分別修改。
在初始化 curses 之后,可以用各種方式修改或完全禁用面向流的控制臺(tái)輸入和輸出。這基本上就是使用 curses 的全部重點(diǎn)??墒且坏└牧肆魇娇刂婆_(tái)交互,如果程序出錯(cuò),將不會(huì)以正常方式顯示 Python 追溯事件。Andrew Kuchling 使用一個(gè)很好的 curses 程序頂級(jí)框架解決了這個(gè)問(wèn)題(請(qǐng)參閱 參考資料中他的教程)。
以下模板(基本上與 Kuchling 的相同)保留在正常命令行 Python 的錯(cuò)誤報(bào)告功能:
Python [curses] 程序的頂層設(shè)置代碼
?123456789101112131415161718192021222324252627282930313233343536373839 import curses, traceback if __name__== '__main__': try : # Initialize curses stdscr=curses.initscr() # Turn off echoing of keys, and enter cbreak mode, # where no buffering is performed on keyboard input curses.noecho() curses.cbreak() # In keypad mode, escape sequences for special keys # (like the cursor keys) will be interpreted and # a special value like curses.KEY_LEFT will be returned stdscr.keypad(1) main(stdscr) # Enter the main loop # Set everything back to normal stdscr.keypad(0) curses.echo() curses.nocbreak() curses.endwin() # Terminate curses except : # In event of error, restore terminal to sane state. stdscr.keypad(0) curses.echo() curses.nocbreak() curses.endwin() traceback.print_exc() # Print the exception
try 代碼塊執(zhí)行一些初始化,調(diào)用 main() 函數(shù)來(lái)執(zhí)行實(shí)際工作,然后執(zhí)行最后的清除。如果出錯(cuò), except 代碼塊會(huì)將控制臺(tái)恢復(fù)成缺省狀態(tài),然后報(bào)告遇到的異常。
main() 事件循環(huán)
現(xiàn)在,我們研究 main() 函數(shù),看看 curses_txt2html 做些什么:
curses_txt2html.py main() 函數(shù)和事件循環(huán)
?123456789101112131415161718192021222324252627282930313233 defmain (stdscr): # Frame the interface area at fixed VT100 size global screen screen = stdscr.subwin(23, 79, 0, 0) screen.box() screen.hline(2, 1, curses.ACS_HLINE, 77) screen.refresh() # Define the topbar menus file_menu = ( "File", "file_func()") proxy_menu = ( "Proxy Mode", "proxy_func()") doit_menu = ( "Do It!", "doit_func()") help_menu = ( "Help", "help_func()") exit_menu = ( "Exit", "EXIT") # Add the topbar menus to screen object topbar_menu((file_menu, proxy_menu, doit_menu, help_menu, exit_menu)) # Enter the topbar menu loop while topbar_key_handler(): draw_dict()
根據(jù)由空行隔開(kāi)的三部分,很容易理解 main() 函數(shù)。
第一部分執(zhí)行應(yīng)用程序外觀的常規(guī)設(shè)置。為了建立應(yīng)用程序元素之間的可預(yù)期間隔,交互式區(qū)域限制在 80 x 25 VT100/PC 屏幕大?。词箤?shí)際的終端窗口更大)。程序圍繞這個(gè)子窗口繪制一個(gè)框,并使用水平線畫出頂欄菜單的視覺(jué)偏移量。
第二部分建立應(yīng)用程序所使用的菜單。函數(shù) topbar_menu() 使用一些技巧將熱鍵綁定到應(yīng)用程序操作并用期望的視覺(jué)屬性來(lái)顯示菜單。請(qǐng)獲取源碼檔案(請(qǐng)參閱 參考資料 )以查看所有代碼。 topbar_menu() 應(yīng)該是非常普通的。(歡迎將它合并到您自己的應(yīng)用程序中。)非常重要的是一旦綁定了熱鍵,它們就 eval() 與菜單相關(guān)的字節(jié)組第二個(gè)元素中包含的字符串。例如,激活以上設(shè)置中的 "File" 菜單將調(diào)用 "eval("file_func()")"。所以就要求應(yīng)用程序定義叫做 file_func() 的函數(shù),要求它返回一個(gè)布爾 (Boolean) 值以表示是否達(dá)到應(yīng)用程序終止?fàn)顟B(tài)。
第三部分只有兩行,但這正是整個(gè)應(yīng)用程序?qū)嶋H運(yùn)行的部分。函數(shù) topbar_key_handler() 就像它的名稱所暗示的:它等待擊鍵,然后處理它們。擊鍵處理程序可以會(huì)返回 Boolean false 值。(如果是這樣,則應(yīng)用程序終止。)該應(yīng)用程序中,鍵處理程序主要是檢查第二段中綁定的鍵。但即使您的 curses 應(yīng)用程序綁定鍵的方式與該應(yīng)用程序不同,您仍要使用類似的事件循環(huán)。處理程序的關(guān)鍵部分很可能使用以下這行代碼:
c = screen.getch()# read a keypress
對(duì) draw_dict() 的調(diào)用只是事件循環(huán)中唯一的代碼。此函數(shù)繪制了 screen 窗口中幾處位置中的值。但在應(yīng)用程序中,您可能想要將以下這行代碼:
screen.refresh() # redraw the screen w/ any new output
加到繪制/刷新函數(shù)中(或只加到事件循環(huán)本身中)。
獲取用戶輸入
curses 應(yīng)用程序以擊鍵事件的形式獲取所有用戶輸入。我們已經(jīng)看過(guò)了 .getch() 方法,現(xiàn)在讓我們看一下將 .getch() 與其它輸入方法組合在一起的例子 .getstr() 。以下就是我們以前提到的 file_func() 函數(shù)的縮寫版本(它由 "File" 菜單激活)。
curses_txt2html.py file_func() 函數(shù)
?123456789101112131415161718192021222324252627282930313233343536373839404142 deffile_func (): s = curses.newwin(5,10,2,1) s.box() s.addstr(1,2, "I", hotkey_attr) s.addstr(1,3, "nput", menu_attr) s.addstr(2,2, "O", hotkey_attr) s.addstr(2,3, "utput", menu_attr) s.addstr(3,2, "T", hotkey_attr) s.addstr(3,3, "ype", menu_attr) s.addstr(1,2, "", hotkey_attr) s.refresh() c = s.getch() if c in (ord( 'I'), ord( 'i'), curses.KEY_ENTER, 10): curses.echo() s.erase() screen.addstr(5,33, " "*43, curses.A_UNDERLINE) cfg_dict[ 'source'] = screen.getstr(5,33) curses.noecho() else : curses.beep() s.erase() return CONTINUE
此函數(shù)組合了幾個(gè) curses 特性。它做的第一件事就是創(chuàng)建另一個(gè)窗口對(duì)象。由于這個(gè)新窗口對(duì)象是 "File" 選擇項(xiàng)的實(shí)際下拉菜單,所以程序使用 .box() 方法圍著它繪制了一個(gè)框架。在窗口 s 中,程序繪制了幾個(gè)下拉菜單選項(xiàng)。使用了一種稍微費(fèi)力的方法突出顯示了每個(gè)選項(xiàng)的熱鍵,這樣就與選項(xiàng)描述的其余部分形成了對(duì)比。(請(qǐng)查看完整源碼(請(qǐng)參閱 參考資料 )中的 topbar_menu() 以學(xué)習(xí)一種能稍微自動(dòng)處理突出顯示的方法。)最后的 .addstr() 調(diào)用將光標(biāo)移到缺省菜單選項(xiàng)。如同主屏幕一樣, s.refresh() 實(shí)際上顯示了畫到窗口對(duì)象上的元素。
繪制了下拉菜單后,程序使用簡(jiǎn)單的 s.getch() 調(diào)用來(lái)獲取用戶的選擇項(xiàng)。在演示應(yīng)用程序中,菜單只響應(yīng)熱鍵,但不響應(yīng)箭頭鍵或可移動(dòng)突出顯示欄。可以通過(guò)捕捉附加鍵操作并在下拉菜單中設(shè)置事件循環(huán)來(lái)構(gòu)建這些更復(fù)雜的菜單功能。但這個(gè)例子已經(jīng)足夠說(shuō)明這種概念了。
接著,程序?qū)傋x取的擊鍵與各種熱鍵值做比較。在本例中,熱鍵的大小寫都可以激活下拉菜單選項(xiàng),并且可以使用 ENTER 鍵激活缺省選項(xiàng)。(curses 特殊鍵常量看上去并不完全可靠,我發(fā)現(xiàn)必須添加實(shí)際的 ASCII 值 "10" 來(lái)捕捉 ENTER 鍵。)請(qǐng)注意,如果要執(zhí)行字符值比較,那么要將字符串封裝到 ord() 內(nèi)置 Python 函數(shù)中。
當(dāng)選中 "Input" 選項(xiàng)時(shí),程序會(huì)使用 .getstr() 方法,該方法提供帶有原始編輯能力的字段輸入(可以使用退格鍵)。由 ENTER 鍵終止輸入,然后方法返回輸入的值。通常會(huì)像上例中一樣,將這個(gè)值分配給一個(gè)變量。
為了在視覺(jué)上區(qū)別輸入字段,我使用了一點(diǎn)小技巧,預(yù)先向?qū)⒁l(fā)生數(shù)據(jù)輸入的區(qū)域添加了下劃線。無(wú)論如何,這都是必要的,但它添加了一種視覺(jué)效果。由以下這行代碼畫出下劃線:
?1 screen.addstr(5,33, " "*43, curses.A_UNDERLINE)
當(dāng)然,程序還必須除去下劃線,這項(xiàng)工作在 draw_dict() 刷新函數(shù)中由以下這行代碼執(zhí)行:
?1 screen.addstr(5,33, " "*43, curses.A_NORMAL)
結(jié)束語(yǔ)
這里概述的技術(shù)以及在完整應(yīng)用程序源代碼(請(qǐng)參閱 參考資料 )中使用的那些技術(shù)應(yīng)該可以讓您初步了解 curses 編程。請(qǐng)使用它來(lái)編寫您的應(yīng)用程序。它并不難使用。告訴您一個(gè)好消息,除了 Python 以外,有許多語(yǔ)言可以訪問(wèn) curses 庫(kù),因此您學(xué)到的使用 Python curses 模塊的知識(shí)同樣適用于其它語(yǔ)言。
如果經(jīng)檢驗(yàn),基本 curses 模塊不能滿足您的要求,“參考資料”節(jié)中提供了許多模塊的鏈接,他們?cè)鎏砹?curses 的功能并提供了非常好的發(fā)展方向。
更多信息請(qǐng)查看IT技術(shù)專欄