ncurses(new curses)是一款程序庫,提供了一種API,使程序員能夠編寫獨立于終端的文本用戶界面(TUI)。它是一個用于開發“類GUI”應用軟件的虛擬終端工具箱。ncurses還優化了屏幕刷新方法,以降低使用遠程shell時遇到的延遲。
簡短說明
captoinfo 將termcap描述轉化成terminfo描述。
clear 如果可能,就進行清屏操作。
infocmp 比較或顯示 terminfo 描述。
infotocap 將 terminfo 描述轉化成 termcat 描述。
reset 重新初始化終端。
tack terminfo 動作檢測器。主要用來測試 terminfo 數據庫中某一條目的正確性。
ticTic是terminfo項說明的編譯器。這個程序通過ncurses庫將源代碼格式的terminfo文件轉換成編譯后格式(二進制)的文件。 Terminfo文件包含終端能力的信息。
toe 列出所有可用的終端類型,每種分別列出名稱和描述。
tput序利用terminfo數據庫使與終端相關的能力和信息值對shell可用,初始化和重新設置終端,或返回所要求終端為類型的長名。
tset 可以用來初始化終端。
libncurses*這些庫是基于系統用來在顯示器上顯示文本. 一個例子就是,ncurses用在內核的"make menuconfig"進程中。
libform* 在ncurses中使用表格。
libmenu* 在ncurses中使用菜單。.
libpanel*在ncurses中使用面板。
依賴關系
Ncurses 依賴于: Bash, Binutils, Coreutils, Diffutils, Gawk, GCC, Glibc, Grep, Make, Sed.
產品用途
Ncurses 提供字符終端處理庫,包括面板和菜單。
安裝下列程序: captoinfo (link to tic), clear, infocmp, infotocap (link to tic), reset (link to tset), tack, tic, toe, tput 和 tset
安裝下列庫文件: libcurses.[a,so] (link to libncurses.[a,so]), libform.[a,so], libmenu.[a,so], libncurses++.a, libncurses.[a,so] 和 libpanel.[a,so]
應用及步驟
為了能夠使用ncurses庫,您必須在您的源程序中將curses.h包括(include)進來,而且在編譯的需要與它連接起來. 在gcc中您可以使用參數-lcurses進行編譯.
在使用ncurses的時候,您有必要了解它的基礎數據結構。它有一個WINDOW結構,從名字就很容易知道,它是用來描述 您創建的窗體的,所有ncurse庫中的函數都帶有一個WINDOW指針參數.
在ncurses中使用最多的組件是窗體。即使您沒有創建自己的窗體,當前屏幕會認為是您自己的窗體. 如同標準輸入輸出系統提供給屏幕的文件描述符stdout一樣(假設沒有管道轉向),ncurses提供一個 WINDOW指針stdscr做相同工作。除了stdscr外,ncurses還定義了一個WINDOW指針curscr. 和stdscr描述當前屏幕一樣,curscr描述當前在庫中定義的屏幕,您可以帶著"他們有什么區別?"這個問題繼續閱讀.
為了在您的程序中使用ncurses的函數和變量,您必須首先調用initscr函數(初始化工作),它會給一些變量比如 stdscr,curscr等分配內存,并且讓ncurses庫處于準備使用狀態,換句話說,所有ncurses函數必須跟在initscr后面. 同樣的約定,您在結束使用ncurses后,應該使用endwin來釋放所有ncurses使用的內存。在使用endwin后,您將不能在使用 任何ncurses的函數,除非您再一次調用initscr函數
注意事項
WINDOW結構不會經常保持同一高度寬度以及在窗體中的位置,但是會保持在窗體中的內容。當您向窗體寫入數據時, 會改變窗體中的內容,但并不意味著在屏幕中會立即顯示出來,要更新屏幕內容,必須調用refresh或者wrefres函數.
這里介紹了stdscr和curscr兩者之間的區別.curscr保存著當前屏幕的內容,在調用ncurse的輸出函數后,stdscr和curscr可能會有不同的內容, 如果您想在最近一次屏幕內容改變后讓stdscr和curscr保持一致,您必須使用refresh函數。換句話說, refresh是唯一一個處理curscr的函數。千萬不要弄混淆了curscr和stdscr,應該在refresh函數中更新curscr中的內容
refresh有一個能盡可能快的更新屏幕的機制,當調用refresh時,它只更新窗體中內容改變的行,這節省了CPU的處理時間 ,防止程序往屏幕上寫相同的信息(譯者注:在屏幕的同一位置不用重新顯示同樣的內容.)這種機制就是為什么同時使用ncurses的函數 和標準的輸入輸出函數會造成屏幕內容錯位的原因。當調用ncurses的輸出函數時,它會設置一個標志,能讓refresh 知道是哪一行改變了。但是您調用標準輸入輸出函數時,就不會產生這種結果.
refresh和wrefresh其實做了同樣的事情.wrefresh需要一個WINDOW的指針參數,它僅僅刷新該窗體的內容. refresh()等同于wrefresh(stdscr).我在后面會提到,和wrefresh一樣,ncursers的許多函數都有許多這種為stdscr定義的宏函數
創建新窗體
下面我們來談談能定義新窗體的subwin和newwin函數。他們都需要一個來定義新窗體的高度,寬度以及 左上角位置的參數,并且返回一個WINDOW指針來指向該窗體。您可以使用它來作為wrefresh的參數或者一些其他我將要 談到的函數.
您可能會問:"如果他們做同樣的事情,為什么要有兩個函數?",您是對的,他們之間有一些細微的差別.subwin創建一個 窗體的子窗體,它將繼承了父窗體的所有屬性。但如果在子窗體中改變了這些屬性的值,它將不會影響父窗體.
除此之外,父窗體和子窗體之間還有一些聯系。父窗體和子窗體中的內容將彼此共享,彼此影響。換句話說, 在父窗口和子窗體重疊的區域的字符會被任意一個窗體改變。如果父窗體寫入了數據到這塊區域,子窗體中這塊區域同樣 改變了,反之也是如此.
和subwin不同的是,newwin創建一個獨有的窗體。這樣的窗體,在沒有他們的子窗體之前,是不會和其他窗體共享 任何文本數據的。使用subwin的好處是可以使用較少的內存就可以方便的共享字符數據了。但是如果您擔心窗體數據會互相影響 那么就應該使用newwin.
您可以創建任意多層的子窗體,每一個子窗體又可以有它自己的子窗體,但是一定要記住,窗體的字符內容是被兩個以上 的窗體共享的.
當您調用完您定義的窗體后,您可以使用delwin函數來刪除該窗體。我建議您使用man pages來得到這些函數的詳細參數.
輸入數據操作
我們談到了stdscr,curscr,以及刷新屏幕和定義一個新窗體,但是我們怎樣向一個窗體寫入數據?我們怎樣從一個窗體中讀入數據?
實現以上目的函數如同標準輸入輸出庫中的一些函數一樣,我們使用printw來替換printf輸出內容,scanw替換scanf接受輸入, addch替換putc或者putchar,getch替換getc或者getchar.他們用法一樣,僅僅名字不同,類似的,addstr可以用來向窗體 寫入一個字符串,getstr用來從窗體中讀入一個字符串。所有這些函數都是以一個"w"字母開頭,后面再跟上函數的?字, 如果需要操作另外一個窗體內容,第一個參數必須是該窗體的WINDOWS結構指針,舉個例子,printw(...)和wprintw(stdscr,...) 是相同的,就如同refresh()和wrefresh(stdscr)一樣.
如果要寫這些函數的詳細說明,這篇文章將會變的很長。要得到他們的?述,原型以及返回值或者其他信息,man pages是一個不錯的選擇. 我建議您對照man pages檢查您使用的?一個函數。他們提供了詳細和非常有用的信息。在這篇文章的最后一節,我提供了 一個示例程序,可以當作是一個ncurses函數的使用指南.
物理和邏輯指針
在講完寫入數據和從窗體讀出數據后,我們需要解釋一下物理指針和邏輯指針 物理指針是一個常用指針,它只有一個,從另一個方面講,邏輯指針屬于ncurses窗體, 每一個窗體都只有一個物理指針,但是他們可以有多個邏輯指針.
當窗體準備寫入和讀出的時候,邏輯指針會指向窗體中將要進行操作的區域。因此, 通過移動邏輯指針,您可以任何時候向窗體中的任意位置寫入數據。這個是區別與標準輸入輸出庫的優勢之處.
移動邏輯指針的函數是move或者另外一個您非常容易猜出來的函數wmove.move是wmove的一個宏函數,專門用來處理 stdscr的.
另外一個需要確認的是物理指針和邏輯指針的協作關系,物理指針的位置將會在一段寫入程序后無效,但是我們通過可以 通過WINDOW結構的_leave標志定位它。如果設置了_leave標志,在寫操作結束后,邏輯指針將會移動到物理指針指向窗體中最后寫入的區域. 如果沒有設置_leave位,在寫操作結束后,物理指針將返回到邏輯指針指向窗體的第一個字符寫入位置._leave標志是由leaveok函數控制的.
移動物理指針的函數是mvcur,不象其他的函數,mvcur在不用等待refresh動作就會立即生效。如果您想隱藏物理指針, 您可以使用curs_set函數,使用man pages來獲得詳細信息.
同樣存在一些宏函數簡化了上述的移動和寫入等函數。您可以在addch,addstr,printw,getch,getstr,scanw等函數 的man pages頁得到更多的解釋.
清除內容
當我們向窗體寫完內容后,我們怎么樣清除窗體,行和字符?
在ncurses中,清除意味著用空白字符填充整塊區域,整行或者整個窗體的內容. 下面我介紹的函數將會使用空白字符填充必要的區域,達到我們清屏的目的.
首先我們談到能清楚字符和行的函數,delch和wdelch能刪除掉窗體邏輯指針指向的字符,下一個字符和一直到行末的字符都會左移 一個位置.deleteln和wdeleteln能刪除掉邏輯指針指向的行,并且上移下一行.
clrteol和wclrtoeol能清除掉從邏輯指針指向位置右邊字符開始到行末的所有字符.clrtbot和wclrtobot 首先清除掉從邏輯指針所在位置右邊字符開始到行末的所有字符,接著刪除下面所有行.
除了這些,還有一些函數能清除整個屏幕和窗體。有兩種方法可以清除掉整個屏幕。第一個是先用空白字符填充 屏幕所有區域,然后再調用refresh函數。另外一種方法是用固定的終端控制字符清除。第一種方法比較慢,因為它需要重寫 當前屏幕。第二種能迅速清除整個屏幕內容.
erase和werase用空白字符替換窗體的文本字符,在下一次調用refresh后屏幕內容將會被清除掉。但是如果窗體 需要清掉整個屏幕, 這將一個比較苯的辦法。您可以使用上面講的第一種方法來完成。當窗體需要被清除的是一個屏幕那么寬, 您可以使用下面講的函數來非常好的完成您的任務.
在涉及到其他函數之前,我們先來討論一下_clear標志位。如果設置了該標志,那么它會存在WINDOW結構中. 當調用它時,它會用refresh來發送控制代碼到終端,refresh檢查窗體的寬度是否是屏幕的寬度(使用_FULLWIN標志位). 如果是的話,它將用內置的終端方法刷新屏幕,它將寫入除了空白字符外的文本字符到屏幕,這是一種非常快速清屏的方法. 為什么僅僅當窗體的寬度和屏幕寬度相等時才用內置的終端方法清屏呢?那是因為控制終端代碼不僅僅只清除窗體自身 ,它還可以清除當前屏幕._clear標志位由clearok函數控制.
函數clear和wclear被用來清除和屏幕寬度一樣的窗體內容。實際上,這些函數等同與使用werase和clearok. 首先,它用空白字符填充窗體的文本字符。接著,設置_clear標志位,如果窗體寬度和屏幕寬度一樣,就使用內置的終端方法 清屏,如果不一樣就用空白字符填充窗體所有區域再用refresh刷新.
總而言之,如果您知道窗體的寬度和屏幕寬度一樣,就使用clear或者wclear,這個速度將非常快。如果窗體寬度 不是和屏幕寬度一樣,那么使用wclear和werase將沒有任何分別.
使用顏色
您在屏幕上看到的顏色其實都是顏色對,因為每一個區域都有一個背景色和一個前景色。使用ncurses顯示彩色 意味著您定義自己的顏色對并且將這些顏色對寫入到窗體.
如同使用ncurses函數必須先調用initscr一樣,start_color需要首先調用以初始化色素. 您用來定義自己的顏色對的函數是init_pair,當您使用它定義了一個顏色對后,它將會和您在函數中的設置的第一個參數聯系起來. 在程序中,無論您什么時候需要用該顏色對,您只需用COLOR_PAIR調用該參數就可以了.
除了定義顏色對,您還必須使用函數來保證寫入的使用是用不同的顏色對,attron和wattron可以滿足您的要求. 使用這些函數將會用您選擇的顏色對寫入數據到相應的屏幕上,直到調用了attroff或者wattroff函數
bkgd和wbkgd函數可以改變相應的整個窗體的顏色對,調用時,它將會改變窗體所有區域的前景色和背景色。也就 是說,在下一個刷新動作前,窗體上所有的區域將會使用新的顏色對重寫.
使用剛才提到的那些函數man pages來得到詳細的關于顏色資料和信息.
窗體邊框
您可以給您的程序里面的窗體一個很好看的邊框,在庫中有一個box宏函數可以替您做到這一點,和其他函數所 不同的是,沒有wbox函數.box需要一個WINDOW指針來作為參數.
您可以在box的man pages頁輕松獲得詳細的幫助,這里有一些需要注意的是,給一個窗體設置邊框其實只是 在窗體的相應邊框區域寫入了一些字符。如果您在邊框區域一下寫如了某些數據,邊框將會被中斷. 解決的辦法就是在您在原始窗體里面再建一個子窗體,將原始窗體放入到邊框里面然后使用里面的子窗體作為需要的輸入數據窗體.
功能鍵
為了能夠使用功能鍵,必須在我們需要接受輸入的窗體中設置_use_keypad標志位,keypad是一個能設置 _use_keypad值的函數,當您設置了_use_keypad后,您就可以使用鍵盤的功能鍵(快捷鍵),如同普通輸入一樣.
在這里,如果您想使用getch來作個簡單接受數據輸入的例子,您需要注意的是要將數據賦給整形變量(int)而不是 字符型(char).這是因為整形變量能容納的功能鍵比字符型更多。您不需要知道這些功能鍵的值,您只需要使用庫中定義的 宏名就可以了,在getch的man page中有這些數值的列表.
使用范例
我們將來分析一個非常簡單實用的程序。在這個程序中,將使用ncurses定義菜單,菜單中的?一個選擇項都會被證明選種. 這個程序比較有意思的一面就是使用了ncurses的窗體來達到菜單效果。您可以看下面的屏幕截圖.
程序開始和普通一樣,包括進去了一個頭文件。接著我們定義了回車鍵和escape鍵的ASCII碼值.
#include
#define ENTER 10
#define ESCAPE 27
當程序的時候,下面的函數會被調用。它首先調用initscr初始化指針接著調用start_color來顯示彩色. 整個程序中所使用的顏色對會在后面定義。調用curs_set(0)會屏蔽掉物理指針.noecho()將終止鍵盤上的輸入會在屏幕上顯示出來. 您可以使用noecho函數控制鍵盤輸入進來的字符,只允許需要的字符顯示.echo()將會屏蔽掉這種效果. 接著的函數keypad設置了可以在stdscr中接受鍵盤的功能鍵(快捷鍵),我們需要在后面的程序中定義一級方程式錦標賽,F2以及移動的光標鍵.
下面定義的這個函數定義了一個顯示在屏幕最頂部的菜單欄, 您可以看下面的main段程序,它看上去好象只是屏幕最頂部的一行,其實實際上是stdscr窗體的一個子窗體,該子窗體只有 一行。下面的程序將指向該子窗體的指針作為它的參數,首先改變它的背景色,接著定義菜單的?字,我們使用waddstr定義菜單 的?字。需要注意的是wattron調用了另外一個不同的顏色對(序號3)以取代缺省的顏色對(序號2).記住2號顏色對在最開始就 由wbkgd設置成缺省的顏色對了.wattroff函數可以讓我們切換到缺省的顏色對狀態.
void draw_menubar(WINDOW *menubar){ wbkgd(menubar,COLOR_PAIR(2));
waddstr(menubar,"Menu1");
wattron(menubar,COLOR_PAIR(3));
waddstr(menubar,"(F1)");
wattroff(menubar,COLOR_PAIR(3));
wmove(menubar,0,20);
waddstr(menubar,"Menu2");
wattron(menubar,COLOR_PAIR(3));
waddstr(menubar,"(F2)");
wattroff(menubar,COLOR_PAIR(3));
}
下一個函數顯示了當按下F1或者F2鍵顯示的菜單,定義了一個在藍色背景上 菜單欄顏色一樣的白色背景窗體,我們不希望這個新窗口會被顯示在背景色上的字覆蓋掉。它們應該停留在那里直到 關閉了菜單。這就是為什么菜單窗體不能定義為stdscr的子窗體,下面會提到,窗體items是用newwin函數定義的, 其他8個窗體則都是定義成items窗體的子窗體。這里的items被用來繪制一個圍繞在菜單旁邊的邊框,其他的 窗體則用來顯示菜單中選中的單元。同樣的,他們不會覆蓋掉菜單上的邊框。為了區別選中和沒選中的狀態,有必要讓 選中的單元背景色和其他的不一樣。這就是這個函數中倒數第三句的作用了,菜單中的第一個單元背景色和其他的不一樣, 這是因為菜單彈出來后,第一個單元是選中狀態.
WINDOW **draw_menu(int start_col){ int i;
WINDOW **items;
items=(WINDOW **)malloc(9*sizeof(WINDOW *));
items=newwin(10,19,1,start_col);
wbkgd(items,COLOR_PAIR(2));
box(items,ACS_VLINE,ACS_HLINE);
items=subwin(items,1,17,2,start_col+1);
items=subwin(items,1,17,3,start_col+1);
items=subwin(items,1,17,4,start_col+1);
items=subwin(items,1,17,5,start_col+1);
items=subwin(items,1,17,6,start_col+1);
items=subwin(items,1,17,7,start_col+1);
items=subwin(items,1,17,8,start_col+1);
items=subwin(items,1,17,9,start_col+1);
for (i=1;i<9;i++)
wprintw(items[i],"item%d",i);
wbkgd(items,COLOR_PAIR(1));
wrefresh(items);
return items;
}
下面這個函數簡單的刪除了上面函數定義的菜單窗體.它首先用delwin函數刪除窗體, 接著釋放items指針的內存單元.
void delete_menu(WINDOW **items,int count){ int i;
for (i=0;i delwin(items[i]); free(items); } scroll_menu函數允許我們在菜單選擇項上上下移動,它通過getch讀取鍵盤上的鍵值,如果按下了鍵盤上的上移或者下移方向鍵, 菜單選擇項的上一個項或者下一個項被選中。回憶一下剛才所講的,選中項的背景色將會和沒選中的不一樣。如果是向左或者向右 的方向鍵,當前菜單將會關閉,另一個菜單打開。如果按下了回車鍵,則返回選中的單元值。如果按下了ESC鍵,菜單將會被關閉,并且沒有任何選擇項 ,下面的函數忽略了其他的輸入鍵.getch能從鍵盤上讀取鍵值,這是因為我們在程序開始使用了keypad(stdscr,TRUE) 并且將返回值賦給一個int型變量而不是char型變量,這是因為int型變量能表示比char型更大的值. int scroll_menu(WINDOW **items,int count,int menu_start_col){ int key; int selected=0; while (1) { key=getch(); if (key==KEY_DOWN || key==KEY_UP) { wbkgd(items[Selected+1],COLOR_PAIR(2)); wnoutrefresh(items[selected+1]); if (key==KEY_DOWN) { selected=(selected+1) % count; } else { selected=(selected+count-1) % count; } wbkgd(items[selected+1],COLOR_PAIR(1)); wnoutrefresh(items[Selected+1]); doupdate(); } else if (key==KEY_LEFT || key==KEY_RIGHT) { delete_menu(items,count+1); touchwin(stdscr); refresh(); items=draw_menu(20-menu_start_col); return scroll_menu(items,8,20-menu_start_col); } else if (key==ESCAPE) { return -1; } else if (key==ENTER) { return selected; } } } 最后就是我們的main部分了。它使用了上面所有我們所講述和編寫的函數來使程序合適的工作. 它同樣通過getch讀取鍵值來判斷一級方程式錦標賽或者F2是否按下了,并且用draw_menu來在相應的菜單窗體上繪制菜單. 接著調用scroll_menu函數讓用戶選擇某一個菜單,當scroll_menu返回后,它刪除菜單窗體并且顯示所選擇的單元內容 在信息欄里. 我必須提到的是函數touchwin.如果在菜單關閉后沒有調用touchwin而立即刷新,那么最后打開的菜單將一直停留在 屏幕上。這是因為在調用refresh時,menu函數根本就沒有完全改變stdscr的內容。它沒有重新寫入數據到stdscr上, 因為它以為窗體內容沒有改變.touchwin函數設置了所有WINDOW結構中的標志位,它通知refresh刷新窗體中所有的行, 值都改變了,這樣在下一次刷新整個窗體時,即使窗體內容沒有改變也要重新寫入一次。在菜單關閉后,選擇的菜單信息會一直停留在 stdscr上面。菜單沒有在stdscr上寫數據,因為它是開了一個新的子窗口. int main() { int key; WINDOW *menubar,*messagebar; init_curses(); bkgd(COLOR_PAIR(1)); menubar=subwin(stdscr,1,80,0,0); messagebar=subwin(stdscr,1,79,23,1); draw_menubar(menubar); move(2,1); printw("Press F1 or F2 to open the menus. "); printw("ESC quits."); refresh(); do { int selected_item; WINDOW **menu_items; key=getch(); werase(messagebar); wrefresh(messagebar); if (key==KEY_F(1)) { menu_items=draw_menu(0); selected_item=scroll_menu(menu_items,8,0); delete_menu(menu_items,9); wprintw(messagebar,"You haven't selected any item."); else wprintw(messagebar, "You have selected menu item %d.",selected_item+1); touchwin(stdscr); refresh(); } else if (key==KEY_F(2)) { menu_items=draw_menu(20); Selected_item=scroll_menu(menu_items,8,20); delete_menu(menu_items,9); if (selected_item<0) wprintw(messagebar,"You haven't selected any item."); else wprintw(messagebar, "You have selected menu item %d.",selected_item+1); touchwin(stdscr); refresh(); } } while (key!=ESCAPE); delwin(menubar); delwin(messagebar); endwin(); return 0; } 如果您拷貝了代碼到一個文件,假設名字是example.c,并且移走了我所有的注釋,您可以用下面這個方法編譯: gcc -Wall Examplec -o example -lcurses 參考資料 >