(超詳細) ncurses 庫使用介紹: 實現終端 GUI
這次 ENGG1340 課程的 group project 是設計并實現一個 text-base game,作為終端上運行的 text-base game,有一個出彩的 GUI 肯定是一個加分項!
在未老師的介紹下,我知道了有 <ncurses.h> 庫這么一個神奇的東西;最重要的是,雖然它不屬于 C++ 標準庫,但是在學校 server 里居然默認下載好了 (可見其出名的功能強大)
據說很多熱門的終端程序,例如 Vim,SL 都用到了 ncurses
花了一個下午學習了一下用法,在這里簡單的總結一下,并且附上一些簡單 GUI 組成的實例
Installation
官方 release 網址在這里
在終端上輸入命令 sudo apt-get install libncurses5 進行安裝
Compilation
所有使用了 <ncurses.h> 庫的程序,在編譯時需要添加參數 -l ncurses 使編譯器鏈接到 ncurses 庫
例: g++ -o test test.cpp -lncurses
初始化窗口對象
使用 initscr() 初始化標準窗口對象 stdscr (WINDOW 類)
初始化過后,我們接下來將與該窗口對象進行交互: std::cin/out, scan/printf 等標準輸入輸出將失效
返回常規終端模式
當使用完 ncurses,想回到常規終端模式時,使用 endwin() 來關閉窗口
通常在程序退出前調用 endwin()
輸入/輸出
在調用 endwin() 之前,標準輸入輸出將失效
想要與初始化后的窗口對象交互,我們需要使用 ncurses 庫提供的輸入/輸出函數
#include <ncurses.h>
int scanw(char* format, ...); // 用法與 scanf 相同
int printw(char* format, ...); // 用法與 printf 相同
int getch(void); // 用法同 getchar, 有阻礙程序繼續的功能
由于 gp 中使用了很多 string,我自定義了兩個接口用來處理 string 類型的輸入輸出
#include <string>
#include <ncurses.h>
string instr() {
char s[1024];
scanw("%[^\n]", s); // 這里對標的是 getline, 回車時結束讀入
return string(s);
}
void outstr(string s) {
printw("%s", s.c_str());
}
在指定位置輸出
在 ncurses 模式中,我們可以通過 mv() 函數輕松控制光標的位置,從而實現在終端的指定位置輸出
mv(x, y) 將光標移動到當前窗口的第 \(x\) 行第 \(y\) 列 (行列均從 \(0\) 開始),且接下來輸出的內容都將從該位置開始
以下實例將在從屏幕中央開始 (注意,是開始而不是位于) 輸出 "Hello World!"
#include <ncurses.h>
...
int scrLine, scrCol;
getmaxyx(stdscr, scrLine, scrCol); // 獲取標準屏幕的行/列數
move(scrLine / 2 - 1, scrCol / 2 - 1); // 將光標移至屏幕中央
printw("Hello World!");
...
移動光標和輸出可以在同一個函數 mvprintw() 中完成: 例如上例程序可寫成 mvprintw(scrLine / 2 - 1, scrCol / 2 - 1, "Hello World!")
在輸出完畢后,使用 refresh() 進行刷新,將輸出顯示到當前屏幕上
新窗口/子窗口
創建新窗口/子窗口
在未指定的情況下,initscr() 初始化的窗口是標準屏幕 stdscr
而有時我們需要多個窗口;通過創建新窗口 (newwin) 或子窗口 (subwin)
創建新窗口時,其會被分配一個新的內存;而子窗口與其父窗口共用內存
WINDOW* newwin(int line, int col, int x, int y); // 創建新窗口
WINDOW* submin(WINDOW* parent, int line, int col, int x, int y); // 創建子窗口,其父窗口為 parent
刪除新窗口/子窗口
使用 del(win) 刪除窗口 win,即,釋放其所占用的內存
-
刪除窗口 win 并不代表 屏幕上 win 輸出的內容會消失,因此在刪除之前調用
wclear(win)與wrefresh(win)進行清屏 -
不要刪除 ncurses 的默認窗口
stdscr, 結束它使用endwin()即可
非標準窗口的交互操作
超級簡單易懂,之前我們介紹的所有對 stdscr 的函數,加上前綴 w,再添加對應窗口的指針作為參數就得到了與非標準窗口交互的函數
例如:printw() 對應 wprintw(win), refresh() 對應 wrefresh(win), mvprinrw() 對應 mvwprintw(win) (這個位置稍有不同)
WINDOW* win = newwin(30, 30, 0, 0);
wmvprintw(win, 1, 1, "Joker"); // 注意,這里的 (1, 1) 是對于窗口 win 的相對位置,而不是對于 stdscr 的絕對位置
wrefresh(win); // 進行刷新,使內容顯示到屏幕上
or
WINDOW* sub = subwin(stdscr, 30, 30, 0, 0); // sub 是標準窗口的子窗口
wmvprintw(sub, 1, 1, "Skull");
touchwin(stdscr); // 由于 sub 與標準窗口共用內存,我們用 touchwin() 函數標記標準窗口被修改即可,而非使用 wrefresh()
子窗口與父窗口
當我們使用 wmove() 或其他對非標準窗口的交互函數對子窗口進行操作時,父窗口 (或其他子窗口) 輸出的內容會暫時消失
解決方法是:在 touchwin(fascr) 過后,再對父窗口進行刷新
即 wrefresh(father_scr) 或 refresh() (當父窗口是標準窗口時)
WINDOW* upwin = subwin(stdscr, scrLine / 2, scrCol, 0, 0);
WINDOW* downwin = subwin(stdscr, scrLine / 2, scrCol, scrLine / 2, 0);
box(upwin, '|', '+');
box(downwin, '|', '+');
touchwin(stdscr);
wmove(downwin, 1, 1);
// refresh();
int a;
wscanw(downwin, "%d", &a);
getch();
當未加 refresh() 時,upwin 窗口的輸出內容 (即 box 繪出的邊框) 將不會顯示

只有將對父窗口重新刷新 refresh() 之后,所有的輸出內容才會顯示

窗口轉儲
這在實現 text-base game 中是一個很實用的功能,它可以用來實現場景的切換,與讀/存檔功能
例如,"開始" 界面可以進入 "游戲" 界面,但從 "游戲" 界面返回時,由于屏幕已經被 "游戲" 界面所在窗口覆蓋,我們需要重新加載 "開始" 界面
此時,利用窗口的轉儲,我們可以直接恢復之前的界面
標準窗口轉儲
標準窗口 stdscr 可以利用下面的兩個函數進行存儲與讀取:
int scr_dump(const char*); // 參數是當前目錄下文件的名稱: scr_dump 將會把標準窗口中的內容存儲到對應名稱的文件中
// 若當前目錄下沒有該名稱的文件,將會創建一個
int scr_restore(const char*); // scr_restore 函數將會讀取對應文件中存儲的內容,并將其載入到標準窗口中
以下是一個簡單的例程 (gp 中的界面轉換可以以此為基礎)
printw("Main Menu\n"); // 標準窗口作為主界面 main menu
refresh();
scr_dump("a"); // 儲存當前標準窗口的內容
getch();
WINDOW* win = newwin(scrLine, scrCol, 0, 0);
wprintw("Game Started!\n"); // 一個與標準窗口相同大小的新窗口 win 作為游戲界面
wrefresh(win);
getch();
wclear(win);
wrefresh(win); // 游戲界面結束,將屏幕清空
delwin(win); // 釋放游戲界面所在窗口 win 的內存
// getch();
scr_restore("a"); // 返回主界面 main menu, 使用 scr_restore() 恢復
refresh();
若有時我們需要暫時退出 ncurses 模式,回到行緩沖模式,我們可以儲存當前標準窗口的內容后再退出
(或使用 def_prog_mode() 與 reset_prog_mode() 函數,由于 gp 中可能不會用到,這里不多介紹)
非標準窗口轉儲
當然,除了標準窗口 stdscr,其他任何我們新創建的窗口都可以進行轉儲
使用以下的兩個函數:
int putwin(WINDOW*, FILE*); // 存
int getwin(WINDOW*, FILE*); // 讀
// 注意,這兩個函數的文件參數不是文件名 (字符串),而是文件指針
滾屏操作
在行緩沖模式中,若輸出的內容超過了終端的 bottom line,將會自動滾屏 (舊的輸出將會向上滾動,為新的輸出留位置)
我以為 ncurses 中的窗口默認滾屏,結果當輸出超出屏幕時,反而向右溢出了
我翻了好久的庫文檔,終于找到了對應的函數 (這里)
int scrollok(WIN*, bool); // 在 WIN 指針指向的窗口中開啟 (true)/關閉 (false) 滾屏
setscrreg(int x, int line); // 在 scrollok 開啟后,可以在窗口中設立一個滾屏區,在該區域內有滾屏
// 滾屏區從第 [x] 行開始,共占 [line] 行
顏色設置
有時候我們希望改變窗口的背景與文本顏色, ncurses 庫對此也提供了支持
在使用之前,我們先初始化顏色設置
bool has_color(void); // 返回該環境是否支持顏色設置
int start_color(void); // 初始化顏色設置
在 start_color() 成功調用后,一系列的常量將會產生,例 COLOR (支持的顏色數量),COLOR_BLACK (黑色), COLOR_WHITE...
我們使用 init_pair() 函數來創建 背景-文本 的顏色對,并用 attron() 函數激活
接下來,直到 attroff() 關閉之前,所有輸出的 背景-文本 都將是指定的顏色對
init_pair(1, COLOR_BLACK, COLOR_WHITE); // [黑]底[白]字為第 1 個顏色對
// 注意,每一種顏色對的編號必須是不同的!
attron(COLOR_PAIR(1)); // 傳入之前定義的顏色對作為參數 (以編號表示),開啟顏色設置
printw("Black background white text\n");
attroff(COLOR_PAIR(1)); // 關閉顏色設置,恢復默認
// 此處也可以直接 attron 其他顏色對,而無需先 attroff
對非標準窗口的操作使用 wattron(WINDOW*, ...) 與 wattroff(WINDOW*, ...)
其他輸出文本效果
attron() 與 attroff() 不僅可以用來設定顏色,還能夠實現許多輸出文本效果
這些效果通過一系列常量來代表: A_BLINK, A_DIM, A_UNDERLINE
例如: attron(A_BLINK),那么在 attronoff(A_BLINK) 之前輸出的文本都將閃爍顯示
這些效果的復合 (甚至與顏色的復合) 可以通過常量間的或 | 運算簡單的實現 (這與 windows 對終端文本顏色的操作很相似)
例如 attron(A_BLINK | A_UNDERLINE) 代表文本閃爍且加下劃線顯示
這里附上 ncurses 提供的所有輸出文本效果常量
A_NORMAL // 普通字符輸出(不加亮顯示)
A_STANDOUT // 終端字符最亮
A_UNDERLINE // 下劃線
A_REVERSE // 字符反白顯示
A_BLINK // 閃動顯示
A_DIM // 半亮顯示
A_BOLD // 加亮加粗
A_PROTECT // 保護模式
A_INVIS // 空白顯示模式
A_ALTCHARSET // 字符交替
A_CHARTEXT // 字符掩蓋
COLOR_PAIR(n) // 前景、背景色設置
Reference
- Debian ncurses manpage
- tiga-Unix/Linux下的Curses庫開發指南——第三章curses庫窗口 (這好像也是轉載的,但是翻譯的很好,也很全)
- KeBlog-ncurses (Kewth NB)
- ztq-ncurses庫 常用函數及基本使用 (有一個通過子窗口實現分屏的程序,gp 的時候可以借鑒,修改一下參數)
- ncurses 輸出修飾 (包含了 ncurses 提供的一系列 attr 前綴函數,對輸出進行修飾,很有用)

浙公網安備 33010602011771號