C nicht blockierende Tastatureingabe

82

Ich versuche, ein Programm in C (unter Linux) zu schreiben, das sich wiederholt, bis der Benutzer eine Taste drückt, aber keinen Tastendruck benötigt, um jede Schleife fortzusetzen.

Gibt es eine einfache Möglichkeit, dies zu tun? Ich denke, ich könnte es möglicherweise damit machen, select()aber das scheint eine Menge Arbeit zu sein.

Gibt es alternativ eine Möglichkeit, einen ctrl- cTastendruck abzufangen, um eine Bereinigung durchzuführen, bevor das Programm geschlossen wird, anstatt io nicht zu blockieren?

Zxaos
quelle
Was ist mit dem Öffnen eines Threads?
Nadav B

Antworten:

65

Wie bereits erwähnt, können Sie sigactionStrg-C oder selectjede Standardeingabe abfangen .

Beachten Sie jedoch, dass Sie bei der letzteren Methode auch die TTY so einstellen müssen, dass sie sich im Zeichen-zu-Zeit-Modus und nicht im Zeilen-zu-Zeit-Modus befindet. Letzteres ist die Standardeinstellung. Wenn Sie eine Textzeile eingeben, wird diese erst dann an den Standard des laufenden Programms gesendet, wenn Sie die Eingabetaste drücken.

Sie müssten die tcsetattr()Funktion verwenden, um den ICANON-Modus auszuschalten und wahrscheinlich auch ECHO zu deaktivieren. Aus dem Speicher müssen Sie das Terminal auch beim Beenden des Programms wieder in den ICANON-Modus versetzen!

Der Vollständigkeit halber hier ein Code, den ich gerade aufgerissen habe (nb: keine Fehlerprüfung!), Der ein Unix-TTY einrichtet und die DOS- <conio.h>Funktionen emuliert kbhit()und 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
quelle
1
Soll getch () zurückgeben, welche Taste gedrückt wurde, oder soll es einfach das Zeichen verbrauchen?
Salepate
@Salepate soll den Charakter zurückgeben. Es ist das (void)Stück, das diesen Charakter bekommt und ihn dann wegwirft.
Alnitak
1
Oh, ich verstehe, vielleicht mache ich es falsch, aber wenn ich getch () für einen printf ("% c") verwende; Es zeigt seltsame Zeichen auf dem Bildschirm an.
Salepate
Leichte Bearbeitung, müssen Sie #include <unistd.h>, damit read () funktioniert
jernkuan
Gibt es eine einfache Möglichkeit, die gesamte Zeile (zum Beispiel mit 'getline') anstelle eines einzelnen Zeichens zu lesen?
Azmeuk
15

select()ist ein bisschen zu niedrig für die Bequemlichkeit. Ich schlage vor, dass Sie die ncursesBibliothek verwenden, um das Terminal in den Break- und Delay-Modus zu versetzen, und dann aufrufen getch(), der zurückkehrt, ERRwenn kein Zeichen bereit ist:

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

An diesem Punkt können Sie anrufen, getchohne zu blockieren.

Norman Ramsey
quelle
Ich habe auch über die Verwendung von Flüchen nachgedacht, aber das potenzielle Problem dabei ist, dass der Aufruf von initscr () den Bildschirm löscht und möglicherweise auch die Verwendung von normalem stdio für die Bildschirmausgabe behindert.
Alnitak
5
Ja, Flüche sind so ziemlich alles oder nichts.
Norman Ramsey
11

Auf UNIX-Systemen können Sie mit sigactioncall einen Signalhandler für ein Signal registrieren, SIGINTdas die Tastenfolge Control + C darstellt. Der Signalhandler kann ein Flag setzen, das in der Schleife überprüft wird, damit es entsprechend bricht.

Mehrdad Afshari
quelle
Ich denke, ich werde dies wahrscheinlich zur Erleichterung tun, aber ich werde die andere Antwort akzeptieren, da sie näher an dem liegt, was der Titel verlangt.
Zxaos
6

Sie wollen wahrscheinlich kbhit();

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

using namespace std;

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

Dies funktioniert möglicherweise nicht in allen Umgebungen. Eine tragbare Möglichkeit wäre, einen Überwachungsthread zu erstellen und ein Flag zu setzengetch();

Jon Clegg
quelle
4
definitiv nicht tragbar. kbhit () ist eine DOS / Windows-Konsolen-E / A-Funktion.
Alnitak
1
Es ist immer noch eine gültige Antwort, die viele Verwendungen verwendet. Das OP hat keine Portabilität oder eine bestimmte Plattform angegeben.
AShelly
3
Alnitak: Mir war nicht bewusst, dass dies eine Plattform impliziert - was ist der entsprechende Windows-Begriff?
Zxaos
2
@Annak, warum sollte "Nicht-Blockieren" als Programmierkonzept in der Unix-Umgebung besser bekannt sein?
MestreLion
4
@Alnitak: Ich meine, ich war mit dem Begriff "nicht blockierender Anruf" vertraut, lange bevor ich einen * nix berührte. Genau wie bei Zxaos würde ich nie realisieren, dass dies eine Plattform bedeuten würde.
MestreLion
4

Eine andere Möglichkeit, nicht blockierende Tastatureingaben zu erhalten, besteht darin, die Gerätedatei zu öffnen und zu lesen!

Sie müssen die gesuchte Gerätedatei kennen, eine von / dev / input / event *. Sie können cat / proc / bus / input / Geräte ausführen, um das gewünschte Gerät zu finden.

Dieser Code funktioniert für mich (als Administrator ausführen).

  #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
quelle
Fantastisches Beispiel, aber ich stieß auf ein Problem, bei dem Xorg weiterhin Schlüsselereignisse empfing, wenn das Ereignisgerät nicht mehr ausgegeben wurde, wenn mehr als sechs Schlüssel gehalten wurden: \ seltsam, oder?
ThorSummoner
3

Zu diesem Zweck kann die Curses-Bibliothek verwendet werden. Natürlich select()können bis zu einem gewissen Grad auch Signalhandler verwendet werden.

PolyThinker
quelle
1
Flüche sind sowohl der Name der Bibliothek als auch das, was Sie murmeln, wenn Sie sie verwenden;) Da ich sie Ihnen jedoch +1 nennen wollte.
Wayne Werner
2

Wenn Sie glücklich sind, nur Control-C zu fangen, ist es ein abgeschlossenes Geschäft. Wenn Sie wirklich nicht blockierende E / A möchten, aber die Curses-Bibliothek nicht möchten, können Sie auch Sperre, Lagerbestand und Barrel in die AT & T- sfioBibliothek verschieben . Es ist eine schöne Bibliothek mit C-Muster, stdioaber flexibler, threadsicherer und leistungsfähiger. (sfio steht für sichere, schnelle E / A.)

Norman Ramsey
quelle
1

Es gibt keine tragbare Möglichkeit, dies zu tun, aber select () ist möglicherweise eine gute Möglichkeit. Weitere mögliche Lösungen finden Sie unter http://c-faq.com/osdep/readavail.html .

Nate879
quelle
1
Diese Antwort ist unvollständig. Ohne Terminalmanipulation mit tcsetattr () [siehe meine Antwort] erhalten Sie auf fd 0 nichts, bis Sie die Eingabetaste drücken.
Alnitak
0

Sie können dies mit select wie folgt tun:

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

Nicht zu viel Arbeit: D.

Stange
quelle
2
Diese Antwort ist unvollständig. Sie müssen das Terminal auf den nicht-kanonischen Modus einstellen. Auch nfdssollte auf 1 gesetzt werden.
Luser Droog
0

Hier ist eine Funktion, die dies für Sie erledigt. Sie benötigen termios.hdas, was mit POSIX-Systemen geliefert wird.

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

tcgetattrAufschlüsselung : Ruft die aktuellen Terminalinformationen ab und speichert sie in t. Wenn cmd1 ist, wird das lokale Eingabeflag in tauf nicht blockierende Eingabe gesetzt. Andernfalls wird es zurückgesetzt. Ändert dann tcsetattrdie Standardeingabe in t.

Wenn Sie die Standardeingabe am Ende Ihres Programms nicht zurücksetzen, treten Probleme in Ihrer Shell auf.

112
quelle
Der switch-Anweisung fehlt eine Klammer :)
étale-kohomology