OSX系统下ncurses如何正确实现鼠标事件捕获功能
核心问题原因
这不是教程代码过时,是macOS环境下ncurses的几个典型坑,你之前判断终端没问题的逻辑不成立——Vim根本不依赖ncurses内置的鼠标解析逻辑,它自己实现了全平台终端的鼠标序列识别,所以不能用Vim的表现反推ncurses运行环境正常。具体问题点有三个:
- 第一,macOS系统自带的ncurses是严重阉割的老旧版本,没有内置适配现代终端鼠标上报的转义序列解析逻辑,你直接用
-lncurses编译默认链的就是这个系统库,终端发过来的鼠标事件序列它认不出来,只会拆成单个随机字符返回,自然不会出KEY_MOUSE。 - 第二,你的代码漏了最关键的
keypad()调用:这个接口是告诉ncurses开启特殊键(包括功能键、方向键、鼠标事件)的转义序列解析,不给对应窗口开这个选项,哪怕ncurses收到了完整的鼠标事件,也不会转成KEY_MOUSE返回。 - 第三,你给
mousemask传了REPORT_MOUSE_POSITION掩码,这个是用来开启鼠标移动实时上报的,macOS下绝大多数终端对这个事件的兼容有问题,不需要的话乱加会直接干扰正常点击事件的识别。
正确实现步骤
- 替换系统自带的阉割版ncurses:先通过Homebrew安装官方全功能版ncurses,执行
brew install ncurses即可。编译的时候必须手动指定brew安装的ncurses头文件和库路径,不能直接用-lncurses链系统库:- Intel芯片Mac编译命令:
g++ -std=c++17 -o curses_play curses_play.cc -I/usr/local/opt/ncurses/include -L/usr/local/opt/ncurses/lib -lncurses- M系列/ARM架构Mac编译命令:
g++ -std=c++17 -o curses_play curses_play.cc -I/opt/homebrew/opt/ncurses/include -L/opt/homebrew/opt/ncurses/lib -lncurses - 代码初始化阶段,在调用
wgetch之前,必须给所有要接收输入的窗口开启keypad模式:你用menu_win接输入就调keypad(menu_win, TRUE),用默认的stdscr接输入就调keypad(stdscr, TRUE),漏了这行什么特殊键都识别不了。 - 按需设置鼠标掩码:不需要鼠标移动跟踪的话,别加
REPORT_MOUSE_POSITION,只开你要监听的事件即可,比如只需要左键点击就传BUTTON1_PRESSED | BUTTON1_CLICKED,要监听所有按键点击再用ALL_MOUSE_EVENTS。 - 终端配置检查:用系统自带Terminal.app的话,把设置里「将Option键当作Meta键」的选项关掉,这个选项开着会截断鼠标转义序列;iTerm2用户用默认配置即可,不需要额外改。
修正后的可运行代码
#include <ncurses.h> #include <string.h> #include <stdio.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; /* 初始化curses基础模式 */ initscr(); clear(); noecho(); cbreak(); // 必须开启stdscr的keypad模式 keypad(stdscr, TRUE); /* 计算菜单窗口居中位置 */ startx = (80 - WIDTH) / 2; starty = (24 - HEIGHT) / 2; attron(A_REVERSE); mvprintw(23, 1, "Click on Exit to quit"); refresh(); attroff(A_REVERSE); /* 初始化菜单子窗口 */ menu_win = newwin(HEIGHT, WIDTH, starty, startx); // 必须给子窗口也开keypad模式,不然KEY_MOUSE识别不了 keypad(menu_win, TRUE); print_menu(menu_win, 1); // 只监听需要的鼠标事件,去掉有兼容问题的位置上报 mousemask(BUTTON1_PRESSED | BUTTON1_CLICKED, NULL); while(1) { c = wgetch(menu_win); switch(c) { case KEY_MOUSE: mvprintw(24, 0, "Found mouse!"); if(getmouse(&event) == OK) { /* 处理左键点击事件 */ if(event.bstate & BUTTON1_PRESSED) { report_choice(event.x + 1, event.y + 1, &choice); if(choice == -1) // 点击Exit选项直接退出 goto end; mvprintw(22, 1, "Choice made is : %d String Chosen is \"%10s\"", choice, choices[choice - 1]); refresh(); } } print_menu(menu_win, choice); break; default: mvprintw(24, 0, "Charcter pressed is = %3d Hopefully it can be printed as '%c'", c, c); refresh(); 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); } /* 根据鼠标点击坐标计算选中的菜单项 */ 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; } }
排错提示
- 不要在tmux、screen这类终端复用器里直接测试,这类工具默认会拦截鼠标事件,需要手动开启鼠标事件透传配置才能正常传给ncurses程序。
- 如果改完还是识别不到,可以在
mousemask调用后打印返回值,如果返回的掩码是空的,说明你还是链到了系统自带的阉割版ncurses,重新检查编译时的头文件和库路径配置即可。
内容的提问来源于stack exchange,提问作者merlin2011
相关产品推荐
相关产品推荐

