在 tkinter 中中断嵌入式 pygame 会跳过 KEYUP 事件并认为该键仍被按下

Noa*_*oah 4 python pygame tkinter

在这段代码中,我按住 'shift' 将屏幕变为绿色。如果在按住 'shift' 时 pygame 焦点被中断,它会跳过 KEYUP 事件,并且 pygame 继续认为正在按住 'shift'。模拟 KEYUP 事件不起作用。我发现的唯一解决方法是手动按下和释放“shift”,但我不希望用户必须这样做。

为了演示,运行代码并按住“Shift”,然后按住“Enter”打开一个对话框。然后松开“Shift”,然后退出对话框。即使没有按住“Shift”键,绿屏也会保留。

如果在将 'embedding_pygame_and_showing_the_bug' 设置为 False 后再次运行代码,您将看到 KEYUP 事件没有被跳过。

import tkinter as tk
import pygame
import os
from tkinter.simpledialog import askstring

root = tk.Tk()
root.geometry("200x100")

embedding_pygame_and_showing_the_bug = True
if embedding_pygame_and_showing_the_bug:
    embed_frame = tk.Frame(root)
    embed_frame.pack(fill='both', expand=True)
    os.environ['SDL_WINDOWID'] = str(embed_frame.winfo_id())
    os.environ['SDL_VIDEODRIVER'] = 'windib'

pygame.init()
screen = pygame.display.set_mode((200, 100))
pygame.event.set_blocked([pygame.MOUSEMOTION, pygame.ACTIVEEVENT])
while True:
    root.update()
    for event in pygame.event.get():
        print(event)
        screen.fill((255, 255, 255))
        if pygame.key.get_mods() & pygame.KMOD_SHIFT:
            screen.fill((50, 205, 50))

        if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
            askstring(' ', ' ', parent=root)
            # simulating a KEYUP does not convince pygame think that shift is not being pressed
            pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key': 304, 'mod': 0, 'scancode': 42, 'window': None}))
            pygame.event.pump()

        pygame.display.flip()
Run Code Online (Sandbox Code Playgroud)

Nem*_*rić 9

解释

Pygame 的窗口

您已经注意到,KEYUP每当 pygame 的窗口失去焦点时,就会发送一个事件。这样做的原因是 pygame 的关键事件在很大程度上是 SDL 关键事件的包装器(SDL 是一个用 C 编写的库,可以对许多不同的组件进行低级访问,其中一个是图形硬件),如果您查看SDL_KeyboardEvent的数据字段,可以看到有一个数据字段被调用windowID

windowID数据字段是负责保持从其中它抓住键盘输入窗口的窗口ID -仅当窗口处于聚焦另有窗口具有在键盘输入的任何信息。作为一种保护措施,KEYUP每当 中指定的窗口windowID失去焦点时,SDL 的窗口会自动发送一个人工事件(这反过来使 pygame 也发送KEYUP事件)。另一件要注意的事情是,KEYDOWN只要按住某个键,操作系统就会向窗口发送多个事件,但 SDL 会自动忽略KEYDOWN除第一个事件之外的所有事件。

Tkinter 的窗口

Tkinter 的窗口与任何其他窗口一样,也从操作系统获取键盘输入——但原始输入。这就是为什么当您按住某个键一段时间时,tkinter 的窗口会显示KeyPress它从操作系统获取的所有事件。当 tkinter 的窗口失去焦点时,它不会发送任何KeyRelease事件,因为它没有从操作系统获得任何事件(与 SDL 的窗口不同,其中KEYUP事件是人工生成的)。

Tkinter 窗口内的 Pygame

当 pygame 使用 tkinter 的窗口时,它无法捕获操作系统的键盘输入——只能捕获来自 tkinter 的键盘事件。这样做的原因是以下代码行:

os.environ['SDL_WINDOWID'] = str(embed_frame.winfo_id())
Run Code Online (Sandbox Code Playgroud)

SDL_KeyboardEvent现在使用embed_framewindowID. 这意味着 pygame 捕获的所有键盘事件都将来自 tkinter。这就是为什么KEYUP当 tkinter 的窗口失去焦点时,tkinter 内部的 pygame 没有额外事件的原因,因为它KEYUP在使用自己的窗口时有事件。

解决方案

除非您愿意编辑和编译 pygame、tkinter 或 SDL,否则无法使用 pygame 的默认事件队列来解决此问题。但是您仍然可以使用自定义事件处理程序或编写自己的事件处理程序并解决此问题。

对于这个例子,只要有一个键监听器就足够了(解决方案使用pynput):

import tkinter as tk
import pygame
import os
from tkinter.simpledialog import askstring
from pynput import keyboard


on_enter = False


def on_press(key):
    # Uses global 'on_enter' variable
    global on_enter

    # If key's name is a single letter ('a', '1', etc.) then 'key.char' is used,
    # otherwise ('shift', 'enter', etc.) 'key.name' is used
    try:
        key_ = key.char
    except AttributeError:
        key_ = key.name

    # When 'shift' is down - set color to dark* green
    if key_ == 'shift':
        screen.fill((50, 205, 50))
    # When 'enter' is down - set color to white, run 'askstring'
    elif key_ == 'enter':
        screen.fill((255, 255, 255))
        on_enter = True

def on_release(key):
    # If key's name is a single letter ('a', '1', etc.) then 'key.char' is used,
    # otherwise ('shift', 'enter', etc.) 'key.name' is used
    try:
        key_ = key.char
    except AttributeError:
        key_ = key.name

    # When 'shift' is up - set color to white
    if key_ == 'shift':
        screen.fill((255, 255, 255))


root = tk.Tk()
root.geometry("200x100")

embedding_pygame_and_showing_the_feature = True
if embedding_pygame_and_showing_the_feature:
    embed_frame = tk.Frame(root)
    embed_frame.pack(fill='both', expand=True)
    os.environ['SDL_WINDOWID'] = str(embed_frame.winfo_id())
    os.environ['SDL_VIDEODRIVER'] = 'windib'

pygame.init()
screen = pygame.display.set_mode((200, 100))
screen.fill((255, 255, 255))

# Runs 'on_press' when it detects key down,
# runs 'on_release' when it detects key up
listener = keyboard.Listener(on_press=on_press, on_release=on_release)
listener.start()

while True:
    # on_enter == True only when 'enter' is down
    if on_enter:
        # This display flip is necessary as keyboard.Listener runs
        # in a separate thread - 'on_enter' can change at any point
        # in the while-loop
        pygame.display.flip()

        askstring(' ', ' ', parent=root)
        on_enter = False

    root.update()
    pygame.display.flip()

Run Code Online (Sandbox Code Playgroud)