Cノンブロッキングキーボード入力
-
19-08-2019 - |
質問
ユーザーがキーを押すまでループするプログラムをC(Linux)で記述しようとしていますが、各ループを続行するためにキーを押す必要はありません。
これを行う簡単な方法はありますか? select()
でできるかもしれないと思いますが、それは大変な作業のようです。
また、 ctrl - c キーを押して、非ブロッキングioではなくプログラムが閉じる前にクリーンアップを実行する方法はありますか?
解決
すでに述べたように、sigaction
を使用してctrl-cをトラップするか、select
を使用して標準入力をトラップできます。
ただし、後者の方法では、行単位モードではなく文字単位モードになるようにTTYを設定する必要があることに注意してください。後者がデフォルトです-テキストを入力した場合、Enterキーを押すまで実行中のプログラムの標準入力に送信されません。
tcsetattr()
関数を使用してICANONモードをオフにし、おそらくECHOも無効にする必要があります。メモリから、プログラムの終了時に端末をICANONモードに戻す必要もあります!
完全を期すために、Unix TTYをセットアップし、DOS <conio.h>
関数kbhit()
およびgetch()
:をエミュレートするコード(nb:エラーチェックなし!)を示します:
#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. 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:
WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);
At that point you can call getch
without blocking.
On UNIX systems, you can use sigaction
call to register a signal handler for SIGINT
signal which represents the Control+C key sequence. The signal handler can set a flag which will be checked in the loop making it to break appropriately.
You probably want 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();
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*. You can run cat /proc/bus/input/devices to find the device you want.
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;
}
The curses library can be used for this purpose. Of course, select()
and signal handlers can be used too to a certain extent.
If you are happy just catching Control-C, it's a done deal. 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. It's nice library patterned on C stdio
but more flexible, thread-safe, and performs better. (sfio stands for safe, fast I/O.)
There is no portable way to do this, but select() might be a good way. See http://c-faq.com/osdep/readavail.html for more possible solutions.
Here's a function to do this for you. You need termios.h
which comes with POSIX systems.
#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
. If cmd
is 1, the local input flag in t
is set to non-blocking input. Otherwise it is reset. Then tcsetattr
changes standard input to t
.
If you don't reset standard input at the end of your program you will have problems in your shell.
You can do that using select as follow:
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