flame boi
flame boi

Reputation: 41

Why does tkinter's after() function freeze my window?

I am creating a replica of dodger using tkinter. I am facing a problem with timing object movement. I was told the time module does not work well with tkinter, therefore I should use after() instead. However, I face the same problem with the after() function as I did with the time module. Here is my code:

from tkinter import *
from random import randint


class Window(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)

        self.master = master
        self.initWindow()

    def initWindow(self):
        self.master.title('Dodger')
        self.pack(fill=BOTH, expand=1)
        self.master.geometry('600x800')
        self.master.config(bg='black')

        menu = Menu(self.master)
        self.master.config(menu=menu)

        def clientExit():
            exit()

        file = Menu(menu)
        file.add_command(label='Exit', command=clientExit)
        file.add_command(label='Start', command=self.game)

        menu.add_cascade(label='File', menu=file)

    def game(self):
        canvas = Canvas(self.master, width='600', height='800', borderwidth='0', highlightthickness='0')
        canvas.pack()
        canvas.create_rectangle(0, 0, 600, 800, fill='black', outline='black')

        character = canvas.create_rectangle(270, 730, 330, 760, fill='magenta', outline='cyan', width='2')

        def left(event):
            cord = canvas.coords(character)
            if cord[0] <= 5:
                pass
            else:
                canvas.move(character, -10, 0)

        def right(event):
            cord = canvas.coords(character)
            if cord[2] >= 595:
                pass
            else:
                canvas.move(character, 10, 0)

        self.master.bind('<Left>', left)
        self.master.bind('<Right>', right)

        class variables:
            sizeMin = 10
            sizeMax = 80

            y = 10
            minX = 5
            maxX = 545

        def createShape():
            size = randint(variables.sizeMin, variables.sizeMax)

            x = randint(variables.minX, variables.maxX)
            topLeft = [x, variables.y]
            bottomRight = [x + size, variables.y + size]

            shape = canvas.create_rectangle(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1],
                                            fill='red', outline='red')
            return shape

        def moveShape(shape):
            canvas.move(shape, 0, 800)

        for x in range(5):
            x = createShape()
            self.master.after(1000, moveShape(x))


root = Tk()
app = Window(root)
app.mainloop()

As you can see, at the bottom of the game instance, I created a square and moved it down five times at 1 second intervals. However, this did not work; my window just froze for the allotted time, then resumed afterwards. I am not sure if this is because my computer sucks or if I did something wrong. Please run my code in you editor and explain to me if I did something wrong.

Upvotes: 0

Views: 143

Answers (2)

Novel
Novel

Reputation: 13729

You need to replace the sleep function and the loop with after. Try this:

def moveShape():
    if self.step < 5:
        canvas.move(shape, 0, 10)
        self.master.after(1000, moveShape)
        self.step += 1

self.step = 0
shape = createShape()
moveShape()

Also, if you move it by 800 pixels you won't see it after the first tick, so I reduced the amount to move to 10 pixels.

Edit: this plus a lot of other bugfixes and improvements:

import tkinter as tk
from random import randint

class variables:
    sizeMin = 10
    sizeMax = 80

    y = 10
    minX = 5
    maxX = 545

class Window(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)

        self.master = master
        self.initWindow()

    def initWindow(self):
        self.master.title('Dodger')
        self.pack(fill=tk.BOTH, expand=1)
        self.master.geometry('600x800')
        self.master.config(bg='black')

        menu = tk.Menu(self.master)
        self.master.config(menu=menu)

        file = tk.Menu(menu)
        file.add_command(label='Exit', command=self.quit)
        file.add_command(label='Start', command=self.game)

        menu.add_cascade(label='File', menu=file)

    def game(self):
        self.canvas = tk.Canvas(self.master, width='600', height='800', borderwidth='0', highlightthickness='0')
        self.canvas.pack()
        self.canvas.create_rectangle(0, 0, 600, 800, fill='black', outline='black')

        self.character = self.canvas.create_rectangle(270, 730, 330, 760, fill='magenta', outline='cyan', width='2')
        self.createShape()
        self.moveShape() # start the moving

        self.master.bind('<Left>', self.left)
        self.master.bind('<Right>', self.right)

    def left(self, event):
        cord = self.canvas.coords(self.character)
        if cord[0] <= 5:
            pass
        else:
            self.canvas.move(self.character, -10, 0)

    def right(self, event):
        cord = self.canvas.coords(self.character)
        if cord[2] >= 595:
            pass
        else:
            self.canvas.move(self.character, 10, 0)

    def createShape(self):
        size = randint(variables.sizeMin, variables.sizeMax)

        x = randint(variables.minX, variables.maxX)
        topLeft = [x, variables.y]
        bottomRight = [x + size, variables.y + size]

        self.shape = self.canvas.create_rectangle(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1],
                                        fill='red', outline='red')

    def moveShape(self, x=0):
        if x < 5: # loop 5 times
            self.canvas.move(self.shape, 0, 10)
            self.after(1000, self.moveShape, x+1) # run this method again in 1,000 ms

root = tk.Tk()
app = Window(root)
app.mainloop()

Upvotes: 0

Bryan Oakley
Bryan Oakley

Reputation: 385970

The reason it freezes is because you're calling after wrong.

Consider this code:

self.master.after(1000, moveShape(x))

... it is exactly the same as this code:

result = moveShape(x)
self.master.after(1000, result)

... which is the same as this, since moveShape returns None:

result  = moveShape(x)
self.master.after(1000, None)

... which is the same as this:

result = moveShape(x)
self.master.after(1000)

... which is the same as

result = moveShape(x)
time.sleep(1)

In other words, you're telling it to sleep, so it sleeps.

after requires a callable, or a reference to a function. You can pass additional args as arguments to after. So the proper way to call it is this:

self.master.after(1000, moveShape, x)

Though, I doubt that is exactly what you want, since all five iterations will try to run the code 1000ms after the loop starts, rather than 1000ms apart. That's just a simple matter of applying a little math.

Upvotes: 1

Related Questions