从标准输入中捕获字符而无需等待按下回车键

232

我经常忘记如何操作,因为这个问题在我的工作中很少遇到。但在C或C++中,最好的方法是什么,可以从标准输入读取一个字符而无需等待换行符(按回车键)。

最理想的情况是不要将输入字符显示在屏幕上。我只想捕获按键,而不影响控制台屏幕。


2
@adam - 你能澄清一下吗:你想要一个函数,如果没有字符可用就立即返回,还是想要一个始终等待单个按键的函数? - Roddy
5
@Roddy - 我想要一个函数,它可以一直等待单个按键。 - Adam
21个回答

125

在纯C++中,这是不可能以可移植的方式实现的,因为它太过依赖于终端的使用,而终端通常与stdin相连(它们通常是行缓冲的)。但是你可以使用一个库来实现:

  1. conio 可用于 Windows 编译器。使用 _getch() 函数获取字符而无需等待输入回车键。虽然我并不是经常开发 Windows 应用程序的开发人员,但我看到我的同学只需包含 <conio.h> 即可使用它。请参见维基百科上的conio.h。它列出了已在 Visual C++ 中声明为过时的 getch() 函数。

  2. curses 可用于 Linux 平台。Windows 平台也有兼容的 curses 实现。它还具有getch()函数。(尝试 man getch 查看其手册页面)。请参阅 维基百科上的Curses

如果您的目标是跨平台兼容性,我建议您使用 curses。话虽如此,我相信有一些函数可以用于关闭行缓冲(我认为这被称为“原始模式”,与“熟悉模式”相对应——请参阅 man stty)。如果没有弄错,curses会以可移植的方式处理这个问题。


17
请注意,现在ncursescurses的推荐变体。 - anon
15
如果你确实需要一个库,那么它是如何制作的?要制作这个库,肯定需要编程,这意味着任何人都可以编写它,那么为什么我们不能自己编写所需的库代码呢?这也会使得“这在纯C++中不可能被移植”这句话成立。 - user6053405
2
@kid8 我不明白。 - Johannes Schaub - litb
5
他可能想知道图书馆实现者如何在纯C/C++中实现了那个可移植的方法,当你说这是不可能的时候。 - Abhinav Gauniyal
2
@AbhinavGauniyal 哦,我明白了。然而,我并没有说库实现者没有制作可移植的方法(即供相对可移植的程序使用)。我是暗示他们没有使用可移植的C++。显然他们没有这样做,我不明白他的评论为什么说我的回答有误。 - Johannes Schaub - litb
显示剩余5条评论

101

在Linux(和其他类Unix系统)上,可以按以下方式完成:

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

char getch() {
        char buf = 0;
        struct termios old = {0};
        if (tcgetattr(0, &old) < 0)
                perror("tcsetattr()");
        old.c_lflag &= ~ICANON;
        old.c_lflag &= ~ECHO;
        old.c_cc[VMIN] = 1;
        old.c_cc[VTIME] = 0;
        if (tcsetattr(0, TCSANOW, &old) < 0)
                perror("tcsetattr ICANON");
        if (read(0, &buf, 1) < 0)
                perror ("read()");
        old.c_lflag |= ICANON;
        old.c_lflag |= ECHO;
        if (tcsetattr(0, TCSADRAIN, &old) < 0)
                perror ("tcsetattr ~ICANON");
        return (buf);
}

基本上,您需要关闭规范模式(并将回显模式设置为抑制回显)。


7
可能是因为这段代码有误,因为read()是在unistd.h中定义的POSIX系统调用。 stdio.h可能只是巧合包含了它,但实际上对于这段代码,你根本不需要stdio.h; 将其替换为unistd.h就可以了。 - Falcon Momot
1
我不知道我是怎么到这里来的,当我在寻找如何在树莓派ROS终端获取键盘输入时。这段代码片段对我很有用。 - aknay
3
在我的Kali Linux上的NetBeans IDE 8.1中,当编译时它会显示:error: ‘perror’ was not declared in this scope,但是当我同时包含stdio.hunistd.h时就可以正常工作。 - Abhishek Kashyap
3
有没有简单的方法可以扩展它而不阻塞? - Joe Strout
包括 stdio.h,然而不包括 perror。 - Alon
显示剩余5条评论

29

在寻求解决同样问题时,我在另一个论坛上发现了这个。我对其进行了一些修改。它运行得很好。我正在运行OS X,所以如果你运行的是Microsoft,你需要找到正确的system()命令来切换到原始模式和熟悉模式。

#include <iostream> 
#include <stdio.h>  
using namespace std;  

int main() { 
  // Output prompt 
  cout << "Press any key to continue..." << endl; 

  // Set terminal to raw mode 
  system("stty raw"); 

  // Wait for single character 
  char input = getchar(); 

  // Echo input:
  cout << "--" << input << "--";

  // Reset terminal to normal "cooked" mode 
  system("stty cooked"); 

  // And we're out of here 
  return 0; 
}

18
个人认为,尽管这种方法可行,但在我看来,通过调用系统程序并不是最佳方法。stty程序是用C语言编写的,因此您可以包含<termios.h>或<sgtty.h>,并调用与stty相同的代码,而无需依赖于外部程序/分叉/其他内容。 - Chris Lutz
1
我需要这个来进行一些随机的概念证明和瞎折腾。正合我意。谢谢你。需要注意的是:如果不在程序结尾加上“stty cooked”命令,你的shell会一直停留在"stty raw"模式下,这基本上会破坏我的shell lol,并且在程序停止之后也不会有任何变化。 - Benjamin
我认为你忘记了 #include <stdlib.h> - NerdOfCode
1
使用 system 是一个非常糟糕的想法。 - Sapphire_Brick

14
如果你使用的是Windows系统,可以使用PeekConsoleInput函数(详情请点击)来检测用户是否有输入。
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD events;
INPUT_RECORD buffer;
PeekConsoleInput( handle, &buffer, 1, &events );

那么使用ReadConsoleInput来“消耗”输入字符...
PeekConsoleInput(handle, &buffer, 1, &events);
if(events > 0)
{
    ReadConsoleInput(handle, &buffer, 1, &events);  
    return buffer.Event.KeyEvent.wVirtualKeyCode;
}
else return 0

说实话,这是我一些旧代码里的内容,所以你需要稍微调整一下。

不过很酷的是,它可以在不提示任何内容的情况下读取输入,因此字符根本不会显示出来。


13

CONIO.H

你需要的函数是:

int getch();
Prototype
    int _getch(void); 
Description
    _getch obtains a character  from stdin. Input is unbuffered, and this
    routine  will  return as  soon as  a character is  available  without 
    waiting for a carriage return. The character is not echoed to stdout.
    _getch bypasses the normal buffering done by getchar and getc. ungetc 
    cannot be used with _getch. 
Synonym
    Function: getch 


int kbhit();
Description
    Checks if a keyboard key has been pressed but not yet read. 
Return Value
    Returns a non-zero value if a key was pressed. Otherwise, returns 0.

libconio http://sourceforge.net/projects/libconio

或者

Linux c++ 实现的 conio.h http://sourceforge.net/projects/linux-conioh


3
conio.h已经过时,仅适用于旧的DOS API,请不要使用它。 - πάντα ῥεῖ
1
这个在Windows上(Visual Studio 2022)可以工作。旧的DOS conio.h有getch(); 当前的版本有_getch(),就像上面的文档一样。它在Windows控制台中完美地工作。 - AHelps
过时了! - Cosmo

8
#include <conio.h>

if (kbhit() != 0) {
    cout << getch() << endl;
}

这段代码使用kbhit()函数检查键盘是否正在被按下,并使用getch()函数获取被按下的字符。


7
conio.h是一个用于旧版MS-DOS编译器中创建文本用户界面的C头文件。看上去有些过时。 - Kijewski

7

ncurses提供了一种很好的方法来实现这一点!此外,这是我记得的第一篇帖子,所以任何评论都受欢迎。我会感激有用的评论,但所有评论都受欢迎!

编译命令:g++ -std=c++11 -pthread -lncurses .cpp -o

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

char get_keyboard_input();

int main(int argc, char *argv[])
{
    initscr();
    raw();
    noecho();
    keypad(stdscr,true);

    auto f = std::async(std::launch::async, get_keyboard_input);
    while (f.wait_for(std::chrono::milliseconds(20)) != std::future_status::ready)
    {
        // do some work
    }

    endwin();
    std::cout << "returned: " << f.get() << std::endl;
    return 0;
}

char get_keyboard_input()
{
    char input = '0';
    while(input != 'q')
    {
        input = getch();
    }
    return input;
}

7

我使用kbhit()函数来检测字符是否存在,然后使用getchar()函数读取数据。在Windows系统中,可以使用“conio.h”头文件。在Linux系统中,需要自行实现kbhit()函数。

请参考下面的代码:

// kbhit
#include <stdio.h>
#include <sys/ioctl.h> // For FIONREAD
#include <termios.h>
#include <stdbool.h>

int kbhit(void) {
    static bool initflag = false;
    static const int STDIN = 0;

    if (!initflag) {
        // Use termios to turn off line buffering
        struct termios term;
        tcgetattr(STDIN, &term);
        term.c_lflag &= ~ICANON;
        tcsetattr(STDIN, TCSANOW, &term);
        setbuf(stdin, NULL);
        initflag = true;
    }

    int nbbytes;
    ioctl(STDIN, FIONREAD, &nbbytes);  // 0 is STDIN
    return nbbytes;
}

// main
#include <unistd.h>

int main(int argc, char** argv) {
    char c;
    //setbuf(stdout, NULL); // Optional: No buffering.
    //setbuf(stdin, NULL);  // Optional: No buffering.
    printf("Press key");
    while (!kbhit()) {
        printf(".");
        fflush(stdout);
        sleep(1);
    }
    c = getchar();
    printf("\nChar received:%c\n", c);
    printf("Done.\n");

    return 0;
}

我在这里发布了一个解决方案的变体:https://dev59.com/MnRC5IYBdhLWcg3wD83r#67363091 它运行良好,谢谢。 - Andrew

7

由于以前在这里提供的解决方案无法跨平台并且对特殊键存在问题,因此我提供了一个解决方案,它在Windows和Linux上都可以工作,并且只使用最少量的外部库(Windows.h用于Windows,sys/ioctl.h+termios.h用于Linux)。

对于ASCII字符(换行符/制表符/空格/退格符/删除符,!"#$%&'()*+,-./0-9:;<=>?@A-Z[]^_`a-z{|}~üäÄöÖÜßµ´§°¹³²),返回ASCII码(正数),对于特殊键(箭头键、页面上/下、pos1/end、esc、insert、F1-F12),返回Windows虚拟键代码的负值(负数)。

#include <iostream>
#include <string>
#include <thread> // contains <chrono>
using namespace std;

void println(const string& s="") {
    cout << s << endl;
}
void sleep(const double t) {
    if(t>0.0) this_thread::sleep_for(chrono::milliseconds((int)(1E3*t+0.5)));
}



// ASCII codes (key>0): 8 backspace, 9 tab, 10 newline, 27 escape, 127 delete, !"#$%&'()*+,-./0-9:;<=>?@A-Z[]^_`a-z{|}~üäÄöÖÜßµ´§°¹³²
// control key codes (key<0): -38/-40/-37/-39 up/down/left/right arrow, -33/-34 page up/down, -36/-35 pos1/end
// other key codes (key<0): -45 insert, -144 num lock, -20 caps lock, -91 windows key, -93 kontext menu key, -112 to -123 F1 to F12
// not working: ¹ (251), num lock (-144), caps lock (-20), windows key (-91), kontext menu key (-93), F11 (-122)
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define VC_EXTRALEAN
#include <Windows.h>
int key_press() { // not working: F11 (-122, toggles fullscreen)
    KEY_EVENT_RECORD keyevent;
    INPUT_RECORD irec;
    DWORD events;
    while(true) {
        ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &irec, 1, &events);
        if(irec.EventType==KEY_EVENT&&((KEY_EVENT_RECORD&)irec.Event).bKeyDown) {
            keyevent = (KEY_EVENT_RECORD&)irec.Event;
            const int ca = (int)keyevent.uChar.AsciiChar;
            const int cv = (int)keyevent.wVirtualKeyCode;
            const int key = ca==0 ? -cv : ca+(ca>0?0:256);
            switch(key) {
                case  -16: continue; // disable Shift
                case  -17: continue; // disable Ctrl / AltGr
                case  -18: continue; // disable Alt / AltGr
                case -220: continue; // disable first detection of "^" key (not "^" symbol)
                case -221: continue; // disable first detection of "`" key (not "`" symbol)
                case -191: continue; // disable AltGr + "#"
                case  -52: continue; // disable AltGr + "4"
                case  -53: continue; // disable AltGr + "5"
                case  -54: continue; // disable AltGr + "6"
                case  -12: continue; // disable num block 5 with num lock deactivated
                case   13: return  10; // enter
                case  -46: return 127; // delete
                case  -49: return 251; // ¹
                case    0: continue;
                case    1: continue; // disable Ctrl + a (selects all text)
                case    2: continue; // disable Ctrl + b
                case    3: continue; // disable Ctrl + c (terminates program)
                case    4: continue; // disable Ctrl + d
                case    5: continue; // disable Ctrl + e
                case    6: continue; // disable Ctrl + f (opens search)
                case    7: continue; // disable Ctrl + g
                //case    8: continue; // disable Ctrl + h (ascii for backspace)
                //case    9: continue; // disable Ctrl + i (ascii for tab)
                case   10: continue; // disable Ctrl + j
                case   11: continue; // disable Ctrl + k
                case   12: continue; // disable Ctrl + l
                //case   13: continue; // disable Ctrl + m (breaks console, ascii for new line)
                case   14: continue; // disable Ctrl + n
                case   15: continue; // disable Ctrl + o
                case   16: continue; // disable Ctrl + p
                case   17: continue; // disable Ctrl + q
                case   18: continue; // disable Ctrl + r
                case   19: continue; // disable Ctrl + s
                case   20: continue; // disable Ctrl + t
                case   21: continue; // disable Ctrl + u
                case   22: continue; // disable Ctrl + v (inserts clipboard)
                case   23: continue; // disable Ctrl + w
                case   24: continue; // disable Ctrl + x
                case   25: continue; // disable Ctrl + y
                case   26: continue; // disable Ctrl + z
                default: return key; // any other ASCII/virtual character
            }
        }
    }
}
#elif defined(__linux__)
#include <sys/ioctl.h>
#include <termios.h>
int key_press() { // not working: ¹ (251), num lock (-144), caps lock (-20), windows key (-91), kontext menu key (-93)
    struct termios term;
    tcgetattr(0, &term);
    while(true) {
        term.c_lflag &= ~(ICANON|ECHO); // turn off line buffering and echoing
        tcsetattr(0, TCSANOW, &term);
        int nbbytes;
        ioctl(0, FIONREAD, &nbbytes); // 0 is STDIN
        while(!nbbytes) {
            sleep(0.01);
            fflush(stdout);
            ioctl(0, FIONREAD, &nbbytes); // 0 is STDIN
        }
        int key = (int)getchar();
        if(key==27||key==194||key==195) { // escape, 194/195 is escape for °ß´äöüÄÖÜ
            key = (int)getchar();
            if(key==91) { // [ following escape
                key = (int)getchar(); // get code of next char after \e[
                if(key==49) { // F5-F8
                    key = 62+(int)getchar(); // 53, 55-57
                    if(key==115) key++; // F5 code is too low by 1
                    getchar(); // take in following ~ (126), but discard code
                } else if(key==50) { // insert or F9-F12
                    key = (int)getchar();
                    if(key==126) { // insert
                        key = 45;
                    } else { // F9-F12
                        key += 71; // 48, 49, 51, 52
                        if(key<121) key++; // F11 and F12 are too low by 1
                        getchar(); // take in following ~ (126), but discard code
                    }
                } else if(key==51||key==53||key==54) { // delete, page up/down
                    getchar(); // take in following ~ (126), but discard code
                }
            } else if(key==79) { // F1-F4
                key = 32+(int)getchar(); // 80-83
            }
            key = -key; // use negative numbers for escaped keys
        }
        term.c_lflag |= (ICANON|ECHO); // turn on line buffering and echoing
        tcsetattr(0, TCSANOW, &term);
        switch(key) {
            case  127: return   8; // backspace
            case  -27: return  27; // escape
            case  -51: return 127; // delete
            case -164: return 132; // ä
            case -182: return 148; // ö
            case -188: return 129; // ü
            case -132: return 142; // Ä
            case -150: return 153; // Ö
            case -156: return 154; // Ü
            case -159: return 225; // ß
            case -181: return 230; // µ
            case -167: return 245; // §
            case -176: return 248; // °
            case -178: return 253; // ²
            case -179: return 252; // ³
            case -180: return 239; // ´
            case  -65: return -38; // up arrow
            case  -66: return -40; // down arrow
            case  -68: return -37; // left arrow
            case  -67: return -39; // right arrow
            case  -53: return -33; // page up
            case  -54: return -34; // page down
            case  -72: return -36; // pos1
            case  -70: return -35; // end
            case    0: continue;
            case    1: continue; // disable Ctrl + a
            case    2: continue; // disable Ctrl + b
            case    3: continue; // disable Ctrl + c (terminates program)
            case    4: continue; // disable Ctrl + d
            case    5: continue; // disable Ctrl + e
            case    6: continue; // disable Ctrl + f
            case    7: continue; // disable Ctrl + g
            case    8: continue; // disable Ctrl + h
            //case    9: continue; // disable Ctrl + i (ascii for tab)
            //case   10: continue; // disable Ctrl + j (ascii for new line)
            case   11: continue; // disable Ctrl + k
            case   12: continue; // disable Ctrl + l
            case   13: continue; // disable Ctrl + m
            case   14: continue; // disable Ctrl + n
            case   15: continue; // disable Ctrl + o
            case   16: continue; // disable Ctrl + p
            case   17: continue; // disable Ctrl + q
            case   18: continue; // disable Ctrl + r
            case   19: continue; // disable Ctrl + s
            case   20: continue; // disable Ctrl + t
            case   21: continue; // disable Ctrl + u
            case   22: continue; // disable Ctrl + v
            case   23: continue; // disable Ctrl + w
            case   24: continue; // disable Ctrl + x
            case   25: continue; // disable Ctrl + y
            case   26: continue; // disable Ctrl + z (terminates program)
            default: return key; // any other ASCII character
        }
    }
}
#endif // Windows/Linux

最后,这里有一个关于如何使用它的例子:

int main() {
    while(true) {
        const int key = key_press(); // blocks until a key is pressed
        println("Input is: "+to_string(key)+", \""+(char)key+"\"");
    }
    return 0;
}

1
这正是我在寻找的确切答案!谢谢! 只是想知道,循环的目的是什么:fflush(); ioctl(0, FIONREAD,...)? - Eugene K
@Eugene K 不用谢!这个循环使key_press()等待按键,通过定期检查按键输入来实现。 - ProjectPhysX
1
明白了!我原以为getchar()会阻塞(并等待)键盘输入。 - Eugene K

4
假设您使用的是Windows系统,可以查看ReadConsoleInput函数。

1
@JohnHenckel 那是针对C#的,你可能想要https://learn.microsoft.com/en-us/windows/console/readconsoleinput - Xantium

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接