Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dispatching keypresses to embedded Pygame

I have been working with creating some code that I can use in future in order to embed a pygame window within a tkinter window in order to make use of tkinter menus and buttons. I am currently having some issues with dealing with key presses. i want all key presses to be dealt with by pygame rather than tkinter so that if the pygame element is made fullscreen (thus meaning tkinter is not used) then tkinter key bindings are left ignored.

My problem is that when the window is initially opened (or after it has been clicked off and back on again), only tkinter is registering key bindings. Once the user clicks on the pygame window, only pygame registers key bindings. My question is how can I detect whether tkinter or pygame is detecting the key presses and also how can I make it so that pygame detects the presses rather than tkinter when I have detected it?

My code is below (sorry it's quite long)

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()
like image 513
Owen683 Avatar asked Sep 05 '17 11:09

Owen683


1 Answers

If there is no direct solution (of which I do not know), you could make a handler that passes the keypresses detected in tkinter to pygame.

The idea is to bind the keys to dispatch to a dispatch_event_to_pygame function, which will create a corresponding pygame.event.Event object, and to inject the latter into pygame's event loop, through the pygame.event.post function.

First, I define a dictionary that establishes the correspondence between the keys I want to dispatch from tkinter to pygame, and the corresponding symbols in pygame:

tkinter_to_pygame = {
    'Down':     pygame.K_DOWN,
    'Up':       pygame.K_UP,
    'Left':     pygame.K_LEFT,
    'Right':    pygame.K_RIGHT}

Then, I define a dispatch_event_to_pygame function, that takes a tkinter event, creates a corresponding pygame event, and posts it:

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)

Finally, I bind on the root widget all the keys that I want to dispatch to pygame:

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

References for the key names:

  • tkinter

  • pygame

like image 183
Right leg Avatar answered Oct 14 '22 06:10

Right leg