Entrada de teclado sin bloqueo C

82

Estoy tratando de escribir un programa en C (en Linux) que se repite hasta que el usuario presiona una tecla, pero no debería requerir presionar una tecla para continuar cada ciclo.

¿Existe una forma sencilla de hacer esto? Me imagino que posiblemente podría hacerlo con select()pero eso parece mucho trabajo.

Por otra parte, hay una manera de coger un ctrl- cpulsación de tecla para hacer la limpieza antes de que el programa se cierra en lugar de io sin bloqueo?

Zxaos
fuente
¿Qué hay de abrir un hilo?
Nadav B

Respuestas:

65

Como ya se dijo, puede usar sigactionpara capturar ctrl-c, o selectpara capturar cualquier entrada estándar.

Sin embargo, tenga en cuenta que con el último método también debe configurar el TTY para que esté en el modo carácter a la vez en lugar de línea a la vez. Este último es el predeterminado: si escribe una línea de texto, no se envía al stdin del programa en ejecución hasta que presione Intro.

tcsetattr()Debería usar la función para desactivar el modo ICANON y probablemente también desactivar ECHO. Desde la memoria, también debe volver a configurar el terminal en modo ICANON cuando el programa sale.

Solo para completar, aquí hay un código que acabo de crear (nb: ¡sin verificación de errores!) Que configura un TTY Unix y emula las <conio.h>funciones de DOS kbhit()y 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 */
}
Alnitak
fuente
1
¿Se supone que getch () devuelve qué tecla se presionó, o simplemente se supone que consume el carácter?
Salepate
@Salepate se supone que debe devolver el personaje. Es lo (void)que consigue ese carácter y luego lo tira.
Alnitak
1
oh, ya veo, tal vez lo estoy haciendo mal pero cuando uso getch () en un printf ("% c"); muestra personajes extraños en la pantalla.
Salepate
Ligera edición, debe #incluir <unistd.h> para que read () funcione
jernkuan
¿Existe una forma sencilla de leer toda la línea (por ejemplo, con 'getline') en lugar de un solo carácter?
azmeuk
15

select()es un nivel demasiado bajo para su conveniencia. Le sugiero que use la ncursesbiblioteca para poner el terminal en modo cbreak y modo de retraso, luego llame getch(), que regresará ERRsi no hay ningún personaje listo:

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

En ese momento puede llamar getchsin bloquear.

Norman Ramsey
fuente
También pensé en usar curses, pero el problema potencial con eso es que la llamada initscr () borra la pantalla, y también puede interferir con el uso de stdio normal para la salida de pantalla.
Alnitak
5
Sí, las maldiciones son prácticamente todo o nada.
Norman Ramsey
11

En los sistemas UNIX, puede utilizar la sigactionllamada para registrar un gestor de SIGINTseñales para la señal que representa la secuencia de teclas Control + C. El manejador de señales puede establecer una bandera que se verificará en el bucle para que se rompa adecuadamente.

Mehrdad Afshari
fuente
Creo que probablemente terminaré haciendo esto con facilidad, pero aceptaré la otra respuesta ya que está más cerca de lo que pide el título.
Zxaos
6

Probablemente quieras kbhit();

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

using namespace std;

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

esto puede no funcionar en todos los entornos. Una forma portátil sería crear un hilo de monitoreo y establecer alguna bandera engetch();

Jon Clegg
fuente
4
definitivamente no es portátil. kbhit () es una función de E / S de la consola de DOS / Windows.
Alnitak
1
Sigue siendo una respuesta válida para muchos usos. El OP no especificó la portabilidad ni ninguna plataforma en particular.
AShelly
3
Alnitak: No sabía que implicaba una plataforma, ¿cuál es el término equivalente de Windows?
Zxaos
2
@Alnitak ¿Por qué el concepto de "no bloqueo", como concepto de programación, sería más familiar en el entorno Unix?
MestreLion
4
@Alnitak: Quiero decir que estaba familiarizado con el término "llamada sin bloqueo" mucho antes de tocar cualquier * nix ... así que, al igual que Zxaos, nunca me daría cuenta de que implicaría una plataforma.
MestreLion
4

Otra forma de obtener entrada de teclado sin bloqueo es abrir el archivo del dispositivo y leerlo.

Debe conocer el archivo de dispositivo que está buscando, uno de / dev / input / event *. Puede ejecutar cat / proc / bus / input / devices para encontrar el dispositivo que desea.

Este código funciona para mí (ejecutar como administrador).

  #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;
   }
Justin B
fuente
fantástico ejemplo, pero me encontré con un problema en el que Xorg seguiría recibiendo eventos clave, cuando el dispositivo de eventos dejó de emitir cuando se mantuvieron más de seis teclas: \ extraño, ¿verdad?
ThorSummoner
3

La biblioteca de curses se puede utilizar para este propósito. Por supuesto, select()y los manejadores de señales también se pueden usar hasta cierto punto.

PolyThinker
fuente
1
maldiciones es tanto el nombre de la biblioteca como lo que murmurarás cuando la uses;) Sin embargo, ya que te lo iba a mencionar +1.
Wayne Werner
2

Si estás contento con solo ver Control-C, es un trato hecho. Si realmente desea E / S sin bloqueo pero no desea la biblioteca de curses, otra alternativa es mover el candado, el stock y el barril a la biblioteca de AT&Tsfio . Es una bonita biblioteca con un patrón en C stdiopero más flexible, segura para subprocesos y funciona mejor. (sfio significa E / S rápida y segura).

Norman Ramsey
fuente
1

No hay una forma portátil de hacer esto, pero select () podría ser una buena forma. Consulte http://c-faq.com/osdep/readavail.html para obtener más soluciones posibles.

Nate879
fuente
1
esta respuesta es incompleta. sin manipulación de terminal con tcsetattr () [ver mi respuesta] no obtendrá nada en fd 0 hasta que presione enter.
Alnitak
0

Puede hacerlo usando seleccionar como sigue:

  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;
      }
     }
  }

No mucho trabajo: D

varilla
fuente
2
Esta respuesta está incompleta. Debe configurar el terminal en modo no canónico. También nfdsdebe establecerse en 1.
luser droog
0

Aquí hay una función para hacer esto por usted. Necesita lo termios.hque viene con los sistemas POSIX.

#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);
}

Desglosando esto: tcgetattrobtiene la información actual de la terminal y la almacena t. Si cmdes 1, el indicador de entrada local tse establece en entrada sin bloqueo. De lo contrario, se reinicia. Luego tcsetattrcambia la entrada estándar a t.

Si no restablece la entrada estándar al final de su programa, tendrá problemas en su shell.

112
fuente
A la declaración de cambio le falta un corchete :)
étale-cohomology