2
\$\begingroup\$

I am trying to learn how to animate with tkinter. The code below is an example I was able to build. It creates a small 5x5 map of cubes, then one of them randomly moves around the screen (preferably on the map, but it can wander off).

Is this a good way to do it? Is there a more pythonic, or more efficient way of doing it?

from tkinter import *
from time import sleep
from random import randrange

class alien(object):
    def __init__(self):
        self.root = Tk()
        self.canvas = Canvas(self.root, width=400, height=400)
        self.canvas.pack()
        self.map = [[1, 0, 0, 1, 0], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 1, 0, 0], [1, 0, 0, 1, 0]]
        self.x = 0
        self.y = 0
        for row in range(len(self.map)):
            for column in range(len(self.map[row])):
                color = "green"
                if self.map[row][column] == 1:
                    color = "black"                
                #print(50 * row + 50, 50 * column + 50)
                self.canvas.create_rectangle(50 * row , 50 * column , 50 * row + 50, 50 * column + 50,
                    outline=color, fill=color) 
        self.creature = self.canvas.create_rectangle(50 * self.x, 50 * self.y, 50 * self.x + 50, 50 * self.y + 50,
            outline="red", fill="red")     
        self.canvas.pack(fill=BOTH, expand=1)   
        self.root.after(0, self.animation)
        self.root.mainloop()

    def animation(self):
        while True:
            i = randrange(1, 5)
            print(i)
            if i == 1:
                y = -1
            elif i == 2:
                y = 1
            elif i == 3:
                x = -1
            elif i == 4:
                x = 1
            sleep(0.025)
            print(self.x, self.y)
            self.canvas.move(self.creature, self.x, self.y)
            self.canvas.update()
alien()
\$\endgroup\$
3
\$\begingroup\$

Here is anoted version of your code

from tkinter import *
from time import sleep
from random import randrange

# lowercase tkinter suggest you are using python3, in this case
# inheriting (object) is not needed anymore to create newstyle
# class
class alien(object):
    def __init__(self):
        self.root = Tk()
        self.canvas = Canvas(self.root, width=400, height=400)
        self.canvas.pack()
        self.map = [[1, 0, 0, 1, 0], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 1, 0, 0], [1, 0, 0, 1, 0]]
        self.x = 0
        self.y = 0
        for row in range(len(self.map)):
            for column in range(len(self.map[row])):
                color = "green"
                #since 0 evaluate to False, you can drop '== 1'
                if self.map[row][column] == 1:
                    color = "black"                
                self.canvas.create_rectangle(50 * row , 50 * column , 50 * row + 50, 50 * column + 50,
                    outline=color, fill=color) 
        self.creature = self.canvas.create_rectangle(50 * self.x, 50 * self.y, 50 * self.x + 50, 50 * self.y + 50,
            outline="red", fill="red")
        #this call overrides the previous 'canvas.pack'. Remove first.
        self.canvas.pack(fill=BOTH, expand=1)   
        self.root.after(0, self.animation)
        self.root.mainloop()

    def animation(self):
        #see below for further explanations on animations.
        while True:
            i = randrange(1, 5)
            print(i)
            #note: randrange ouput from 1 to 5 (included)
            #thus i = 5 is not handled in your switch (might be on purpose)
            if i == 1:
                y = -1
            elif i == 2:
                y = 1
            elif i == 3:
                x = -1
            elif i == 4:
                x = 1
            # after you switch, either x or y (or both) are not
            # initialized
            sleep(0.025)
            # self.x and self.y are never updated
            print(self.x, self.y)
            # move is a relative command (expect delta x,delta y)
            # if self.x,self.y is your creature absolute position,
            # you might want to use 'coords' on (self.x, self.y) 
            # or 'move' on (x,y)
            # in addition, your "*50" factor has disapeared.
            self.canvas.move(self.creature, self.x, self.y)
            # it is good practice to use 'update_idletasks' instead
            # of 'update' to avoid race conditions. By the way
            # the use of one of them is seldom required (see below)
            self.canvas.update()
alien()

Regarding how you handle the animation, it could be profitable to run your simulation in the python mainloop. What you does now is to provide your own mainloop (while True: [...] sleep(0.025) [...] canvas.update()). This can work, but will fail as soon as you will want to add interactivity to your application (other widgets, or react to events on canvas). This will not work because the execution path is stuck in alien.animation. The solution there would be to rely on tkinter mainloop and to beg for alien.animation to be repeatedly called.

def animation(self):
    #while True:  no longer needed
        i = randrange(1, 5)
        #[...]
        elif i == 4:
            x = 1
        #sleep(0.025) becomes
        self.root.after(25, animation)
        print(self.x, self.y)
        self.canvas.move(self.creature, x*50, y*50)
        #self.canvas.update() no longer needed
\$\endgroup\$
  • \$\begingroup\$ why is update note needed? \$\endgroup\$ – EasilyBaffled Jun 2 '13 at 22:42
  • \$\begingroup\$ update is a way to ask tkinter to process pending events, by implicitly returning to the mainloop (with the after mechanism), you let tkinter process event freely (here the redraw is what you are interested in. \$\endgroup\$ – FabienAndre Jun 3 '13 at 5:04
0
\$\begingroup\$
  • Whenever you do range(len(self.map)) in Python, you are most probably doing something wrong. The pythonic way to do this is to use enumerate.

If you also use the ternary operator, remove the useless '== 1', store magic numbers in a unique place and factorize the mathematical expressions, you can transform this :

    for row in range(len(self.map)):
        for column in range(len(self.map[row])):
            color = "green"
            if self.map[row][column] == 1:
                color = "black"                
            #print(50 * row + 50, 50 * column + 50)
            self.canvas.create_rectangle(50 * row , 50 * column , 50 * row + 50, 50 * column + 50,
                outline=color, fill=color) 
    self.creature = self.canvas.create_rectangle(50 * self.x, 50 * self.y, 50 * self.x + 50, 50 * self.y + 50,
        outline="red", fill="red")

Into this

    r = 50
    for i,row in enumerate(self.map):
        for j,cell in enumerate(row)):
            color = "black" if cell else "green"
            self.canvas.create_rectangle(r*i, r*j , r*(i+1), r*(j+1),
                outline=color, fill=color)
    self.creature = self.canvas.create_rectangle(r*self.x, r*self.y, r*(self.x+1), r*(self.y+1),
        outline="red", fill="red")
  • Does map need to be an object member or could it be a variable local to __init__ ?

  • Is animation() working properly or should it update self.x and self.y ?

\$\endgroup\$
  • \$\begingroup\$ What do you mean by your question about map? Is it does it need the self. designation? Then yes. Animation does work, it's messy but really it's a place holder. \$\endgroup\$ – EasilyBaffled Jun 2 '13 at 22:17
  • \$\begingroup\$ If you are not using map outside the unit method, you could get rid of the self designation. As for animation, I guessed I misunderstood how it was supposed to work. \$\endgroup\$ – SylvainD Jun 3 '13 at 5:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.