Cattura i caratteri dall'input standard senza attendere che venga premuto Invio


174

Non riesco mai a ricordare come lo faccio perché viene fuori così di rado per me. Ma in C o C ++, qual è il modo migliore per leggere un carattere dall'input standard senza aspettare una nuova riga (premi invio).

Inoltre, idealmente non farebbe eco al carattere di input sullo schermo. Voglio solo catturare sequenze di tasti senza influire sullo schermo della console.


1
@adam - Puoi chiarire: vuoi una funzione che ritorni immediatamente se non è disponibile alcun carattere o che aspetterà sempre un singolo tasto?
Roddy,

2
@Roddy - Voglio una funzione che aspetterà sempre un singolo tasto.
Adam,

Risposte:


99

Questo non è possibile in modo portatile in puro C ++, perché dipende troppo dal terminale utilizzato che può essere collegato stdin(di solito sono buffer di linea). Puoi, tuttavia, utilizzare una libreria per questo:

  1. conio disponibile con compilatori di Windows. Usa la _getch()funzione per darti un personaggio senza aspettare il tasto Invio. Non sono uno sviluppatore frequente di Windows, ma ho visto che i miei compagni di classe lo includono <conio.h>e lo usano. Vedi conio.hsu Wikipedia. Elenca getch(), che è dichiarato obsoleto in Visual C ++.

  2. maledizioni disponibili per Linux. Le implementazioni di maledizioni compatibili sono disponibili anche per Windows. Ha anche una getch()funzione. (prova man getcha visualizzare la sua manpage). Vedi Maledizioni su Wikipedia.

Ti consiglierei di usare maledizioni se miri alla compatibilità multipiattaforma. Detto questo, sono sicuro che ci sono funzioni che è possibile utilizzare per disattivare il buffering di linea (credo che si chiama "modalità raw", al contrario di "modalità cotta" - esaminare man stty). Le maledizioni lo gestiranno in modo portatile, se non sbaglio.


7
Si noti che al giorno d'oggi, ncursesè la variante consigliata di curses.
Fondi Monica's Lawsuit

4
se hai bisogno di una biblioteca, come hanno fatto? per creare la libreria che sicuramente dovevi programmare e ciò significa che chiunque poteva programmarla, quindi perché non possiamo semplicemente programmare il codice della libreria che ci serve? ciò renderebbe errato anche il Non è portabilmente in puro c ++
OverloadedCore

@ kid8 non capisco
Johannes Schaub - litb

1
@ JohannesSchaub-litb sta probabilmente cercando di dire come ha fatto l'implementatore della libreria a rendere quel metodo portatile in puro c / c ++ quando dici che non è possibile.
Abhinav Gauniyal,

@AbhinavGauniyal ah, capisco. Tuttavia, non ho detto che l'implementatore della libreria non abbia creato metodi portatili (ovvero che siano utilizzati da programmi ragionevolmente portatili). Ho insinuato che non hanno usato il C ++ portatile. Come chiaramente non hanno fatto, non capisco perché il suo commento affermi che questa parte della mia risposta è sbagliata.
Johannes Schaub - litb

81

Su Linux (e altri sistemi unix-like) questo può essere fatto nel modo seguente:

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

Fondamentalmente devi disattivare la modalità canonica (e la modalità eco per sopprimere l'eco).


Ho provato a implementare questo codice, ma ho ricevuto un errore durante la chiamata a read. Ho incluso entrambe le intestazioni.
Michael Dorst,

6
Forse perché questo codice è sbagliato, poiché read () è una chiamata di sistema POSIX definita in unistd.h. stdio.h potrebbe includerlo per coincidenza, ma in realtà non è necessario stdio.h per questo codice; sostituirlo con unistd.h e dovrebbe essere buono.
Falcon Momot,

1
Non so come finisco qui mentre cercavo l'input da tastiera al terminale ROS in Raspberry Pi. Questo frammento di codice funziona per me.
Akkay

2
@FalconMomot Nel mio IDE 8.1 di NetBeans (su Kali Linux) dice: error: ‘perror’ was not declared in this scopedurante la compilazione, ma funziona bene se incluso stdio.hinsieme a unistd.h.
Abhishek Kashyap,

3
C'è un modo semplice per estenderlo per non bloccare?
Joe Strout,

18

L'ho trovato su un altro forum mentre cercavo di risolvere lo stesso problema. L'ho modificato un po 'da quello che ho trovato. Funziona benissimo. Sto usando OS X, quindi se stai usando Microsoft, dovrai trovare il comando system () corretto per passare alle modalità raw e cotte.

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

13
Mentre funziona, per quello che vale, sborsare il sistema raramente è il modo "migliore" per farlo secondo me. Il programma stty è scritto in C, quindi puoi includere <termios.h> o <sgtty.h> e chiamare lo stesso codice che stty usa, senza dipendere da un programma esterno / fork / whatnot.
Chris Lutz,

1
Ne avevo bisogno per una prova casuale di cose concettuali e per scherzare. Proprio quello di cui avevo bisogno. Grazie. Va notato: metterei sicuramente lo stty cotto alla fine del programma, altrimenti la shell rimarrà in modalità raw stty, che sostanzialmente ha rotto la mia shell lol dopo l'arresto del programma.
Benjamin,

Penso che ti sei dimenticato#include <stdlib.h>
NerdOfCode

14

conio.h

le funzioni di cui hai bisogno sono:

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

o

Implementazione Linux c ++ di conio.h http://sourceforge.net/projects/linux-conioh


9

Se sei su Windows, puoi usare PeekConsoleInput per rilevare se c'è qualche input,

HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD events;
INPUT_RECORD buffer;
PeekConsoleInput( handle, &buffer, 1, &events );

quindi utilizzare ReadConsoleInput per "consumare" il carattere di input.

PeekConsoleInput(handle, &buffer, 1, &events);
if(events > 0)
{
    ReadConsoleInput(handle, &buffer, 1, &events);  
    return buffer.Event.KeyEvent.wVirtualKeyCode;
}
else return 0

a dire il vero, questo proviene da un vecchio codice che ho, quindi devi giocarci un po '.

La cosa interessante è che legge l'input senza richiedere nulla, quindi i caratteri non vengono visualizzati affatto.


9
#include <conio.h>

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

Questo serve kbhit()a verificare se la tastiera viene premuta e utilizza getch()per ottenere il carattere che viene premuto.


5
conio.h ? "conio.h è un file di intestazione C utilizzato nei vecchi compilatori MS-DOS per creare interfacce utente di testo." Sembra un po 'obsoleto.
kay - SE è malvagio l'


5

C e C ++ hanno una visione molto astratta dell'I / O e non esiste un modo standard di fare ciò che si desidera. Esistono modi standard per ottenere caratteri dal flusso di input standard, se ce ne sono, e nient'altro è definito da entrambe le lingue. Ogni risposta dovrà quindi essere specifica per la piattaforma, forse a seconda non solo del sistema operativo ma anche del framework software.

Ci sono alcune ipotesi ragionevoli qui, ma non c'è modo di rispondere alla tua domanda senza sapere qual è il tuo ambiente di destinazione.


5

Uso kbhit () per vedere se è presente un carattere e quindi getchar () per leggere i dati. Su Windows, puoi usare "conio.h". Su Linux, dovrai implementare il tuo kbhit ().

Vedi il codice qui sotto:

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

4

La cosa più vicina al portatile è usare la ncurseslibreria per mettere il terminale in "modalità cbreak". L'API è gigantesca; le routine che vorrai di più sono

  • initscr e endwin
  • cbreak e nocbreak
  • getch

In bocca al lupo!


3

Quella che segue è una soluzione estratta dalla programmazione Expert C: Deep Secrets , che dovrebbe funzionare su SVr4. Usa stty e ioctl .

#include <sys/filio.h>
int kbhit()
{
 int i;
 ioctl(0, FIONREAD, &i);
 return i; /* return a count of chars available to read */
}
main()
{
 int i = 0;
 intc='';
 system("stty raw -echo");
 printf("enter 'q' to quit \n");
 for (;c!='q';i++) {
    if (kbhit()) {
        c=getchar();
       printf("\n got %c, on iteration %d",c, i);
    }
}
 system("stty cooked echo");
}

3

Ho sempre voluto un loop per leggere il mio input senza premere il tasto Invio. questo ha funzionato per me.

#include<stdio.h>
 main()
 {
   char ch;
    system("stty raw");//seting the terminal in raw mode
    while(1)
     {
     ch=getchar();
      if(ch=='~'){          //terminate or come out of raw mode on "~" pressed
      system("stty cooked");
     //while(1);//you may still run the code 
     exit(0); //or terminate
     }
       printf("you pressed %c\n ",ch);  //write rest code here
      }

    }

1
una volta entrati nella MODALITÀ RAW è difficile terminare il processo. quindi tieni un punto per interrompere il processo come ho fatto "premendo" ~ "". ................ in altri casi potresti uccidere il processo da un altro terminale usando KILL.
Setu Gupta,

3

ncurses fornisce un bel modo per farlo! Anche questo è il mio primo post (che ricordo), quindi tutti i commenti sono i benvenuti. Apprezzerò quelli utili, ma tutti sono i benvenuti!

per compilare: 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;
}

2

funziona per me su Windows:

#include <conio.h>
char c = _getch();

1

Puoi farlo in modo portabile usando SDL (la libreria Simple DirectMedia), anche se sospetto che potrebbe non piacerti il ​​suo comportamento. Quando l'ho provato, ho dovuto fare in modo che SDL creasse una nuova finestra video (anche se non ne avevo bisogno per il mio programma) e questa finestra "prendeva" quasi tutti gli input da tastiera e mouse (il che andava bene per il mio utilizzo ma poteva essere fastidioso o impraticabile in altre situazioni). Ho il sospetto che sia eccessivo e non ne valga la pena a meno che non sia indispensabile la completa portabilità, altrimenti prova una delle altre soluzioni suggerite.

A proposito, questo ti darà la chiave premere e rilasciare gli eventi separatamente, se ti interessa.


1

Ecco una versione che non sborserà al sistema (scritta e testata su macOS 10.14)

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

char* getStr( char* buffer , int maxRead ) {
  int  numRead  = 0;
  char ch;

  struct termios old = {0};
  struct termios new = {0};
  if( tcgetattr( 0 , &old ) < 0 )        perror( "tcgetattr() old settings" );
  if( tcgetattr( 0 , &new ) < 0 )        perror( "tcgetaart() new settings" );
  cfmakeraw( &new );
  if( tcsetattr( 0 , TCSADRAIN , &new ) < 0 ) perror( "tcssetattr makeraw new" );

  for( int i = 0 ; i < maxRead ; i++)  {
    ch = getchar();
    switch( ch )  {
      case EOF: 
      case '\n':
      case '\r':
        goto exit_getStr;
        break;

      default:
        printf( "%1c" , ch );
        buffer[ numRead++ ] = ch;
        if( numRead >= maxRead )  {
          goto exit_getStr;
        }
        break;
    }
  }

exit_getStr:
  if( tcsetattr( 0 , TCSADRAIN , &old) < 0)   perror ("tcsetattr reset to old" );
  printf( "\n" );   
  return buffer;
}


int main( void ) 
{
  const int maxChars = 20;
  char      stringBuffer[ maxChars+1 ];
  memset(   stringBuffer , 0 , maxChars+1 ); // initialize to 0

  printf( "enter a string: ");
  getStr( stringBuffer , maxChars );
  printf( "you entered: [%s]\n" , stringBuffer );
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.