在Linux机器上,检测Python 3中按键的最简单方法是什么?

18

现在我正在尝试使用树莓派和makey makey制作一个小代码。makey makey是一个小板子,当某些接触点通电时,它会充当USB键盘。我的问题是,在Python脚本中检测这些按键的最简单方法是什么?我知道使用GPIO引脚会更容易,但现在我正在寻找其他方法。我已经看到了一些示例,如使用msvcrt的getch()(据我所知,仅适用于Windows),使用pygame.key和使用getKey。其中哪个最容易使用?有没有可以检测按下和释放按键的方法?

伪代码:

import whatever needs importing    

if the "W" key is pressed:
   print ("You pressed W")

elif the "S" is pressed:
    print ("You pressed S")

等等其他的。谢谢。


你可以查看带有按键绑定的图形界面,参见tk:https://docs.python.org/3/library/tk.html - LittleByBlue
3个回答

25

这是一个简单的循环,将标准输入设置为原始模式(禁用缓冲,这样您就不必按回车键),以获取单个字符。您应该做一些更明智的事情(比如使用with语句来禁用它),但是您可以从这里得到灵感:

import tty
import sys
import termios

orig_settings = termios.tcgetattr(sys.stdin)

tty.setcbreak(sys.stdin)
x = 0
while x != chr(27): # ESC
    x=sys.stdin.read(1)[0]
    print("You pressed", x)

termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings)    

我认为你需要循环来检测Python中的键松开。

稍微解释一下:

在Linux上,输入到程序中将是行缓冲的。这意味着操作系统会缓存输入直到收集到整行,所以你的程序甚至不会看到用户输入的任何东西,直到用户也按下“回车”键。换句话说,如果你的程序期望用户键入'w',而用户确实这样做了,'w'将一直停留在操作系统的缓冲区中,直到用户按下“回车”键。此时整个行被传递给你的程序,因此你将获得字符串"w\n"作为用户的输入。

你可以通过将tty置于原始模式来禁用此功能。您可以使用Python函数tty.setcbreak来实现这一点,该函数会向linux中的tty驱动程序发出调用以告诉它停止缓冲。我将其传递给了sys.stdin参数,以告诉它我要为其关闭缓冲的流1。因此,在调用tty.setcbreak之后,上面的循环将为用户按下的每个键提供输出。

但是,一个问题是,一旦您的程序退出,tty仍处于原始模式。您通常会发现这很不满意,因为您没有使用现代终端设置所提供的任何功能(例如使用控制或转义序列时)。例如,请注意您可能会在使用ctrl-C退出程序时遇到麻烦。因此,在完成读取输入字符后,应将终端恢复为熟悉的模式。调用termios.tcsetattr只是说“将终端恢复为我找到的方式”。它通过在程序开始时首先调用termios.tcgetattr来了解“告诉我终端的所有当前设置”来进行此操作。

一旦您理解了所有这些,您应该可以轻松地将该功能封装在适合您的程序的函数中。

1stdin是用户输入到您手中的流。维基百科可以告诉您更多关于标准流的信息。


1
我遇到了一个问题,即循环内的 print 语句没有返回到行的开头。在“熟悉模式”和“cbreak模式”中, \n 被转换为 \r\n。 在“raw”模式下,这种转换不会发生。请参见此答案以获取更多详细信息:[https://dev59.com/QGct5IYBdhLWcg3wPbMm#43929760]。TLDR;请使用 cbreak 替代 setraw - Fauzan
只是对于将来的用户而言的一个附加说明。Python 自带 curses 模块,预装在 Linux 上,它具有清除任何标志的好处 - 可以冻结终端窗口的任何活动。 - Danilo

18

使用一个好的轻量级模块 curtsies,你可以像下面这样做(摘自它们的 examples/ 目录):

from curtsies import Input

def main():
    with Input(keynames='curses') as input_generator:
        for e in input_generator:
            print(repr(e))

if __name__ == '__main__':
    main()

在键盘上按下按键会得到类似这样的内容:

'a'
's'
'KEY_F(1)'
'KEY_F(2)'
'KEY_F(3)'
'KEY_F(4)'
'KEY_F(5)'
'KEY_LEFT'
'KEY_DOWN'
'KEY_UP'
'KEY_RIGHT'
'KEY_NPAGE'
'\n'

curtsies被bpython用作与终端相关的低级抽象。解码输入的基本问题在于不同的终端和终端仿真程序(如xtermgnome-terminals)产生不同的键码序列,即使是物理相同的按键也会如此。这就是为什么需要知道应该使用哪些终端设置来解码输入。这样的模块有助于抽象出这些恶心的细节。

这个解决方案对我来说是最好的,因为它处理了“KEY_UP”(以及其他一些),而被接受的答案则没有。 - vvvvv
有没有办法同时检测多个按键的按下? - Ebenezer Isaac
你需要检测哪种多键按键?普通的“多键”比如'Shift'+'l'或者'Alt'+'b'会被转换成它们对应的序列(例如上面的例子中'L'或'<Esc>b'),并且该库将这些序列返回给调用者。如果你需要检测更复杂的序列,例如'Ctrl'+'Shift'+'q',那么这个库可能不是合适的工具。这些序列应该在X Window层面捕获(尝试使用xev命令查看X Window系统输入事件)。 - user3159253
一个非常易于使用的库。可以检测多个按键和单个普通按键。完全推荐使用,而不是cursespynput等。不需要X服务器或“root”权限。 - famzah

2

由于您的问题说明您正在使用树莓派和USB HID键盘外设,但未指定您是否已将Pi配置为启动到文本或图形模式,在那里运行您的脚本,我建议使用libinput,它可以在任何情况下工作。

您可以使用libinput的Python绑定直接从内核读取键盘(以及大多数其他输入设备)事件。

pip3 install python-libinput

这个子系统的接口通过字符设备暴露,通常存储在/dev/input/中。它们由udev规则管理,每个连接的输入设备都会创建一个或多个字符设备,并在USB键盘连接或拔出、蓝牙鼠标连接或断开等情况下动态添加和删除。
Libinput为您处理打开和读取所有已连接输入设备的任务,并在添加和删除设备时分别打开和关闭设备。
使用Python从libinput读取按键事件的示例如下:
import libinput

def read_key_events():
    # init libinput
    li = libinput.LibInput(udev=True)
    li.udev_assign_seat('seat0')
    
    # loop which reads events
    for event in li.get_event():
    
        # test the event.type to filter out only keyboard events 
        if event.type == libinput.constant.Event.KEYBOARD_KEY:
        
            # get the details of the keyboard event
            kbev = event.get_keyboard_event()
            kcode = kbev.get_key() # constants in  libinput.define.Key.KEY_xxx
            kstate = kbev.get_key_state() # constants   libinput.constant.KeyState.PRESSED or .RELEASED 
            
            # your key handling will look something like this...
            if kstate == libinput.constant.KeyState.PRESSED:
                print(f"Key {kcode} pressed") 
                
            elif kstate == libinput.constant.KeyState.RELEASED:
                
                if kbev.get_key() == libinput.define.Key.KEY_ENTER:
                    print("Enter key released")
                    
                elif kcode == libinput.define.Key.KEY_SPACE:
                    print("Space bar released")
                else:
                    print(f"Key {kcode} released")

需要注意的一个小问题是,udev通常被配置为在/dev/input/中创建的事件设备上设置权限,只允许属于特殊附加组“input”的用户访问,因为允许不受限制地访问原始用户键盘和鼠标输入将是一个重大的安全漏洞。因此,如果在libinput初始化期间运行此命令时出现错误,则可能需要通过运行以下命令将input添加到您的用户附加组中:

sudo usermod -G input -a "${USERNAME}"

听起来这个代码很值得新建一个用户,否则你基本上让任何程序都可以查看你的所有按键记录,对吧? - Ben Thayer
OSError: libinput.so.10:无法打开共享对象文件:没有那个文件或目录。 - CS QGB
属性错误: 模块'libinput'没有'constant'属性。 - CS QGB
libinput python3: ../src/util-list.c:72: list_remove: 断言 `(elm->next != NULL && elm->prev != NULL) || !"list->next|prev is NULL, possibly missing list_init()"' 失败. 已中止 - CS QGB

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