将按键发送到嵌入式 Pygame

Dispatching keypresses to embedded Pygame

我一直在努力创建一些我将来可以使用的代码,以便在 tkinter window 中嵌入 pygame window 以便使用 tkinter 菜单和按钮。我目前在处理按键方面遇到一些问题。我希望所有按键都由 pygame 而不是 tkinter 处理,这样如果 pygame 元素全屏显示(因此意味着不使用 tkinter),那么 tkinter 键绑定将被忽略。

我的问题是,当 window 最初打开时(或者在它被点击关闭并再次打开之后),只有 tkinter 正在注册键绑定。一旦用户点击 pygame window,只有 pygame 注册键绑定。我的问题是如何检测 tkinter 或 pygame 是否正在检测按键,以及如何做到 pygame 在我检测到按键时检测按键而不是 tkinter?

我的代码在下面(对不起,它很长)

import pygame, os, _tkinter, sys
try:
    import Tkinter as tk
    BOTH,LEFT,RIGHT,TOP,BOTTOM,X,Y = tk.BOTH,tk.LEFT,tk.RIGHT,tk.TOP,tk.BOTTOM,tk.X,tk.Y
    two = True
except ImportError:
    import tkinter as tk
    from tkinter.constants import *
    two = False
from pygame.locals import *

class PygameWindow(tk.Frame):
    """ Object for creating a pygame window embedded within a tkinter window.

        Please note: Because pygame only supports a single window, if more than one
        instance of this class is created then updating the screen on one will update
        all of the windows.
    """
    def __init__(self, pygame_size, pygame_side, master=None, **kwargs):
        """
            Parameters:
            pygame_size - tuple - The initial size of the pygame screen
            pygame_side - string - A direction to pack the pygame window to
            master - The window's master (often a tk.Tk() instance
            pygame_minsize - tuple - The minimum size of the pygame window.
                If none is specified no restrictions are placed
            pygame_maxsize - tuple - The maximum size of the pygame window.
                If none is specified no restrictions are placed.
                Note: This includes the pygame screen even when fullscreen.
            tkwin - string - A direction to pack a tkinter frame to designed to be
                used to contain widgets to interact with the pygame window.
                If none is specified the frame is not included.
            fullscreen - boolean - Whether fullscreen should be allowed
            menu - boolean - Whether a menu bar should be included.
                the menu bar contains a File menu with quit option and an Options
                menu with fullscreen option (If enabled)
        """
        # I have decided to use a global variable here because pygame only supports a single screen,
        # this should limit confusion if multiple instances of the class are created because each
        # instance will always contain the same screen. A global variable should hopefully make this
        # clearer than screen being a seperate attribute for each instance
        global screen
        self.master = master
        self.fullscreen = tk.BooleanVar(value=False)
        if two:
            tk.Frame.__init__(self,master)
        else:
            super().__init__(self,master)
        self.pack(fill=BOTH,expand=1)

        if 'pygame_minsize' in kwargs:
            w,h = kwargs['pygame_minsize']
            master.minsize(w,h)
            del kwargs['pygame_minsize']

        w,h = pygame_size
        self.embed = tk.Frame(self, width = w, height = h)
        self.embed.pack(side=pygame_side,fill=BOTH,expand=1)

        if 'tkwin' in kwargs:
            if kwargs['tkwin'] != None:
                self.tk_frame = tk.Frame(self,bg='purple')
                if kwargs['tkwin'] in [TOP,BOTTOM]:
                    self.tk_frame.pack(side=kwargs['tkwin'],fill=X)
                elif kwargs['tkwin'] in [LEFT,RIGHT]:
                    self.tk_frame.pack(side=kwargs['tkwin'],fill=Y)
                else:
                    raise ValueError('Invalid value for tkwin: "%r"' %kwargs['tkwin'])
            del kwargs['tkwin']

        if 'fullscreen' in kwargs:
            if kwargs['fullscreen']:
                self.fs_okay = True
            else:
                self.fs_okay = False
        else:
            self.fs_okay = False

        os.environ['SDL_WINDOWID'] = str(self.embed.winfo_id())
        if sys.platform == "win32":
            os.environ['SDL_VIDEODRIVER'] = 'windib'
        pygame.display.init()

        if 'pygame_maxsize' in kwargs:
            w,h = kwargs['pygame_maxsize']
            self.pygame_maxsize = (w,h)
            screen = pygame.display.set_mode((w,h),RESIZABLE)
            del kwargs['pygame_maxsize']
        else:
            screen = pygame.display.set_mode((0,0),RESIZABLE)
            self.pygame_maxsize = (0,0)
        screen.fill((255,255,255))

        if 'menu' in kwargs:
            if kwargs['menu']:
                self.menubar = tk.Menu(self.master)
                self.master.config(menu=self.menubar)

                self.filemenu = tk.Menu(self.menubar,tearoff=0)
                self.filemenu.add_command(label='Quit',command=self.close,accelerator='Ctrl+Q')
                self.menubar.add_cascade(label='File',menu=self.filemenu)

                self.optionmenu = tk.Menu(self.menubar,tearoff=0)
                if self.fs_okay:
                    self.optionmenu.add_checkbutton(label='Fullscreen',command=self.updatefs,variable=self.fullscreen,accelerator='F11')
                self.menubar.add_cascade(label='Options',menu=self.optionmenu)

    def update(self):
        """ Update the both the contents of the pygame screen and
            the tkinter window. This should be called every frame.
        """
        pressed = pygame.key.get_pressed()
        if self.fullscreen.get():
            if pressed[K_ESCAPE] or pressed[K_F11] or not pygame.display.get_active():
                self.togglefs()
        else:
            if pressed[K_q] and (pressed[K_LCTRL] or pressed[K_RCTRL]):
                self.close()
            for event in pygame.event.get(KEYDOWN):
                if event.key == K_F11:
                    self.togglefs()
                pygame.event.post(event)
        pygame.event.pump()
        pygame.display.flip()
        self.master.update()

    def close(self,*args):
        """ Closes the open window."""
        self.master.destroy()

    def togglefs(self,*args):
        """Toggles the self.fullscreen variable and then calls
            the updatefs function.
        """
        self.fullscreen.set(not self.fullscreen.get())
        self.updatefs()

    def updatefs(self):
        """Updates whether the window is fullscreen mode or not
            dependent on the value of the fullscreen attribute.
        """
        if not self.fs_okay:
            self.fullscreen.set(False)
        global screen
        tmp = screen.convert()
        cursor = pygame.mouse.get_cursor()
        flags = screen.get_flags()
        bits = screen.get_bitsize()

        if self.fullscreen.get():
            pygame.display.quit()
            del os.environ['SDL_WINDOWID']
            if sys.platform == "win32":
                del os.environ['SDL_VIDEODRIVER']
            pygame.display.init()
            screen = pygame.display.set_mode(self.pygame_maxsize,FULLSCREEN|(flags&~RESIZABLE),bits)
        else:
            pygame.display.quit()
            os.environ['SDL_WINDOWID'] = str(self.embed.winfo_id())
            if sys.platform == "win32":
                os.environ['SDL_VIDEODRIVER'] = 'windib'
            pygame.display.init()
            screen = pygame.display.set_mode(self.pygame_maxsize,RESIZABLE|(flags&~FULLSCREEN),bits)
        screen.blit(tmp,(0,0))
        pygame.mouse.set_cursor(*cursor)


class TestWindow(PygameWindow):
    def __init__(self, pygame_size, pygame_side, master=None, **kwargs):
        if two:
            PygameWindow.__init__(self,pygame_size, pygame_side, master=master, **kwargs)
        else:
            super().__init__(self,pygame_size, pygame_side, master=master, **kwargs)
        self.drawn = False
        self.button1 = tk.Button(self.tk_frame,text = 'Draw',  command=self.draw)
        self.button1.pack(side=LEFT)
        screen.fill((255,255,255))
        pygame.display.flip()

    def draw(self):
        if not self.drawn:
            pygame.draw.circle(screen, (0,255,175), (250,250), 125)
        else:
            screen.fill((255,255,255))
        self.drawn = not self.drawn

if __name__ == '__main__':
    root = tk.Tk()
    window = TestWindow((500,500),LEFT,root,pygame_minsize=(500,500),tkwin=LEFT,menu=True,fullscreen=True)

    while True:
        try:
            window.update()
        except _tkinter.TclError:
            break
quit()

如果没有直接的解决方案(我不知道),您可以制作一个处理程序,将在 tkinter 中检测到的按键传递给 pygame。

我们的想法是将键绑定到一个 dispatch_event_to_pygame 函数,这将创建一个相应的 pygame.event.Event object, and to inject the latter into pygame's event loop, through the pygame.event.post 函数。

首先,我定义了一个字典,它建立了我想从 tkinter 分派到 pygame 的键与 pygame:

中相应符号之间的对应关系
tkinter_to_pygame = {
    'Down':     pygame.K_DOWN,
    'Up':       pygame.K_UP,
    'Left':     pygame.K_LEFT,
    'Right':    pygame.K_RIGHT}

然后,我定义了一个 dispatch_event_to_pygame 函数,它接受一个 tkinter 事件,创建一个相应的 pygame 事件,并发布它:

def dispatch_event_to_pygame(tkEvent):
    if tkEvent.keysym in tkinter_to_pygame:
        pgEvent = pygame.event.Event(pygame.KEYDOWN,
                                     {'key': tkinter_to_pygame[tkEvent.keysym]})
        pygame.event.post(pgEvent)

最后,我在根小部件上绑定了我想要分派给 pygame:

的所有键
for key in tkinter_to_pygame:
    root.bind("<{}>".format(key), dispatch_event_to_pygame)

键名参考: