在Python中轮询键盘(检测按键)

78

我怎样从控制台Python应用程序中轮询键盘?具体而言,在许多其他I/O活动(套接字选择,串口访问等)的中途,我想执行类似于以下操作:

while True:
    # doing amazing pythonic embedded stuff
    # ...

    # periodically do a non-blocking check to see if
    # we are being told to do something else
    x = keyboard.read(1000, timeout = 0)

    if len(x):
        # ok, some key got pressed
        # do something

在Windows上进行这项任务的正确Python方式是什么?同时,能够在Linux上实现也不错,但并非必须。


1
只是想让其他人知道,我发现大多数涉及选择或线程库的解决方案在IDLE中无法正常工作。 但是,在CLI上(即“python / home / pi / poll_keyboard.py”)它们都可以正常工作。 - davidhood2
总的来说,我认为对按键的反应而不是定期轮询它们是更健壮的解决方案,因为您不会潜在地错过按键。请参见下面的答案。 - ilon
12个回答

38

标准方法是使用select模块。

然而,在Windows上无法使用此方法。因此,可以使用msvcrt模块的键盘轮询。

通常,这是通过多个线程完成的--每个被“监视”的设备一个线程,以及可能需要被设备中断的后台进程。


1
请纠正我如果我错了,但根据我的经验,msvcrt仅在您在命令窗口中运行程序时起作用,也就是说,在IDLE和GUI中不起作用... - DarthVlader
@digitalHamster0:任何用自定义对象(例如IDLE,大多数GUI)替换sys.stdin的东西都会产生这种效果。当sys.stdin不是真正的文件时,您无法使用select;当它未连接到“真实”控制台时,您无法使用msvcrt键盘轮询函数(这些函数隐式依赖于“真实”控制台)。 - ShadowRanger
一般而言,我认为对按键的反应而非定期轮询它们是更稳健的解决方案,因为您不会错过按键。请参见下面的答案。 - ilon
如果您能提供一个最小的可工作示例,那将非常好。链接未提供足够的信息。 - Danyal

23

使用curses模块的解决方案。打印与每个按键对应的数值:

import curses

def main(stdscr):
    # do not wait for input when calling getch
    stdscr.nodelay(1)
    while True:
        # get keyboard input, returns -1 if none available
        c = stdscr.getch()
        if c != -1:
            # print numeric value
            stdscr.addstr(str(c) + ' ')
            stdscr.refresh()
            # return curser to start position
            stdscr.move(0, 0)

if __name__ == '__main__':
    curses.wrapper(main)

OZ123:可以。请参见https://dev59.com/71wY5IYBdhLWcg3wno0r。 - Joshua Clayton
1
在无头主机上使用SSH终端时遇到了curses的问题。问题非常严重,会导致终端被“reset”以便每次运行。虽然它确实可以工作,例如检测按键操作,但一定有更智能的解决方案。 - Mark

18

对我来说,这些答案都不太好用。pynput这个包正好满足了我的需求。

https://pypi.python.org/pypi/pynput

from pynput.keyboard import Key, Listener

def on_press(key):
    print('{0} pressed'.format(
        key))

def on_release(key):
    print('{0} release'.format(
        key))
    if key == Key.esc:
        # Stop listener
        return False

# Collect events until released
with Listener(
        on_press=on_press,
        on_release=on_release) as listener:
    listener.join()

3
这对我有效,但是按键被按下后立即回显到屏幕上,没有办法禁用它。此外,字符被缓冲并在程序退出时在命令行上显示。这似乎是Linux实现的限制,但在Windows上运行得很好。 - Trevor
当脚本通过ssh运行时,此解决方案无法工作。它会出现错误:'Xlib.error.DisplayNameError: Bad display name "".' - David Stein
正如David所提到的那样,这不是无头实例的好解决方案,因为它依赖于Xserver。import Xlib.display - Mark

17

好的,由于我的尝试在评论中发布我的解决方案失败了,这是我想要说的。我可以使用以下代码从本机Python(在Windows上,不是其他任何地方)完全做到我想要的:

import msvcrt 

def kbfunc(): 
   x = msvcrt.kbhit()
   if x: 
      ret = ord(msvcrt.getch()) 
   else: 
      ret = 0 
   return ret

16
import sys
import select

def heardEnter():
    i,o,e = select.select([sys.stdin],[],[],0.0001)
    for s in i:
        if s == sys.stdin:
            input = sys.stdin.readline()
            return True
    return False

1
我听说过不止一次,在MS Windows上,select系统调用不支持常规文件描述符,只能在套接字上工作。(我不知道Python实现的select()是否曾经在底层解决了这个问题)。 - Jim Dennis
这个奇怪的超时时间是什么意思?使用timeout=0对我有效,但是使用0.0001就不行了。 - frans
3
对我来说,只有在我按下回车键后才会检测到按键。 - Mark Smith
1
@MarkSmith:这是因为程序在接收输入之前,直到按下回车或控制-D(*)键时,它仍然在内核的“行编辑缓冲区”中。 (如果在缓冲区中没有字符,则按下控制-D将关闭终端。)// 为了使此功能在类Unix系统上正常工作,终端必须设置为“原始”或“cbreak”模式,而不是“熟悉”的模式。我认为这是通过stdin上的某些ioctl完成的。 - Oskar Skog
1
@JonathanHartley:(请参阅我之前的评论。) - Oskar Skog
显示剩余6条评论

6

根据评论:

import msvcrt # built-in module

def kbfunc():
    return ord(msvcrt.getch()) if msvcrt.kbhit() else 0

感谢帮助。我最终编写了一个名为PyKeyboardAccess.dll的C DLL,并访问了crt conio函数,导出了这个例程:

#include <conio.h>

int kb_inkey () {
   int rc;
   int key;

   key = _kbhit();

   if (key == 0) {
      rc = 0;
   } else {
      rc = _getch();
   }

   return rc;
}

我可以使用 Python 中内置的 ctypes 模块来访问它:

import ctypes
import time

# first, load the DLL
try:
    kblib = ctypes.CDLL("PyKeyboardAccess.dll")
except:
    raise ("Error Loading PyKeyboardAccess.dll")

# now, find our function
try:
    kbfunc = kblib.kb_inkey
except:
    raise ("Could not find the kb_inkey function in the dll!")

# Ok, now let's demo the capability  
while True:
    x = kbfunc()

    if x != 0:
        print "Got key: %d" % x
    else:
        time.sleep(.01)

2
这个有什么比内置的msvcrt.kbhit()更好的地方吗?它有什么优势? - S.Lott
你说得完全正确!我误读了你的帖子;我没有意识到有一个名为msvcrt的Python模块!我只是认为你的意思是“使用ms crt”,然后我被线程问题吸引了,没有联系起来。你说得完全正确。 - K. Brafford
1
我用以下代码实现了同样的功能:import msvcrtdef kbfunc(): x = msvcrt.kbhit() if x: ret = ord(msvcrt.getch()) else: ret = 0 return ret - K. Brafford
1
请不要使用这样的lambda表达式。"x = lambda" 应该写成 "def x():" 保存一个lambda表达式会让新手感到困惑,同时也会让有经验的人为了解释而发疯。 - S.Lott
哈哈!那不是一个 lambda 表达式。那只是“注释”字段重新格式化了我尝试将代码放入注释中的尝试。顺便说一下,保存 lambda 表达式也会让我困惑,虽然我不是 Python 新手 :-) - K. Brafford
这个解决方案真的让我很感兴趣。我发现即使在“优化”的微型循环中,msvcrt方法也很慢,正如此处提供的许多解决方案一样。关于此,请参阅链接。虽然这个解决方案不是很可移植。 - DevPlayer

5
我发现了一个跨平台实现kbhit的方法,网址是http://home.wlu.edu/~levys/software/kbhit.py(已编辑去除不相关的代码):
import os
if os.name == 'nt':
    import msvcrt
else:
    import sys, select

def kbhit():
    ''' Returns True if a keypress is waiting to be read in stdin, False otherwise.
    '''
    if os.name == 'nt':
        return msvcrt.kbhit()
    else:
        dr,dw,de = select.select([sys.stdin], [], [], 0)
        return dr != []

请确保使用read()读取等待的字符 - 直到您这样做, 函数才会返回True!


这还是最新的吗?当我调用选择版本时,我总是得到 dr 中的内容。如果它仍然有效,你能把它放在上下文中吗?我有一个“while true”循环,如果按下键,我想退出。 - Mastiff
@Mastiff也许你没有像建议的那样read()等待检测到的字符。 - ivan_pozdeev
@ivan_pozdeev 你从哪里read()这个字符? - Thomas Browne
1
@ThomasBrowne 标准输入 - ivan_pozdeev

4

我正在使用这个来检测按键,它不能更简单了:

#!/usr/bin/python3
# -*- coding: UTF-8 -*-

import curses, time

def main(stdscr):
    """checking for keypress"""
    stdscr.nodelay(True)  # do not wait for input when calling getch
    return stdscr.getch()

while True:
    print("key:", curses.wrapper(main)) # prints: 'key: 97' for 'a' pressed
                                        # '-1' on no presses
    time.sleep(1)

虽然 curses 在 Windows 上无法使用,但有一个名为“unicurses”的版本,据说可以在 Linux、Windows 和 Mac 上使用,但我无法使其正常工作。


PyPI上还有windows-curses - Oskar Skog

4

你可以查看pygame是如何处理这个问题的,以获取一些灵感。


3
PyGame事件处理只适用于GUI,而非提问者所问的控制台。 - Ricardo Magalhães Cruz

2

还有一种选择是使用sshkeyboard库来启用对按键的反应,而不是定期轮询它们,从而可能会错过按键操作:

from sshkeyboard import listen_keyboard, stop_listening

def press(key):
    print(f"'{key}' pressed")
    if key == "z":
        stop_listening()

listen_keyboard(on_press=press)

只需执行pip install sshkeyboard即可使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接