本篇暂未完成……

前言

NCurses 是一个在类 Unix 系统下用于开发 文本用户界面(TUI) 的编程库。
它提供了处理终端屏幕、键盘输入和彩色文本输出等的 API。

你可能对 TUI 一词比较陌生,但想必你一定听说过 GUI。

GUI(Graphical User Interface),即图形用户界面
简单来说,就是电脑、手机或其他电子设备屏幕上,你能够用鼠标点击、用手指触摸、用眼睛看的那种操作界面,而不是只有命令符的黑白文字界面。
我们所熟知的 VSCode 、 微信、QQ 之类,它们都属于 GUI 应用。

TUI(Text-based User Interface), 译为文本用户界面
TUI 是 GUI 和命令行界面(CLI)之间的一种中间形态。它不依赖鼠标和图形窗口,但仍然在字符界面中提供相对直观、可交互的布局。
TUI是基于字符的,不依赖图形系统。几个常见的例子是:Vim、Nano、htop、mc等。

打开你的 Shell,输入vim体验一下吧!(进入后输入:q!退出)

NCurses 最初由 Pavel Curtis 于 1993年开始开发,目标是创建一个免费、开源的 curses 实现。

虽说现在 Python、Rust、Golang 之类已经有很多非常强大的TUI库,但它们大多是组件化,基于单个字符的控制并不方便。
学习 NCurses ,我个人认为灵活度高,能加深对 TUI 的理解,这和其他任何库都是通用的。
偶尔拿它写点小程序(打砖块,贪吃蛇,简易Nano之类),是件很有成就感的事情呢。

安装 NCurses

Linux

Debian/Ubuntu (apt)

1
2
sudo apt install ncurses5-dev
sudo apt install ncurses5w-dev  # 可选,提供宽字符支持,本篇暂不作要求

Red Hat/Fedora/CentOS (yum/dnf)

1
2
3
4
5
sudo dnf install ncurses-devel  # Fedora...

sudo yum install ncurses-devel  # CentOS...

sudo dnf install ncursesw-devel # 可选

Arch Linux (pacman)

1
sudo pacman -S ncurses

其他的就自行查阅吧 qwq

MacOS

1
brew install ncurses

Windows

前文讲过,NCurses 是提供给类 Unix 操作系统(例如 Linux)的,并不原生支持 Windows。
我也是 Windows,个人认为最方便的就是使用 WSL (Windows Subsystem for Linux)。

WSL(Windows Subsystem for Linux,Windows 下的 Linux 子系统)是微软开发的一项兼容层技术,允许用户在 Windows 操作系统上直接运行原生 Linux 二进制可执行文件,而无需安装传统的虚拟机或双系统。

打开你的 Microsoft Store,搜索 Ubuntu(推荐,其他发行版也可以),安装即可。

要在 WSL 里使用 VSCode,插件市场搜索安装 WSL 插件即可。

WSL

以上文推荐的 Ubuntu 为例。

1
2
sudo apt install ncurses5-dev
sudo apt install ncurses5w-dev  # 可选,提供宽字符支持,本篇暂不作要求

Hello, NCurses!

打开你的代码编辑器,输入以下代码,保存为hello.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <ncurses.h>

int main() {
    initscr();  // 初始化 NCurses,必需
    
    printw("Hello, NCurses!\n");    // 打印一行字,类似 printf

    getch();    // 等待用户输入一个按键(作阻塞效果,若没有,程序会马上退出)

    endwin();   // 恢复终端设置并退出
    return 0;
}

编译!运行!(注意需要-lncurses参数来链接 ncurses)

1
gcc hello.c -o hello -lncurses && ./hello

应该类似这样:

基础速通

初始化和退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <ncurses.h>

int main() {
    initscr();  // 初始化屏幕,通常是必需的
    cbreak();   // 禁用行缓冲,立即响应输入
    // nocbreak(); 恢复行缓冲
    noecho();   // 输入时不显示字符
    // echo();     显示输入的字符
    keypad(stdscr, TRUE)    // 启用功能键(F1,KEYUP等)
    curs_set(0);    // 0=隐藏光标,1=正常,2=高亮

    // 启用颜色(关于颜色和属性后面会讲)
    if (has_colors()) {
        start_color();

        // ...
    }

    // ...

    endwin();   // 退出 TUI 前必需要调用(返回到命令行)

    // ...

    return 0;
}

代码中的stdscr 是 NCurses 中的一个标准窗口指针,代表整个终端屏幕。

基本操作

在本部分开始前,我们需要先了解 NCurses 的坐标系统。

1
2
3
4
(0,0) →→→→→ X (列)


 Y (行)

NCurses 使用标准的笛卡尔坐标系:

  • (0,0) 在屏幕左上角
  • Y 轴正方向向下(行号增加)
  • X 轴正方向向右(列号增加)

很多函数都会用到坐标。

基本输出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <ncurses.h>

// 在当前光标位置输出
printw("Hello World");
printw("Number: %d", 100);  // 类似 printf 的格式符

// 指定位置输出(mv代表move)
mvprintw(5, 10, "Row 5, Column 10");
mvprintw(10, 20, "Value: %f", 3.14);

// 输出单个字符(ch代表单个字符)
addch('A');
addch('B' | A_BOLD);  // 带属性

// 指定位置输出字符
mvaddch(5, 10, '*');

清屏与擦除

1
2
3
4
clear();      // 清空整个屏幕,光标归位
erase();      // 清空窗口内容
clrtobot();   // 从光标清除到屏幕底部
clrtoeol();   // 从光标清除到行尾

字符输入

1
2
3
4
5
6
7
8
9
10
11
12
int ch;

// 等待输入(阻塞)
ch = getch();

// 有等待时间的输入
timeout(1000);  // 等待1秒
ch = getch();   // 超时返回ERR

// 非阻塞输入
nodelay(stdscr, TRUE);
ch = getch();   // 立即返回,无输入时返回ERR

字符串输入

1
2
3
4
5
6
7
8
9
10
11
char buffer[100];

// 基本字符串输入
echo();         // 显示输入字符
getstr(buffer); // 读取字符串(不推荐,可能溢出),优先使用 getnstr

// 安全的字符串输入
getnstr(buffer, 99);  // 最多读取99个字符

// 指定位置输入
mvgetnstr(10, 5, buffer, 99);

功能键捕获

1
2
3
4
5
6
7
8
9
10
11
12
keypad(stdscr, TRUE);  // 必须启用

int ch = getch();
switch(ch) {
    case KEY_UP:    printw("Up Key"); break;
    case KEY_DOWN:  printw("Down Key"); break;
    case KEY_LEFT:  printw("Left Key"); break;
    case KEY_RIGHT: printw("Right Key"); break;
    case KEY_F(1):  printw("F1 Key"); break;
    case KEY_BACKSPACE: printw("Backspace"); break;
    case KEY_DC:    printw("Delete Key"); break;
}

文本属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 基本属性
attron(A_BOLD);      // 开启粗体
printw("Bold text");
attroff(A_BOLD);     // 关闭粗体

attron(A_UNDERLINE); // 下划线
printw("Underlined");
attroff(A_UNDERLINE);

attron(A_REVERSE);   // 反色
printw("Reverse");
attroff(A_REVERSE);

// 组合属性
attron(A_BOLD | A_UNDERLINE);
printw("Bold and underlined");

颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (has_colors()) {
    start_color();
    
    // 定义颜色对:编号,前景,背景
    init_pair(1, COLOR_RED, COLOR_BLACK);
    init_pair(2, COLOR_GREEN, COLOR_BLACK);
    init_pair(3, COLOR_YELLOW, COLOR_BLUE);
    
    // 使用颜色
    attron(COLOR_PAIR(1));
    printw("Red text");
    attroff(COLOR_PAIR(1));
    
    // 颜色和属性可以组合
    attron(COLOR_PAIR(2) | A_BOLD);
    printw("Bold green");
}

属性与颜色快速参考

文本属性完整列表:

属性常量 效果 说明
A_NORMAL 普通 关闭所有特殊属性
A_BOLD 粗体/高亮 通常会使文字更亮或更粗
A_DIM 暗淡 与粗体相反,使文字变暗(部分终端支持)
A_UNDERLINE 下划线 文字下方添加下划线
A_REVERSE 反色 前景色和背景色互换
A_BLINK 闪烁 文字闪烁(部分终端不支持)
A_INVIS 不可见 文字隐藏(可用于特殊效果)
A_PROTECT 保护 受保护区域(xt/terminfo特性)
A_ALTCHARSET 备用字符集 使用图形字符(如边框、符号)
A_CHARTEXT 字符掩码 用于提取字符位的位掩码
A_COLOR 颜色掩码 用于提取颜色信息的位掩码
A_STANDOUT 突出显示 类似反色,通常与A_REVERSE相同

预定义颜色常量:

颜色常量 说明
COLOR_BLACK 0 黑色
COLOR_RED 1 红色
COLOR_GREEN 2 绿色
COLOR_YELLOW 3 黄色
COLOR_BLUE 4 蓝色
COLOR_MAGENTA 5 品红/洋红色
COLOR_CYAN 6 青色
COLOR_WHITE 7 白色

光标移动

1
2
3
4
5
6
7
8
9
10
11
// 移动光标位置
move(10, 20);           // 移动到第10行,第20列
printw("Here");

// 移动并输出
mvprintw(5, 5, "Position 5,5");

// 获取当前位置
int y, x;
getyx(stdscr, y, x);
printw("Cursor at (%d, %d)", y, x);

光标可见性

1
2
3
curs_set(0);  // 隐藏光标
curs_set(1);  // 正常光标(下划线)
curs_set(2);  // 高亮光标(块状)

其他实用函数

1
2
box(stdscr, 0, 0);  // 绘制一个边框
refresh();  // 手动刷新

试着根据上面的知识,写一个非常简单的游戏。玩家是一个字符#,可以在窗口中上下左右移动。
实现核心功能后,可以尝试添加窗口边界检测、按c键切换玩家颜色之类的功能。

参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <ncurses.h>

int main() {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    curs_set(0);
    box(stdscr, 0, 0);  // 绘制边框

    // 获取窗口大小
    int max_y = 0, max_x = 0;
    getmaxyx(stdscr, max_y, max_x);

    // 记录玩家位置并初始化打印
    int curr_y = 1, curr_x = 1;
    mvaddch(curr_y, curr_x, '#');

    int ch;
    while ((ch = getch()) != 'q') {

        // 先清除原来位置的字符(空格代替)
        mvaddch(curr_y, curr_x, ' ');

        switch (ch) {
            case KEY_UP:
                // 要预留一个字符给窗口边框
                if (curr_y >= 2) curr_y--;
                break;
            case KEY_DOWN:
                if (curr_y <= max_y - 3) curr_y++;
                break;
            case KEY_LEFT:
                if (curr_x >= 2) curr_x--;
                break;
            case KEY_RIGHT:
                if (curr_x <= max_x - 3) curr_x++;
                break;
        }

        // 在新位置打印字符
        mvaddch(curr_y, curr_x, '#');
    }

    endwin();
    return 0;
}