Sofortige Tastenabfrage im Terminal/Eingabeauffurderung

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
jtk
User
Beiträge: 37
Registriert: Montag 19. November 2007, 17:16

Hallo!
kann man irgendwie abfragen, ob im Terminal eine taste gedrückt wurde und die dann einlesen?
oder irgendwie auf eine Taste warten, ohne dass (wie bei input) return gedrückt werden muss?
in C gibt es ja sowas wie kbhit, aber für python hab ich da nix gefunden.

Schonmal vielen Dank für antworten:-)
jtk
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

So ohne weiteres ist das nicht möglich, da die Terminals normalerweise im Zeilenmodus agieren und Chars erst mit <return> abgesetzt werden. Für unixoide Systeme gibts das termios-Modul, in dem ICANON von Hand False setzen könntest, funktioniert aber nicht an der DOS-Konsole.

Einfacher gehts mit dem Modul curses, da übernimmt ncurses die komplette Terminalsteuerung. Für Windows gabs früher separate Module, wcurses und console. Keine Ahnung, ob die noch aktuell sind.

In der Pythondokumentation findest Du auch ein Tutorial für curses: http://docs.python.org/howto/curses.html

Edit:
Beide Module sind zumindest für Python 2.5 erhältlich.
wcurses: http://adamv.com/dev/python/curses/
console: http://effbot.org/zone/console-index.htm
BlackJack

@jtk: `kbhit()` ist bei C aber nicht in der Standardbibliothek.
Benutzeravatar
str1442
User
Beiträge: 520
Registriert: Samstag 31. Mai 2008, 21:13

So ohne weiteres ist das nicht möglich, da die Terminals normalerweise im Zeilenmodus agieren und Chars erst mit <return> abgesetzt werden.
Ist das immer durch den Zeilenmodus de verwendeten Terminals verursacht? Genau dieses Problem hat man in C nämlich auch, und mir wurde das mit einer freiwilligen Restriktion der C Stdlib erklärt. Ein C Programm hätte derartige Beschränkungen also nicht, wenn man es von einem Terminal ohne diesen Zeilenmodus ausführen würde?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Ist das immer durch den Zeilenmodus de verwendeten Terminals verursacht?
Hm, gute Frage. Spätestens seit vt100 gibts das ICANON-Flag in der Termios-Struktur, welches dann von den Shells auf 1 gesetzt wird. Damit wird das Terminal kanonisch, sprich die Characterverwaltung (Pufferung und Rausschreiben) wird vom Terminaltreiber vorgenommen bis ein CR erscheint. Dein Programm weiß also noch garnichts von einem geschriebenen Zeichen. Ohne ICANON fällt die treiberseitige Pufferung und Ausgabe weg und jedes Zeichen wird direkt durchgereicht.

Wie das unter der DOS-Konsole läuft, weiß ich nicht.
BlackJack

@str1442: Das hat mit der Programmiersprache nichts zu tun, sondern eben wirklich mit den Terminals. Wenn die erst nach Betätigen der Eingabetaste den Inhalt ihres Zeilenpuffers an das Programm schicken, ist das von der Sprache, in der das Programm geschrieben ist, völlig unabhängig.

Weil Terminals recht kompliziert werden können, und sich teilweise auch sehr stark unterscheiden -- "damals" gab's deutlich mehr Hardware-Wildwuchs als die VT-100-Varianten, die heute in Software emuliert werden -- ist dafür wohl nichts in der C-Standardbibliothek gelandet. Denn bei den meisten Terminals kann man auch von Zeilenmodus in einen "Raw"-Modus umschalten. Dann muss man aber im Programm mit den ganzen Codes umgehen können, die die senden und die auch wieder von Gerät zu Gerät unterschiedlich sein können.

Um diese Komplexität ein wenig in den Griff zu bekommen, gibt's Bibliotheken wie `curses`.

Ansonsten ist das direkte Abfragen der Tastatur auch ohne Terminal von Rechnersystem und Betriebssystem abhängig und sehr unterschiedlich gelöst.
Benutzeravatar
HerrHagen
User
Beiträge: 430
Registriert: Freitag 6. Juni 2008, 19:07

Unter Windows ist es einfach. Schau dir mal das Modul msvcrt an, speziell die Funktionen kbhit, getch dürften das bieten was du suchst.

MFG HerrHagen
problembär

Hallo,

für Linux-Terminals hatte ich mal das hier zusammenprobiert:

Code: Alles auswählen

#!/usr/bin/env python

import os
import sys
import tty
import termios
import fcntl

import time

class KeyHandler:

    def __init__(self):

        self.fd = sys.stdin.fileno()

        self.oldterm = termios.tcgetattr(self.fd)
        self.oldflags = fcntl.fcntl(self.fd, fcntl.F_GETFL)

        tty.setcbreak(sys.stdin.fileno())
        self.newattr = termios.tcgetattr(self.fd)
        self.newattr[3] = self.newattr[3] & ~termios.ICANON & ~termios.ECHO

        termios.tcsetattr(self.fd, termios.TCSANOW, self.newattr)
        fcntl.fcntl(self.fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)


    def oldTerminalSettings(self):
        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.oldterm)
        fcntl.fcntl(self.fd, fcntl.F_SETFL, self.oldflags)


    def newTerminalSettings(self):
        # tty.setcbreak(sys.stdin.fileno())
        termios.tcsetattr(self.fd, termios.TCSANOW, self.newattr)
        fcntl.fcntl(self.fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)

    def getKey(self):

        try:
            c = sys.stdin.read(1)
            return ord(c)

        except IOError:
            return 0

        except KeyboardInterrupt:
            self.quit(1)

    def quit(self, retcode):
        self.oldTerminalSettings()
        sys.exit(retcode)



if os.name == "posix":
    os.system("clear")

print "\nPress 'q' to stop.\n"

kh = KeyHandler()

keycode = None

while keycode != ord("q"):

    print "Running"

    keycode = kh.getKey()

    if keycode != 0:
        print "\t\t\t" + chr(keycode)

    time.sleep(0.2)

kh.oldTerminalSettings()
HTH

Gruß

P.S.: Wenn das Programm irgendwie abstürzt oder unterbrochen wird, ist das Terminal danach falsch eingestellt und nimmt deshalb ggf. keine Tasten mehr an. Dann STRG+d oder ALT+F4 bzgl. des Terminals drücken.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@str1442
Hier eine Lösung in C unter Linux.

Achtung:
Die Beispiele auf der Seite sind z.T. fehlerhaft. Bitte das Beispiel my_getpass.c so nicht übernehmen, da die Funktion my_getpass() einen ungültigen Pointer zurückgibt.
Zuletzt geändert von jerch am Samstag 28. März 2009, 23:11, insgesamt 1-mal geändert.
Benutzeravatar
str1442
User
Beiträge: 520
Registriert: Samstag 31. Mai 2008, 21:13

Danke für die Erläuterungen und Beispiele.

@jerch Der Link zeigt in ähnliches wie das Posting von problembär, aber die erste Funktion ist recht kaputt. Diese erzeugt zuerst ein neues char array und danach einen pointer auf dieses und nutzt dazu statisch allozierten Speicher. Dann wird am Ende der Pointer auf das Array zurückgegeben und die aufrufende Funktion bekommt einen netten ungültigen Pointer zurück, da C statisch allozierten Speicher ja funktionsweise freigibt.

@problembär: Danke & Deine Klasse sieht aus wie eine Ansammlung von Funktionen. Methoden sollten niemals krasse Seiteneffekte haben, die sich nicht auf das eigene Objekt beziehen, außer man hat einen guten Grund dazu. Für sowas sollte man Funktionen nehmen. Insbesondere sys.exit() innerhalb einer Methode (oder einer Funktion, die nicht sehr weit oben in der Aufrufhierarchie steht) aufrufen sollte man nie tun. Ansonsten dient die Klasse nur zur Statusspeicherung. Und den Code auf Modulebene am besten in eine main() Funktion packen, zusammen mit einer if __name__ == "__main__": main() Zeile am Ende des Modules, das bewirkt, das das Modul auch dann verwendet werden kann, wenn jemand es importieren will.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@str1442
Huch, die Beispiele weiter oben hatte ich mir nicht angesehen :oops: . Danke, ich werds nochmal vermerken.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Mein Linux-getch fällt etwas übersichtlicher aus:

Code: Alles auswählen

import sys, os, tty, termios

def getch(fd=None):
    if fd is None: fd = sys.stdin.fileno()
    term_settings = termios.tcgetattr(fd)
    try:
        tty.setcbreak(fd)
        char = os.read(fd, 1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, term_settings)
    return char
Anstelle von setcbreak() könnte man auch setraw() nehmen, dann schluckt das Terminal auch ein Ctrl+c ohne KeyboardInterrupt.
Unter Windows:

Code: Alles auswählen

import msvcrt
msvcrt.getch()
jtk
User
Beiträge: 37
Registriert: Montag 19. November 2007, 17:16

D A N K E ! ! :D :D
jkt
jtk
User
Beiträge: 37
Registriert: Montag 19. November 2007, 17:16

und noch ne frage zu curses:

Code: Alles auswählen

from curses import *
from random import *
from signal import *

ATTRS=[A_ALTCHARSET,A_BLINK,A_BOLD,A_DIM,A_NORMAL,A_STANDOUT,A_UNDERLINE]

def ende(*args):
        global running
        running=False
        print("--- SIGNAL EMPFANGEN ---")
signal(SIGINT, ende)
scr=initscr()
HOEHE, BREITE = scr.getmaxyx()
curs_set(0)
running=True
for i, att in enumerate(ATTRS):
        scr.addch(ord(str(i)), att)
        scr.getch()
while running:
        scr.addch(randrange(HOEHE), randrange(BREITE),
                  ord("#"), choice(ATTRS))
        scr.refresh()
        napms(50)
endwin()
das programm bricht irgendwann einfach ab!?!?!?
_curses.error: addch() returned ERR
und noch was?
wieso steht am anfang schon ein "#" da?
---------
Jonny
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Benutze curses.wrapper und packe deinen curses-Code in eine Funktion.
BlackJack

@jtk: Du solltest Dir dringend die Sternchen-Importe und ``global`` abgewöhnen.

Namen komplett in Grossbuchstaben dsind per Konvention für Konstanten vorgesehen. Das trifft auf `HOEHE` und `BREITE` aber nicht zu.

Wenn Du den Fehler behandelst und Dir mal die Position ausgeben lässt an der die Ausgabe stattfinden sollte, wirst Du feststellen, dass es immer die Position ganz unten rechts ist, bei der der Fehler auftritt. Da kann wahrscheinlich nichts ausgegeben werden, weil das Terminal dann den ganzen Inhalt um eine Zeile nach oben scrollen würde.
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Der Code sieht übrigens ziemlich schlimm aus.
Globals, Code auf Modulebene, Sternchen-Importe, uneinheitlicher Stil ... von der denglischen Namensgebung will ich gar nicht erst anfangen ;)
jtk
User
Beiträge: 37
Registriert: Montag 19. November 2007, 17:16

man könnte ja mal ins python was einbauen, was ne warning oder nen error ausspuckt, wenn man ein * import verwendet.
dann würd ichs ja nicht mehr machen.
:-)
jtk
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Man könnte aber auch mal reflektieren, warum das in den meisten Fällen eine blöde Idee ist, statt gleich Repressionen zu fordern.
Im übrigen ist es für den interaktiven Modus ganz nett, da es einem Tipparbeit spart (vor allem da man da meist Wegwerf-Code produziert)
jtk
User
Beiträge: 37
Registriert: Montag 19. November 2007, 17:16

hast recht.
ich produzier auch im nicht interaktiven modus fast nur wegwerfcode.
Antworten