Объяснение
Окно Pygame
Вы уже заметили, что событие KEYUP
отправляется всякий раз, когда окно Pygame теряет фокус. Причина этого заключается в том, что ключевые события pygame являются, по большей части, оболочками ключевых событий SDL (SDL - это библиотека, написанная на C с низкоуровневым доступом ко многим различным компонентам, одним из которых является графическое оборудование), и если вы посмотрите на SDL_KeyboardEvent
поля данных , вы можете увидеть, что одно поле данных называется windowID
:
![](https://i.stack.imgur.com/4Av5X.png)
Поле данных windowID
отвечает за удержание идентификатора окна, из которого он захватывает ввод с клавиатуры, - только когда окно находится в фокусе, так как в противном случае окно не имеет информации о вводах с клавиатуры. В качестве меры защиты окно SDL автоматически отправляет искусственное событие KEYUP
всякий раз, когда окно, указанное в windowID
, теряет фокус (что, в свою очередь, заставляет pygame также отправлять событие KEYUP
). Следует также отметить, что операционная система отправляет несколько событий KEYDOWN
в окно всякий раз, когда удерживается клавиша, но SDL автоматически игнорирует каждое KEYDOWN
событие, кроме первого.
окно Tkinter
Окно Tkinter, как и любое другое окно, также получает клавиатурные вводы из ОС - но необработанные вводы. Вот почему, когда вы удерживаете клавишу некоторое время, в окне tkinter отображаются все события KeyPress
, которые он получает от ОС. Когда окно tkinter теряет фокус, оно не отправляет событие KeyRelease
, поскольку оно не получало ничего от ОС (в отличие от окна SDL, где событие KEYUP
генерируется искусственно).
Pygame внутри окна Tkinter
Когда pygame использует окно tkinter, оно не может перехватить ввод с клавиатуры операционной системы - только события клавиатуры от tkinter. Причиной этого является следующая строка кода:
os.environ['SDL_WINDOWID'] = str(embed_frame.winfo_id())
SDL_KeyboardEvent
теперь использует embed_frame
windowID
. Это означает, что все клавиатурные события, пойманные пигеймами, будут от tkinter. Именно поэтому у pygame внутри tkinter нет дополнительного события KEYUP
, когда окно tkinter теряет фокус, где оно имеет событие KEYUP
, когда использует свое собственное окно.
Solution
Если вы не хотите редактировать и компилировать 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()