Ben Wo Ben Wo - 4 days ago 4
Python Question

How to use .after with .create_rectangle?

I am trying to create a program that draws rectangles on a canvas on at a time, and I want to use the .after function to stop them from being drawn (near) instantly.

Currently my (stripped down) code looks like this:

root = Tk()
gui = ttk.Frame(root, height=1000, width=1000)
root.title("Test GUI")

rgb_colour = "#%02x%02x%02x" % (0, 0, 255)

def func(*args):
for item in sorted_list:
canvas.create_rectangle(xpos, ypos, xpos+10, ypos+10, fill=rgb_colour)
xpos += 10

canvas = Canvas(gui, width=1000, height=1000)
canvas.grid()

func() # Code doesn't actually look exactly like this

root.mainloop()


and I want there to be a delay between each rectangle being drawn. I have gathered that I should be doing:

def draw(*args):
canvas.create_rectangle(xpos, ypos, xpos+10, ypos+10, fill=rgb_colour)

for item in sorted_list:
root.after(10, draw)


However I cannot do this because my original for loop is nested within a function that contains the xpos, ypos, and colour variables, so a new function to create the rectangle would lack the required variables. I realise I could solve this by nesting my entire function within a class, and then calling the variables from the class, however I want to keep this code very simple and I was wondering if there was a way to delay the creation of rectangles without the use of a class?

Edit: This:

from tkinter import *

root = Tk()
canvas = Canvas(root, width=400, height=400, bg="white")
canvas.pack()

items = [1, 2, 3, 4, 5]
delay = 100

def draw_all(*args):
global delay
x, y = 0, 10
for item in items:
canvas.after(delay, canvas.create_rectangle(x, y, x+10, y+10, fill="red"))
delay += 10
x += 10

root.bind('<Return>', draw_all)
root.mainloop()


still returns an AttributeError

Answer

The simplest solution is to create a function that takes a list of items along with any other data it needs, pops one item from the list and creates the rectangle, and then calls itself until there are no more items.

def draw_one(items):
    item = items.pop()
    canvas.create_rectangle(...)
    if len(items) > 0:
        canvas.after(10, draw_one, items)

If you prefer to not use a function that calls itself, simply call the create_rectangle method using after:

def draw_all():
    delay = 0
    for item in items:
        canvas.after(delay, canvas.create_rectangle, x0, y0, x1, y1, ...)
        delay += 10

With that, the first will be drawn immediately, the next in 10ms, the next in 20ms, etc.

Comments