将按键发送到嵌入式 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)
键名参考:
我一直在努力创建一些我将来可以使用的代码,以便在 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)
键名参考: