将按键调度到嵌入式Pygame

时间:2017-09-05 11:17:18

标签: python python-3.x tkinter pygame keypress

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

我的问题是,当窗口最初打开时(或者在单击它​​并再次打开之后),只有tkinter正在注册键绑定。一旦用户点击了pygame窗口,只有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()

1 个答案:

答案 0 :(得分:5)

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

我们的想法是将密钥绑定到dispatch_event_to_pygame函数,这将创建一个相应的pygame.event.Event对象,并通过{将后者注入到pygame的事件循环中。 {3}}功能。

首先,我定义一个字典,用于建立我想从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)

关键名称的引用: