如何拦截在 Python 程序的终端 window 中(且仅在)中按下的键?

How to intercept keys pressed in (and only in) the terminal window of a Python program?

我正在编写一个简单的 Python 3 脚本,我希望能够拦截终端内的按键操作 window 以便根据返回值执行某些操作,例如。
我也想要一个跨平台的解决方案。

我想重现这样的东西:

import msvcrt
key = ord(msvcrt.getch()) # Wait for a key to be pressed.
if key == 27: # The ESC key
    print("You have pressed the ESC key!")

但是根据 Python docs(和我的测试)msvcrt 是一个 Windows 特定的模块:

These functions provide access to some useful capabilities on Windows platforms.

我找到了 keyboard 模块,它使用起来非常简单(并且 more 跨平台)但我没能 "catch" 只有 终端 window 内按下的键 window.

例如:

import keyboard as kb
key = kb.read_hotkey()
if key == "esc": # The ESC key
    print("You have pressed the ESC key!")

上面给出的代码不仅在执行脚本的终端 window 获得焦点时拦截按键,而且在没有获得焦点时也拦截。

因此,总而言之,您是否知道一种 pythonic 方式来拦截执行脚本的终端 window(而不是外部)内的按键(类似于 input() 而无需按 Enter),并且它是跨平台的(至少兼容 GNU/Linux 和 Windows)?

提前感谢您的回答,
问候,
亚历克西斯

这是适用于 Windows 的部分解决方案,也适用于 GNU/Linux。
我注意到在 GNU/Linux(至少在 Debian 9 上)相同的数字被分配给箭头键和 ESC 键。

下面的代码是受题解的启发

# coding: utf8
import sys


def read() -> int:
    if sys.platform == "win32":
        import msvcrt
        key = ord(msvcrt.getch())  # Wait for a key to be pressed.
    elif sys.platform == "linux":
        import tty
        import termios
        try:
            orig_settings = termios.tcgetattr(sys.stdin)
            tty.setcbreak(sys.stdin)
            key = ord(sys.stdin.read(1)[0])
        except KeyboardInterrupt:
            key = 3 # The code for Ctrl+C got on Windows.
        finally:  # To make sure that the terminal will return to its original state.
            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings)
    else:
        raise RuntimeError("Your platform is not supported")
    return key

if read() == 27:  # The ESC key (also the UP-DOWN-RIGHT-LEFT on GNU/Linux)
    print("You have pressed the ESC key!")

此致,
亚历克西斯

看看 curses 模块。它在 python 标准库中,但不支持开箱即用的 windows。 有一个定期维护的项目叫做 "windows-curses" 你可以看看。我没有测试过它,但它支持osedly 允许您在 windows 上使用 python curses 模块。 https://pypi.org/project/windows-curses/

import curses

def listen(window):
    while True:
        key = window.getch()
        window.addstr(f'You pressed the "{key}" key!\n')
        if key == 'q':
            break
        handle_keypress(key)

curses.wrapper(listen)

如果 curses 方法对您不起作用,或者您仍然需要更细粒度,那么您可以相当轻松地推出自己的 cross-platform 方法。你可以尝试这样的事情:

from sys import platform

class Keyboard:
    def __new__(cls):
        if platform in ['Windows', 'win32', 'cygwin']:
            cls = winKeyboard
        elif platform in ['Mac', 'darwin', 'os2', 'os2emx']:
            cls = MacKeyboard
        else:
            raise Exception(f'Unrecognized platform, {platform}')
        return super(Keyboard, cls).__new__(cls)

    def listen(self):
        while True:
            key = self.getch()
            print(f'You pressed the "{key}" key!')
            if key == 27:
                break
            return self.handle_key(key)

class MacKeyboard(Keyboard):
    def getch(self):
        implement_mac_logic()


class WinKeyboard(Keyboard):
    def getch(self):
        implement_win_logic()

keyboard = Keyboard()
keyboard.listen()

Keyboard.__new__ 的工作是在运行时为当前 os.

提供适当的解决方案

无论活动 window.
,此方法仍会注册 key-presses 为此,您需要访问活动的 window,这将是另一个 os-specific 过程。 看看这个:

您可以实现一个检查当前 window

名称的函数
class Keyboard:
    ...
    def listen(self):
        while True:
            if self.get_active_window() != desired_window:
                continue
            key = self.getch()
            print(f'You pressed the "{key}" key!')
            if key == 27:
                break
            return self.handle_key(key)

然后你可以在WinKeyboard.get_active_window和MacKeyboard.get_active_window中实现适当的逻辑 这不会考虑在不同的选项卡中。这可能是possible,但我对api不够熟悉,无法告诉你。

还有 pygame 等选项需要您创建和管理自己的 windows 但会满足您的要求。

编辑:将 WinKeyboard 和 MacKeyboard 更改为从键盘继承。