Die Antworten hier waren informativ, aber ich wollte auch eine Möglichkeit, Tastendrücke asynchron zu erhalten und Tastendrücke in separaten Ereignissen abzufeuern, alles auf threadsichere, plattformübergreifende Weise. PyGame war mir auch zu aufgebläht. Also habe ich Folgendes gemacht (in Python 2.7, aber ich vermute, es ist leicht portierbar), das ich hier teilen würde, falls es für andere nützlich wäre. Ich habe dies in einer Datei namens keyPress.py gespeichert.
class _Getch:
"""Gets a single character from standard input. Does not echo to the
screen. From http://code.activestate.com/recipes/134892/"""
def __init__(self):
try:
self.impl = _GetchWindows()
except ImportError:
try:
self.impl = _GetchMacCarbon()
except(AttributeError, ImportError):
self.impl = _GetchUnix()
def __call__(self): return self.impl()
class _GetchUnix:
def __init__(self):
import tty, sys, termios # import termios now or else you'll get the Unix version on the Mac
def __call__(self):
import sys, tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
class _GetchWindows:
def __init__(self):
import msvcrt
def __call__(self):
import msvcrt
return msvcrt.getch()
class _GetchMacCarbon:
"""
A function which returns the current ASCII key that is down;
if no ASCII key is down, the null string is returned. The
page http://www.mactech.com/macintosh-c/chap02-1.html was
very helpful in figuring out how to do this.
"""
def __init__(self):
import Carbon
Carbon.Evt #see if it has this (in Unix, it doesn't)
def __call__(self):
import Carbon
if Carbon.Evt.EventAvail(0x0008)[0]==0: # 0x0008 is the keyDownMask
return ''
else:
#
# The event contains the following info:
# (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
#
# The message (msg) contains the ASCII char which is
# extracted with the 0x000000FF charCodeMask; this
# number is converted to an ASCII character with chr() and
# returned
#
(what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
return chr(msg & 0x000000FF)
import threading
# From https://stackoverflow.com/a/2022629/2924421
class Event(list):
def __call__(self, *args, **kwargs):
for f in self:
f(*args, **kwargs)
def __repr__(self):
return "Event(%s)" % list.__repr__(self)
def getKey():
inkey = _Getch()
import sys
for i in xrange(sys.maxint):
k=inkey()
if k<>'':break
return k
class KeyCallbackFunction():
callbackParam = None
actualFunction = None
def __init__(self, actualFunction, callbackParam):
self.actualFunction = actualFunction
self.callbackParam = callbackParam
def doCallback(self, inputKey):
if not self.actualFunction is None:
if self.callbackParam is None:
callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,))
else:
callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,self.callbackParam))
callbackFunctionThread.daemon = True
callbackFunctionThread.start()
class KeyCapture():
gotKeyLock = threading.Lock()
gotKeys = []
gotKeyEvent = threading.Event()
keyBlockingSetKeyLock = threading.Lock()
addingEventsLock = threading.Lock()
keyReceiveEvents = Event()
keysGotLock = threading.Lock()
keysGot = []
keyBlockingKeyLockLossy = threading.Lock()
keyBlockingKeyLossy = None
keyBlockingEventLossy = threading.Event()
keysBlockingGotLock = threading.Lock()
keysBlockingGot = []
keyBlockingGotEvent = threading.Event()
wantToStopLock = threading.Lock()
wantToStop = False
stoppedLock = threading.Lock()
stopped = True
isRunningEvent = False
getKeyThread = None
keyFunction = None
keyArgs = None
# Begin capturing keys. A seperate thread is launched that
# captures key presses, and then these can be received via get,
# getAsync, and adding an event via addEvent. Note that this
# will prevent the system to accept keys as normal (say, if
# you are in a python shell) because it overrides that key
# capturing behavior.
# If you start capture when it's already been started, a
# InterruptedError("Keys are still being captured")
# will be thrown
# Note that get(), getAsync() and events are independent, so if a key is pressed:
#
# 1: Any calls to get() that are waiting, with lossy on, will return
# that key
# 2: It will be stored in the queue of get keys, so that get() with lossy
# off will return the oldest key pressed not returned by get() yet.
# 3: All events will be fired with that key as their input
# 4: It will be stored in the list of getAsync() keys, where that list
# will be returned and set to empty list on the next call to getAsync().
# get() call with it, aand add it to the getAsync() list.
def startCapture(self, keyFunction=None, args=None):
# Make sure we aren't already capturing keys
self.stoppedLock.acquire()
if not self.stopped:
self.stoppedLock.release()
raise InterruptedError("Keys are still being captured")
return
self.stopped = False
self.stoppedLock.release()
# If we have captured before, we need to allow the get() calls to actually
# wait for key presses now by clearing the event
if self.keyBlockingEventLossy.is_set():
self.keyBlockingEventLossy.clear()
# Have one function that we call every time a key is captured, intended for stopping capture
# as desired
self.keyFunction = keyFunction
self.keyArgs = args
# Begin capturing keys (in a seperate thread)
self.getKeyThread = threading.Thread(target=self._threadProcessKeyPresses)
self.getKeyThread.daemon = True
self.getKeyThread.start()
# Process key captures (in a seperate thread)
self.getKeyThread = threading.Thread(target=self._threadStoreKeyPresses)
self.getKeyThread.daemon = True
self.getKeyThread.start()
def capturing(self):
self.stoppedLock.acquire()
isCapturing = not self.stopped
self.stoppedLock.release()
return isCapturing
# Stops the thread that is capturing keys on the first opporunity
# has to do so. It usually can't stop immediately because getting a key
# is a blocking process, so this will probably stop capturing after the
# next key is pressed.
#
# However, Sometimes if you call stopCapture it will stop before starting capturing the
# next key, due to multithreading race conditions. So if you want to stop capturing
# reliably, call stopCapture in a function added via addEvent. Then you are
# guaranteed that capturing will stop immediately after the rest of the callback
# functions are called (before starting to capture the next key).
def stopCapture(self):
self.wantToStopLock.acquire()
self.wantToStop = True
self.wantToStopLock.release()
# Takes in a function that will be called every time a key is pressed (with that
# key passed in as the first paramater in that function)
def addEvent(self, keyPressEventFunction, args=None):
self.addingEventsLock.acquire()
callbackHolder = KeyCallbackFunction(keyPressEventFunction, args)
self.keyReceiveEvents.append(callbackHolder.doCallback)
self.addingEventsLock.release()
def clearEvents(self):
self.addingEventsLock.acquire()
self.keyReceiveEvents = Event()
self.addingEventsLock.release()
# Gets a key captured by this KeyCapture, blocking until a key is pressed.
# There is an optional lossy paramater:
# If True all keys before this call are ignored, and the next pressed key
# will be returned.
# If False this will return the oldest key captured that hasn't
# been returned by get yet. False is the default.
def get(self, lossy=False):
if lossy:
# Wait for the next key to be pressed
self.keyBlockingEventLossy.wait()
self.keyBlockingKeyLockLossy.acquire()
keyReceived = self.keyBlockingKeyLossy
self.keyBlockingKeyLockLossy.release()
return keyReceived
else:
while True:
# Wait until a key is pressed
self.keyBlockingGotEvent.wait()
# Get the key pressed
readKey = None
self.keysBlockingGotLock.acquire()
# Get a key if it exists
if len(self.keysBlockingGot) != 0:
readKey = self.keysBlockingGot.pop(0)
# If we got the last one, tell us to wait
if len(self.keysBlockingGot) == 0:
self.keyBlockingGotEvent.clear()
self.keysBlockingGotLock.release()
# Process the key (if it actually exists)
if not readKey is None:
return readKey
# Exit if we are stopping
self.wantToStopLock.acquire()
if self.wantToStop:
self.wantToStopLock.release()
return None
self.wantToStopLock.release()
def clearGetList(self):
self.keysBlockingGotLock.acquire()
self.keysBlockingGot = []
self.keysBlockingGotLock.release()
# Gets a list of all keys pressed since the last call to getAsync, in order
# from first pressed, second pressed, .., most recent pressed
def getAsync(self):
self.keysGotLock.acquire();
keysPressedList = list(self.keysGot)
self.keysGot = []
self.keysGotLock.release()
return keysPressedList
def clearAsyncList(self):
self.keysGotLock.acquire();
self.keysGot = []
self.keysGotLock.release();
def _processKey(self, readKey):
# Append to list for GetKeyAsync
self.keysGotLock.acquire()
self.keysGot.append(readKey)
self.keysGotLock.release()
# Call lossy blocking key events
self.keyBlockingKeyLockLossy.acquire()
self.keyBlockingKeyLossy = readKey
self.keyBlockingEventLossy.set()
self.keyBlockingEventLossy.clear()
self.keyBlockingKeyLockLossy.release()
# Call non-lossy blocking key events
self.keysBlockingGotLock.acquire()
self.keysBlockingGot.append(readKey)
if len(self.keysBlockingGot) == 1:
self.keyBlockingGotEvent.set()
self.keysBlockingGotLock.release()
# Call events added by AddEvent
self.addingEventsLock.acquire()
self.keyReceiveEvents(readKey)
self.addingEventsLock.release()
def _threadProcessKeyPresses(self):
while True:
# Wait until a key is pressed
self.gotKeyEvent.wait()
# Get the key pressed
readKey = None
self.gotKeyLock.acquire()
# Get a key if it exists
if len(self.gotKeys) != 0:
readKey = self.gotKeys.pop(0)
# If we got the last one, tell us to wait
if len(self.gotKeys) == 0:
self.gotKeyEvent.clear()
self.gotKeyLock.release()
# Process the key (if it actually exists)
if not readKey is None:
self._processKey(readKey)
# Exit if we are stopping
self.wantToStopLock.acquire()
if self.wantToStop:
self.wantToStopLock.release()
break
self.wantToStopLock.release()
def _threadStoreKeyPresses(self):
while True:
# Get a key
readKey = getKey()
# Run the potential shut down function
if not self.keyFunction is None:
self.keyFunction(readKey, self.keyArgs)
# Add the key to the list of pressed keys
self.gotKeyLock.acquire()
self.gotKeys.append(readKey)
if len(self.gotKeys) == 1:
self.gotKeyEvent.set()
self.gotKeyLock.release()
# Exit if we are stopping
self.wantToStopLock.acquire()
if self.wantToStop:
self.wantToStopLock.release()
self.gotKeyEvent.set()
break
self.wantToStopLock.release()
# If we have reached here we stopped capturing
# All we need to do to clean up is ensure that
# all the calls to .get() now return None.
# To ensure no calls are stuck never returning,
# we will leave the event set so any tasks waiting
# for it immediately exit. This will be unset upon
# starting key capturing again.
self.stoppedLock.acquire()
# We also need to set this to True so we can start up
# capturing again.
self.stopped = True
self.stopped = True
self.keyBlockingKeyLockLossy.acquire()
self.keyBlockingKeyLossy = None
self.keyBlockingEventLossy.set()
self.keyBlockingKeyLockLossy.release()
self.keysBlockingGotLock.acquire()
self.keyBlockingGotEvent.set()
self.keysBlockingGotLock.release()
self.stoppedLock.release()
Die Idee ist, dass Sie entweder einfach anrufen können keyPress.getKey()
, um eine Taste von der Tastatur zu lesen, und sie dann zurückgeben können.
Wenn Sie etwas mehr als das wollen, habe ich ein KeyCapture
Objekt gemacht. Sie können eine über so etwas wie erstellen keys = keyPress.KeyCapture()
.
Dann gibt es drei Dinge, die Sie tun können:
addEvent(functionName)
übernimmt jede Funktion, die einen Parameter akzeptiert. Jedes Mal, wenn eine Taste gedrückt wird, wird diese Funktion mit der Zeichenfolge dieser Taste als Eingabe aufgerufen. Diese werden in einem separaten Thread ausgeführt, sodass Sie alles blockieren können, was Sie möchten, und die Funktionalität des KeyCapturer nicht beeinträchtigt oder die anderen Ereignisse verzögert werden.
get()
Gibt einen Schlüssel auf dieselbe blockierende Weise wie zuvor zurück. Es wird jetzt hier benötigt, da die Schlüssel jetzt über das KeyCapture
Objekt erfasst werden. Dies keyPress.getKey()
würde zu einem Konflikt mit diesem Verhalten führen und beide würden einige Schlüssel übersehen, da jeweils nur ein Schlüssel erfasst werden kann. Angenommen, der Benutzer drückt 'a', dann 'b', Sie rufen an get()
, der Benutzer drückt 'c'. Dieser get()
Aufruf gibt sofort 'a' zurück. Wenn Sie ihn erneut aufrufen, wird 'b' und dann 'c' zurückgegeben. Wenn Sie es erneut aufrufen, wird es blockiert, bis eine andere Taste gedrückt wird. Dies stellt sicher, dass Sie keine Schlüssel verpassen, auf Wunsch blockierend. Auf diese Weise ist es ein bisschen anders als keyPress.getKey()
zuvor
Wenn Sie das Verhalten von getKey()
zurück wollen, get(lossy=True)
ist wie get()
, außer dass es nur Tasten zurückgibt, die nach dem Anruf an gedrückt wurden get()
. Im obigen Beispiel get()
würde blockiert, bis der Benutzer 'c' drückt, und wenn Sie es erneut aufrufen, wird es blockiert, bis eine andere Taste gedrückt wird.
getAsync()
ist ein bisschen anders. Es wurde für etwas entwickelt, das viel verarbeitet, dann gelegentlich zurückkommt und überprüft, welche Tasten gedrückt wurden. Somit wird getAsync()
eine Liste aller seit dem letzten Aufruf gedrückten Tasten getAsync()
in der Reihenfolge von der ältesten gedrückten bis zur zuletzt gedrückten Taste zurückgegeben. Es wird auch nicht blockiert, was bedeutet, dass getAsync()
ein Leerzeichen zurückgegeben []
wird , wenn seit dem letzten Aufruf von keine Tasten gedrückt wurden .
Um tatsächlich mit der Erfassung von Schlüsseln zu beginnen, müssen Sie keys.startCapture()
mit Ihrem oben erstellten keys
Objekt anrufen . startCapture
ist nicht blockierend und startet einfach einen Thread, der nur die Tastendrücke aufzeichnet, und einen anderen Thread, um diese Tastendrücke zu verarbeiten. Es gibt zwei Threads, um sicherzustellen, dass der Thread, der Tastendrücke aufzeichnet, keine Tasten übersieht.
Wenn Sie die Erfassung von Schlüsseln beenden möchten, können Sie anrufen keys.stopCapture()
und die Erfassung von Schlüsseln wird beendet. Da das Erfassen eines Schlüssels jedoch eine Blockierungsoperation ist, erfassen die Thread-Erfassungsschlüssel nach dem Aufruf möglicherweise einen weiteren Schlüssel stopCapture()
.
Um dies zu verhindern, können Sie einen oder mehrere optionale Parameter startCapture(functionName, args)
an eine Funktion übergeben, die nur prüft, ob eine Taste gleich 'c' ist, und dann beendet wird. Es ist wichtig, dass diese Funktion nur sehr wenig bewirkt, bevor beispielsweise ein Schlaf hier dazu führt, dass wir Schlüssel verpassen.
Wenn stopCapture()
diese Funktion jedoch aufgerufen wird, werden die Tastenerfassungen sofort gestoppt, ohne dass versucht wird, weitere zu erfassen, und alle get()
Anrufe werden sofort mit "Keine" zurückgegeben, wenn noch keine Tasten gedrückt wurden.
Auch da get()
und getAsync()
speichern alle vorherigen Tasten gedrückt (bis Sie sie abrufen), können Sie anrufen clearGetList()
und clearAsyncList()
die Schlüssel vorher gedrückt zu vergessen.
Beachten Sie, dass get()
, getAsync()
und Ereignisse sind unabhängig, so dass , wenn eine Taste gedrückt wird: 1. Ein Anruf , get()
dass wartet, mit verlustbehafteten auf, wird diese Taste zurück. Die anderen wartenden Anrufe (falls vorhanden) warten weiter. 2. Diese Taste wird in der Warteschlange der Get-Schlüssel gespeichert, sodass get()
bei Verlust die älteste Taste zurückgegeben wird, die get()
noch nicht zurückgegeben wurde . 3. Alle Ereignisse werden mit dieser Taste als Eingabe ausgelöst. 4. Diese Taste wird in der Liste der getAsync()
Schlüssel gespeichert, in der diese Liste zurückgegeben und beim nächsten Aufruf an auf eine leere Liste gesetzt wirdgetAsync()
Wenn dies alles zu viel ist, ist hier ein Beispiel für einen Anwendungsfall:
import keyPress
import time
import threading
def KeyPressed(k, printLock):
printLock.acquire()
print "Event: " + k
printLock.release()
time.sleep(4)
printLock.acquire()
print "Event after delay: " + k
printLock.release()
def GetKeyBlocking(keys, printLock):
while keys.capturing():
keyReceived = keys.get()
time.sleep(1)
printLock.acquire()
if not keyReceived is None:
print "Block " + keyReceived
else:
print "Block None"
printLock.release()
def GetKeyBlockingLossy(keys, printLock):
while keys.capturing():
keyReceived = keys.get(lossy=True)
time.sleep(1)
printLock.acquire()
if not keyReceived is None:
print "Lossy: " + keyReceived
else:
print "Lossy: None"
printLock.release()
def CheckToClose(k, (keys, printLock)):
printLock.acquire()
print "Close: " + k
printLock.release()
if k == "c":
keys.stopCapture()
printLock = threading.Lock()
print "Press a key:"
print "You pressed: " + keyPress.getKey()
print ""
keys = keyPress.KeyCapture()
keys.addEvent(KeyPressed, printLock)
print "Starting capture"
keys.startCapture(CheckToClose, (keys, printLock))
getKeyBlockingThread = threading.Thread(target=GetKeyBlocking, args=(keys, printLock))
getKeyBlockingThread.daemon = True
getKeyBlockingThread.start()
getKeyBlockingThreadLossy = threading.Thread(target=GetKeyBlockingLossy, args=(keys, printLock))
getKeyBlockingThreadLossy.daemon = True
getKeyBlockingThreadLossy.start()
while keys.capturing():
keysPressed = keys.getAsync()
printLock.acquire()
if keysPressed != []:
print "Async: " + str(keysPressed)
printLock.release()
time.sleep(1)
print "done capturing"
Nach dem einfachen Test, den ich gemacht habe, funktioniert es gut für mich, aber ich nehme auch gerne andere Rückmeldungen entgegen, wenn ich etwas verpasst habe.
Ich habe das auch hier gepostet .
msvcrt.getch
durch zu ersetzenmsvcrt.getwch
, wie dort vorgeschlagen.