C++ getch() eingegebenes Zeichen löschen



  • Hallo ihr,

    ich erstelle aktuell ein C++ Programm, welches auf Archlinux in der Konsole laufen lassen. Hierbei werden einige Eingaben vom User abgefragt u. a. wird nach einer Festplatte, einem Netzwerkdevice, sowie auch nach einem Hostname und einem Passwort gefragt.

    Der Hostname und das Passwort werden per getstr() abgefragt.

    Der Kunde hat mich nun darum gebeten, dass wir noch eine Funktion integrieren, sodass man mit der ESC-Taste zur Eingabe zuvor zurückkehren kann. Ich habe jedoch ein Problem, dies mit getstr() umzusetzen.

    Der Hostname wird validiert, sodass keine unerwünschten Zeichen vorhanden sind.

    Ich habe mir nun gedacht, dass ich eine Funktion erstelle, welche jedes einzelnes Zeichen mit getch() abfragt und dies auswertet. Falls es gültig ist, wird es in ein Char-Array geschrieben. Falls die ESC-Taste gedrückt wurde, soll die Funktion "ESCAPE" zurückliefern.

    Ich habe nun das Problem, dass der User auch seine ungültigen Zeichen noch sieht und ich diese nicht entfernen kann.

    Hat jemand eine Idee? Ich würde mich sehr freuen über eine Idee oder einen Tipp. Vielen Dank!!

    std::string MeineFunktion()
    {
    	char EingabeChar, EingabeString[255];
    	std::string AusgabeString;
    	int Zaehler = 0;
    	bool ESCgedrueckt = false;
    
    	// ASCII Codes
    	// 13 = Carriage return, Enter-Taste
    	// 27 = Escape, ESC-Taste
    	// 
    	// solange bis nicht Enter gedrueckt wurde noch keine 255 Zeichen abgefragt wurden
    	while (((EingabeChar=getch())!= 13) && (Zaehler < 255 -1)) {
    		std::cout << Zaehler;
    		// 27 = Escape, ESC-Taste
    		if (EingabeChar == 27) {
    			ESCgedrueckt = true;
    			break;
    		}
    		// Es werden nur folgende Zeichen erlaubt:
    		// 48 - 57 = Zahlen 0 bis 9
    		// 65 - 90 = Buchstaben A bis Z
    		// 97 - 122 = Buchstaben a bis z
    		if (((EingabeChar > 47) && (EingabeChar < 55)) || // Zahlen 0 - 9
    		   ((EingabeChar > 64) && (EingabeChar < 91)) || // Buchstaben A - Z
    		   ((EingabeChar > 96) && (EingabeChar < 123)))   // Buchstaben a- z
    		   {
    		   		EingabeString[Zaehler] = EingabeChar;
    		   		Zaehler++;
    		   }
    		   // Falls ein unerlaubtes Zeichen.. dann einfach von vorn weiter machen
    		   else {
    			   continue;
    		   }
    	}
    	EingabeString[Zaehler] = '\0'; // Null-Terminierung
    	// Falls die ESC-Taste waehrend einer Eingabe gedrueckt wurde, geben wir in dieser Funktion
    	// den String "ESCAPE" zurueck. So weiss die uebergeordnete Funktion, dass der User die
    	// Eingabe abbrechen moechte und zu der Eingabe zuvor zurueck moechte.
    	if (ESCgedrueckt) {
    		return "ESCAPE";
    	}
    	// ansonsten ...
    
    	AusgabeString = convertCharToString(EingabeString);
    
    	return AusgabeString;
    }
    

  • Mod

    Wichtig ist mal, dass das nicht direkt mit klassischem C++ geht, daher habe ich das nach Linux verschoben (und C++/CLI mit .NET, wo dieser Thread ursprünglich war, ist so Windowszeug nochmals eine ganz andere Sprache als C++). Ein klassisches C++-Programm kennt keine Terminals, Tastaturen, etc., das ist nur eine schwarze Kiste, da gehen auf der einen Seite Zeichen rein und auf der anderen Seite Zeichen wieder raus. Du brauchst also Zusatzfunktionen, die das Terminal, in dem das Programm läuft, kennen, und dieses steuern können. Theoretisch könntest du das selber schreiben, indem du die Dokumentation typischer Terminalsteuerzeichen liest. Aber das willst du nicht, denn zu viel Arbeit. Letztlich willst du daher eine Bibliothek für Textuserinterfaces benutzen. PS: Für diesen einen Spezialzweck mag es noch überschaubar sein, das selber mit Steuerzeichen zu programmieren, aber dann kommt bloß das nächste Feature…

    Der Klassiker dafür ist ncurses. ncurses ist sehr, sehr alt. Und es setzt auf einem noch viel, viel älteren Interface auf. Und ist für C. Ich weiß nicht recht, ob man damit Spaß hat, über Simpelbeispiele hinausgehend habe ich noch nie etwas damit gemacht. Aber ncurses ist bekannt und weit verbreitet, das heißt du wirst im Internet viel Hilfestellung dazu finden können.

    Es gibt auch modernere Frameworks für Text-UIs, aber die sind oft von Einzelpersonen gemacht und relativ obskur. Mögen zwar besser sein als ncurses, aber es gilt dann halt das Gegenteil was Dokumentation und Hilfe angeht.

    Generell solltest du dir in dem Moment aber die Sinnfrage stellen, überhaupt eine Textoberfläche zu machen. Denn wenn du sowieso ein Framework für die Oberfläche nimmst, wieso dann nicht gleich eine grafische Schnittstelle. Die sind für den Programmierer auch nicht schwerer zu nutzen (eher im Gegenteil), sehen schöner aus und haben mehr Möglichkeiten. Und vor allem sind sie moderner und viel weiter verbreitet. Das heißt, du findest noch viel mehr Hilfestellung dazu. Wir haben sogar mehrere Unterforen hier, speziell für verschiedene Frameworks. Der Nachteil ist nur, dass das Programm dann nicht mehr im Textmodus läuft. Kann ein Killer sein, aber für interaktive Programme (und deines klingt danach) ist es aber meistens egal.



  • Das klassische getch() echoed eh nicht. Dafür gab es getche()??



  • @Bernd25 sagte in C++ getch() eingegebenes Zeichen löschen:

    ((EingabeChar > 47) && (EingabeChar < 55)) || // Zahlen 0 - 9

    Warum schreibst du dann nicht direkt (EingabeChar >= '0') && (EingabeChar <= '9')) || oder nutzt isdigit?

    Ähnlich mit Buchstaben: es gibt https://en.cppreference.com/w/cpp/string/byte/isalpha oder du könntest einfach (ASCII vorausgesetzt) mit ((EingabeChar >= 'A') && (EingabeChar <= 'Z')) testen und hättest keine komischen Zahlen in deinem Programm.



  • Die ganze Funktion ist doch eher laienhaft.
    Warum wird "ESCAPE" zurückgeliefert? Dann muß nur mal ein User, Passwort oder sonstige Eingabe so heißen...
    Und dann noch das begrenzte char-Array (wo es dann erst zur Übergabe in einen std::string konvertiert wird, anstatt gleich diesen Typ zu benutzen)?!?



  • Vielen Dank für deine ausführliche Rückmeldung sowie für die Erklärung @SeppJ !

    Ich verwende für dieses Programm ncurses, es handelt sich insgesamt um ein Programm mit aktuell 1.500 Zeilen. Die Dokumentation zu ncurses ist naja, ich hatte schon einige Fehler, wo ich sehr lange gebraucht habe diese zu finden und zu beheben.

    Der Kunde hat mich darum gebeten oder vorgeschlagen ncurses zu verwenden. Ich habe das Projekt vor 3 Monaten begonnen, zuvor hatte ich nur Erfahrung mit PHP / Webentwicklung.

    Hätte ich früher gewusst, dass es auch andere womöglich bessere Lösungen als ncurses gibt, die besser aussehen - hätte ich diese wahrscheinlich sogar genommen, aber diese Möglichkeit habe ich nun nicht mehr - da es zu aufwendig wäre.

    Das Programm soll später in eine ISO integriert werden, sodass anschließend Archlinux installiert wird mit den angebenen Daten sowie Zusatzsoftware.



  • Danke @Swordfish für deine Antwort.

    Aktuell verwende ich wirklich nur den Befehl getch(). Eventuell erhalte ich aufgrund meines Befehls echo() welcher vor der Funktion ausgeführt wird, die Zeichen in der Konsole angezeigt.

    Mein Problem ist, dass wenn ein ungültiges Zeichen eingegeben wurde, ich dieses nicht mehr löschen kann. Daher suche ich eine andere Möglichkeit, einen String eingeben zu lassen und gleichzeitig die Möglichkeit habe auf die ESC-Taste reagieren zu können.



  • Danke für deine Antwort @Th69.

    Ich verstehe deine Anmerkung sehr gut, wenn ein User "ESCAPE" heißen soll, dann würde es zu einem Problem werden. Ich habe mein Programm auf Funktionen aufgebaut, welche alle Eingaben abfragen und diese anschließend in einen String speichern.

    Mir ist keine bessere Lösung eingefallen, als den String nach der Eingabe auszuwerten und falls "ESCAPE" "eingeben" wurde, eine Funktion zurückzuspringen. Mir fällt gerade ein, ich könnte vom User eine Eingabe von mind. 4 Zeichen erwarten, und falls ESC gedrückt wurde "ESC" zurückgeben. So hat der User auch keine Möglichkeit "ESC" zu verwenden.

    Gibt es denn eine Möglichkeit, dass ich direkt mit einem String arbeite? Ich bin davon ausgegangen, dass es die beste Möglichkeit wäre mit einem Char-Array zu arbeiten und anschließend dieses in einen String umzuwandeln.

    Ich entschuldige mich dafür, dass ich kein Experte bin und diese Programmiersprache nicht wirklich gelernt habe. Ich versuche einfach mein bestes... oft muss ich auch noch google verwenden, jedoch bin ich damit gut zurecht gekommen, jedoch finde ich leider auf meine Fragestellung dort auch keine wirkliche Antwort..



  • @wob Danke für deine Antwort! Das probiere ich definitiv aus, vielen Dank!



  • @Bernd25 Wie Dir glaube ich schon gesagt worden ist solltest Du Dich nach einer Bibliothek umsehen die Dir den Umgang mit der Konsole erleichtert. Da Du Linux erwähnt hast: https://www.linuxjournal.com/content/getting-started-ncurses



  • Danke für deine Antwort @Swordfish! Ich verwende schon ncurses, aber ich werde mir den Link von dir gleich näher ansehen. Danke für deine Mühe!



  • @Bernd25: Wenn der Anwender bei der Funktion immer etwas eingeben muß, dann könntest du bei ESC auch einfach einen Leerstring zurückgeben (anstatt eines Magic-Strings):

    return std::string();
    

    Alternative wäre z.B. eine Exception zu werfen (throw) und diese dann zu fangen (catch) - je nachdem, ob du dies als eine Programmlogik-Ausnahme ansiehst.
    Oder es ginge auch noch per zusätzlichem Parameter:

    std::string MeineFunktion(bool& ESCgedrueckt)
    {
      ESCgedrueckt = false;
    
      // ...
    }
    

    Oder ab C++11 auch ein std::tuple zurückgeben:

    std::tuple<std::string, bool> MeineFunktion()
    {
      // ...
      return std::make_tuple(AusgabeString, ESCgedrueckt);
      // bzw.
      return { AusgabeString, ESCgedrueckt };
    }
    

    (ich habe mal extra hier jetzt die deutschen Bezeichner gelassen, auch wenn ich dir anrate, alles in englisch zu erstellen!)



  • @Th69
    Was spricht gegen:

    /**
     * @brief 
     * @param s Vom Benutzer eingegebenen String
     * @return false wenn die Eingabe vom Benutzer abgebrochen wurde, sonst true
     */
    bool MeineFunktion(std::string& s)
    {
        char c;             // EingabeChar habe ich auf c (= Char) verkürzt
        int Zaehler = 0;
    
        s.clear();
        while ((c = getch()) != 13)     // 13 = Carriage return, Enter-Taste
        {
            std::cout << Zaehler;		
            if (c == 27)                // 27 = Escape, ESC-Taste
                return false;
            if (isalnum(c))
            {
                s.push_back(c);
                Zaehler++;
            }
        }
        return true;
    }
    


  • Ab C++ 17 gibt´s auch noch std::optional.



  • Genau so gut wie mein Vorschlag "per zusätzlichem Parameter" (welches nun der Rückgabewert und welches der zusätzliche Parameter ist, ist wohl Geschmackssache).



  • Ich bedanke mich für die vielen Rückmeldungen und vorallem für die Beispiele. Ihr habt mir sehr geholfen!!

    Jetzt bin ich aber dran, die Funktion entsprechend anzupassen und es auszuprobieren. Ich werde mich morgen melden, sobald ich es umgesetzt habe.

    Vielen Dank!



  • Ohne Backspace ist das allerdings ziemlich benutzerunfreundlich...
    Allerdings flackerten meine Versuche diesbezüglich (wenn BS und length()>0: Springen zum Zeilenanfang, Ausgabe von Leerzeichen, pop_back() und Ausgabe des Strings.
    Blieben noch die Cursortasten und Einfügen.
    Vermutlich wäre es einfacher, einen modalen Dialog anzuzeigen, in dem sämtliche Eingaben vorgenommen werden können.



  • Früher gab's auch mal Turbo Vision von Borland. Und da ist ein linux port: http://tvision.sourceforge.net/



  • @Quiche-Lorraine Vielen Dank für das tolle Beispiel, damit hast du mir sehr geholfen. Ich habe leider noch keine Informationen im Internet gefunden, wie ich den string anschließend auswerten kann. Jedoch denke ich, dass ich die Funktion so aufrufen muss:

    if (MeineFunktion(std::string test)) { 
    // kein ESC und Eingabe in "test"
    }
    else {
    // ESC
    }
    

    Ich nutze die Funktion aktuell so:

    bool MeineFunktion(std::string& s)
    {
        char c;             // EingabeChar habe ich auf c (= Char) verkürzt
        int Zaehler = 0;
    
        s.clear();
        while ((c = getch()) != 13)     // 13 = Carriage return, Enter-Taste
        {
            std::cout << Zaehler;		
            if (c == 27)                // 27 = Escape, ESC-Taste
                return false;
            if (isalnum(c))
            {
                s.push_back(c);
                Zaehler++;
            }
        }
        return true;
    }
    

    Da ich ncurses verwende, befindet sich der Cursor in der Mitte des Bildschirms, nach meiner Text-Ausgabe mit mvprintw. Also z. B. nach: "Hier eingeben: ". Dies ist auch so gewünscht. Gebe ich nun etwas ein und drücke ENTER verschiebt sich der Cursor ganz nach links in die Zeile, also in die erste Spalte und die Eingabe wird weiterhin erwartet.

    Gebe ich bei der Eingabe "test" ein und drücke anschließend die ESC-Taste, wird meine Eingabe einfach gelöscht und der Curser ist wieder in der Mitte vom Bildschirm wo er ursprünglich war.

    Sehr komisch irgendwie...

    EDIT: Und ich habe bei der Eingabe die Möglichkeit mit der zurück Taste, so weit zu löschen, bis ich in der ersten Spalte bin. Das ist natürlich ganz doof.... Hast du für mich einen Tipp, was ich versuchen kann? Ich danke dir!



  • @Bernd25
    Mit ncurses kenne ich mich nicht aus.

    Da ich aber eine Idee mit Control Codes hatte, habe ich folgendes ausprobiert:

    #include <string>
    #include <iostream>
    #include <cstdio>
    #include <conio.h>
    
    /**
     * @brief 
     * 
     * @todo Die Funktion nutzt die Control Codes 13 = Carriage Return, 27 = Escape und 
     * 8 = Backspace. Ich bin mit aber nicht sicher ob auf allen Terminals diese auch so
     * unterstuetzt werden, wie wir sie hier benötigen. Daher ist es wichtig diese auf 
     * dem Zielterminal zu testen. Getestete Terminals: Windows cmd.exe
     * 
     * @todo Die Funktion würde ich als Hack einstufen. Eine grafische Variante mit GUI
     * wäre mir lieber. 
     * 
     * @param s Vom Benutzer eingegebenen String
     * 
     * @return false wenn die Eingabe vom Benutzer abgebrochen wurde, sonst true
     */
    bool MeineFunktion2(std::string& s)
    {
        char c;             // EingabeChar habe ich auf c (= Char) verkürzt
    
        std::cout << "Eingabe: ";
        s.clear();
        while ((c = getch()) != 13)     // 13 = Carriage return, Enter-Taste
        {
            if (c == 27)                // 27 = Escape, ESC-Taste
            {
                std::cout << "\n";
                return false;
            }
            else if (c == 8)            // 8 = Backspace
            {
                if (!s.empty())
                {
                    s.pop_back();            
                    std::cout << c << ' ' << c;
                }
            }
            if (isalnum(c))
            {
                s.push_back(c);
                std::cout << c;
            }
            //std::cout << s.size();      // Sobald wie ein Zeichen eingegeben wurde, wollen wir auch die Länge ausgeben        
        }
        std::cout << "\n";
        return true;
    }
    
    int main(int argc, char** argv)
    {
        std::string s;
        
        if (MeineFunktion2(s))
            std::cout << "Benutzereingabe: " << s << "\n";
        else
            std::cout << "Benutzereingabe wurde mittels ESC abgebrochen\n";
        return 0;
    }
    

    Aber ich würde diesen Code als Hack bezeichnen, da ich mir nicht sicher bin ob jedes Terminal auch den ASCII Code 8 = Backspace versteht. Auf der Windows Kommandozeile cmd.exe scheint er zu funktionieren.

    Ich habe leider noch keine Informationen im Internet gefunden, wie ich den string anschließend auswerten kann.

    Doku zu std::string auf cppreference