Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I update a Tkinter canvas in the middle of a function call?

Tags:

python

tkinter

I'm writing Conway's game of life using Tkinter, and I want to have a "Go" button which allows the animation to begin, and continue stepping automatically until terminated. I'm using a Canvas to draw the environment, but since the "Go" button needs to complete the function call before it updates the canvas, the window just hangs until I kill the process. I've attempted to use canvas.update_idletasks() and canvas.update() (followed by a few seconds of sleeping) at points where I want to updated the canvas but that doesn't seem to do the trick. Any Ideas? Below is my GameOfLife class, the Environment class just manages the "board" of cells.

from Tkinter import *
from random import *
from time import time
from Environment import *

class GameOfLife(object):
    def __init__(self, master, envDim):
        self.unitSize = 10
        self.dimension = envDim * self.unitSize
        self.environment = Environment(envDim)
        self.environment.seedBoard()
        self.started = False

        frame = Frame(master)
        frame.pack()
        Button(frame, text = "Go", command = self.go_call).pack(side = LEFT)
        Button(frame, text = "Clear", command = self.reset_call).pack(side = LEFT)
        Button(frame, text = "Close", command = frame.quit).pack(side = RIGHT)
        canvas = self.drawCanvas(master, self.dimension)

def drawCanvas(self, master, dimension):
    self.canvas = Canvas(master, width = self.dimension, height = self.dimension)
    self.canvas.pack()
    return self.canvas

def go_call(self):
    print "<< Go Call >>"
    if self.environment.started == False:
        self.environment.seedBoard()

    self.drawState(self.environment)
    self.environment.nextBoard()
    self.started = True
    while True:
        self.environment.nextBoard()
        self.canvas.delete(ALL)
        self.drawState(self.environment)
        self.canvas.update_idletasks()
        sleep(4)

def reset_call(self):
    print "<< Reset Call >>"
    self.canvas.delete(ALL)
    self.environment = Environment(self.environment.dim)

def drawState(self, environment):
    size = self.unitSize
    for x in range(environment.dim):
        for y in range(environment.dim):
            if environment.matrix[x][y].alive == True:
                xs = x * size
                ys = y * size
                self.canvas.create_rectangle(xs, ys, xs+size, ys+size, fill = 'black')

envDim = 70
root = Tk()
gol = GameOfLife(root, envDim)
root.mainloop()
like image 208
Chironex Avatar asked Jun 09 '12 21:06

Chironex


1 Answers

You should never put an infinite loop inside your GUI program, and you definitely should never, ever call sleep. You already have an infinite loop running -- the event loop -- so take advantage of it.

The way to do animations like this is to write a function that draws one frame of the animation (or turn of the game, pick your metaphor). Then, have that function call itself via after.

Roughly speaking your code should look like this:

def draw(self):
    # draw the board according to the current state
    ...
    # arrange for the next frame to draw in 4 seconds
    self.after(4000, self.draw)

def __init__(self, ...):
    ...
    self.go = tk.Button(self, text="Go", command=self.draw)
    ...

If you want to add a stop button, all it needs to do is set a flag. Then, in draw, simply check for the flag before calling self.after

like image 135
Bryan Oakley Avatar answered Oct 15 '22 09:10

Bryan Oakley