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

Python 在tkinter中中断嵌入式pygame会跳过KEYUP事件,并认为该键仍被按下,python,tkinter,pygame,Python,Tkinter,Pygame,在这段代码中,我按住shift键将屏幕变成绿色。如果pygame焦点在按住“shift”时被中断,它将跳过键控事件,pygame将继续认为正在按住“shift”。模拟KEYUP事件不起作用。我找到的唯一修复方法是手动按下并释放“shift”,但我不希望用户必须这样做 要演示,请运行代码并按住Shift键,同时按住Enter键打开对话框。然后释放“Shift”,然后退出对话框。即使未按住“Shift”,绿色屏幕仍将保持 如果在将“Embedded_pygame_”和“showing_the_the

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

要演示,请运行代码并按住Shift键,同时按住Enter键打开对话框。然后释放“Shift”,然后退出对话框。即使未按住“Shift”,绿色屏幕仍将保持

如果在将“Embedded_pygame_”和“showing_the_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()
解释 Pygame的窗口

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

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

Tkinter的窗口

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

Tkinter窗口内的Pygame

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

os.environ['SDL\u WINDOWID']=str(embed\u frame.winfo\u id())
SDL\u键盘事件
现在使用
embed\u frame
windowID
。这意味着pygame捕获的所有键盘事件都将来自tkinter。这就是为什么tkinter内部的pygame在tkinter的窗口失去焦点时没有额外的
KEYUP
事件,因为它在使用自己的窗口时有
KEYUP
事件

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

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

将tkinter作为tk导入
导入pygame
导入操作系统
从tkinter.simpledialog导入askstring
从pynput导入键盘
on_enter=False
def on_按(键):
#使用全局“on_enter”变量
全球登录
#如果key的名称是单个字母('a','1',等),则使用'key.char',
#否则('shift'、'enter'等)将使用'key.name'
尝试:
key=key.char
除属性错误外:
key=key.name
#当“shift”向下时,将颜色设置为深*绿色
如果键=移位:
屏幕填充((50205,50))
#当“回车”向下设置颜色为白色时,运行“askstring”
elif键=='enter':
屏幕填充((255、255、255))
on_enter=True
def on_释放(钥匙):
#如果key的名称是单个字母('a','1',等),则使用'key.char',
#否则('shift'、'enter'等)将使用'key.name'
尝试:
key=key.char
除属性错误外:
key=key.name
#当“shift”设置为上时,将颜色设置为白色
如果键=移位:
屏幕填充((255、255、255))
root=tk.tk()
根几何(“200x100”)
嵌入\u pygame\u和\u显示\u功能=True
如果嵌入\u pygame\u和\u显示\u功能:
嵌入框架=传统框架(根)
embed_frame.pack(fill='both',expand=True)
os.environ['SDL\u WINDOWID']=str(embed\u frame.winfo\u id())
os.environ['SDL\u VIDEODRIVER']='windib'
pygame.init()
screen=pygame.display.set_模式((200100))
屏幕填充((255、255、255))
#当检测到按键按下时,运行“on_press”,
#当检测到钥匙打开时,运行“on_release”
监听器=键盘。监听器(按下=按下,释放=释放)
listener.start()
尽管如此:
#on_enter==仅当“enter”关闭时才为真
如果启用,请输入:
#此显示翻转作为键盘是必需的。侦听器运行
#在一个单独的线程中—“on_enter”可以在任意点更改
#在while循环中
pygame.display.flip()
askstring(“”,,,parent=root)
on_enter=False
root.update()
pygame.display.flip()

我尝试了pynput路径,它通常是有效的,但似乎有时会错过事件。