PYGAME and TKINTER in HARMONY

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.


Contents


A Simple Example

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:

  1. An introduction to Tkinter by Fredrik Lundh (c)1999
  2. Documentation and tutorials at the tkinter wiki
  3. Documentation and tutorials at the pygame web site.

A More Complex Example

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.


The Complex Example Using The Tkfront Modules

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.


Resizing the Pygame Window

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