# ncurses 教學
[TOC]
## 什麼是 ncurses
ncurses(new curses)是一個程式函式庫,它提供了API 讓程式設計師編寫獨立於終端的基於文字的使用者介面 (TUI,**t**ext-based **u**ser **i**nterfaces)
## 安裝
- Manual
從 https://ftp.gnu.org/gnu/ncurses/ 下載,並照著 README 和 INSTALL 檔案中的步驟安裝
- Ubuntu/Debian
```bash!
sudo apt install libncurses5-dev libncursesw5-dev
```
- Fedora/RHEL
```bash!
sudo dnf install ncurses-devel
```
- Arch
```bash!
sudo pacman -S ncurses
```
## 開始使用
### 引入 ncurses
使用 ncurses 時要引用 `ncurses.h` 標頭檔
```c!
#include <ncurses>
```
### 編譯
編譯時要加上 `-lncurses` 使編譯器連結 ncurses 函式庫
### Hello World
這是一個使用 ncurses 並顯示 Hello World! 的範例程式,之後會詳細說明函式的用法。
```c=
// helloworld.c
#include <ncurses.h>
int main(){
initscr();
printw("Hello World!");
refresh()
getch();
endwin();
return 0;
}
```
編譯指令
```bash!
gcc -o helloworld helloworld.c -lncurses
```
執行畫面

## 初始設定
一開始當然要先進行一些初始設定,這是讓你從終端邁入 TUI 的第一步
### 初始化 curses 視窗
- `initscr()` 初始化並開啟 curses 視窗
### 結束 curses 視窗
- `endwin()` 結束 curses 視窗並回到終端
### 禁用輸入緩衝
通常,終端會緩衝用戶輸入的字符,直到遇到 tab 或 enter 為止。
但是我們希望開發能夠在使用者輸入後直接送給程式
- `cbreak()` 關閉行緩衝,鍵盤輸入即時送出
- `raw()` 更嚴格的即時輸入模式,連 Ctrl-C 等中斷鍵也會被程式接收。
### 鍵盤輸入顯示
控制是否將鍵盤輸入的字符顯示到終端。
- `echo()` 開啟顯示
- `noecho()` 關閉顯示
### 特殊鍵讀取
- `keypad(stdscr, TRUE)` 啟用讀取特殊鍵,像是 F1 ~ F12、方向鍵
## 輸出
開啟 curses 視窗會蓋住整個終端,因此使用 printf 會看不到
### 輸出函式
- `printw(const char *fmt, ...)` 使用方法和 printf 一樣,會將文字輸出到 curses 視窗上
- `addch(const chtype ch)` 輸出單一字元
:::info
可以發現,addch 的參數型態是 chtype 不是 char,因為它可以設定一些屬型,例如輸出一個體加底線的字元可以這樣
```c!
addch(ch | A_BOLD | A_UNDERLINE);
```
詳細的屬性會在後面提到
:::
- `addstr(const char * str)` 輸出一個字串
### 清空內容
- `clear()` 清空螢幕上所有內容
### 刷新
使用前面的輸出和清空後都要使用 `refresh()` ,螢幕上的內容才會刷新
### 移動游標
- `move(int y, int x)` 移動游標到 x 行第 y 個字 ( x 和 y 從0開始 )
在前面介紹的輸出函式前加上 mv 就可以直接移動並輸出,例如
```c!
move(10, 10);
addch('A');
```
可以用 `mvaddch()` 一個函式代替
```c!
mvaddch(10, 10, 'A');
```
### 屬性
ncurses 有 attr 屬性,用來為輸出的文字添加特殊效果
```
A_NORMAL 正常顯示
A_BOLD 粗體或高亮
A_DIM 暗淡(比正常文字亮度低)
A_UNDERLINE 底線
A_REVERSE 反相顯示(黑底白字)
A_STANDOUT 突出顯示(通常是反相文字)
A_BLINK 閃爍
A_INVIS 不可見或空白
A_ALTCHARSET 使用替代字元集
A_PROTECT 文字保護,防止文字被覆蓋
```
- `attron(attr)` 開啟指定屬性
- `attroff(attr)` 關閉指定屬性
- `attrset(attr)` 設置屬性並覆蓋當前視窗的所有屬性
如果想要同時使用多個屬性,可以使用 `|` (or) 來隔開,例如同時開啟粗體和底線
```c~
attron(A_BOLD | A_UNDERLINE);
```
### 顏色
#### 初始化顏色功能
- `bool has_colors()` 判斷終端是否支持顏色功能
- `start_color()` 啟用顏色功能
#### 改變輸出顏色
1. 建立顏色對 `init_pair(short pair, short f, short b)`
`pair` 是顏色對的編號(0 到 COLOR_PAIRS - 1)
`f` 是前景(文字顏色)顏色,使用顏色編號(0 到 COLOR - 1)
`b` 是背景顏色,同樣用顏色編號
有一些預定義的顏色巨集可以使用
```
COLOR_BLACK 0
COLOR_RED 1
COLOR_GREEN 2
COLOR_YELLOW 3
COLOR_BLUE 4
COLOR_MAGENTA 5
COLOR_CYAN 6
COLOR_WHITE 7
```
2. 套用顏色對
把 `COLOR_PAIR(n)` 當作屬性套用,n 是剛剛的顏色對編號
#### 範例
```c=
#include <ncurses.h>
int main(){
initscr();
if (has_colors() == FALSE){
endwin();
printf("Your terminal does not support color\n");
return 1;
}
start_color();
init_pair(1, COLOR_RED, COLOR_BLUE);
attron(COLOR_PAIR(1));
printw("This text is red and blue background\n");
attroff(COLOR_PAIR(1));
getch();
endwin();
return 0;
}
```

#### 更改顏色定義
如果不喜歡預定義的顏色,你可以使用
`init_color(short color, short r, short g, short b)`
color 是顏色編號,將改顏色編號改成指定的rgb,rgb的值最小為0,最大為1000
## 輸入
前面介紹了輸出,當然也會有輸入
### 鍵盤輸入
- `scanw(const char *fmt, ...)` 使用方法和 scanf 相同
- `int getch()` 等待用戶按下一個鍵,並回傳該鍵的鍵值
如果輸入的是普通字元,那就會回傳該字元的ascii值;否則會返回和可以與 ncurses.h 中定義的巨集相匹配的數字
例如按下「向下鍵」時輸出 "press down"
```c!
if(getch == KEY_DOWN){
printw("press down");
}
```
:::warning
要使用特殊鍵記得先啟用特殊鍵讀取 `keypad(stdscr, TRUE)`
:::
- `getstr(char *str)` 取得輸入字串到 str
### 滑鼠輸入
#### 設定滑鼠事件類型
要使用滑鼠輸入,需要先啟用並指定接收類型
`mousemask(mmask_t new-mask, mmask_t *old-mask)`
在 new-mask 填入滑鼠事件類型,old-mask 可以填入 NULL
滑鼠事件類型:
```
BUTTON1_PRESSED 滑鼠按鈕 1 按下
BUTTON1_RELEASED 滑鼠按鈕 1 放開
BUTTON1_CLICKED 滑鼠按鈕 1 單擊
BUTTON1_DOUBLE_CLICKED 滑鼠按鈕 1 雙擊
BUTTON1_TRIPLE_CLICKED 滑鼠按鈕 1 三擊
BUTTON2_PRESSED 滑鼠按鈕 2 按下
BUTTON2_RELEASED 滑鼠按鈕 2 放開
BUTTON2_CLICKED 滑鼠按鈕 2 單擊
BUTTON2_DOUBLE_CLICKED 滑鼠按鈕 2 雙擊
BUTTON2_TRIPLE_CLICKED 滑鼠按鈕 2 三擊
BUTTON3_PRESSED 滑鼠按鈕 3 按下
BUTTON3_RELEASED 滑鼠按鈕 3 放開
BUTTON3_CLICKED 滑鼠按鈕 3 單擊
BUTTON3_DOUBLE_CLICKED 滑鼠按鈕 3 雙擊
BUTTON3_TRIPLE_CLICKED 滑鼠按鈕 3 三擊
BUTTON4_PRESSED 滑鼠按鈕 4 按下
BUTTON4_RELEASED 滑鼠按鈕 4 放開
BUTTON4_ CLICKED 滑鼠按鈕 4 單擊
BUTTON4_DOUBLE_CLICKED 滑鼠按鈕 4 雙擊
BUTTON4_TRIPLE_CLICKED 滑鼠按鈕 4 三擊
BUTTON_SHIFT shift鍵在滑鼠狀態變更時按下
BUTTON_CTRL control鍵在滑鼠狀態變更時按下
BUTTON_ALT alt鍵在滑鼠狀態變更時按下
ALL_MOUSE_EVENTS 接收所有滑鼠狀態
REPORT_MOUSE_POSITION 回報滑鼠位置
```
同樣可以使用 `|` 隔開兩個類型,如接收所有滑鼠事件及回報滑鼠位置
```c!
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
```
#### 取得滑鼠事件
1. 先宣告一個滑鼠事件結構 `MEVENT` 的變數
2. 使用 `getch()` 等待並偵測動作
:::warning
滑鼠為特殊鍵記得先啟用特殊鍵讀取 `keypad(stdscr, TRUE)`
:::
3. 如果鍵值是滑鼠,再取得滑鼠事件 `getmouse(&event)`
4. 接著利用 `event.bstate` 判斷事件類型
#### 範例
按下滑鼠鍵1時顯示按下的位置,直到按下鍵盤 Q 鍵退出
```c=
#include <ncurses.h>
int main(){
initscr();
keypad(stdscr, TRUE);
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
MEVENT event;
int ch = 0;
while(1){
ch = getch();
if(ch == KEY_MOUSE){
if(getmouse(&event) == OK){
if(event.bstate & BUTTON1_CLICKED){
printw("mouse left button clicked at (%d, %d)\n", event.x, event.y);
refresh();
}
}
}
else if(ch == 'q'){
break;
}
}
endwin();
return 0;
}
```
## 視窗
視窗是 ncurses 中最重要的一部分。一開始我們使用 initscr() 建立了一個標準視窗 (stdscr),所有的函式默認執行在這個視窗上。有時候我們需要更多視窗來顯示更多資訊或提高效率。
### 建立新視窗
- `WINDOW *newwin(int nlines, int ncols, int begin_y, int begin_x)` 建立一個新視窗,並返回一個 WINDOW 類型指標
### 刪除視窗
- `int delwin(WINDOW * win)` 刪除視窗並釋放記憶體
:::warning
刪除視窗時螢幕上的內容不會消失,因此刪除前先使用 `wclear()` 和 `wrefresh()` 清理顯示的內容
:::
### 視窗邊框
- `box(WINDOW *win, chtype verch, chtype horch)`
繪製簡單邊框
`verch` 是左右邊框字元,0為預設邊框
`horch` 是上下邊框字元,0為預設邊框
- `wborder(WINDOW *win, chtype ls, chtype rs, chtype ts, chtype bs, chtype tl, chtype tr, chtype bl, chtype br)` 自訂邊框的 8 個字元
`ls` 左邊框,`rs` 右邊框
`ts` 上邊框,`bs` 下邊框
`tl` 左上角,`tr` 右上角
`bl` 左下角,`br` 右下角
### 視窗函式
要在上窗中使用前面介紹的輸入輸出等函式很簡單,只需要再函式名稱前方加上 w,並且第一個參數代入視窗指標。
### 子視窗
我們可以在視窗中建立與該視窗共用緩衝區的子視窗
- `WINDOW *subwin(WINDOW *orig, int nlines, int ncols, int begin_y, int begin_x)`
建立與父視窗共用緩衝區的子視窗,座標為「螢幕絕對座標」
- `WINDOW *derwin(WINDOW *orig, int nlines, int ncols, int begin_y, int begin_x)`
建立與父視窗共用緩衝區的子視窗,座標為「父視窗相對座標」
## reference
- [NCURSES Programming HOWTO](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html)
- [(超详细) ncurses 库使用介绍: 实现终端 GUI](https://www.cnblogs.com/VeniVidiVici/p/17318232.html)