Despite what is said on some forums, Tkinter, the middle weight python GUI, works quite well in conjunction with pygame, the python SDL layer - at least when pygame is not in full screen mode.
Pygame has been used as the graphics/media engine for many serious application (eg. pyschtoolbox) and I have used it for a number of projects (some may even appear on this web site one day). Pygame is particularly useful as a layer over openGL but is berefet of even a simple menu system and, in my applications twin screen applications, I don't actually want any gui/menu controls display on the pygame window.
In the following examples two windows are created, one pygame and the other tkinter - in a manner similar to gimps multitude of seperate windows.
Initially I shall only use raw tkinter and pygame without any added modules and then I shall demonstrate some of the features of the tkfront module and it's children to simplify the setup.
The following is the simplest hybrid of pygame and Tkinter I could conceive.
import pygame
from pygame.locals import *
import Tkinter
import sys # for exit and arg
def Draw(surf):
#Clear view
surf.fill((80,80,80))
pygame.display.flip()
def GetInput():
for event in pygame.event.get():
if event.type == QUIT:
return True
if event.type == KEYDOWN:
print event
if event.type == MOUSEBUTTONDOWN:
print event
sys.stdout.flush() # get stuff to the console
return False
Done = False
def quit_callback():
global Done
Done = True
def main():
# initialise pygame
pygame.init()
ScreenSize = (200,100)
Surface = pygame.display.set_mode(ScreenSize)
#initialise tkinter
root = Tkinter.Tk()
root.protocol("WM_DELETE_WINDOW", quit_callback)
main_dialog = Tkinter.Frame(root)
main_dialog.pack()
# start pygame clock
clock = pygame.time.Clock()
gameframe = 0
# main loop
while not Done:
try:
main_dialog.update()
except:
print "dialog error"
if GetInput(): # input event can also comes from diaglog
break
Draw(Surface)
clock.tick(100) # slow it to something slightly realistic
gameframe += 1
main_dialog.destroy()
if __name__ == '__main__': main()
Running this should result in two windows appearing:
Since two windows are created, much like Gimp, putting pygame into full screen mode is not really an option with a single screen. On platforms that use a Xwindows server it may be possible to send Tkinter to one screen (or PC) and Pygame to another (if anyone has managed this - please let me know).
For an explanation of the code I would suggest reading:
In this example a menu and key press handler is added to the tkinter window.
import pygame
from copy import *
from pygame.locals import *
import Tkinter, tkMessageBox
import sys # for exit and arg
# initialise
pygame.init()
ScreenSize = (200,100)
pygame.display.set_caption("Pygame Tkinter Demo 1 - Robert Parker (c2009)")
Surface = pygame.display.set_mode(ScreenSize)
testMode = 0
def Draw():
global view_surface, centres
#Clear view
Surface.fill((80,80,80))
pygame.display.flip()
def refreshDraw():
Draw()
def about_callback():
t1 = "Pygame Meets Tkinter by Robert Parker (c)2009"
t2 = "Pygame is python interface to SDL media layer indended for game creation but used for much more (ref. www.pygame.com)"
t3 = "Tkinter is based on Tk GUI toolkit developed by Scriptics (http://www.scriptics.com) (formerly developed by Sun Labs)."
t4 = "Thanks to Fredrik Lundh 'An introduction to Tkinter' (c)1999"
about_text = t1 + "\n" + t2 + "\n"+ t3 + "\n" + t4
tkMessageBox.showinfo("ABOUT",about_text)
def GetInput():
for event in pygame.event.get():
global status_mode, status_str, Done
if event.type == QUIT:
if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
Done = True
#return True
if event.type == KEYDOWN:
print event
print event.key
print event.unicode
status_mode = True
if type(event.unicode) == chr:
status_str = "Key %c[%3d]"%(event.unicode,event.key)
else:
status_str = "Key [%3d]"%(event.key,)
#keystate = pygame.key.get_pressed()
keymod = pygame.key.get_mods()
if event.type == MOUSEBUTTONDOWN:
print event.button
keystate = pygame.key.get_pressed()
return False
def tk_key_event(event):
''' transfer key event in tkinter dialog to pygame event handler'''
print event.keycode, event.keysym, event.keysym_num
trans_dict = { 192:96,107:270,109:269,107:270,109:269,37:276,39:275,38:273,40:274,45:277,36:278,33:280,46:127,35:279,34:281 }
if trans_dict.has_key(event.keycode):
code = trans_dict[event.keycode]
else:
code = event.keycode
pygame.event.post(pygame.event.Event(KEYDOWN,{'key':code,'unicode':event.keysym}))
Done = False
def quit_callback():
global main_dialog, Done
if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
Done = True
def main():
global status_mode, status_str, main_dialog
menu_design = (("Help",
(("About",about_callback),
)
),
)
status_str = "Status"
fps_str = "FPS"
root = Tkinter.Tk()
root.protocol("WM_DELETE_WINDOW", quit_callback)
main_dialog = Tkinter.Frame(root)
root.title("Test dialog")
status_line = Tkinter.Label(main_dialog, text=status_str, bd=1, relief=Tkinter.SUNKEN, anchor=Tkinter.W)
status_line.pack(side=Tkinter.BOTTOM, fill=Tkinter.X)
main_dialog.pack()
main_dialog.bind('',tk_key_event)
menubar = Tkinter.Menu(root)
menubar.add_command(label="About", command=about_callback)
root.config(menu=menubar)
clock = pygame.time.Clock()
# lets get some performance statistics
gameframe = 0
ticks = pygame.time.get_ticks()
status_mode= False # show FPS for the moment
main_dialog_status_str = ""
while not Done:
try:
main_dialog.update()
except:
print "dialog error"
if status_str != main_dialog_status_str:
main_dialog_status_str = status_str
status_line.config(text=status_str) #format % args)
status_line.update_idletasks()
if GetInput(): # input event can also comes from diaglog
break
Draw()
clock.tick(100) # slow it to something slightly realistic
gameframe += 1
# display stats in pygame title bar and in tk window status line
if gameframe % 10 == 0:
t = pygame.time.get_ticks()
fps = (float(10)*1000.0)/(t-ticks)
ticks = t
#rint "TestMode#%d fps: %.2f over %d frames" % (testMode,fps,gameframe)
period = 1.0/fps
fps_str = "%5.1f FPS"%fps
pygame.display.set_caption(fps_str)
if not status_mode:
status_str = fps_str
if gameframe % 100 == 0:
sys.stdout.flush()
ticks = pygame.time.get_ticks() # don't include stdout time in frame period
print "Average fps: %.2f over %d frames" % ((float(gameframe)*1000.0)/(pygame.time.get_ticks()-ticks),gameframe)
main_dialog.destroy()
if __name__ == '__main__': main()
The callback function tk_key_event(event) attempts to convert key presses in the tkinter window to match key press codes for pygame and then passes them to the common key press event handler GetInput(). This is only partially successful - if anyone can be bothered sorting it all out let me know.
In the previous example, even with only one menu item, there is a lot of content that hides the actual purpose of the application and having to remember the sequence of initialising the menu is mind numbing. For rapid development or where you return to code irregularly, the complicated stuff needs to be hidden. To this end the tkfront suite of modules have been developed and are shown here to demonstrate how they effectively tidy the code - though this may not be obvious as I have also added some more effects.
import pygame
from copy import *
from pygame.locals import *
import sys # for exit and arg
import tkfront
# initialise
pygame.init()
ScreenSize = (200,100)
pygame.display.set_caption("Pygame Tkinter Demo 3 - Robert Parker (c2009)")
Surface = pygame.display.set_mode(ScreenSize)
testMode = 0
def Draw():
global view_surface, centres
#Clear view
Surface.fill((80,80,80))
pygame.display.flip()
def refreshDraw():
Draw()
def about_callback():
t1 = "Pygame Meets Tkinter by Robert Parker (c)2009"
t2 = "Pygame is python interface to SDL media layer indended for game creation but used for much more (ref. www.pygame.com)"
t3 = "Tkinter is based on Tk GUI toolkit developed by Scriptics (http://www.scriptics.com) (formerly developed by Sun Labs)."
t4 = "Thanks to Fredrik Lundh 'An introduction to Tkinter' (c)1999"
about_text = t1 + "\n" + t2 + "\n"+ t3 + "\n" + t4
tkfront.about_dialog(about_text)
def GetInput():
for event in pygame.event.get():
global status_mode, status_str
if event.type == QUIT:
tkfront.quit_callback()
#return True
if event.type == KEYDOWN:
print event
print event.key
print event.unicode
status_mode = True
if type(event.unicode) == chr:
status_str = "Key %c[%3d]"%(event.unicode,event.key)
else:
status_str = "Key [%3d]"%(event.key,)
#keystate = pygame.key.get_pressed()
keymod = pygame.key.get_mods()
if event.type == MOUSEBUTTONDOWN:
print event.button
keystate = pygame.key.get_pressed()
return False
def tk_key_event(event):
''' transfer key event in tkinter dialog to pygame event handler'''
print event.keycode, event.keysym, event.keysym_num
trans_dict = { 192:96,107:270,109:269,107:270,109:269,37:276,39:275,38:273,40:274,45:277,36:278,33:280,46:127,35:279,34:281 }
if trans_dict.has_key(event.keycode):
code = trans_dict[event.keycode]
else:
code = event.keycode
pygame.event.post(pygame.event.Event(KEYDOWN,{'key':code,'unicode':event.keysym}))
Done = False
def program_destroy_callback():
global Done
Done = True
def main():
global status_mode, status_str
menu_design = (("Help",
(("About",about_callback),
)
),
)
status_str = "Status"
fps_str = "FPS"
tkfront.parent_destroy_callback = program_destroy_callback
tkfront.make_gui(menu_design,tk_key_event,None,status_str)
clock = pygame.time.Clock()
# lets get some performance statistics
gameframe = 0
ticks = pygame.time.get_ticks()
status_mode= False # show FPS for the moment
while not Done:
tkfront.update(status_str)
if GetInput(): # input event can also comes from diaglog
break
Draw()
clock.tick(100) # slow it to something slightly realistic
gameframe += 1
# display stats in pygame title bar and in tk window status line
if gameframe % 10 == 0:
t = pygame.time.get_ticks()
fps = (float(10)*1000.0)/(t-ticks)
ticks = t
#rint "TestMode#%d fps: %.2f over %d frames" % (testMode,fps,gameframe)
period = 1.0/fps
fps_str = "%5.1f FPS"%fps
pygame.display.set_caption(fps_str)
if not status_mode:
status_str = fps_str
if gameframe % 100 == 0:
sys.stdout.flush()
ticks = pygame.time.get_ticks() # don't include stdout time in frame period
print "Average fps: %.2f over %d frames" % ((float(gameframe)*1000.0)/(pygame.time.get_ticks()-ticks),gameframe)
if __name__ == '__main__': main()
To run the above example you will need to have loaded the tkfront modules to somewhere in your python path - refer to the tkfront installation instructions.
You will note that a large chunk of messy code has been replaced with:
tkfront.make_gui(menu_design,tk_key_event,None,status_str)
with all the menu design information held in the tuple make_design.
Also note that the function quit_callback() has been replaced by program_destroy_callback()
but the quite confirmation has been retained. Tkfront does much more but that's enough for now.
While, as far as I know, only one pygame window can be run per application, and for some environments it is possible to switch from Windowed to FullScreen modes on the fly, it is not immediately apparent that it is possible to change the window size. The following application demonstrates this using either a menu choice on the Tkinter window or the plus and minus keys on either window.
import pygame
from copy import *
from pygame.locals import *
import sys # for exit and arg
import tkfront
# initialise
pygame.init()
ScreenSize = (200,100)
pygame.display.set_caption("Pygame Tkinter Demo 3 - Robert Parker (c2009)")
Surface = pygame.display.set_mode(ScreenSize)
testMode = 0
def Draw():
global view_surface, centres
#Clear view
Surface.fill((80,80,80))
pygame.display.flip()
def refreshDraw():
Draw()
def small_callback():
global ScreenSize, status_str
close_display()
ScreenSize = (50,50)
init_display()
status_str = "Small"
tk_dialog.menu_set("view_big",False,False)
def big_callback():
global ScreenSize, status_str
close_display()
ScreenSize = (800,500)
init_display()
status_str = "Big"
tk_dialog.menu_set("view_small",False,False)
def GetInput():
for event in pygame.event.get():
if event.type == QUIT:
tkfront.quit_callback()
#return True
if event.type == KEYDOWN:
print "Event:",event
print "Key:",event.key
print "Unicode:",event.unicode
status_mode = True
if event.key == 270:
tk_dialog.menu_set("view_big")
elif event.key == 269:
tk_dialog.menu_set("view_small")
#keystate = pygame.key.get_pressed()
keymod = pygame.key.get_mods()
if event.type == MOUSEBUTTONDOWN:
print "Event:",event.button
keystate = pygame.key.get_pressed()
return False
def tk_key_event(event):
''' transfer key event in tkinter dialog to pygame event handler'''
#rint event.keycode, event.keysym, event.keysym_num
trans_dict = { 192:96,107:270,109:269,107:270,109:269,37:276,39:275,38:273,40:274,45:277,36:278,33:280,46:127,35:279,34:281 }
if trans_dict.has_key(event.keycode):
code = trans_dict[event.keycode]
else:
code = event.keycode
pygame.event.post(pygame.event.Event(KEYDOWN,{'key':code,'unicode':event.keysym}))
Done = False
def program_destroy_callback():
global Done
Done = True
def init_display():
global Surface
Surface = pygame.display.set_mode(ScreenSize)
def close_display():
global Surface
pygame.display.quit()
Surface = None
def main():
global status_str, tk_dialog
menu_design = (("View",
(("Small",small_callback,None,"view_small"),
("Big",big_callback,None,"view_big")
)
),
)
status_str = "Status"
fps_str = "FPS"
tkfront.parent_destroy_callback = program_destroy_callback
tk_dialog = tkfront.make_gui(menu_design,tk_key_event,None,status_str)
clock = pygame.time.Clock()
# lets get some performance statistics
gameframe = 0
ticks = pygame.time.get_ticks()
while not Done:
tkfront.update(status_str)
if GetInput(): # input event can also comes from diaglog
break
Draw()
clock.tick(100) # slow it to something slightly realistic
gameframe += 1
print "Average fps: %.2f over %d frames" % ((float(gameframe)*1000.0)/(pygame.time.get_ticks()-ticks),gameframe)
if __name__ == '__main__': main()
The window size change is achieved only by closing the window (ie. pygame.display.quit()) and then reopening it with the new screen size.
The window resizing callbacks are made either from the menu (as indicated in the tuple menu_design) or from the GetInput() function. Passing the Tkinter key events to the pygame event handler has the possibly important value of processing the events in an orderly manner. Note that the GetInput() event handler doesn't call the window resizing callbacks directly but rather attacks the menu items directly so that the tick status is preserved. The calls to menu_set() demonstate how to achieve a radio button type behavior from menu check options.
Happy tkgaming!
In case you didn't notice them, please heed the warnings on the microde page.
Copyright Robert Parker November 2009