
C 获取键盘按键的 N 种方式一篇讲透前言写游戏需要检测按键写控制台工具需要检测按键写快捷键功能也需要检测按键。C 里获取键盘输入的方式五花八门Windows 和 Linux 下还不一样初学者很容易搞混。我自己最早接触的是getch()后来发现 Windows 下还有GetAsyncKeyState、_kbhitLinux 下又有ncurses、termios这些玩意。每种方法各有各的适用场景用错了轻则功能不达预期重则程序卡死。这篇文章就把 C 里各种获取按键的方式捋一遍Windows 和 Linux 分开讲每种都带上能直接跑的代码。文章末尾有个表格对比方便你按需选择。一、Windows 平台Windows 下获取键盘输入主要有这么几条路控制台专用的_kbhit_getch、实时检测物理按键的GetAsyncKeyState、依赖消息队列的GetKeyState、窗口消息WM_KEYDOWN以及更底层的ReadConsoleInput。1._kbhit()_getch()—— 最简单粗暴这是 Windows 控制台程序里最常用的组合。_kbhit()检查键盘缓冲区有没有按键有的话返回非零_getch()从缓冲区读取一个字符不需要按回车。#includeiostream#includeconio.h// _kbhit 和 _getch 都在这个头文件里intmain(){std::cout按任意键试试按 ESC 退出std::endl;while(true){if(_kbhit()){// 有按键按下了intkey_getch();// 读取按键值if(key27){// ESC 的 ASCII 码是 27std::coutESC 按下退出std::endl;break;}std::cout按下了键码值: keystd::endl;}}return0;}特点非阻塞可以在循环里一直跑有按键才处理只能用于控制台程序GUI 程序用不了方向键、功能键会返回两个字节先 0 或 224再是实际码值需要额外处理2.GetAsyncKeyState()—— 实时检测物理按键这个函数直接查询键盘的硬件状态不管你程序有没有焦点不管消息队列里有没有消息调用时按键是什么状态就返回什么状态。返回值是SHORT类型最高位第 15 位为 1 表示按键当前被按下。#includeiostream#includewindows.hintmain(){std::cout检测 W 键是否被按下按 ESC 退出std::endl;while(true){// 检测 W 键 (虚拟键码 0x57)if(GetAsyncKeyState(0x57)0x8000){std::coutW 键正在被按下std::endl;}// 检测 ESC 键退出if(GetAsyncKeyState(VK_ESCAPE)0x8000){std::coutESC 按下退出std::endl;break;}Sleep(50);// 别让 CPU 跑满}return0;}虚拟键码Windows 定义了一堆VK_XXX常量比如VK_SPACE空格、VK_UP上箭头、VK_RETURN回车。也可以直接用字符的 ASCII 码比如A就是检测 A 键。特点真正的实时检测适合游戏循环、热键监控不依赖消息队列程序没焦点也能检测但需要程序在前台才能可靠响应频繁调用开销稍大3.GetKeyState()—— 消息队列里的按键状态GetKeyState()和GetAsyncKeyState()名字像但工作机制完全不同。它不直接查硬件而是查消息队列里最近一次键盘消息处理时的按键状态。简单说GetAsyncKeyState()问的是“现在这个键按着没”GetKeyState()问的是“刚才处理键盘消息的时候这个键按着没”。#includeiostream#includewindows.hintmain(){std::cout检测 Shift 键状态需要在消息处理中调用才准确std::endl;// 注意在普通控制台程序的主循环里直接调用 GetKeyState 是不准的// 这个函数应该在窗口消息处理函数如 WM_KEYDOWN里调用while(true){// 这样直接调用的结果可能不准确SHORT stateGetKeyState(VK_SHIFT);if(state0x8000){std::coutShift 被按下可能不准std::endl;}Sleep(200);}return0;}正确的用法是在 MFC 或者 Win32 窗口程序的WM_KEYDOWN消息处理里调用// 在窗口过程或 MFC 的 OnKeyDown 中voidOnKeyDown(UINT nChar,UINT nRepCnt,UINT nFlags){SHORT shiftStateGetKeyState(VK_SHIFT);if(shiftState0x8000){// Shift 键在按下这个字母时是处于按下状态的// 比如检测 CtrlS 快捷键}}特点只能在消息处理函数里用才有意义适合检测组合键比如 CtrlS 这种不能用在游戏主循环里否则可能死循环4.WM_KEYDOWN窗口消息 —— GUI 程序的标准做法如果你在写 Win32 窗口程序或者 MFC 程序最正统的方式就是处理WM_KEYDOWN消息。// Win32 窗口过程里的处理LRESULT CALLBACKWndProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam){switch(message){caseWM_KEYDOWN:// wParam 就是虚拟键码switch(wParam){caseVK_LEFT:// 左箭头被按下break;caseVK_RIGHT:// 右箭头被按下break;caseVK_ESCAPE:// ESC 被按下break;}break;}returnDefWindowProc(hWnd,message,wParam,lParam);}wParam存的是虚拟键码lParam里包含了重复次数、扫描码等额外信息。特点Windows GUI 程序的标准做法依赖窗口焦点只有当前获得焦点的窗口才能收到消息配合GetKeyState()可以检测组合键5.ReadConsoleInput()—— 控制台底层事件这个方法直接读取控制台的输入缓冲区能拿到比_getch()更详细的信息比如按键是按下还是释放、重复次数、Alt/Ctrl/Shift 的状态等。#includeiostream#includewindows.hintmain(){HANDLE hInputGetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hInput,mode);SetConsoleMode(hInput,mode~ENABLE_LINE_INPUT~ENABLE_ECHO_INPUT);INPUT_RECORD ir;DWORD read;std::cout监听键盘事件按 ESC 退出std::endl;while(true){ReadConsoleInput(hInput,ir,1,read);if(ir.EventTypeKEY_EVENT){KEY_EVENT_RECORDkerir.Event.KeyEvent;if(ker.bKeyDown){// 按键按下std::cout按键: ker.wVirtualKeyCode, 字符: ker.uChar.AsciiCharstd::endl;if(ker.wVirtualKeyCodeVK_ESCAPE){break;}}}}SetConsoleMode(hInput,mode);return0;}特点信息最丰富能区分按下和释放代码比较啰嗦一般场景用不上适合需要精细控制键盘输入的场景二、Linux 平台Linux 下没有 Windows 那套 WinAPI获取键盘输入的方式也不太一样。主要有三条路ncurses库、termios修改终端属性、X11 库图形界面。1.ncurses库 —— 终端程序的最佳选择ncurses是 Linux 下做终端界面程序的标准库提供了getch()函数可以不等待回车直接读取按键。先安装sudoapt-getinstalllibncurses5-dev libncursesw5-dev# Ubuntu/Debian代码示例#includencurses.h#includeiostreamintmain(){initscr();// 初始化 ncursescbreak();// 禁用行缓冲按键立即响应noecho();// 不显示输入的字符keypad(stdscr,TRUE);// 开启功能键支持方向键、F1 等printw(按任意键查看码值按 ESC 退出\n);refresh();intch;while((chgetch())!27){// 27 是 ESCprintw(按键: %c, ASCII: %d\n,ch,ch);refresh();}endwin();// 退出 ncurses 模式return0;}编译时需要链接 ncurses 库g your_program.cpp-lncurses-oyour_program特点功能强大支持方向键、F1-F12 等特殊键可以设置非阻塞模式nodelay()适合做终端游戏、TUI 工具需要额外安装库2.termios—— 不依赖第三方库的方案如果不想装ncurses可以用termios直接修改终端属性关闭行缓冲和回显然后用read()读取。#includeiostream#includeunistd.h#includetermios.hintmain(){structtermiosoldt,newt;// 获取当前终端设置tcgetattr(STDIN_FILENO,oldt);newtoldt;// 关闭行缓冲和回显newt.c_lflag~(ICANON|ECHO);newt.c_cc[VMIN]1;// 读到 1 个字符就返回newt.c_cc[VTIME]0;// 不设超时tcsetattr(STDIN_FILENO,TCSANOW,newt);std::cout按任意键查看码值按 ESC 退出std::endl;charch;while(true){intnread(STDIN_FILENO,ch,1);if(n1){intkeystatic_castunsignedchar(ch);std::cout按键码值: keystd::endl;if(key27)break;}}// 恢复终端设置tcsetattr(STDIN_FILENO,TCSANOW,oldt);return0;}特点不依赖第三方库代码稍复杂需要自己管理终端状态方向键等特殊键会返回多个字节需要额外处理3. X11 —— 图形界面下的方案如果你在 Linux 下写的是图形界面程序有 X Server可以用 X11 库来捕获键盘事件。#includeX11/Xlib.h#includeX11/keysym.h#includeiostreamintmain(){Display*dpyXOpenDisplay(nullptr);if(!dpy){std::cerr无法打开 X 显示std::endl;return1;}Window rootDefaultRootWindow(dpy);XSelectInput(dpy,root,KeyPressMask);XEvent ev;while(true){XNextEvent(dpy,ev);if(ev.typeKeyPress){KeySym keysymXLookupKeysym(ev.xkey,0);std::cout按键: XKeysymToString(keysym)std::endl;if(keysymXK_Escape)break;}}XCloseDisplay(dpy);return0;}编译时需要链接 X11g your_program.cpp-lX11-oyour_program特点适用于 Linux 图形界面程序可以捕获全局键盘事件用XGrabKey依赖 X Server纯终端环境用不了三、完整示例代码我把 Windows 和 Linux 下最常用的几种方式整理成完整的可运行代码方便你直接复制测试。Windows 完整示例综合版#includeiostream#includewindows.h#includeconio.h// 方式1: _kbhit _getchvoidtest_kbhit(){std::cout _kbhit _getch 方式 std::endl;std::cout按任意键按 ESC 退出std::endl;while(true){if(_kbhit()){intkey_getch();if(key27)break;std::cout按键码: keystd::endl;}Sleep(50);}}// 方式2: GetAsyncKeyStatevoidtest_async(){std::cout GetAsyncKeyState 方式 std::endl;std::cout检测 W/A/S/D 按键按 ESC 退出std::endl;while(true){if(GetAsyncKeyState(W)0x8000)std::coutW std::flush;if(GetAsyncKeyState(A)0x8000)std::coutA std::flush;if(GetAsyncKeyState(S)0x8000)std::coutS std::flush;if(GetAsyncKeyState(D)0x8000)std::coutD std::flush;if(GetAsyncKeyState(VK_ESCAPE)0x8000)break;Sleep(50);}std::coutstd::endl;}intmain(){test_kbhit();// test_async(); // 取消注释可测试另一种方式return0;}Linux 完整示例termios 方式#includeiostream#includeunistd.h#includetermios.h#includefcntl.hintmain(){structtermiosoldt,newt;tcgetattr(STDIN_FILENO,oldt);newtoldt;newt.c_lflag~(ICANON|ECHO);newt.c_cc[VMIN]1;newt.c_cc[VTIME]0;tcsetattr(STDIN_FILENO,TCSANOW,newt);// 设置为非阻塞intflagsfcntl(STDIN_FILENO,F_GETFL,0);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);std::cout按任意键按 ESC 退出std::endl;charch;while(true){intnread(STDIN_FILENO,ch,1);if(n1){intkeystatic_castunsignedchar(ch);if(key27)break;std::cout按键码: keystd::endl;}usleep(50000);}tcsetattr(STDIN_FILENO,TCSANOW,oldt);return0;}四、方法对比表格方法平台阻塞/非阻塞适用场景优点缺点_kbhit_getchWindows非阻塞控制台游戏、简单按键检测简单易用头文件自带只支持控制台方向键需额外处理GetAsyncKeyStateWindows非阻塞游戏循环、热键、全局监听实时检测不依赖消息队列频繁调用有性能开销GetKeyStateWindows非阻塞窗口消息处理、组合键检测适合检测快捷键组合只能在消息处理中用WM_KEYDOWNWindows阻塞消息驱动Win32/MFC GUI 程序Windows 标准做法信息完整依赖窗口焦点ReadConsoleInputWindows阻塞需要精细控制按键事件的场景信息最丰富区分按下/释放代码复杂ncursesLinux可配置终端游戏、TUI 工具功能强大支持特殊键需要安装库termiosLinux可配置不想装第三方库的场景不依赖外部库代码稍复杂特殊键需额外处理X11Linux阻塞事件驱动Linux 图形界面程序可捕获全局事件依赖 X Server五、怎么选我的建议Windows 控制台程序直接用_kbhit_getch简单够用。Windows 游戏或实时交互用GetAsyncKeyState实时性好。Windows GUI 程序走WM_KEYDOWN消息这是正道。Linux 终端程序优先用ncurses省心省力不想装库就用termios。跨平台需求用条件编译分别实现Windows 用GetAsyncKeyStateLinux 用termios或ncurses。