Unix Curses 庫導論
Norman Matloff
http://heather.cs.ucdavis.edu/~matloff/
原版地址:http://heather.cs.ucdavis.edu/~matloff/UnixAndC/CLanguage/Curses.pdf
加州大學戴維斯分校計算機科學系
翻譯:Mark
goonyangxiaofang@163.com
目錄
1 歷史
1.1 Curses 庫的目的
1.2 Curses 角色的演化
2 包含和庫文件
3 兩個例子
3.1 一個簡單的,快速引導的例子
3.2 第二個,更有用的例子
3.3 例子中 Coded 和 Raw 模式注釋
4 重要的調試筆記
4.1 GDB
4.2 DDD
5 一些主要的 Curses APIs,屬性和環境變量
5.1 環境
5.2 APIs
5.3 屬性
6 進一步學習
1 歷史
1.1 Curses 庫的目的
許多被廣泛使用的程序需要有一個終端光標移動功能。比如 vi (或者 vim 變種) 編輯器,它的許多功能都需要這樣的功能。例如,當輸入 j 鍵的時候,光標會移動到上一行;輸入 dd 當前行會被刪除,下面的行都向上移動一行,上面的行保持不變。
不同的終端有不同類型的光標動作,這樣導致了一個潛在的問題。例如,如果一個程序想在 VT100 終端上向上移動一行,這個程序需要發送 Escape、[ 和 A 字符。
printf(“%c%c%c”, 27, ‘[’, ‘A’);
(Escape 鍵的 ASCII 值碼為 27)。但是對于 Televideo 920C 終端,程序必須發送 ctrl-K 字符,它的 ASCII 值為 11 。
printf(“%c”, 11);
很明顯,像 vi 這樣的程序的作者為了適應各種終端將變的瘋狂和更糟。其他的每一個需要光標運動的程序員需要重新發明輪子,和 vi 作者做的工作一樣,這樣將會浪費很多時間。
這就是開發 curses 庫的原因。它的目的就是為了減輕那些面向不同的終端需要寫不同的代碼的程序員的減輕。程序只需要去調用庫,庫可以知道針對什么樣的終端具體做什么。
例如,如果你需要清屏,這里就不需要直接使用上面說到的任何字符序列。相反地,你只需要調用
clear();
curses 可以幫助這個程序做具體的工作,例如可以輸出字符 Escape、[ 和 A ,以清屏。
1.2 Curses 角色的演化
在 Unix 軟件的演化進程中,curses 庫的發展是重要的一步。即便 VT100 終端成為了標準,curses 庫繼續扮演著重要的角色,它為程序員提供了一個抽象層,使其避免了解具體的 VT100 的光標動作代碼。
Curses 庫在今天的面向 GUI 世界里一直起著重要的作用,因為在很多應用中,使用鍵盤要比使用鼠標更為方便(許多 Microsoft Windows 應用程序里都提供了鍵盤快捷鍵)。今天,物理終端被很少使用了,但是典型的 Unix 工作包含了一些效仿 VT100 終端的 文本窗口。Vi 編輯器和其他基于 curses 的應用程序繼續很流行。
2 包含和庫文件
為了使用 curses ,你必須在你的源代碼中包含這條語句
#include <curses.h>
你必須鏈接 curses 庫
gcc –g sourcefile.c –lcurses
3 兩個例子
在例子中學習
試著去運行這些程序!你不鍵入這些源代碼,相反地,你可以獲取從產生這個文檔的生文件中獲取它們,http://heather.cs.ucdavis.edu/~matloff/UnixAndC/CLanguage/Curses.tex,然后將非代碼刪除。
如果你比較急,你可以直接去第二個例子,有更好的注釋并且使用了更多的 curses 特性。
3.1 一個簡單的,快速引導的例子
第一個例子幾乎沒做什么。你可以認為這是一個原始的乏味的游戲。但是它給我們一個開頭。
源文件最上面的注釋告訴了你這個游戲做什么。編譯并運行這個程序,輸入你喜歡的字符。然后參考解釋的注釋閱讀代碼(從 main() 函數開始,一般情況下你應該如此)。
1 // 簡單的 curses 例子;畫輸入的字符,按照列向下的方式,當到達最后一行時轉向
2 // 右,當最右邊到達時環繞。
3
4 #include <curses.h>
5
6 int r, c; // 當前行與列
7 int nrows, ncols; // 窗口的行列數
8
9 void draw(char dc)
10 {
11 move(r, c); // 移動到當前行和列
12 delch();
13 insch(dc); // 替換字符
14 refresh(); // 更新屏幕
15 ++r; // 下一行
16 // 檢查是否需要轉向右或是環繞
17 if (r == nrows)
18 {
19 r = 0;
20 ++c;
21 if (c == ncols)
22 {
23 c = 0;
24 }
25 }
26 }
27
28 int main()
29 {
30 int i;
31 char d;
32 WINDOW* wnd;
33
34 wnd = initscr(); // 初始化窗口
35 cbreak(); // 為鍵入鍵設置不要等
36 noecho(); // 設置不回送
37 getmaxyx(wnd, nrows, ncols); //窗口的大小
38 clear(); // 清屏,設置光標到 (0, 0)
39 refresh(); // 實現從上次刷新所有改變
40
41 r = 0;
42 c = 0;
43 while (1)
44 {
45 d = getch(); // 鍵盤輸入
46 if (d == 'q')
47 {
48 break;
49 }
50 draw(d);
51 }
52 endwin(); // 還原原始窗口和離開
53 }
3.2 第二個,更有用的例子
這個程序你可以實際使用。如果你需要結束掉一些進程,這個程序可以允許瀏覽和刪除你想刪的進程。
同樣,編譯和運行這個程序,結束一些你已經用于其他目的已創建的垃圾進程。(特別地,你可以刪除 psax 它自身。)然后依據解釋的注釋閱讀該代碼。
1 // psax.c
2
3 // 運行 shell 命令 'ps ax', 在最后行顯示它的輸出,盡可能多的窗口適合;允許用戶在窗口中上下移動,可以選擇結束那些高亮的進程。
4
5 // 用法: psax
6
7 // 用戶命令
8
9 // 'u': 上移
10 // 'd': 下移
11 // 'k': 刪除高亮的進程
12 // 'r': 重新運行 'ps ax' 更新
13 // 'q': 結束
14
15 // 可能的擴展:允許滾動,這樣可以使用戶查看所有的 'ps ax' 輸出,而不只是最后行;
16 // 允許卷上長行;
17 // 詢問用戶是否確認結束一個進程
18
19 #define MAXROW 1000
20 #define MAXCOL 500
21
22 #include <curses.h>
23
24 WINDOW* scrn; // 指向 curses 窗口對象
25
26 char cmdoutlines[MAXROW][MAXCOL]; // 'ps ax' 的輸出,最好使用 malloc()
27
28 int ncmdlines; // cmdoutlines 的行數
29 int nwinlines; // xterm 或 equiv. 窗口中 'ps ax' 輸出的行數
30 int winrow; // 屏幕上當前行的位置
31 int cmdstartrow; // 顯示的 cmdoutlines 的第一行的索引
32 int cmdlastrow; // 顯示的 cmdoutlines 的最后一行的索引
33
34 // 在 winrow 上用黑體重寫
35 void highlight()
36 {
37 int clinenum;
38 attron(A_BOLD);
39
40 clinenum = cmdstartrow + winrow;
41
42 mvaddstr(winrow, 0, cmdoutlines[clinenum]);
43 attroff(A_BOLD);
44 refresh();
45 }
46
47 void runpsax()
48 {
49 FILE* p;
50 char ln[MAXCOL];
51 int row, tmp;
52 p = popen("ps ax", "r");
53 for (row = 0; row < MAXROW; ++row)
54 {
55 tmp = fgets(ln, MAXCOL, p);
56 if (tmp == NULL)
57 {
58 break;
59 }
60 strncpy(cmdoutlines[row], ln, COLS);
61 cmdoutlines[row][MAXCOL - 1] = 0;
62 }
63 ncmdlines = row;
64 close(p);
65 }
66
67 void showlastpart()
68 {
69 int row;
70 clear();
71
72 if (ncmdlines <= LINES)
73 {
74 cmdstartrow = 0;
75 nwinlines = ncmdlines;
76 }
77 else
78 {
79 cmdstartrow = ncmdlines - LINES;
80 nwinlines = LINES;
81 }
82 cmdlastrow = cmdstartrow + nwinlines - 1;
83
84 for (row = cmdstartrow, winrow = 0; row <= cmdlastrow; ++row, ++winrow)
85 {
86 mvaddstr(winrow, 0, cmdoutlines[row]);
87 }
88 refresh();
89 --winrow;
90 highlight();
91 }
92
93 void updown(int inc)
94 {
95 int tmp = winrow + inc;
96 if (tmp >= 0 && tmp < LINES)
97 {
98 mvaddstr(winrow, 0, cmdoutlines[cmdstartrow + winrow]);
99 winrow = tmp;
100 highlight();
101 }
102 }
103
104 void rerun()
105 {
106 runpsax();
107 showlastpart();
108 }
109
110 void prockill()
111 {
112 char* pid;
113 pid = strtok(cmdoutlines[cmdstartrow + winrow], " ");
114 kill(atoi(pid), 9);
115 rerun();
116 }
117
118 int main()
119 {
120 char c;
121 scrn = initscr();
122 noecho();
123 cbreak();
124 runpsax();
125 showlastpart();
126 while (1)
127 {
128 c = getch();
129 if (c == 'u')
130 {
131 updown(-1);
132 }
133 else if (c == 'd')
134 {
135 updown(1);
136 }
137 else if (c == 'r')
138 {
139 rerun();
140 }
141 else if (c == 'k')
142 {
143 prockill();
144 }
145 else
146 {
147 break;
148 }
149 }
150 endwin();
151 return 0;
152 }
3.3 例子中 Coded 和 Raw 模式注釋
在你寫的大多數程序中,鍵盤操作是出于熟模式下的。也就是說,在你按下回車鍵之前你的鍵盤輸入是不會發送給程序的(例如,不會發送給 scanf() 函數或 cin )。這種方式可以允許你使用退格鍵來刪除那些你鍵入的但是又想撤銷字符。并且,你的程序不能識別到你刪除的字符以及退格鍵(其 ASCII 碼為 8)。
記住,當你在鍵盤上敲擊字符時,字符被送到操作系統。操作系統一般情況下將這些字符傳遞給你的應用程序(例如,傳遞給調用接口 scanf() 或者 cin ),但是如果操作系統發現了退格鍵字符,操作系統不會將這個字符傳遞給應用程序。事實上,操作系統在用戶鍵入回車鍵之前不會將任何字符傳遞給程序。
另一種是原生模式。這種模式下,操作系統將每個字符傳遞給應用程序。如果用戶鍵入了退格鍵,它被看作與其他字符一樣,沒有什么特殊的操作。應用程序接收到一個 ASCII 碼為 8 的字符,這由程序員決定如何處理這個字符。
你可以調用 cbreak() 函數從熟模式切換到原生模式,也可以調用 nocbreak() 函數從原生模式切換到熟模式。
相似地,默認模式是為了操作系統來回顯字符。如果用戶鍵入 A 鍵(沒有 Shift),操作系統將在屏幕上打印出 ‘a’ 。有些情況使我們不需要回顯字符,例如在我們意見看到的例子中,或者在輸入密碼的情況下。我們可以通過調用 noecho() 函數關閉回顯功能,也可以調用 echo() 函數恢復回顯功能。
4 重要的調試筆記
不用使用 printf() 或者 cout 來調試!確保你使用調試工具,例如 GDB 或者帶有 DDD 界面的 GDB 。如果在日常編程工作中不適用一個調試工具,你將花費大量的不必要的時間和碰到很多的挫折。請看我的調試幻燈片演示:http://heather.cs.ucdavis.edu/~matloff/debug.html
在 curses 程序中,你無法使用 printf() 或 cout 來調試程序,因為那種將會把你的程序輸出弄的一團糟。所以一個調試工具是必要的。這里我們使用 GDB 和 DDD 來調試 curses ,這些調試工具在 Unix 世界里非常常用。你使用這個程序將你的調試信息輸出與你的 curses 應用程序輸出分開。這里我將演示如何調試。
4.1 GDB
在文本窗口中啟動 GDB 。為你的 curses 應用程序選擇另一個窗口來運行,為后面的窗口選擇一個設備名字(我們可以叫做“執行窗口”)。在窗口中運行 tty Unix 命令。在這個例子中,我們假設命令輸出是 “/dev/pts/10” 。在 GDB 中的命令是:
(gdb) tty /dev/pts/10
在 run 命令之前,我們還要做其他事情。在執行窗口中鍵入:
Sleep 10000
Unix 的 sleep 命令讓 shell 在給定的時間內進入失效狀態,在這個例子中是 10 秒。這個是很必要的,因為這樣可以使我們在窗口中鍵入的信息多傳遞給我們的應用程序而不是傳遞給了 shell 。
現在回到 GDB 并且執行 run 命令。記住,不管何時你的程序執行到了斷點并且從鍵盤輸入,你必須在執行窗口中鍵入(你也將要看到程序的輸出)。把這些做完后,在執行窗口中鍵入 ctrl-C 刪除 sleep 命令,確保 shell 可用。
注意如果有什么錯誤并且你的程序提前結束了,執行窗口可以保持一些非標準的終端配置,不如 cbreak 模式。修改這個,回到窗口并鍵入 ctrl-j ‘reset’ 再一次鍵入 ctrl-j 。
4.2 DDD
在 DDD 差不多。只是點擊視圖|執行窗口,一個彈出來的窗口作為執行窗口。
5 一些主要的 Curses APIs,屬性和環境變量
5.1 環境
·窗口中行和列的的號碼從 0 開始,從上到下從左到右。所以左上角的最表是 (0, 0)。
·LINES, COLS
窗口中行和列的數目。
5.2 APIs
(許多 API 是宏而不是真正的函數)
這里有一些你會調用的 curses 函數。注意這里羅列的函數中只有一些是可以使用的??梢允褂?/span> curses 做許多其他的東西,例如子窗口,窗體樣式輸入等。第六部分有對資源的介紹。
·WINDOW* initsrc()
REQUIRED. 為 curses 初始化整個屏幕。返回一個執行 WINDOW 類型結構體的指針,
該指針被其他函數所使用。
·endwin()
重新設置終端,例如恢復回顯功能、熟模式等。
·cbreak()
設置終端,鍵入的字符即是所讀取的字符,不用等待鍵入回車鍵。退格鍵和其他控制鍵(包括回車鍵)失去了他們原來的意義。
·nocbreak()
回到一般模式
·noecho()
關閉將輸入字符顯示到屏幕上的回顯功能。
·echo()
恢復回顯功能
·clear()
清屏,從新將光標設置在左上角。
·move(int, int)
將光標設置到指定的列和行。
·addch(char)
在當前光標位置打印給定的字符,改寫這個位置之前的字符,將光標移到右面的下一個位置。
·insch(char)
與 addch() 一樣,插入代替改寫,所有右邊的字符都向右移動一個位置。
·mvaddstr(int, int, char*)
將光標移動到指定的行和列,在指定位置打印字符串。
·refresh()
更新屏幕,以相應上一次調用這個函數所發生的所有變化。不過我們做了什么變化,例如調用上面的 addch() 或者不會出現在屏幕上的變化,只有調用了 refresh() 才回有效果。
·delch()
將當前光標處的字符刪除掉,右邊的所有字符都將向做移動一個位置,光標的位置不會變化。
·int getch()
從鍵盤處讀取一個字符。
·char inch()
返回當前光標下的字符。
·getyx(WINDOWS*, int, int)
返回窗口的光標當前位置的行和列數。
(注意,這里函數是宏,所以這里是 ints 不是只想 int 的指針)
·getmaxyx(WINDOW*, int, int)
返回指定窗口的行和列數。
·scanw(), printw()
curses 環境下的類似與 scanf() 和 printf() 的函數。避免在 curses 環境下使用 scanf() 和 printf() 函數,否則將會造成奇異的結果。注意 printw() 和 scanw() 函數重復調用 addch(), 所以他們是做改寫工作不是插入。Scanw 知道遇到換行符或者回車符的時候才結束,wgetstr() 函數也是如此。
·attron(const), attroff() const
將給定的屬性開啟或者關閉。
5.3 屬性
字符可以通過許多不同的方式來顯示,比如一般模式(A_NORMAL)和 黑體模式(A_BOLD)??梢酝ㄟ^ APIs attron() 和 attroff() 函數設置。前面的被調用后,所有打印在屏幕上的內容使用給定參數的屬性,后面的被調用則終止這種狀態。
6 進一步學習
在 man 手冊中有基本的幫助教程。
man curses
(你可以用 ncurese 代替 curses) 個別的函數有單獨的 man 頁面。
在網上有很多好的輔導資料。在你最喜愛的搜索引擎中鍵入“curses tutorials”或者“ncurses tutorials”。以可以從這個輔導材料開始:http://www.linux.com/howtos/NCURSES-Programming-HOWTO/index.html
注意,盡管有一些關于非 C 語言的輔導資料,例如 Perl 或者 Python 的。我不推薦這些(包括我自己,Python)。不僅語法存在不同,還有 API 也有很多的不同。
posted on 2011-07-21 17:12
unixfy 閱讀(338)
評論(0) 編輯 收藏 引用