如何在Python控制台中获取键盘事件

6
我希望能够使用Python处理控制台中的键盘事件。正在运行的脚本有一些持久的输出流,当管理员触发按键事件时,脚本将更改其输出内容。
我已经使用以下代码完成了它(按下“q”将触发输出更改),但存在两个问题:
1. 输出中增加了一个空格。经过调试,我发现代码“tty.setraw(fd)”导致了这个问题,但我不知道如何解决它。 2. ctrl+c无法再起作用(如果# "tty.setraw(fd)",ctrl+c将起作用)。
如果这太复杂了,是否还有其他模块可以实现我的需求?我尝试了curse模块,但似乎会冻结窗口输出,并且不能在多线程中协调。
#!/usr/bin/python
import sys
import select
import tty, termios
import threading
import time

def loop():

    while loop_bool:
        if switch:   
            output = 'aaaa'
        else:
            output = 'bbbb'
        print output
        time.sleep(0.2)


def change():
    global switch
    global loop_bool    
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    try: 
        while loop_bool:
            tty.setraw(fd)
            i,o,e = select.select([sys.stdin],[],[],1)
            if len(i)!=0:
                if i[0] == sys.stdin:
                    input = sys.stdin.read(1)

                    if input =='q':
                        if switch:
                            switch = False                                         
                        else: 
                            switch =  True


        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    except KeyboardInterrupt:

        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        loop_bool = False


try:   
    switch = True
    loop_bool = True    
    t1=threading.Thread(target=loop)
    t2=threading.Thread(target=change)        

    t1.start()
    t2.start()

    t1.join(1)
    t2.join(1)
except KeyboardInterrupt:

    loop_bool = False

1
我怀疑这不是你的问题,但是...为什么你每次循环都要调用setraw,但只在最后恢复旧设置呢? - abarnert
每次循环都要设置原始设置,但只在最后恢复旧设置。嗯,那是我的错误...谢谢指出。 - user1675167
1个回答

2
这可能取决于您所在的平台,甚至可能取决于您使用的终端仿真器,我不确定它是否能解决您的问题,但是...
您可以通过关闭“规范模式”来实现逐个字符输入,而无需调用tty.setraw,方法是屏蔽tcgetattr()的lflag属性中的ICANON位。您还可能需要设置VMIN或VTIME属性,但默认值应该已经正确。
有关详细信息,请参见“规范模式和非规范模式”一节Linux man页面,“非规范模式输入处理”一节OS X man页面,或者如果您使用的是其他平台,则参见相应的文档。
最好将其编写为上下文管理器,而不是进行显式清理。特别是因为您现有的代码每次循环都会执行setraw,并且仅在最后恢复;它们应该理想情况下成对出现,并且使用with语句可以保证这一点。(此外,这样您就不需要在except子句和正常流程中重复自己。)因此:
@contextlib.contextmanager
def decanonize(fd):
    old_settings = termios.tcgetattr(fd)
    new_settings = old_settings[:]
    new_settings[3] &= ~termios.ICANON
    termios.tcsetattr(fd, termios.TCSAFLUSH, new_settings)
    yield
    termios.tcsetattr(fd, termios.TCSAFLUSH, old_settings)

现在:

def change():
    global switch
    global loop_bool

    with decanonize(sys.stdin.fileno()):
        try:
            while loop_bool:
                i,o,e = select.select([sys.stdin],[],[],1)
                if i and i[0] == sys.stdin:
                    input = sys.stdin.read(1)                    
                    if input =='q':
                        switch = not switch
        except KeyboardInterrupt:
            loop_bool = False

也许你希望在更低的层级(在while循环内部或至少在try语句内部)使用with块。
(附注:我将你的代码中的几行转换为等效但更简单的形式,以消除几个嵌套级别。)
你的情况可能有所不同,但这是在我的Mac上进行的测试。
Retina:test abarnert$ python termtest.py 
aaaa
aaaa
aaaa
qbbbb
bbbb
bbbb
qaaaa
aaaa
aaaa
^CRetina:test abarnert$

这让我想到您可能想要关闭输入回显(通过new_settings [3]& =〜termios.ECHO实现),这意味着您可能想要用更通用的东西替换decanonize函数,以便临时设置或清除任意termios标志。(此外,如果tcgetattr返回一个namedtuple而不是一个list,那将非常好,这样您就可以使用new_settings.lflag而不是new_settings [3],或者至少为属性索引提供符号常量。)
同时,从您的评论中可以看出,^C仅在开始的一两秒钟内有效,并且与join中的超时有关。这是有道理的-主线程只启动了两个线程,执行了两个join(1)调用,然后完成。因此,在启动后2.x秒后,它完成了所有工作并离开了try:块,因此不再有任何方法触发KeyboardInterrupt并发出loop_bool = False信号,以使工作线程退出。
我不确定为什么首先在join上设置超时,以及超时后应该发生什么,但有三种可能性:
  1. 你不想在按下^C之前退出程序,而且这些超时时间没有任何好处。因此,请将它们去掉。然后主线程将永远等待另外两个线程完成,并且仍然在try块中,因此^C应该能够设置loop_bool = False

  2. 应用程序应该在2秒后正常退出。(我猜你更喜欢对一对线程使用单个join-any或join-all,并带有2秒的超时,但是因为Python没有任何简单的方法来实现这一点,所以你按顺序加入了这些线程。)在这种情况下,你需要在超时结束后立即设置loop_bool = False。因此,只需将except更改为finally即可。

  3. 超时时间总是应该足够充裕(可能这只是你真实应用程序的简化版本),如果你超过了超时时间,那就是一个异常情况。前面的选项可能仍然有效,也可能无效。如果无效,请在这两个线程上设置daemon = True,它们将在主线程完成时被杀死(而不是被要求优雅地关闭)。 (请注意,这种工作方式在Windows和Unix上略有不同,尽管你可能对这个应用程序并不在意Windows。更重要的是,所有文档都说“当没有活着的非守护线程时,整个Python程序将退出”,因此你不应该指望任何守护线程能够进行任何清理,但也不应该指望它们不会进行任何清理。不要在守护线程中执行可能会留下临时文件、未写入关键日志消息等操作。)


谢谢您的快速回答,我已将您的代码合并到我的代码中,空间问题已解决,再次感谢。但是Ctrl+C仍然无法关闭程序,我不得不使用Ctrl+Z来关闭它,这是因为我的平台不兼容吗? - user1675167
根本原因应该是线程中的join()函数,如果将代码更改为t1.join(10),则Ctrl+C仅在10秒内起作用。 - user1675167
new_settings[3] = new_settings[3] & ~termios.ICANON & ~termios.ECHO - user1675167
最后,abarnert,再次非常感谢你。不仅是这个回答,我也从你改变的代码中学到了很多。 - user1675167
@user1675167:很高兴能帮到你。join 阻塞 ^C 的问题似乎是另一个问题。你解决了吗?我会编辑答案并提供一些我脑海中的想法。 - abarnert
显示剩余2条评论

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