ncurses(New curses)是一個提供應用程序編程接口 (API) 的程式庫,允許程序員以獨立於終端的方式編寫基於文本的用戶界面 (TUI) 這是一篇ncurses的中文教學文件 [TOC] 作者:鍾詠傑 學號:41173058H <div style="page-break-after:always;"></div> ## reference [NCURSES Programming HOWTO](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html) by Pradeep Padala [Hackmd](https://hackmd.io/@jbIsegHRTTGhbTgKeAsUxA/B1IO0cyO3) <div style="page-break-after:always;"></div> ## 1 如何安裝 以下是Lunux系安裝指令 請在terminal 輸入以下指令 ``` sudo apt-get install libncurses5-dev libncursesw5-dev ``` <div style="page-break-after:always;"></div> ## 2 Hello World! Hello World! 必須是第一步 ### 2.1 如何編譯 要使用 ncurses 庫函數,您必須在程序中包含 ncurses.h 並且編譯時就像math.h需要-lm一樣 ncurses.h也需要去-lncurses ```clike= #include <ncurses.h>//需要加上 gcc <filename> -lncurses //編譯範例 ``` ```clike= //N02.c #include <ncurses.h> int main() { initscr(); /* 啟動 curses 模式 */ printw("Hello World !!!"); /* 打印 Hello World */ refresh(); /* 將其打印到真實屏幕上 */ getch(); /* 等待用戶輸入 */ endwin(); /* 結束 curses 模式 */ return 0; } ``` ![](https://hackmd.io/_uploads/HkkjHiy_3.png) ### 2.2 解析 initscr() 他會初始化我們的終端,清除成為空白 有點像是clear ,不過他也是進入curses 模式的必須條件 printw() 這跟printf幾乎一樣,除了他會將數據列印在目前座標上面(y,x) 因為目前座標是(0,0),所以出現在左上角 refresh() 上面做的所有事情都不會真的出現在螢幕上,而是被存在一個小小的緩衝區,只有refresh()時才會更新到螢幕上 這也意味著你可以進行多次操作後再一起refresh() 初學者常犯的一個錯誤是 printw() 更新後忘記 refresh() P.S. #define endwin(); aka end windows 這會釋放記憶體與退出curses 模式 你已經學會使用了 讓我們進入細節吧 <div style="page-break-after:always;"></div> ## 3 初始化 ### 3.1. 初始化函數 ### 3.2. raw() 和 cbreak() 通常,終端會緩衝用戶輸入的字符,直到遇到tab或enter為止。 但是我們希望開發能夠在使用者輸入後直接送給程式 他們的兩個函數用都是於禁用輸入緩衝 差異只有在Ctrl+z 或 Ctrl+c之類傳遞的方式 raw() 不產生信號 cbreak() 被解釋為其他符號 ### 3.3. echo() 和 noecho() 這些函數控制將用戶鍵入的字符顯示到終端。 echo() 開啟顯示。 noecho()關閉顯示。 您可能想要這樣做的原因是為了在通過 getch() 等函數從用戶那裡獲取輸入時獲得對顯示的更多控制或關閉不必要的顯示。 大多數交互式程序在初始化時調用 noecho()並以受控方式顯示字符。 它使程序員可以靈活地在窗口中的任何位置顯示字符,而無需更新當前 (y,x) 坐標。 ### 3.4. keypad() 他可以讀取鍵盤上的所有鍵 想是F1~F12 方向鍵等等 因為方向鍵是所有交互式介面的一部分 執行 keypad(stdscr, TRUE)為常規屏幕 (stdscr) 啟用此功能。 ### 3.5. halfdelay() 此功能雖然不經常使用,但有時很有用。 調用 halfdelay() 以啟用半延遲模式,這類似於 cbreak() 模式,因為鍵入的字符可立即用於程式。 但是,它會等待“X”十分之一秒的輸入,然後在沒有可用輸入時返回 ERR。“X”是傳遞給函數 halfdelay() 的超時值。 如果他在特定時間內沒有響應,我們可以做一些其他的事情。 一個例子是當你在開發一個即時彈幕射擊遊戲,這功能很好用 ### 3.6. Example ```clike= //N03.c #include <ncurses.h> int main() { int ch; initscr(); /* Start curses mode */ raw(); /* Line buffering disabled */ keypad(stdscr, TRUE); /* We get F1, F2 etc.. */ noecho(); /* Don't echo() while we do getch */ printw("Type any character to see it in bold\n"); ch = getch(); /* If raw() hadn't been called * we have to press enter before it * gets to the program */ if(ch == KEY_F(1)) /* Without keypad enabled this will */ printw("F1 Key pressed");/* not get to us either */ /* Without noecho() some ugly escape * charachters might have been printed * on screen */ else { printw("The pressed key is "); attron(A_BOLD); printw("%c", ch); attroff(A_BOLD); } refresh(); /* Print it on to the real screen */ getch(); /* Wait for user input */ endwin(); /* End curses mode */ return 0; } ``` ![](https://hackmd.io/_uploads/BJz15oJd3.png) <div style="page-break-after:always;"></div> ## 4 關於 Windows 的一句話 在我們深入研究無數的 ncurses 函數之前,讓我澄清一些關於Windows 的事情 之後會在第八章詳細解析 Windows 是由 curses 系統定義的假想屏幕。(以下簡稱窗口) 當我們初始化 curses 時,它會創建一個名為 stdscr的默認窗口 代表您的 80x25(或您正在運行的窗口的大小)屏幕。如果您正在執行簡單的任務,例如打印少量字符串、讀取輸入等,您可以安全地使用這個單一窗口來完成您的所有目的。您還可以創建窗口並調用在指定窗口上顯式工作的函數。 一般來說 像是這樣 ```clike= printw("Hi There !!!"); refresh(); ``` 如果我們創建了叫做win的窗口,那麼要改成這樣呼叫 且函數要添加前墜w ```clike= wprintw(win, "Hi There !!!"); wrefresh(win); ``` 還有以下三種不同前墜功能 可以自行看看他們的差異 ```clike= printw(string); /* Print on stdscr at present cursor position */ mvprintw(y, x, string);/* Move to (y, x) then print string */ wprintw(win, string); /* Print on window win at present cursor position */ /* in the window */ mvwprintw(win, y, x, string); /* Move to (y, x) relative to window */ /* co-ordinates and then print */ ``` <div style="page-break-after:always;"></div> ### 5 輸出函式 因該等不及到這個部分了吧 我們已經開啟了初始化的 curses 讓我們跟螢幕另一邊說話! 以下是三種Output functions 的分類 1. addch() 類:打印帶有屬性的單個字符 3. printw() 類:打印類似於 printf() 的格式化輸出 5. addstr() 類:打印字符串 這些都可以隨意使用 ### 5.1 addch() 函數類 這些函數將單個字符放入當前光標(座標)位置並使光標位置前進。 如果一個字符與一個屬性相關聯(粗體、反轉等),當 curses 打印該字符時,它被打印在該屬性中。 為了將char與某些屬性結合起來,您有以下選擇: 1.透過 字元與macros 進行 'or' 運算 這些macros可已在標頭檔ncurses.h中找到 舉例您想要打印一個字符 ch(屬於 char 類型)並加下劃線 ```clike= addch(ch | A_BOLD | A_UNDERLINE); ``` 2.通過使用attrset()、attron()、attroff()等函數。 請自行學習或是之後再解釋 總之,它們設定給定窗口的當前屬性。一旦設置,打印在窗口中的字符都將與屬性相關聯,直到它被關閉。 此外,curses還為基於字符的圖形提供了一些特殊字符。您可以繪製表格、水平或垂直線等。您可以在標頭檔 ncurses.h中找到所有可用的字符。嘗試在此文件中查找以ACS_開頭的macros。 ### 5.2. mvaddch()、waddch() 和 mvwaddch() mvaddch()用於將光標移動到給定點, ```clike= move(row,col); /* moves the cursor to rowth row and colth column */ addch(ch); ``` 也可以這樣替代 ```clike= mvaddch(row,col,ch); ``` waddch()類似於 addch() 但是因為w前墜,所以需要給指定窗口,可以去看前面的部分 ### 5.3. printw() 函數類 這些函數類似於printf(),但增加了在屏幕上的任何位置打印的功能。 #### 5.3.1. printw() 和 mvprintw() 這兩個函數的工作方式與printf()非常相似。 mvprintw()可用於將光標移動到某個位置然後打印。 如果你分開使用move跟printw也是一樣的 #### 5.3.2. wprintw() 和 mvwprintw 這兩個函數與上面兩個函數類似,只是它們在作為參數給出的相應窗口中打印。 #### 5.3.3. vwprintw() 此函數類似於vprintf()。這可以在要打印可變數量的參數時使用。 #### 5.3.4. 範例 ```clike= //N05.c #include <ncurses.h> /* ncurses.h includes stdio.h */ #include <string.h> int main() { char mesg[]="Just a string"; /* message to be appeared on the screen */ int row,col; /* to store the number of rows and * * the number of colums of the screen */ initscr(); /* start the curses mode */ getmaxyx(stdscr,row,col); /* get the number of rows and columns */ mvprintw(row/2,(col-strlen(mesg))/2,"%s",mesg); /* print the message at the center of the screen */ mvprintw(row-2,0,"This screen has %d rows and %d columns\n",row,col); printw("Try resizing your window(if possible) and then run this program again"); refresh(); getch(); endwin(); return 0; } ``` ### 5.4. addstr() 函數類 addstr()用於將字符串放入給定窗口。此函數類似於為 給定字符串中的每個字符調用一次addch() 。 這適用於所有輸出函數。該系列還有其他函數,例如mvaddstr()、mvwaddstr()和 waddstr(),它們遵循 curses 的命名約定。 (例如,mvaddstr() 類似於各自調用 move() 和 addstr()。)這個家族的另一個函數是 addnstr(),它另外接受一個整數參數(比如 n)。 此函數最多將 n 個字符放入屏幕。如果 n 為負數,則將添加整個字符串。 ### 5.5. 請注意 所有這些函數在其參數中都首先採用 y 坐標,然後採用 x。初學者常犯的錯誤是按 x,y 的順序傳遞。 如果您對 (y,x) 坐標進行了太多操作,請考慮將屏幕分成多個窗口並分別操作每個窗口。 <div style="page-break-after:always;"></div> ## 6 輸入功能 不接受輸入就打印很無聊。讓我們看看允許我們從用戶那裡獲得輸入的函數。這些功能也可以分為三類。 getch() 類:獲取一個字符 scanw() 類:獲取格式化輸入 getstr() 類:獲取字符串 ### 6.1. getch() 函數類 這些函數從終端讀取單個字符。但有幾個微妙的事實需要考慮。 例如,如果您不使用函數 cbreak(),curses 將不會連續讀取您輸入的字符,而是僅在遇到新行或 EOF 後才開始讀取它們。 為了避免這種情況,必須使用 cbreak() 函數,以便您的程序可以立即使用字符。另一個廣泛使用的函數是 noecho()。 顧名思義,設置(使用)此功能後,用戶輸入的字符將不會顯示在屏幕上。 這些部分在init教學篇有先講過了,這裡先跳過 cbreak() 和 noecho() 這兩個函數是密鑰管理的典型例子。 ### 6.2. scanw() 函數類 這些函數類似於scanf(),增加了從屏幕上的任何位置獲取輸入的功能。 #### 6.2.1. scanw() and mvscanw() 這些函數的用法類似於 sscanf()的用法,其中要掃描的行由wgetstr()函數提供。 也就是說,這些函數調用wgetstr()並使用結果行進行掃描。 關於wgetstr()的部分請自行尋找 #### 6.2.2. wscanw() and mvwscanw() 因為前墜w, 這些類似於上面的兩個函數,只是它們從一個窗口讀取,該窗口作為這些函數的參數之一提供。 #### 6.2.3. vwscanw() 此函數類似於vscanf()。當要掃描可變數量的參數時,可以使用它。 ### 6.3. getstr() 函數類 這些函數用於從終端獲取字符串。 本質上,此函數執行的任務與通過一系列調用 getch()直到收到換行符、回車符或文件結束符所完成的任務相同。 生成的字符串由str指向 ,一個使用者自創的pointer。 ### 6.4 example ```clike= //N06.c #include <ncurses.h> /* ncurses.h includes stdio.h */ #include <string.h> int main() { char mesg[]="Enter a string: "; /* message to be appeared on the screen */ char str[80]; int row,col; /* to store the number of rows and * * the number of colums of the screen */ initscr(); /* start the curses mode */ getmaxyx(stdscr,row,col); /* get the number of rows and columns */ mvprintw(row/2,(col-strlen(mesg))/2,"%s",mesg); /* print the message at the center of the screen */ getstr(str); mvprintw(LINES - 2, 0, "You Entered: %s", str); getch(); endwin(); return 0; } ``` <div style="page-break-after:always;"></div> ## 7.屬性 下面是一個使用屬性來打印具有某些特殊效果的字符的範例。 設置屬性可以以簡單易懂的方式呈現信息。 以下程序將一個 C 文件作為輸入,並以粗體顯示帶有註釋的文件。 ```clike= /* pager functionality by Joseph Spainhour" <spainhou@bellsouth.net> */ //N0701.c #include <ncurses.h> #include <stdlib.h> int main(int argc, char *argv[]) { int ch, prev, row, col; prev = EOF; FILE *fp; int y, x; if(argc != 2) { printf("Usage: %s <a c file name>\n", argv[0]); exit(1); } fp = fopen(argv[1], "r"); if(fp == NULL) { perror("Cannot open input file"); exit(1); } initscr(); /* Start curses mode */ getmaxyx(stdscr, row, col); /* find the boundaries of the screeen */ while((ch = fgetc(fp)) != EOF) /* read the file till we reach the end */ { getyx(stdscr, y, x); /* get the current curser position */ if(y == (row - 1)) /* are we are at the end of the screen */ { printw("<-Press Any Key->"); /* tell the user to press a key */ getch(); clear(); /* clear the screen */ move(0, 0); /* start at the beginning of the screen */ } if(prev == '/' && ch == '*') /* If it is / and * then only * switch bold on */ { attron(A_BOLD); /* cut bold on */ getyx(stdscr, y, x); /* get the current curser position */ move(y, x - 1); /* back up one space */ printw("%c%c", '/', ch); /* The actual printing is done here */ } else printw("%c", ch); refresh(); if(prev == '*' && ch == '/') attroff(A_BOLD); /* Switch it off once we got * * and then / */ prev = ch; } endwin(); /* End curses mode */ fclose(fp); return 0; } ``` 請看在while裡面,它讀取文件中的每個字符並蒐索模式 /*。 一旦發現模式,它就會使用 attron()打開 BOLD 屬性。當我們得到模式 */ 時,它會被attroff()關閉。 上面的程序還向我們介紹了兩個有用的函數 getyx()和 move()。 第一個函數將當前光標的坐標放入變量 y、x 中。 由於 getyx() 是一個macro,我們不必將指針傳遞給變量。 函數 move()將光標移動到給定的坐標。 ### 7.1 屬性細節 attron ()、attroff()、attrset()及其姐妹函數 attr_get()等函數可用於打開/關閉屬性、獲取屬性和生成彩色顯示。 attron 和 attroff 函數採用屬性的位掩碼並分別打開或關閉它們。以下在 <curses.h> 中定義的視頻屬性可以傳遞給這些函數。 ```= 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) 顏色對編號 n ``` 最後一個顏色是豐富多彩的,顏色在下一章中解釋。 我們可以 OR(|) 任意數量的上述屬性以獲得組合效果。如果你想要帶有閃爍字符的反轉,你可以使用 ```= attron(A_REVERSE | A_BLINK); ``` ### 7.2. attron() vs attrset() 那麼attron()和attrset()有什麼區別呢?attrset 設置窗口的屬性,而 attron 只是打開給它的屬性。 因此 attrset() 完全覆蓋了窗口以前具有的任何屬性並將其設置為新屬性。同樣,attroff() 只是關閉作為參數提供給它的屬性。這使我們能夠靈活地輕鬆管理屬性。 ### 7.3. attr_get() 函數 attr_get() 獲取窗口的當前屬性和顏色對。 雖然我們可能不像上述功能那樣經常使用它,但這在掃描屏幕區域時很有用。 假設我們想在屏幕上進行一些複雜的更新,但我們不確定每個角色與什麼屬性相關聯。 然後可以將此函數與 attrset 或 attron 一起使用以產生所需的效果。 ### 7.4. attr_ functions 有一系列函數,如 attr_set()、attr_on 等。這些函數與上述函數類似,只是它們採用 attr_t類型的參數。 ### 7.5. wattr functions 對於上面的每個函數,我們都有一個對應的函數,它在特定窗口上運行。上述函數對 stdscr 進行操作。 ### 7.6. chgat() functions 函數 chgat() 列在手冊頁 curs_attr 的末尾。 該函數可用於在不移動的情況下為一組字符設置屬性。 它會更改從當前光標位置開始的給定字符數的屬性。 我們可以給出 -1 作為要更新到行尾的字符數。如果要將字符的屬性從當前位置更改為行尾。 ``` chgat(-1, A_REVERSE, 0, NULL); ``` w 函數在特定窗口上運行。mv 函數首先移動光標 這些之前講過的就不多敘述了 ### example ``` //N0702.c #include <ncurses.h> int main(int argc, char *argv[]) { initscr(); /* Start curses mode */ start_color(); /* Start color functionality */ init_pair(1, COLOR_CYAN, COLOR_BLACK); printw("A Big string which i didn't care to type fully "); mvchgat(0, 0, -1, A_BLINK, 1, NULL); /* * First two parameters specify the position at which to start * Third parameter number of characters to update. -1 means till * end of line * Forth parameter is the normal attribute you wanted to give * to the charcter * Fifth is the color index. It is the index given during init_pair() * use 0 if you didn't want color * Sixth one is always NULL */ refresh(); getch(); endwin(); /* End curses mode */ return 0; } ``` <div style="page-break-after:always;"></div> ## 8. Windows 窗口 Windows 是 curses 中最重要的概念。 您已經看到上面的標準窗口 stdscr,其中所有函數都隱式地在此窗口上運行。 現在要使設計成為最簡單的 GUI,您需要求助於 Windows。 您可能想要使用窗口的主要原因是為了提高效率,通過僅更新需要更改的窗口和更好的設計來單獨操作屏幕的各個部分。 在Windows 中最重要的部分 您應該始終努力在您的程序中進行更好且易於管理的設計。如果你正在編寫大型、複雜的 GUI,那麼在你開始做任何事情之前這是至關重要的。 ### 8.1. windows基礎 可以通過調用函數 newwin()創建一個窗口。它實際上不會在屏幕上創建任何東西。它為結構分配內存來操作窗口,並使用有關窗口的數據更新結構,如大小、beginy、beginx 等。 因此,在 curses 中,窗口只是假想窗口的抽象,可以獨立於屏幕的其他部分。 函數 newwin() 返回一個指向結構 WINDOW 的指針,該指針可以傳遞給窗口相關函數,如 wprintw() 等。 最後窗口可以用 delwin() 銷毀。它將釋放與窗口結構關聯的內存。 ### 8.2. 開啟一個窗口 如果創建了一個窗口而我們看不到它,這有什麼好玩的。 讓我們來從顯示窗口開始。 函數 box()可用於在窗口周圍繪製邊框。讓我們在這個例子中更詳細地探討這些功能。 ```clike= //N0801.c #include <ncurses.h> WINDOW *create_newwin(int height, int width, int starty, int startx); void destroy_win(WINDOW *local_win); int main(int argc, char *argv[]) { WINDOW *my_win; int startx, starty, width, height; int ch; initscr(); /* Start curses mode */ cbreak(); /* Line buffering disabled, Pass on * everty thing to me */ keypad(stdscr, TRUE); /* I need that nifty F1 */ height = 3; width = 10; starty = (LINES - height) / 2; /* Calculating for a center placement */ startx = (COLS - width) / 2; /* of the window */ printw("Press F1 to exit"); refresh(); my_win = create_newwin(height, width, starty, startx); while((ch = getch()) != KEY_F(1)) { switch(ch) { case KEY_LEFT: destroy_win(my_win); my_win = create_newwin(height, width, starty,--startx); break; case KEY_RIGHT: destroy_win(my_win); my_win = create_newwin(height, width, starty,++startx); break; case KEY_UP: destroy_win(my_win); my_win = create_newwin(height, width, --starty,startx); break; case KEY_DOWN: destroy_win(my_win); my_win = create_newwin(height, width, ++starty,startx); break; } } endwin(); /* End curses mode */ return 0; } WINDOW *create_newwin(int height, int width, int starty, int startx) { WINDOW *local_win; local_win = newwin(height, width, starty, startx); box(local_win, 0 , 0); /* 0, 0 gives default characters * for the vertical and horizontal * lines */ wrefresh(local_win); /* Show that box */ return local_win; } void destroy_win(WINDOW *local_win) { /* box(local_win, ' ', ' '); : This won't produce the desired * result of erasing the window. It will leave it's four corners * and so an ugly remnant of window. */ wborder(local_win, ' ', ' ', ' ',' ',' ',' ',' ',' '); /* The parameters taken are * 1. win: the window on which to operate * 2. ls: character to be used for the left side of the window * 3. rs: character to be used for the right side of the window * 4. ts: character to be used for the top side of the window * 5. bs: character to be used for the bottom side of the window * 6. tl: character to be used for the top left corner of the window * 7. tr: character to be used for the top right corner of the window * 8. bl: character to be used for the bottom left corner of the window * 9. br: character to be used for the bottom right corner of the window */ wrefresh(local_win); delwin(local_win); } ``` ### 8.3 解釋 該程序創建一個矩形窗口,可以使用向左、向右、向上、向下箭頭鍵移動該窗口。當用戶按下一個鍵時,它會重複創建和銷毀窗口。 請不要超出屏幕限制。 create_newwin ()函數使用newwin()創建一個窗口,並在其周圍顯示一個帶框的邊框。函數destroy_win()首先通過繪製帶有 ' ' 字符的邊框從屏幕上擦除窗口,然後調用delwin()來釋放與其相關的內存。根據用戶按下的鍵,starty 或 startx 會發生變化並創建一個新窗口。 在 destroy_win 中,如您所見,我使用 wborder 而不是 box。原因寫在註解中(你錯過了。我知道。:-))。 wborder 在窗口周圍繪製一個邊框,其中的字符作為 4 個角點和 4 條線。 明確地說,如果您按如下方式調用了 wborder: ``` wborder(win, '|', '|', '-', '-', '+', '+', '+', '+'); ``` 它會產生 ``` +------------+ | | | | | | | | | | | | +------------+ ``` ### 8.4 其他內容 您還可以在上面的示例中看到,我使用了變量 COLS、LINES,它們在 initscr() 之後被初始化為屏幕尺寸。如上所述,它們可用於查找屏幕尺寸和查找屏幕的中心坐標。 函數getch()像往常一樣從鍵盤獲取鍵,並根據鍵執行相應的工作。這種類型的開關盒在任何基於 GUI 的程序中都很常見。 ### 8.5. 其他邊框功能 上面的程序非常低效,因為每次按下一個鍵,一個窗口都會被銷毀並創建另一個。因此,讓我們編寫一個使用其他邊界相關函數的更高效的程序。 下面的程序使用mvhline()和 mvvline()來達到類似的效果。這兩個函數很簡單。它們在指定位置創建指定長度的水平或垂直線。 ``` //N0802.c #include <ncurses.h> typedef struct _win_border_struct { chtype ls, rs, ts, bs, tl, tr, bl, br; }WIN_BORDER; typedef struct _WIN_struct { int startx, starty; int height, width; WIN_BORDER border; }WIN; void init_win_params(WIN *p_win); void print_win_params(WIN *p_win); void create_box(WIN *win, bool flag); int main(int argc, char *argv[]) { WIN win; int ch; initscr(); /* Start curses mode */ start_color(); /* Start the color functionality */ cbreak(); /* Line buffering disabled, Pass on * everty thing to me */ keypad(stdscr, TRUE); /* I need that nifty F1 */ noecho(); init_pair(1, COLOR_CYAN, COLOR_BLACK); /* Initialize the window parameters */ init_win_params(&win); print_win_params(&win); attron(COLOR_PAIR(1)); printw("Press F1 to exit"); refresh(); attroff(COLOR_PAIR(1)); create_box(&win, TRUE); while((ch = getch()) != KEY_F(1)) { switch(ch) { case KEY_LEFT: create_box(&win, FALSE); --win.startx; create_box(&win, TRUE); break; case KEY_RIGHT: create_box(&win, FALSE); ++win.startx; create_box(&win, TRUE); break; case KEY_UP: create_box(&win, FALSE); --win.starty; create_box(&win, TRUE); break; case KEY_DOWN: create_box(&win, FALSE); ++win.starty; create_box(&win, TRUE); break; } } endwin(); /* End curses mode */ return 0; } void init_win_params(WIN *p_win) { p_win->height = 3; p_win->width = 10; p_win->starty = (LINES - p_win->height)/2; p_win->startx = (COLS - p_win->width)/2; p_win->border.ls = '|'; p_win->border.rs = '|'; p_win->border.ts = '-'; p_win->border.bs = '-'; p_win->border.tl = '+'; p_win->border.tr = '+'; p_win->border.bl = '+'; p_win->border.br = '+'; } void print_win_params(WIN *p_win) { #ifdef _DEBUG mvprintw(25, 0, "%d %d %d %d", p_win->startx, p_win->starty, p_win->width, p_win->height); refresh(); #endif } void create_box(WIN *p_win, bool flag) { int i, j; int x, y, w, h; x = p_win->startx; y = p_win->starty; w = p_win->width; h = p_win->height; if(flag == TRUE) { mvaddch(y, x, p_win->border.tl); mvaddch(y, x + w, p_win->border.tr); mvaddch(y + h, x, p_win->border.bl); mvaddch(y + h, x + w, p_win->border.br); mvhline(y, x + 1, p_win->border.ts, w - 1); mvhline(y + h, x + 1, p_win->border.bs, w - 1); mvvline(y + 1, x, p_win->border.ls, h - 1); mvvline(y + 1, x + w, p_win->border.rs, h - 1); } else for(j = y; j <= y + h; ++j) for(i = x; i <= x + w; ++i) mvaddch(j, i, ' '); refresh(); } ``` <div style="page-break-after:always;"></div> ## 9. 顏色 ### example ```clike= //N09.c #include <ncurses.h> void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string); int main(int argc, char *argv[]) { initscr(); /* Start curses mode */ if(has_colors() == FALSE) { endwin(); printf("Your terminal does not support color\n"); exit(1); } start_color(); /* Start color */ init_pair(1, COLOR_RED, COLOR_BLACK); attron(COLOR_PAIR(1)); print_in_middle(stdscr, LINES / 2, 0, 0, "Viola !!! In color ..."); attroff(COLOR_PAIR(1)); getch(); endwin(); } void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string) { int length, x, y; float temp; if(win == NULL) win = stdscr; getyx(win, y, x); if(startx != 0) x = startx; if(starty != 0) y = starty; if(width == 0) width = 80; length = strlen(string); temp = (width - length)/ 2; x = startx + (int)temp; mvwprintw(win, y, x, "%s", string); refresh(); } ``` 如您所見,要開始使用顏色,您應該首先調用函數 start_color()。之後,您可以使用各種功能使用終端的顏色功能。要確定終端是否具有顏色功能,可以使用 has_colors()函數,如果終端不支持顏色,該函數將返回 FALSE。 當調用 start_color() 時,Curses 會初始化終端支持的所有顏色。這些可以通過定義常量訪問,如 COLOR_BLACK等。現在要真正開始使用顏色,您必須正確定義。 顏色總是成對使用。這意味著您必須使用函數init_pair()來為您提供的配對編號定義前景和背景。 之後,該對號可以用作COLOR_PAIR()的普通屬性功能。 起初這似乎很麻煩。但這種優雅的解決方案使我們能夠非常輕鬆地管理顏色對。要理解它,您必須查看“dialog”的源代碼,這是一個用於從 shell 腳本顯示對話框的實用程序。 開發人員已經為他們可能需要的所有顏色定義了前景和背景組合,並在開始時進行了初始化。這使得僅通過訪問我們已經定義為常量的一對來設置屬性變得非常容易。 以下顏色在curses.h中定義。您可以將它們用作各種顏色函數的參數。 ```= COLOR_BLACK 0 COLOR_RED 1 COLOR_GREEN 2 COLOR_YELLOW 3 COLOR_BLUE 4 COLOR_MAGENTA 5 COLOR_CYAN 6 COLOR_WHITE 7 ``` ### 9.2 更改顏色定義 函數init_color()可用於更改最初由 curses 定義的顏色的 rgb 值。假設您想稍微減輕紅色的強度。 可以這樣做 ```= init_color(COLOR_RED, 700, 0, 0); /* param 1 : color name * param 2, 3, 4 : rgb content min = 0, max = 1000 */ ``` <div style="page-break-after:always;"></div> ## 10 與鍵盤互動 沒有強大的用戶界面和與用戶交互的 GUI 是不完整的,curses 程序應該對用戶的按鍵或鼠標操作敏感。讓我們先處理鍵。 正如您在上述幾乎所有示例中所看到的,從用戶那裡獲取鍵輸入非常容易。獲取按鍵的一種簡單方法是使用 getch()函數。當您有興趣閱讀單個按鍵命中而不是完整的文本行(通常以回車符結尾)時,應啟用 cbreak 模式以讀取按鍵。應啟用小鍵盤以獲得功能鍵、箭頭鍵等。有關詳細信息,請參閱初始化部分。 getch()返回對應於按下的鍵的整數。如果是普通字符,整數值將等同於該字符。否則它返回一個可以與curses.h中定義的常量相匹配的數字。例如,如果用戶按下 F1,則返回的整數為 265。這可以使用 curses.h 中定義的宏 KEY_F() 進行檢查。這使得閱讀鍵便攜且易於管理。 例如,如果你像這樣調用 getch() ``` int ch; ch = getch(); ``` getch() 將等待用戶按下一個鍵,(除非您指定了超時)並且當用戶按下一個鍵時,將返回相應的整數。然後您可以檢查使用 curses.h 中定義的常量返回的值,以匹配您想要的鍵。 下面的代碼片段將完成這項工作。 ``` if(ch == KEY_LEFT) printw("Left arrow is pressed\n"); ``` 讓我們編寫一個程式序來創建一個可以通過向上和向下箭頭導航的選單。 ``` N10.c #include <stdio.h> #include <ncurses.h> #define WIDTH 30 #define HEIGHT 10 int startx = 0; int starty = 0; char *choices[] = { "Choice 1", "Choice 2", "Choice 3", "Choice 4", "Exit", }; int n_choices = sizeof(choices) / sizeof(char *); void print_menu(WINDOW *menu_win, int highlight); int main() { WINDOW *menu_win; int highlight = 1; int choice = 0; int c; initscr(); clear(); noecho(); cbreak(); /* Line buffering disabled. pass on everything */ startx = (80 - WIDTH) / 2; starty = (24 - HEIGHT) / 2; menu_win = newwin(HEIGHT, WIDTH, starty, startx); keypad(menu_win, TRUE); mvprintw(0, 0, "Use arrow keys to go up and down, Press enter to select a choice"); refresh(); print_menu(menu_win, highlight); while(1) { c = wgetch(menu_win); switch(c) { case KEY_UP: if(highlight == 1) highlight = n_choices; else --highlight; break; case KEY_DOWN: if(highlight == n_choices) highlight = 1; else ++highlight; break; case 10: choice = highlight; break; default: mvprintw(24, 0, "Charcter pressed is = %3d Hopefully it can be printed as '%c'", c, c); refresh(); break; } print_menu(menu_win, highlight); if(choice != 0) /* User did a choice come out of the infinite loop */ break; } mvprintw(23, 0, "You chose choice %d with choice string %s\n", choice, choices[choice - 1]); clrtoeol(); refresh(); endwin(); return 0; } void print_menu(WINDOW *menu_win, int highlight) { int x, y, i; x = 2; y = 2; box(menu_win, 0, 0); for(i = 0; i < n_choices; ++i) { if(highlight == i + 1) /* High light the present choice */ { wattron(menu_win, A_REVERSE); mvwprintw(menu_win, y, x, "%s", choices[i]); wattroff(menu_win, A_REVERSE); } else mvwprintw(menu_win, y, x, "%s", choices[i]); ++y; } wrefresh(menu_win); } ``` <div style="page-break-after:always;"></div> ## 11 與滑鼠互動 現在您已經了解瞭如何獲取鍵,讓我們用鼠標做同樣的事情。通常每個 UI 都允許用戶與鍵盤和鼠標進行交互。 ### 11.1 Basic 在你做任何事情之前,你想要接收的事件必須使用mousemask()啟用。 ``` mousemask( mmask_t newmask, /* The events you want to listen to */ mmask_t *oldmask) /* The old events mask */ ``` 上述函數的第一個參數是您想要收聽的事件的位掩碼(bit Mask)。默認情況下,所有事件都處於關閉狀態。位掩碼(bit Mask)ALL_MOUSE_EVENTS可用於獲取所有事件。 以下是所有事件掩碼: ``` 名稱 描述 ---------------------------------------------- ------------------ 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 在按鈕狀態更改期間向下移動 BUTTON_CTRL 控件在按鈕狀態更改期間按下 BUTTON_ALT alt 在按鈕狀態更改期間按下 ALL_MOUSE_EVENTS 報告所有按鈕狀態更改 REPORT_MOUSE_POSITION 報告鼠標移動 ``` ### 11.2 獲取事件 一旦啟用了一類鼠標事件,每次發生鼠標事件時,getch() 類函數都會返回 KEY_MOUSE。然後可以使用getmouse()檢索鼠標事件。 代碼大概是這樣的: getmouse() 將事件返回給它的指針。這是一個包含的結構 bstate是我們感興趣的主要變量。它告訴鼠標的按鈕狀態。 然後使用如下代碼片段,我們可以找出發生了什麼。 ### 11.3. 把它們放在一起 example 這幾乎是與鼠標的接口。讓我們創建相同的選單並啟用鼠標交互。 ```clike= //N11.c #include <ncurses.h> #define WIDTH 30 #define HEIGHT 10 int startx = 0; int starty = 0; char *choices[] = { "Choice 1", "Choice 2", "Choice 3", "Choice 4", "Exit", }; int n_choices = sizeof(choices) / sizeof(char *); void print_menu(WINDOW *menu_win, int highlight); void report_choice(int mouse_x, int mouse_y, int *p_choice); int main() { int c, choice = 0; WINDOW *menu_win; MEVENT event; /* Initialize curses */ initscr(); clear(); noecho(); cbreak(); //Line buffering disabled. pass on everything /* Try to put the window in the middle of screen */ startx = (80 - WIDTH) / 2; starty = (24 - HEIGHT) / 2; attron(A_REVERSE); mvprintw(23, 1, "Click on Exit to quit (Works best in a virtual console)"); refresh(); attroff(A_REVERSE); /* Print the menu for the first time */ menu_win = newwin(HEIGHT, WIDTH, starty, startx); print_menu(menu_win, 1); /* Get all the mouse events */ mousemask(ALL_MOUSE_EVENTS, NULL); while(1) { c = wgetch(menu_win); switch(c) { case KEY_MOUSE: if(getmouse(&event) == OK) { /* When the user clicks left mouse button */ if(event.bstate & BUTTON1_PRESSED) { report_choice(event.x + 1, event.y + 1, &choice); if(choice == -1) //Exit chosen goto end; mvprintw(22, 1, "Choice made is : %d String Chosen is \"%10s\"", choice, choices[choice - 1]); refresh(); } } print_menu(menu_win, choice); break; } } end: endwin(); return 0; } void print_menu(WINDOW *menu_win, int highlight) { int x, y, i; x = 2; y = 2; box(menu_win, 0, 0); for(i = 0; i < n_choices; ++i) { if(highlight == i + 1) { wattron(menu_win, A_REVERSE); mvwprintw(menu_win, y, x, "%s", choices[i]); wattroff(menu_win, A_REVERSE); } else mvwprintw(menu_win, y, x, "%s", choices[i]); ++y; } wrefresh(menu_win); } /* Report the choice according to mouse position */ void report_choice(int mouse_x, int mouse_y, int *p_choice) { int i,j, choice; i = startx + 2; j = starty + 3; for(choice = 0; choice < n_choices; ++choice) if(mouse_y == j + choice && mouse_x >= i && mouse_x <= i + strlen(choices[choice])) { if(choice == n_choices - 1) *p_choice = -1; else *p_choice = choice + 1; break; } } ```