прерывание встроенного pygame в tkinter пропускает события KEYUP и думает, что клавиша все еще нажата - PullRequest
1 голос
/ 08 апреля 2020

В этом коде, удерживая клавишу «Shift», экран поворачивается зеленым. Если фокус пигмея прерывается при удержании клавиши «shift», он пропускает события 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()

1 Ответ

3 голосов
/ 03 мая 2020

Объяснение

Окно Pygame

Вы уже заметили, что событие KEYUP отправляется всякий раз, когда окно Pygame теряет фокус. Причина этого заключается в том, что ключевые события pygame являются, по большей части, оболочками ключевых событий SDL (SDL - это библиотека, написанная на C с низкоуровневым доступом ко многим различным компонентам, одним из которых является графическое оборудование), и если вы посмотрите на SDL_KeyboardEvent поля данных , вы можете увидеть, что одно поле данных называется windowID:

Поле данных 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()

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...