简体   繁体   English

C 非阻塞键盘输入

[英]C non-blocking keyboard input

I'm trying to write a program in C (on Linux) that loops until the user presses a key, but shouldn't require a keypress to continue each loop.我正在尝试用 C(在 Linux 上)编写一个程序,该程序会循环直到用户按下某个键,但不需要按键来继续每个循环。

Is there a simple way to do this?有没有一种简单的方法可以做到这一点? I figure I could possibly do it with select() but that seems like a lot of work.我想我可以用select()做到这一点,但这似乎需要做很多工作。

Alternatively, is there a way to catch a ctrl - c keypress to do cleanup before the program closes instead of non-blocking io?或者,有没有办法在程序关闭而不是非阻塞 io 之前捕获ctrl - c按键进行清理?

As already stated, you can use sigaction to trap ctrl-c, or select to trap any standard input.如前所述,您可以使用sigaction捕获 ctrl-c,或select捕获任何标准输入。

Note however that with the latter method you also need to set the TTY so that it's in character-at-a-time rather than line-at-a-time mode.但是请注意,使用后一种方法,您还需要设置 TTY,使其处于一次字符而不是一次行模式。 The latter is the default - if you type in a line of text it doesn't get sent to the running program's stdin until you press enter.后者是默认设置——如果你输入一行文本,它不会被发送到正在运行的程序的标准输入,直到你按下回车键。

You'd need to use the tcsetattr() function to turn off ICANON mode, and probably also disable ECHO too.您需要使用tcsetattr()函数来关闭 ICANON 模式,并且可能也禁用 ECHO。 From memory, you also have to set the terminal back into ICANON mode when the program exits!根据记忆,您还必须在程序退出时将终端设置回 ICANON 模式!

Just for completeness, here's some code I've just knocked up (nb: no error checking!) which sets up a Unix TTY and emulates the DOS <conio.h> functions kbhit() and getch() :为了完整kbhit() ,这里有一些我刚刚敲出的代码(注意:没有错误检查!)它设置了一个 Unix TTY 并模拟了 DOS <conio.h>函数kbhit()getch()

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <termios.h>

struct termios orig_termios;

void reset_terminal_mode()
{
    tcsetattr(0, TCSANOW, &orig_termios);
}

void set_conio_terminal_mode()
{
    struct termios new_termios;

    /* take two copies - one for now, one for later */
    tcgetattr(0, &orig_termios);
    memcpy(&new_termios, &orig_termios, sizeof(new_termios));

    /* register cleanup handler, and set the new terminal mode */
    atexit(reset_terminal_mode);
    cfmakeraw(&new_termios);
    tcsetattr(0, TCSANOW, &new_termios);
}

int kbhit()
{
    struct timeval tv = { 0L, 0L };
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(0, &fds);
    return select(1, &fds, NULL, NULL, &tv);
}

int getch()
{
    int r;
    unsigned char c;
    if ((r = read(0, &c, sizeof(c))) < 0) {
        return r;
    } else {
        return c;
    }
}

int main(int argc, char *argv[])
{
    set_conio_terminal_mode();

    while (!kbhit()) {
        /* do some work */
    }
    (void)getch(); /* consume the character */
}

select() is a bit too low-level for convenience. select()为了方便起见有点太低级了。 I suggest you use the ncurses library to put the terminal in cbreak mode and delay mode, then call getch() , which will return ERR if no character is ready:我建议您使用ncurses库将终端置于 cbreak 模式和延迟模式,然后调用getch() ,如果没有字符准备好,它将返回ERR

WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);

At that point you can call getch without blocking.此时,您可以无阻塞地调用getch

On UNIX systems, you can use sigaction call to register a signal handler for SIGINT signal which represents the Control+C key sequence.在 UNIX 系统上,您可以使用sigaction调用为代表 Control+C 键序列的SIGINT信号注册一个信号处理程序。 The signal handler can set a flag which will be checked in the loop making it to break appropriately.信号处理程序可以设置一个标志,该标志将在循环中检查以使其适当中断。

Another way to get non-blocking keyboard input is to open the device file and read it!另一种获得非阻塞键盘输入的方法是打开设备文件并读取它!

You have to know the device file you are looking for, one of /dev/input/event*.你必须知道你正在寻找的设备文件,/dev/input/event* 之一。 You can run cat /proc/bus/input/devices to find the device you want.你可以运行 cat /proc/bus/input/devices 来找到你想要的设备。

This code works for me (run as an administrator).此代码适用于我(以管理员身份运行)。

  #include <stdlib.h>
  #include <stdio.h>
  #include <unistd.h>
  #include <fcntl.h>
  #include <errno.h>
  #include <linux/input.h>

  int main(int argc, char** argv)
  {
      int fd, bytes;
      struct input_event data;

      const char *pDevice = "/dev/input/event2";

      // Open Keyboard
      fd = open(pDevice, O_RDONLY | O_NONBLOCK);
      if(fd == -1)
      {
          printf("ERROR Opening %s\n", pDevice);
          return -1;
      }

      while(1)
      {
          // Read Keyboard Data
          bytes = read(fd, &data, sizeof(data));
          if(bytes > 0)
          {
              printf("Keypress value=%x, type=%x, code=%x\n", data.value, data.type, data.code);
          }
          else
          {
              // Nothing read
              sleep(1);
          }
      }

      return 0;
   }

You probably want kbhit();你可能想要kbhit();

//Example will loop until a key is pressed
#include <conio.h>
#include <iostream>

using namespace std;

int main()
{
    while(1)
    {
        if(kbhit())
        {
            break;
        }
    }
}

this may not work on all environments.这可能不适用于所有环境。 A portable way would be to create a monitoring thread and set some flag on getch();一种可移植的方法是创建一个监控线程并在getch();上设置一些标志getch();

The curses library can be used for this purpose.诅咒库可用于此目的。 Of course, select() and signal handlers can be used too to a certain extent.当然,在一定程度上也可以使用select()和信号处理程序。

If you are happy just catching Control-C, it's a done deal.如果您对抓住 Control-C 感到高兴,那就大功告成了。 If you really want non-blocking I/O but you don't want the curses library, another alternative is to move lock, stock, and barrel to the AT&T sfio library .如果你真的想要非阻塞 I/O 但你不想要 Curses 库,另一种选择是将锁、股票和桶移动到AT&T sfio It's nice library patterned on C stdio but more flexible, thread-safe, and performs better.它是基于 C stdio的不错的库,但更灵活、线程安全且性能更好。 (sfio stands for safe, fast I/O.) (sfio 代表安全、快速的 I/O。)

Here's a function to do this for you.这是为您执行此操作的功能。 You need termios.h which comes with POSIX systems.您需要 POSIX 系统附带的termios.h

#include <termios.h>
void stdin_set(int cmd)
{
    struct termios t;
    tcgetattr(1,&t);
    switch (cmd) {
    case 1:
            t.c_lflag &= ~ICANON;
            break;
    default:
            t.c_lflag |= ICANON;
            break;
    }
    tcsetattr(1,0,&t);
}

Breaking this down: tcgetattr gets the current terminal information and stores it in t .分解一下: tcgetattr获取当前终端信息并将其存储在t If cmd is 1, the local input flag in t is set to non-blocking input.如果cmd为 1,则t的本地输入标志设置为非阻塞输入。 Otherwise it is reset.否则它被重置。 Then tcsetattr changes standard input to t .然后tcsetattr将标准输入更改为t

If you don't reset standard input at the end of your program you will have problems in your shell.如果您没有在程序结束时重置标准输入,您的 shell 就会出现问题。

There is no portable way to do this, but select() might be a good way.没有可移植的方法来做到这一点,但 select() 可能是一个好方法。 See http://c-faq.com/osdep/readavail.html for more possible solutions.有关更多可能的解决方案,请参阅http://c-faq.com/osdep/readavail.html

You can do that using select as follow:您可以使用 select 来做到这一点,如下所示:

  int nfds = 0;
  fd_set readfds;
  FD_ZERO(&readfds);
  FD_SET(0, &readfds); /* set the stdin in the set of file descriptors to be selected */
  while(1)
  {
     /* Do what you want */
     int count = select(nfds, &readfds, NULL, NULL, NULL);
     if (count > 0) {
      if (FD_ISSET(0, &readfds)) {
          /* If a character was pressed then we get it and exit */
          getchar();
          break;
      }
     }
  }

Not too much work :D没有太多的工作:D

In C++, I did this:在 C++ 中,我这样做了:

#include <chrono>
#include <thread>

using namespace std::chrono_literals;

void OnEnter()
{
    while (true)
    {
        getchar();
        // do something when enter is pressed
    }
}

int main()
{
    std::thread keyBoardCommands(OnEnter);

    while(true)
    {
        // code for main loop
        std::this_thread::sleep_for(16ms);
    }
}

This code would be platform-independent.此代码将与平台无关。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM