C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 获取键盘按键

浅谈C++ 获取键盘按键的 N 种方式

作者:ziguo1122

本文主要介绍了C++ 获取键盘按键的 N 种方式,包括getch、GetAsyncKeyState、ncurses、termios等实用方案,具有一定的参考价值,感兴趣的可以了解一下

前言

写游戏需要检测按键,写控制台工具需要检测按键,写快捷键功能也需要检测按键。C++ 里获取键盘输入的方式五花八门,Windows 和 Linux 下还不一样,初学者很容易搞混。

我自己最早接触的是 getch(),后来发现 Windows 下还有 GetAsyncKeyState、_kbhit,Linux 下又有 ncurses、termios 这些玩意。每种方法各有各的适用场景,用错了轻则功能不达预期,重则程序卡死。

这篇文章就把 C++ 里各种获取按键的方式捋一遍,Windows 和 Linux 分开讲,每种都带上能直接跑的代码。文章末尾有个表格对比,方便你按需选择。

一、Windows 平台

Windows 下获取键盘输入主要有这么几条路:控制台专用的 _kbhit + _getch、实时检测物理按键的 GetAsyncKeyState、依赖消息队列的 GetKeyState、窗口消息 WM_KEYDOWN,以及更底层的 ReadConsoleInput。

1._kbhit()+_getch()—— 最简单粗暴

这是 Windows 控制台程序里最常用的组合。_kbhit() 检查键盘缓冲区有没有按键,有的话返回非零;_getch() 从缓冲区读取一个字符,不需要按回车。

#include <iostream>
#include <conio.h>  // _kbhit 和 _getch 都在这个头文件里

int main() {
    std::cout << "按任意键试试,按 ESC 退出" << std::endl;
    
    while (true) {
        if (_kbhit()) {           // 有按键按下了?
            int key = _getch();   // 读取按键值
            
            if (key == 27) {      // ESC 的 ASCII 码是 27
                std::cout << "ESC 按下,退出" << std::endl;
                break;
            }
            
            std::cout << "按下了键,码值: " << key << std::endl;
        }
    }
    return 0;
}

特点:

2.GetAsyncKeyState()—— 实时检测物理按键

这个函数直接查询键盘的硬件状态,不管你程序有没有焦点,不管消息队列里有没有消息,调用时按键是什么状态就返回什么状态。

返回值是 SHORT 类型,最高位(第 15 位)为 1 表示按键当前被按下。

#include <iostream>
#include <windows.h>

int main() {
    std::cout << "检测 W 键是否被按下,按 ESC 退出" << std::endl;
    
    while (true) {
        // 检测 W 键 (虚拟键码 0x57)
        if (GetAsyncKeyState(0x57) & 0x8000) {
            std::cout << "W 键正在被按下!" << std::endl;
        }
        
        // 检测 ESC 键退出
        if (GetAsyncKeyState(VK_ESCAPE) & 0x8000) {
            std::cout << "ESC 按下,退出" << std::endl;
            break;
        }
        
        Sleep(50);  // 别让 CPU 跑满
    }
    return 0;
}

虚拟键码:Windows 定义了一堆 VK_XXX 常量,比如 VK_SPACE(空格)、VK_UP(上箭头)、VK_RETURN(回车)。也可以直接用字符的 ASCII 码,比如 'A' 就是检测 A 键。

特点:

3.GetKeyState()—— 消息队列里的按键状态

GetKeyState() 和 GetAsyncKeyState() 名字像,但工作机制完全不同。它不直接查硬件,而是查消息队列里最近一次键盘消息处理时的按键状态。

简单说:GetAsyncKeyState() 问的是“现在这个键按着没”,GetKeyState() 问的是“刚才处理键盘消息的时候这个键按着没”。

#include <iostream>
#include <windows.h>

int main() {
    std::cout << "检测 Shift 键状态(需要在消息处理中调用才准确)" << std::endl;
    
    // 注意:在普通控制台程序的主循环里直接调用 GetKeyState 是不准的!
    // 这个函数应该在窗口消息处理函数(如 WM_KEYDOWN)里调用
    
    while (true) {
        // 这样直接调用的结果可能不准确
        SHORT state = GetKeyState(VK_SHIFT);
        if (state & 0x8000) {
            std::cout << "Shift 被按下(可能不准)" << std::endl;
        }
        Sleep(200);
    }
    return 0;
}

正确的用法是在 MFC 或者 Win32 窗口程序的 WM_KEYDOWN 消息处理里调用:

// 在窗口过程或 MFC 的 OnKeyDown 中
void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
    SHORT shiftState = GetKeyState(VK_SHIFT);
    if (shiftState & 0x8000) {
        // Shift 键在按下这个字母时是处于按下状态的
        // 比如检测 Ctrl+S 快捷键
    }
}

特点:

4.WM_KEYDOWN窗口消息 —— GUI 程序的标准做法

如果你在写 Win32 窗口程序或者 MFC 程序,最正统的方式就是处理 WM_KEYDOWN 消息。

// Win32 窗口过程里的处理
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_KEYDOWN:
        // wParam 就是虚拟键码
        switch (wParam) {
        case VK_LEFT:
            // 左箭头被按下
            break;
        case VK_RIGHT:
            // 右箭头被按下
            break;
        case VK_ESCAPE:
            // ESC 被按下
            break;
        }
        break;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

wParam 存的是虚拟键码,lParam 里包含了重复次数、扫描码等额外信息。

特点:

5.ReadConsoleInput()—— 控制台底层事件

这个方法直接读取控制台的输入缓冲区,能拿到比 _getch() 更详细的信息,比如按键是按下还是释放、重复次数、Alt/Ctrl/Shift 的状态等。

#include <iostream>
#include <windows.h>

int main() {
    HANDLE hInput = GetStdHandle(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.EventType == KEY_EVENT) {
            KEY_EVENT_RECORD& ker = ir.Event.KeyEvent;
            if (ker.bKeyDown) {  // 按键按下
                std::cout << "按键: " << ker.wVirtualKeyCode 
                          << ", 字符: " << ker.uChar.AsciiChar << std::endl;
                          
                if (ker.wVirtualKeyCode == VK_ESCAPE) {
                    break;
                }
            }
        }
    }
    
    SetConsoleMode(hInput, mode);
    return 0;
}

特点:

二、Linux 平台

Linux 下没有 Windows 那套 WinAPI,获取键盘输入的方式也不太一样。主要有三条路:ncurses 库、termios 修改终端属性、X11 库(图形界面)。

1.ncurses库 —— 终端程序的最佳选择

ncurses 是 Linux 下做终端界面程序的标准库,提供了 getch() 函数,可以不等待回车直接读取按键。

先安装:

sudo apt-get install libncurses5-dev libncursesw5-dev   # Ubuntu/Debian

代码示例:

#include <ncurses.h>
#include <iostream>

int main() {
    initscr();            // 初始化 ncurses
    cbreak();             // 禁用行缓冲,按键立即响应
    noecho();             // 不显示输入的字符
    keypad(stdscr, TRUE); // 开启功能键支持(方向键、F1 等)
    
    printw("按任意键查看码值,按 ESC 退出\n");
    refresh();
    
    int ch;
    while ((ch = getch()) != 27) {  // 27 是 ESC
        printw("按键: %c, ASCII: %d\n", ch, ch);
        refresh();
    }
    
    endwin();  // 退出 ncurses 模式
    return 0;
}

编译时需要链接 ncurses 库:

g++ your_program.cpp -lncurses -o your_program

特点:

2.termios—— 不依赖第三方库的方案

如果不想装 ncurses,可以用 termios 直接修改终端属性,关闭行缓冲和回显,然后用 read() 读取。

#include <iostream>
#include <unistd.h>
#include <termios.h>

int main() {
    struct termios oldt, newt;
    
    // 获取当前终端设置
    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    
    // 关闭行缓冲和回显
    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;
    
    char ch;
    while (true) {
        int n = read(STDIN_FILENO, &ch, 1);
        if (n == 1) {
            int key = static_cast<unsigned char>(ch);
            std::cout << "按键码值: " << key << std::endl;
            if (key == 27) break;
        }
    }
    
    // 恢复终端设置
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    return 0;
}

特点:

3. X11 —— 图形界面下的方案

如果你在 Linux 下写的是图形界面程序(有 X Server),可以用 X11 库来捕获键盘事件。

#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <iostream>

int main() {
    Display* dpy = XOpenDisplay(nullptr);
    if (!dpy) {
        std::cerr << "无法打开 X 显示" << std::endl;
        return 1;
    }
    
    Window root = DefaultRootWindow(dpy);
    XSelectInput(dpy, root, KeyPressMask);
    
    XEvent ev;
    while (true) {
        XNextEvent(dpy, &ev);
        if (ev.type == KeyPress) {
            KeySym keysym = XLookupKeysym(&ev.xkey, 0);
            std::cout << "按键: " << XKeysymToString(keysym) << std::endl;
            if (keysym == XK_Escape) break;
        }
    }
    
    XCloseDisplay(dpy);
    return 0;
}

编译时需要链接 X11:

g++ your_program.cpp -lX11 -o your_program

特点:

三、完整示例代码

我把 Windows 和 Linux 下最常用的几种方式整理成完整的可运行代码,方便你直接复制测试。

Windows 完整示例(综合版)

#include <iostream>
#include <windows.h>
#include <conio.h>

// 方式1: _kbhit + _getch
void test_kbhit() {
    std::cout << "=== _kbhit + _getch 方式 ===" << std::endl;
    std::cout << "按任意键,按 ESC 退出" << std::endl;
    
    while (true) {
        if (_kbhit()) {
            int key = _getch();
            if (key == 27) break;
            std::cout << "按键码: " << key << std::endl;
        }
        Sleep(50);
    }
}

// 方式2: GetAsyncKeyState
void test_async() {
    std::cout << "=== GetAsyncKeyState 方式 ===" << std::endl;
    std::cout << "检测 W/A/S/D 按键,按 ESC 退出" << std::endl;
    
    while (true) {
        if (GetAsyncKeyState('W') & 0x8000)
            std::cout << "W " << std::flush;
        if (GetAsyncKeyState('A') & 0x8000)
            std::cout << "A " << std::flush;
        if (GetAsyncKeyState('S') & 0x8000)
            std::cout << "S " << std::flush;
        if (GetAsyncKeyState('D') & 0x8000)
            std::cout << "D " << std::flush;
        if (GetAsyncKeyState(VK_ESCAPE) & 0x8000)
            break;
        Sleep(50);
    }
    std::cout << std::endl;
}

int main() {
    test_kbhit();
    // test_async();  // 取消注释可测试另一种方式
    return 0;
}

Linux 完整示例(termios 方式)

#include <iostream>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>

int main() {
    struct termios oldt, newt;
    
    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    newt.c_cc[VMIN] = 1;
    newt.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    
    // 设置为非阻塞
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
    
    std::cout << "按任意键,按 ESC 退出" << std::endl;
    
    char ch;
    while (true) {
        int n = read(STDIN_FILENO, &ch, 1);
        if (n == 1) {
            int key = static_cast<unsigned char>(ch);
            if (key == 27) break;
            std::cout << "按键码: " << key << std::endl;
        }
        usleep(50000);
    }
    
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    return 0;
}

四、方法对比表格

方法平台阻塞/非阻塞适用场景优点缺点
_kbhit + _getchWindows非阻塞控制台游戏、简单按键检测简单易用,头文件自带只支持控制台,方向键需额外处理
GetAsyncKeyStateWindows非阻塞游戏循环、热键、全局监听实时检测,不依赖消息队列频繁调用有性能开销
GetKeyStateWindows非阻塞窗口消息处理、组合键检测适合检测快捷键组合只能在消息处理中用
WM_KEYDOWNWindows阻塞(消息驱动)Win32/MFC GUI 程序Windows 标准做法,信息完整依赖窗口焦点
ReadConsoleInputWindows阻塞需要精细控制按键事件的场景信息最丰富,区分按下/释放代码复杂
ncursesLinux可配置终端游戏、TUI 工具功能强大,支持特殊键需要安装库
termiosLinux可配置不想装第三方库的场景不依赖外部库代码稍复杂,特殊键需额外处理
X11Linux阻塞(事件驱动)Linux 图形界面程序可捕获全局事件依赖 X Server

五、怎么选?我的建议

到此这篇关于浅谈C++ 获取键盘按键的 N 种方式的文章就介绍到这了,更多相关C++ 获取键盘按键内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文