如何读取用户输入的单个字符?
- 2024-11-21 08:33:00
- admin 原创
- 5
问题描述:
有没有办法从用户输入中读取一个字符?例如,他们在终端上按下一个键,然后返回(有点像getch()
)。我知道 Windows 中有一个功能,但我想要一个跨平台的功能。
解决方案 1:
这是 ActiveState Recipes 站点的链接,其中介绍了如何在 Windows、Linux 和 OSX 中读取单个字符:
在 Windows 和 Unix 上,使用 getch() 类似方法从 stdin 读取无缓冲字符
class _Getch:
"""Gets a single character from standard input. Does not echo to the
screen."""
def __init__(self):
try:
self.impl = _GetchWindows()
except ImportError:
self.impl = _GetchUnix()
def __call__(self): return self.impl()
class _GetchUnix:
def __init__(self):
import tty, sys
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()
getch = _Getch()
解决方案 2:
sys.stdin.read(1)
基本上会从 STDIN 读取 1 个字节。
如果必须使用不等待的方法,`
`则可以按照前面的答案中的建议使用此代码:
class _Getch:
"""Gets a single character from standard input. Does not echo to the screen."""
def __init__(self):
try:
self.impl = _GetchWindows()
except ImportError:
self.impl = _GetchUnix()
def __call__(self): return self.impl()
class _GetchUnix:
def __init__(self):
import tty, sys
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()
getch = _Getch()
(取自 http://code.activestate.com/recipes/134892/)
解决方案 3:
值得尝试的是readchar库,它部分基于其他答案中提到的 ActiveState 配方(但从那时起已经取得了长足的进步)。
安装:
python -m pip install readchar
用法:
import readchar
print('Reading a char:')
print(repr(readchar.readchar()))
print('Reading a key:')
print(repr(readchar.readkey()))
这已在 Windows 和 Linux 上使用 Python 3.9 进行了测试。它也应该可以在 PyCharm 终端上运行。
Windows 和 Linux 之间的键码并不总是相同的,但是该库提供了特定于平台的定义readchar.key.F1
来帮助解决这个问题。
由于 Linux 将大多数特殊键报告为转义序列(以 开头x1b
),readkey()
因此如果您按下实际的 Esc 键(终端报告为单独的x1b
),就会出现混乱。不幸的是,这是一个常见的 Unix 问题,没有真正可靠的解决方案。
请注意,虽然readkey
按下+ (参见)KeyboardInterrupt
但其他 Linux 信号键(例如+和+ )也会被捕获并返回(分别为和),这可能是理想的,也可能不是理想的。Ctrl
`Creadchar.config
CtrlD
CtrlZ
'x04'`'x1a'
对于类似于 Python 的输入提示功能input()
,请考虑这个问题。
解决方案 4:
两个答案中逐字引用的ActiveState配方是过度设计的。 可以归结为:
def _find_getch():
try:
import termios
except ImportError:
# Non-POSIX. Return msvcrt's (Windows') getch.
import msvcrt
return msvcrt.getch
# POSIX system. Create and return a getch that manipulates the tty.
import sys, tty
def _getch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
return _getch
getch = _find_getch()
解决方案 5:
另一种方法:
import os
import sys
import termios
import fcntl
def getch():
fd = sys.stdin.fileno()
oldterm = termios.tcgetattr(fd)
newattr = termios.tcgetattr(fd)
newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
termios.tcsetattr(fd, termios.TCSANOW, newattr)
oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
try:
while 1:
try:
c = sys.stdin.read(1)
break
except IOError: pass
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
return c
来自这篇博客文章。
解决方案 6:
(目前) 排名第一的答案 (带有 ActiveState 代码) 过于复杂。我认为当一个函数就足够时,没有理由使用类。下面是两个实现相同功能但代码更易读的实现。
这两种实现方式:
在 Python 2 或 Python 3 中运行良好
适用于 Windows、OSX 和 Linux
只读取一个字节(即它们不等待换行符)
不依赖任何外部库
是自包含的(函数定义之外没有代码)
版本 1:易读且简单
def getChar():
try:
# for Windows-based systems
import msvcrt # If successful, we are on Windows
return msvcrt.getch()
except ImportError:
# for POSIX-based systems (with termios & tty support)
import tty, sys, termios # raises ImportError if unsupported
fd = sys.stdin.fileno()
oldSettings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
answer = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)
return answer
版本2:避免重复导入和异常处理:
[编辑]我错过了 ActiveState 代码的一个优点。如果您计划多次读取字符,该代码可避免在类 Unix 系统上重复 Windows 导入和 ImportError 异常处理的(可忽略不计的)成本。虽然您可能应该更关心代码的可读性而不是可忽略不计的优化,但这里有一个替代方案(它类似于 Louis 的答案,但 getChar() 是独立的),其功能与 ActiveState 代码相同且更易读:
def getChar():
# figure out which function to use once, and store it in _func
if "_func" not in getChar.__dict__:
try:
# for Windows-based systems
import msvcrt # If successful, we are on Windows
getChar._func=msvcrt.getch
except ImportError:
# for POSIX-based systems (with termios & tty support)
import tty, sys, termios # raises ImportError if unsupported
def _ttyRead():
fd = sys.stdin.fileno()
oldSettings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
answer = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)
return answer
getChar._func=_ttyRead
return getChar._func()
执行上述任一 getChar() 版本的示例代码:
from __future__ import print_function # put at top of file if using Python 2
# Example of a prompt for one character of input
promptStr = "Please give me a character:"
responseStr = "Thank you for giving me a '{}'."
print(promptStr, end="
> ")
answer = getChar()
print("
")
print(responseStr.format(answer))
解决方案 7:
基于此处的此代码,如果按下Ctrl
+C
或Ctrl
+ ,将正确引发 KeyboardInterrupt 和 EOFError 。D
应该可以在 Windows 和 Linux 上运行。原始来源提供了 OS X 版本。
class _Getch:
"""Gets a single character from standard input. Does not echo to the screen."""
def __init__(self):
try:
self.impl = _GetchWindows()
except ImportError:
self.impl = _GetchUnix()
def __call__(self):
char = self.impl()
if char == 'x03':
raise KeyboardInterrupt
elif char == 'x04':
raise EOFError
return char
class _GetchUnix:
def __init__(self):
import tty
import sys
def __call__(self):
import sys
import tty
import 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()
getch = _Getch()
解决方案 8:
您可以使用click。它经过充分测试,适用于 Linux、Mac 和 Windows。
import click
print('Continue? [yn] ')
c = click.getchar() # Gets a single character
if c == 'y':
print('We will go on')
elif c == 'n':
print('Abort!')
else:
print('Invalid input :(')
解决方案 9:
尝试使用这个: http: //home.wlu.edu/~levys/software/kbhit.py
它是非阻塞的(这意味着你可以有一个while循环并检测按键而不停止它)并且跨平台。
import os
# Windows
if os.name == 'nt':
import msvcrt
# Posix (Linux, OS X)
else:
import sys
import termios
import atexit
from select import select
class KBHit:
def __init__(self):
'''Creates a KBHit object that you can call to do various keyboard things.'''
if os.name == 'nt':
pass
else:
# Save the terminal settings
self.fd = sys.stdin.fileno()
self.new_term = termios.tcgetattr(self.fd)
self.old_term = termios.tcgetattr(self.fd)
# New terminal setting unbuffered
self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)
# Support normal-terminal reset at exit
atexit.register(self.set_normal_term)
def set_normal_term(self):
''' Resets to normal terminal. On Windows this is a no-op.
'''
if os.name == 'nt':
pass
else:
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)
def getch(self):
''' Returns a keyboard character after kbhit() has been called.
Should not be called in the same program as getarrow().
'''
s = ''
if os.name == 'nt':
return msvcrt.getch().decode('utf-8')
else:
return sys.stdin.read(1)
def getarrow(self):
''' Returns an arrow-key code after kbhit() has been called. Codes are
0 : up
1 : right
2 : down
3 : left
Should not be called in the same program as getch().
'''
if os.name == 'nt':
msvcrt.getch() # skip 0xE0
c = msvcrt.getch()
vals = [72, 77, 80, 75]
else:
c = sys.stdin.read(3)[2]
vals = [65, 67, 66, 68]
return vals.index(ord(c.decode('utf-8')))
def kbhit(self):
''' Returns True if keyboard character was hit, False otherwise.
'''
if os.name == 'nt':
return msvcrt.kbhit()
else:
dr,dw,de = select([sys.stdin], [], [], 0)
return dr != []
使用此示例:
import kbhit
kb = kbhit.KBHit()
while(True):
print("Key not pressed") #Do something
if kb.kbhit(): #If a key is pressed:
k_in = kb.getch() #Detect what key was pressed
print("You pressed ", k_in, "!") #Do something
kb.set_normal_term()
或者你可以使用PyPi 中的 getch 模块。但这会阻塞 while 循环
解决方案 10:
这里的答案很有帮助,但是我还想找到一种方法来异步获取按键并在单独的事件中触发按键,所有这些都是线程安全的跨平台方式。PyGame 对我来说也太臃肿了。所以我做了下面的事情(在 Python 2.7 中,但我怀疑它很容易移植),我想我会在这里分享它,以防它对其他人有用。我把它存储在一个名为 keyPress.py 的文件中。
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()
这个想法是,您可以简单地调用keyPress.getKey()
,它将从键盘读取一个键,然后返回它。
如果你想要更多,我制作了一个KeyCapture
对象。你可以通过类似 的方式创建一个对象keys = keyPress.KeyCapture()
。
然后你可以做三件事:
addEvent(functionName)
接受任何接受一个参数的函数。然后每次按下一个键时,都会使用该键的字符串作为输入来调用此函数。这些函数在单独的线程中运行,因此您可以在其中阻止所有您想要的操作,并且不会扰乱 KeyCapturer 的功能,也不会延迟其他事件。
get()
`KeyCapture以与之前相同的阻塞方式返回一个键。现在这里需要它,因为现在键是通过对象捕获的,所以
keyPress.getKey()会与该行为冲突,并且它们都会错过一些键,因为一次只能捕获一个键。另外,假设用户按下“a”,然后按下“b”,你调用
get(),用户按下“c”。该
get()调用将立即返回“a”,然后如果你再次调用它,它将返回“b”,然后“c”。如果再次调用它,它将阻塞,直到按下另一个键。这可以确保你不会错过任何键,如果需要,可以以阻塞方式进行。所以这种方式与
keyPress.getKey()`以前略有不同
getKey()
如果您想要返回的行为,get(lossy=True)
则类似于get()
,只不过它只返回调用后get()
按下的键。因此在上面的例子中,get()
将阻塞直到用户按下“c”,然后如果您再次调用它,它将阻塞直到按下另一个键。
getAsync()
略有不同。它设计用于执行大量处理,然后偶尔返回并检查按下了哪些键。因此,getAsync()
返回自上次调用以来按下的所有键的列表getAsync()
,按从最旧按下的键到最近按下的键的顺序排列。它也不会阻塞,这意味着如果自上次调用以来没有按下任何键,则将返回getAsync()
空值 。[]
要真正开始捕获按键,您需要keys.startCapture()
使用keys
上面创建的对象进行调用。startCapture
它是非阻塞的,只需启动一个线程来记录按键,然后启动另一个线程来处理这些按键。有两个线程可以确保记录按键的线程不会错过任何按键。
如果您想停止捕获键,可以调用keys.stopCapture()
,它将停止捕获键。但是,由于捕获键是一个阻塞操作,因此调用 后,捕获键的线程可能会再捕获一个键stopCapture()
。
为了防止这种情况,您可以将可选参数传入一个startCapture(functionName, args)
函数,该函数只执行一些操作,例如检查某个键是否等于“c”,然后退出。重要的是,在此之前,这个函数几乎不执行任何操作,例如,此处的睡眠会导致我们错过按键。
但是,如果stopCapture()
在此函数中调用,则按键捕获将立即停止,不再尝试捕获任何按键,并且所有get()
调用将立即返回,如果尚未按下任何按键则返回 None。
此外,由于get()
和getAsync()
存储了所有之前按下的键(直到您检索它们),您可以调用clearGetList()
和clearAsyncList()
来忘记之前按下的键。
请注意get()
,getAsync()
和事件是独立的,因此如果按下一个键:
一个等待调用
get()
(开启有损)将返回该密钥。其他等待调用(如果有)将继续等待。该密钥将存储在获取密钥的队列中,以便
get()
在关闭有损的情况下返回尚未返回的最早按下的密钥get()
。所有事件都将以该键作为输入触发
该密钥将存储在密钥列表中
getAsync()
,该列表将在下次调用时返回并设置为空列表getAsync()
如果这一切太多了,这里有一个示例用例:
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"
从我做的简单测试来看,它对我来说效果很好,但如果我遗漏了什么,我也会很乐意接受其他人的反馈。
我也在这里发布了这个。
解决方案 11:
TL;DR:这是你的无依赖跨平台最大密度复制粘贴
我知道我在找那个☝️。你从谷歌来到这里,想要一些不需要 pip install this-and-that 就能正常工作的东西?我相当肯定这个解决方案会持续很长时间。
使用示例
>>> getch_but_it_actually_works() # just normal key like a
'a'
>>> getch_but_it_actually_works() # a but its shift or capslock
'A'
>>> getch_but_it_actually_works() # just bare enter
'
'
>>> getch_but_it_actually_works() # literal ESC key
'x1b'
>>> getch_but_it_actually_works() # one of the arrow keys on linux
'x1b[A'
>>> getch_but_it_actually_works() # one of the arrow keys on windows
'àK'
>>> getch_but_it_actually_works() # some really obscure key-combo. still works.
'x1b[19;6~'
跨平台解决方案,无外部依赖
滚动查看末尾的更详细答案,并附上合理的缩进和注释。这是最大密度预览,便于复制粘贴。只需调用getch_but_it_actually_works()
import os
def _read_one_wide_char_win(): return msvcrt.getwch()
def _char_can_be_escape_win(char): return True if char in ("x00", "à") else False
def _dump_keyboard_buff_win():
try: msvcrt.ungetwch("a")
except OSError: return msvcrt.getwch()
else: _ = msvcrt.getwch(); return ""
def _read_one_wide_char_nix():
old_settings = termios.tcgetattr(sys.stdin.fileno()); tty.setraw(sys.stdin.fileno())
wchar = sys.stdin.read(1)
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings); return wchar
def _char_can_be_escape_nix(char): return True if char == "x1b" else False
def _dump_keyboard_buff_nix():
old_settings = termios.tcgetattr(sys.stdin.fileno())
tty.setraw(sys.stdin.fileno()); os.set_blocking(sys.stdin.fileno(), False)
buffer_dump = ""
while char := sys.stdin.read(1): buffer_dump += char
os.set_blocking(sys.stdin.fileno(), True); termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
if buffer_dump: return buffer_dump
else: return ""
if os.name == "nt":
import msvcrt
read_one_wdchar, char_can_escape, dump_key_buffer = _read_one_wide_char_win, _char_can_be_escape_win, _dump_keyboard_buff_win
if os.name == "posix":
import termios, tty, sys
read_one_wdchar, char_can_escape, dump_key_buffer = _read_one_wide_char_nix, _char_can_be_escape_nix, _dump_keyboard_buff_nix
def getch_but_it_actually_works():
wchar = read_one_wdchar()
if char_can_escape(wchar): dump = dump_key_buffer(); return wchar + dump
else: return wchar
长答案,带有注释和合理缩进的代码
这是包含所有评论的长答案。仍然没有依赖关系。
这很可能会在 Linux 和 Windows 上长期运行。没有外部依赖,只有内置依赖。
它还会处理诸如按下箭头键或一些模糊的按键(如 <ctrl + shift + f12>)之类的边缘情况,这将在 Linux 上产生较长的 ANSI 转义序列,而在 Windows 上则会产生其他内容。它会将 <ctrl+x> 或 <ctrl+z> 或 tab 或 F1-12 之类的内容捕获为单个输入
这些年来,我已经回过这个帖子几十次了,现在是时候回报我的贡献了。下面是完整注释的代码。
这个例子有点长,但你可以跳过阅读大部分内容。相关部分在最后,你可以直接复制粘贴整个内容。
import os
def _read_one_wide_char_win():
"""Wait keyhit return chr. Get only 1st chr if multipart key like arrow"""
return msvcrt.getwch()
def _char_can_be_escape_win(char):
"""Return true if char could start a multipart key code (e.g.: arrows)"""
return True if char in ("x00", "à") else False # x00 is null character
def _dump_keyboard_buff_win():
"""If piece of multipart keycode in buffer, return it. Else return None"""
try: # msvcrt.kbhit wont work with msvcrt.getwch
msvcrt.ungetwch("a") # check buffer status by ungetching wchr
except OSError: # ungetch fails > something in buffer so >
return msvcrt.getwch() # return the buffer note: win multipart keys
else: # are always 2 parts. if ungetwch does not fail
_ = msvcrt.getwch() # clean up and return empty string
return ""
def _read_one_wide_char_nix():
"""Wait keyhit return chr. Get only 1st chr if multipart key like arrow"""
old_settings = termios.tcgetattr(sys.stdin.fileno()) # save settings
tty.setraw(sys.stdin.fileno()) # set raw mode to catch raw key w/o enter
wchar = sys.stdin.read(1)
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
return wchar
def _char_can_be_escape_nix(char):
"""Return true if char could start a multipart key code (e.g.: arrows)"""
return True if char == "x1b" else False # "x1b" is literal esc-key
def _dump_keyboard_buff_nix():
"""If parts of multipart keycode in buffer, return them. Otherwise None"""
old_settings = termios.tcgetattr(sys.stdin.fileno()) # save settings
tty.setraw(sys.stdin.fileno()) # raw to read single key w/o enter
os.set_blocking(sys.stdin.fileno(), False) # dont block for empty buffer
buffer_dump = ""
while char := sys.stdin.read(1):
buffer_dump += char
os.set_blocking(sys.stdin.fileno(), True) # restore normal settings
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
if buffer_dump:
return buffer_dump
else:
return ""
if os.name == "nt":
import msvcrt
read_one_wdchar = _read_one_wide_char_win
char_can_escape = _char_can_be_escape_win
dump_key_buffer = _dump_keyboard_buff_win
if os.name == "posix":
import termios
import tty
import sys
read_one_wdchar = _read_one_wide_char_nix
char_can_escape = _char_can_be_escape_nix
dump_key_buffer = _dump_keyboard_buff_nix
def getch_but_it_actually_works():
"""Returns a printable character or a keycode corresponding to special key
like arrow or insert. Compatible with windows and linux, no external libs
except for builtins. Uses different builtins for windows and linux.
This function is more accurately called:
"get_wide_character_or_keycode_if_the_key_was_nonprintable()"
e.g.:
* returns "e" if e was pressed
* returns "E" if shift or capslock was on
* returns "x1b[19;6~'" for ctrl + shift + F8 on unix
You can use string.isprintable() if you need to sometimes print the output
and sometimes use it for menu control and such. Printing raw ansi escape
codes can cause your terminal to do things like move cursor three rows up.
Enter will return " r" on all platforms (without the space seen here)
as the enter key will produce carriage return, but windows and linux
interpret it differently in different contexts on higher level
"""
wchar = read_one_wdchar() # get first char from key press or key combo
if char_can_escape(wchar): # if char is escapecode, more may be waiting
dump = dump_key_buffer() # dump buffer to check if more were waiting.
return wchar + dump # return escape+buffer. buff could be just ""
else: # if buffer was empty then we return a single
return wchar # key like "e" or "x1b" for the ESC button
解决方案 12:
这可能是上下文管理器的一个用例。撇开 Windows 操作系统的津贴不谈,以下是我的建议:
#!/usr/bin/env python3
# file: 'readchar.py'
"""
Implementation of a way to get a single character of input
without waiting for the user to hit <Enter>.
(OS is Linux, Ubuntu 14.04)
"""
import tty, sys, termios
class ReadChar():
def __enter__(self):
self.fd = sys.stdin.fileno()
self.old_settings = termios.tcgetattr(self.fd)
tty.setraw(sys.stdin.fileno())
return sys.stdin.read(1)
def __exit__(self, type, value, traceback):
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)
def test():
while True:
with ReadChar() as rc:
char = rc
if ord(char) <= 32:
print("You entered character with ordinal {}."\n .format(ord(char)))
else:
print("You entered character '{}'."\n .format(char))
if char in "^C^D":
sys.exit()
if __name__ == "__main__":
test()
解决方案 13:
ActiveState 的配方似乎包含一个针对“posix”系统的小错误,可以防止Ctrl-C
中断(我使用的是 Mac)。如果我将以下代码放入我的脚本中:
while(True):
print(getch())
我将永远无法用 来终止脚本Ctrl-C
,并且我必须终止我的终端才能退出。
我认为下面这句话是原因,而且也太残酷了:
tty.setraw(sys.stdin.fileno())
除此之外,tty
实际上不需要包,termios
足以处理它。
下面是对我有用的改进代码(将会中断),带有在您键入时回显字符的Ctrl-C
额外功能:getche
if sys.platform == 'win32':
import msvcrt
getch = msvcrt.getch
getche = msvcrt.getche
else:
import sys
import termios
def __gen_ch_getter(echo):
def __fun():
fd = sys.stdin.fileno()
oldattr = termios.tcgetattr(fd)
newattr = oldattr[:]
try:
if echo:
# disable ctrl character printing, otherwise, backspace will be printed as "^?"
lflag = ~(termios.ICANON | termios.ECHOCTL)
else:
lflag = ~(termios.ICANON | termios.ECHO)
newattr[3] &= lflag
termios.tcsetattr(fd, termios.TCSADRAIN, newattr)
ch = sys.stdin.read(1)
if echo and ord(ch) == 127: # backspace
# emulate backspace erasing
# https://stackoverflow.com/a/47962872/404271
sys.stdout.write(' ')
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, oldattr)
return ch
return __fun
getch = __gen_ch_getter(False)
getche = __gen_ch_getter(True)
参考:
解决方案 14:
如果我正在做一些复杂的事情,我会使用 curses 来读取按键。但很多时候我只想要一个使用标准库并能读取箭头键的简单 Python 3 脚本,所以我这样做:
import sys, termios, tty
key_Enter = 13
key_Esc = 27
key_Up = '[A'
key_Dn = '[B'
key_Rt = '[C'
key_Lt = '[D'
fdInput = sys.stdin.fileno()
termAttr = termios.tcgetattr(0)
def getch():
tty.setraw(fdInput)
ch = sys.stdin.buffer.raw.read(4).decode(sys.stdin.encoding)
if len(ch) == 1:
if ord(ch) < 32 or ord(ch) > 126:
ch = ord(ch)
elif ord(ch[0]) == 27:
ch = '' + ch[1:]
termios.tcsetattr(fdInput, termios.TCSADRAIN, termAttr)
return ch
解决方案 15:
这是非阻塞的,读取一个键并将其存储在keypress.key中。
import Tkinter as tk
class Keypress:
def __init__(self):
self.root = tk.Tk()
self.root.geometry('300x200')
self.root.bind('<KeyPress>', self.onKeyPress)
def onKeyPress(self, event):
self.key = event.char
def __eq__(self, other):
return self.key == other
def __str__(self):
return self.key
在你的程序中
keypress = Keypress()
while something:
do something
if keypress == 'c':
break
elif keypress == 'i':
print('info')
else:
print("i dont understand %s" % keypress)
解决方案 16:
另一个答案中的评论提到了 cbreak 模式,这对于 Unix 实现很重要,因为您通常不希望 ^C( KeyboardError
) 被 getchar 使用(因为当您将终端设置为原始模式时它会被使用,就像大多数其他答案所做的那样)。
另一个重要细节是,如果您希望读取一个字符而不是一个字节,则应从输入流中读取 4 个字节,因为这是 UTF-8(Python 3+)中单个字符的最大字节数。对于多字节字符(例如键盘箭头),仅读取单个字节会产生意外结果。
以下是我针对 Unix 修改后的实现:
import contextlib
import os
import sys
import termios
import tty
_MAX_CHARACTER_BYTE_LENGTH = 4
@contextlib.contextmanager
def _tty_reset(file_descriptor):
"""
A context manager that saves the tty flags of a file descriptor upon
entering and restores them upon exiting.
"""
old_settings = termios.tcgetattr(file_descriptor)
try:
yield
finally:
termios.tcsetattr(file_descriptor, termios.TCSADRAIN, old_settings)
def get_character(file=sys.stdin):
"""
Read a single character from the given input stream (defaults to sys.stdin).
"""
file_descriptor = file.fileno()
with _tty_reset(file_descriptor):
tty.setcbreak(file_descriptor)
return os.read(file_descriptor, _MAX_CHARACTER_BYTE_LENGTH)
解决方案 17:
使用 pygame 尝试一下:
import pygame
pygame.init() // eliminate error, pygame.error: video system not initialized
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
d = "space key"
print "You pressed the", d, "."
解决方案 18:
python 中的包curses
可用于通过几个语句进入“原始”模式,以便从终端输入字符。Curses 的主要用途是接管屏幕进行输出,这可能不是您想要的。此代码片段使用了print()
语句,这些语句可用,但您必须了解 curses 如何更改附加到输出的行尾。
#!/usr/bin/python3
# Demo of single char terminal input in raw mode with the curses package.
import sys, curses
def run_one_char(dummy):
'Run until a carriage return is entered'
char = ' '
print('Welcome to curses', flush=True)
while ord(char) != 13:
char = one_char()
def one_char():
'Read one character from the keyboard'
print('
? ', flush= True, end = '')
## A blocking single char read in raw mode.
char = sys.stdin.read(1)
print('You entered %s
' % char)
return char
## Must init curses before calling any functions
curses.initscr()
## To make sure the terminal returns to its initial settings,
## and to set raw mode and guarantee cleanup on exit.
curses.wrapper(run_one_char)
print('Curses be gone!')
解决方案 19:
我相信这是最优雅的解决方案之一。
import os
if os.name == 'nt':
import msvcrt
def getch():
return msvcrt.getch().decode()
else:
import sys, tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
def getch():
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
然后在代码中使用它:
if getch() == chr(ESC_ASCII_VALUE):
print("ESC!")
解决方案 20:
最简单的跨平台解决方案是sshkeyboard。使用以下方法安装pip install sshkeyboard
:
然后编写脚本,例如:
from sshkeyboard import listen_keyboard
def press(key):
print(f"'{key}' pressed")
def release(key):
print(f"'{key}' released")
listen_keyboard(
on_press=press,
on_release=release,
)
它将打印:
'a' pressed
'a' released
当A
按下键时,ESC
键默认结束监听。
与 curses、tkinter 和 getch 相比,它需要的编码更少。
解决方案 21:
我针对 python3 的解决方案不依赖于任何 pip 包。
# precondition: import tty, sys
def query_yes_no(question, default=True):
"""
Ask the user a yes/no question.
Returns immediately upon reading one-char answer.
Accepts multiple language characters for yes/no.
"""
if not sys.stdin.isatty():
return default
if default:
prompt = "[Y/n]?"
other_answers = "n"
else:
prompt = "[y/N]?"
other_answers = "yjosiá"
print(question,prompt,flush= True,end=" ")
oldttysettings = tty.tcgetattr(sys.stdin.fileno())
try:
tty.setraw(sys.stdin.fileno())
return not sys.stdin.read(1).lower() in other_answers
except:
return default
finally:
tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN , oldttysettings)
sys.stdout.write("
")
tty.tcdrain(sys.stdin.fileno())
解决方案 22:
接受的答案对我来说效果不太好(我按住一个键,什么也不会发生,然后我按下另一个键它就会起作用)。
在了解了curses模块之后,它似乎确实是正确的选择。现在它可以通过windows-cursors (通过 pip 提供)在 Windows 上使用,因此您可以以与平台无关的方式进行编程。以下是受 YouTube 上这个精彩教程启发的示例:
import curses
def getkey(stdscr):
curses.curs_set(0)
while True:
key = stdscr.getch()
if key != -1:
break
return key
if __name__ == "__main__":
print(curses.wrapper(getkey))
使用扩展名保存它.py
,或者curses.wrapper(getkey)
以交互模式运行。
解决方案 23:
答案在这里:python 中的 raw_input 无需按 Enter
使用此代码-
from tkinter import Tk, Frame
def __set_key(e, root):
"""
e - event with attribute 'char', the released key
"""
global key_pressed
if e.char:
key_pressed = e.char
root.destroy()
def get_key(msg="Press any key ...", time_to_sleep=3):
"""
msg - set to empty string if you don't want to print anything
time_to_sleep - default 3 seconds
"""
global key_pressed
if msg:
print(msg)
key_pressed = None
root = Tk()
root.overrideredirect(True)
frame = Frame(root, width=0, height=0)
frame.bind("<KeyRelease>", lambda f: __set_key(f, root))
frame.pack()
root.focus_set()
frame.focus_set()
frame.focus_force() # doesn't work in a while loop without it
root.after(time_to_sleep * 1000, func=root.destroy)
root.mainloop()
root = None # just in case
return key_pressed
def __main():
c = None
while not c:
c = get_key("Choose your weapon ... ", 2)
print(c)
if __name__ == "__main__":
__main()
参考:https://github.com/unfor19/mg-tools/blob/master/mgtools/get_key_pressed.py
解决方案 24:
如果您希望仅记录一次按键,即使用户多次按下该按键或长时间按下该按键。为避免获取多次按下的输入,请使用 while 循环并传递它。
import keyboard
while(True):
if(keyboard.is_pressed('w')):
s+=1
while(keyboard.is_pressed('w')):
pass
if(keyboard.is_pressed('s')):
s-=1
while(keyboard.is_pressed('s')):
pass
print(s)
解决方案 25:
内置的 raw_input 应该有帮助。
for i in range(3):
print ("So much work to do!")
k = raw_input("Press any key to continue...")
print ("Ok, back to work.")
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件