将按键发送到嵌入式 Pygame

Posted

技术标签:

【中文标题】将按键发送到嵌入式 Pygame【英文标题】:Dispatching keypresses to embedded Pygame 【发布时间】:2018-02-13 16:06:58 【问题描述】:

我一直在努力创建一些我可以在将来使用的代码,以便在 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】:

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

这个想法是把键绑定到一个dispatch_event_to_pygame函数,该函数将创建一个对应的pygame.event.Event对象,并通过pygame.event.post函数将后者注入到pygame的事件循环中。

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

键名参考:

tkinter

pygame

【讨论】:

你可以在pygame.event.Event 参数中使用pygame.KEYDOWN 鼠标事件吗?我有同样的问题,此外,我还想在我的 pygame(嵌入在 tkinter 中)中点击鼠标。

以上是关于将按键发送到嵌入式 Pygame的主要内容,如果未能解决你的问题,请参考以下文章

如何将按键发送到正在运行的进程对象?

有没有办法使用 Capybara 将按键发送到 Webkit?

使用 Python 将按键发送到不同的窗口

怎样模拟发送key event按键消息和touch event触摸消息?

如何将嵌入式代码发送到服务器端-PHP

你如何告诉 Spring Boot 将嵌入式 Tomcat 的访问日志发送到标准输出?