How to use .after with .create_rectangle? - python

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

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.

Related

What is the best way in Python to detect objects' ID being clicked with mouse?

I would like to ask what is the best way in Python to interact with a canvas objects which were created with function.My example code:
import tkinter
window = tkinter.Tk()
canvas = tkinter.Canvas(width=1000, height=600, bg="black")
canvas.pack()
def rectangle(x, y):
canvas.create_rectangle(x, y, x + 5, y + 5, fill="white")
rect1 = rectangle(20, 50)
rect2 = rectangle(180, 30)
rect3 = rectangle(698, 322)
rect4 = rectangle(900, 66)
rect5 = rectangle(10, 506)
rect6 = rectangle(208, 455)
What is the best way to detect ID of the object which is being clicked with a mouse and then use those IDs as arguments in another function? Thank you for the answers.
Use tag_bind.
import tkinter
from functools import partial
window = tkinter.Tk()
canvas = tkinter.Canvas(width=1000, height=600, bg="black")
canvas.pack()
def on_click(item, event=None):
print(f"Item id {item} was clicked!")
def rectangle(x, y):
item_id = canvas.create_rectangle(x, y, x + 5, y + 5, fill="white")
canvas.tag_bind(item_id, '<Button-1>', partial(on_click, item_id))
rect1 = rectangle(20, 50)
rect2 = rectangle(180, 30)
rect3 = rectangle(698, 322)
rect4 = rectangle(900, 66)
rect5 = rectangle(10, 506)
rect6 = rectangle(208, 455)
window.mainloop()
The canvas has a method named find_closest which will return the object nearest the cursor.
def highlight_nearest(event):
canvas = event.widget
x = canvas.canvasx(event.x)
y = canvas.canvasy(event.y)
item = canvas.find_closest(x, y)
canvas.itemconfigure(item, fill="red")
canvas.bind("<1>", highlight_nearest)
The find_closest method takes an x and y coordinate, and an optional halo and start argument.
This is what the official documentation has to say:
Selects the item closest to the point given by x and y. If more than one item is at the same closest distance (e.g. two items overlap the point), then the top-most of these items (the last one in the display list) is used. If halo is specified, then it must be a non-negative value. Any item closer than halo to the point is considered to overlap it. The start argument may be used to step circularly through all the closest items. If start is specified, it names an item using a tag or id (if by tag, it selects the first item in the display list with the given tag). Instead of selecting the topmost closest item, this form will select the topmost closest item that is below start in the display list; if no such item exists, then the selection behaves as if the start argument had not been specified.

Using Python and Tkinter, How would I run code every loop of .mainloop()?

I have a program where I need to move an image object every time the mainloop() loops. I haven't tried doing much, mostly because I don't know where to start. I made a dummy version of my project that simulates the issue I'm having.
from tkinter import *
window = tk.Tk()
window.geometry('%ix%i+400+0' % (500, 600))
canvas = Canvas(window, width=500, height=600, bg='white')
canvas.pack()
w, x, y, z = 300, 300, 200, 200
x = canvas.create_rectangle(w, x, y, z)
def moveRectangle():
canvas.move(x, 10, 0)
# Run the moveRectangle function everytime the mainloop loops
window.mainloop()
To sum up my issue, I need to run mainloop as if it isn't a blocking function. Rather, either run it asynchronous, or maybe pause it and then run the function, though I don't think that's possible.
Anything helps
Thanks
Mainloop in tkinter doesn't loop through your code. It's looping through list of events. You can create an event by clicking buttons etc. Another way is that you can call commands like update() or update_idletasks(). Combinig that with after() can give you results you are looking for. So look up these in documentation, it will be helpful. Also you can read: understanding mainloop.
def moveRectangle():
canvas.move(x, 10, 0)
for i in range(20): # 20 moves 10px every 50ms
window.after(50, canvas.move(x, 10, 0))
window.update()
moveRectangle()
This little code above demonstrate how you could use mentioned commands to get your object move on screen.
I think there are ways to set up a custom mainloop() which could update your UI every time the loop runs but depending on what you want to do the more usual method is to use after. By arranging for the function called by after to call itself with after an effective loop can be created.
I've amended you code to bounce the rectangle as it reaches the sides of the canvas so it can show something as it runs indefinitely.
import tkinter as tk
WAIT = 10 # in milliseconds, can be zero
window = tk.Tk()
window.geometry('%ix%i+400+0' % (500, 600))
canvas = tk.Canvas(window, width=500, height=600, bg='white')
canvas.pack()
w, x, y, z = 300, 300, 200, 200
x = canvas.create_rectangle(w, x, y, z)
amount = 10
def direction( current ):
x0, y0, x1, y1 = canvas.bbox( x )
if x0 <= 0:
return 10 # Move rect right
elif x1 >= 500:
return -10 # Move rect left
else:
return current
def moveRectangle():
global amount
canvas.move(x, amount, 0)
window.update()
# Change Direction as the rectangle hits the edge of the canvas.
amount = direction( amount )
window.after( WAIT, moveRectangle )
# ms , function
# Loop implemented by moveRectangle calling itself.
window.after( 1000, moveRectangle )
# Wait 1 second before starting to move the rectangle.
window.mainloop()

creating and deleting an object (e.g. rectangle) ager a time delay repeatedly in tkinter

I'm rather new in python. Currently I aim tot write a program which must create and delete repeatedly after a few seconds object (rectangle, circle, etc.).If I use the .after method in a separate function I get an error: UnboundLocalError: local variable 'counter' referenced before assignmentWhen, see myself code. Alternatively, if I put the repeating code in the main part of program, it only shows the last state and not in state in between (see the description between """ and """ in the code. I searched the internet for a while but I could not sole it. I hope you can help me.
Here is an simplified version my code (actually I'm working with a list of rec's), which shows my problem:
import tkinter as tk
def master_field(text):
""" work field (master) construction"""
master.geometry('500x500+0+0')
master.title(text)
def stop_button():
"""exit"""
b = tk.Button(master, text = 'Exit', width=2, command = master.destroy)
b.place(x = 50, y = 0, width = 100)
def built_canvas():
"""create a canvas"""
canvas=tk.Canvas(master, width = canvas_width, height = canvas_height, borderwidth = 1, bg='light grey', highlightthickness=0, highlightbackground="blue")
canvas.place(x = 0, y = 20)
return canvas
def update_canvas():
while counter<10:
canvas.delete(rec)
rec = canvas.create_rectangle(x1+i*10,y1+i*10,x2+i*10,y2+i*10, width=0, fill="green")
counter +=1
master.after(500,update_canvas)
# MAIN
master = tk.Tk()
master_field('MY FIELD')
stop_button()
canvas_width = 200
canvas_height = 200
canvas = built_canvas()
cell_width = 10
x1 = 10
y1 = 10
x2 = x1+10
y2 = y1+10
rec = canvas.create_rectangle(x1,y1,x2,y2, width=0, fill="green")
counter = 0
master.after(500,update_canvas)
"""
# The code below only gives the final state
for i in range(1,10):
master.after(500)
canvas.delete(rec)
rec = canvas.create_rectangle(x1+i*10,y1+i*10,x2+i*10,y2+i*10, width=0, fill="green")
"""
master.mainloop()
The tkinter function after does two very different things - if you call it like in the section you put between """ """:
master.after(500)
This makes your python program wait for half a second, and then continues.
If you call it like your loop in update_canvas:
master.after(500, update_canvas)
then it returns immediately, with no wait, but schedules update_canvas to happen half a second into the future.
So the version you have above with
while counter<10:
canvas.delete(rec)
rec = canvas.create_rectangle(x1+i*10,y1+i*10,x2+i*10,y2+i*10, width=0, fill="green")
counter +=1
master.after(500,update_canvas)
Doesn't give the right time delay, since the loop runs through all 10 iterations with no waiting, and schedules update_canvas to happen 10 times, all after half a second from now, not half a second from each other.
The version in quotes:
for i in range(1,10):
master.after(500)
canvas.delete(rec)
rec = canvas.create_rectangle(x1+i*10,y1+i*10,x2+i*10,y2+i*10, width=0, fill="green")
Has a different problem - this runs the loop multiple times, and waits half a second each time, but the window is not even visible yet since you haven't reached master.mainloop() - so it animates the object, but you can't see it yet.
To fix this, you need to use the first version that schedules in the future, so you can schedule it and then call mainloop(), and then have the first one schedule the next one.
If we do this instead:
def update_canvas():
global counter, rec # global lets you change counter and rec from inside the function - avoids 'Unbound local variable'
if counter >= 10:
return # If the counter is big enough, stop and do nothing more
else:
canvas.delete(rec) # Update the rectangle
i = counter
rec = canvas.create_rectangle(x1+i*10,y1+i*10,x2+i*10,y2+i*10, width=0, fill="green")
counter +=1 # Add to the counter so we don't animate forever
master.after(500,update_canvas) # Schedule the next update for half a second in the future
Then it should animate the square. This way the loop is gone, but the first time we call update_canvas, the update_canvas function itself asks tkinter to call it again later on. Eventually the counter gets big enough, and it doesn't ask for another call and the animation stops.
You do not need the while loop in update_canvas(). Also you do not need to recreate the rectangle, just move it:
def update_canvas(counter=0):
if counter < 10:
canvas.move(rec, 10, 10)
master.after(500, update_canvas, counter+1)

'Move object' function bound to a key in Tkinter can only make one object move at a time, how to make more object move at the same time?

I've bound a key to a function which makes an oval (included in a list of other identical ovals) moves a certain distance. I want it to make a new oval moves each time I press the key, without stopping the previous moving oval if its course is not over.
With my code, by pressing 'c', I create a new oval randomly placed on the canvas, and saved in a dictionary. Each new oval is saved with key = 'compteur', 'compteur' increments for every new oval created to make sure every oval is not created over a previous existing one.
By pressing 'm', I want to make a new oval move each time I press the key, without the previous one stopping.
from tkinter import *
import time
from random import *
import time
compteur = 0
dic = {}
w = Tk()
w.geometry('400x400')
c = Canvas(w, width = 400, height = 400)
c.pack()
dic[compteur] = c.create_oval(200,150,250,200,fill = 'pink')
compteur += 1
def create(event):
global compteur
b = randrange(300)
dic[compteur] = c.create_oval(200,b,250,(b+50),fill = 'pink')
compteur += 1
def move(event):
rond = dic[randrange(len(dico))]
if c.coords(rond)[0] == 200:
for x in range (15):
c.move(rond,-10,0)
w.update()
time.sleep(0.15)
w.bind('<m>', move)
w.bind('<c>',create)
w.mainloop()
I'm obviously missing something but as I'm a beginner, I have no idea why only one oval can move at a time. And weirdly, once the second oval finish it's course, the first one starts again to finish its course too.
Thanks for your help :)
I use list to keep all circles.
In move() I move last circle from list only when I press <m>
In move_other() I move all circles except last one and use after() to run move_other() after 100ms (0.1s) so it will move all time.
from tkinter import *
from random import *
# --- fucntions ---
def create(event):
b = randrange(300)
circles.append(c.create_oval(200, b, 250, (b+50), fill='pink'))
def move(event):
item = circles[-1]
c.move(item, -10, 0)
def move_other():
for item in circles[:-1]:
c.move(item, -10, 0)
w.after(100, move_other)
# --- main ---
circles = []
w = Tk()
w.geometry('400x400')
c = Canvas(w, width=400, height=400)
c.pack()
circles.append(c.create_oval(200, 150, 250, 200, fill='pink'))
move_other() # start moving other circles
w.bind('<m>', move)
w.bind('<c>', create)
w.mainloop()

keeping track of own objects on tkinter canvas

Up until now, whenever I've needed to work with multiple shapes on a tkinter canvas, they were just that - shapes. I get their tags with canvas.find_all() and manipulate their geometry by moving, resizing etc.
But I bumped into this problem which I can't seem to solve like this.
If I define a my own class and draw this object to the canvas, how can I keep track of all the objects on the canvas, in order to call their methods?
Say I define a Bubble class, which draws a bubbly thing to the screen. After every second I want it to change all the bubbles' colour to another colour, using their change_colour methods.
my_list = []
for n in range(10):
bubble = Bubble()
my_list.append(bubble)
while True:
time.sleep(1)
for item in my_list:
item.change_colour()
I could append it to a big 'ol list, then iterate through it like I am doing here, but for cases with more objects this is far too slow!
What is the proper way of doing this?
As usual, thanks for any help!
As pointed out, time.sleep() doesn't make any sense, but it is not the problem I am trying to solve.
My advice is to give each item you create at least two tags. One tag would be "bubble" so that you can reference all bubbles at once, and the second would be a tag unique to each bubble.
For example:
class Bubble():
def __init__(...):
self.tag = "b-%d" % id(self)
...
canvas.create_oval(..., tags=("bubble", self.tag))
...
With that, you can implement a change_color method on the Bubble class like the following, which will change all canvas items created by this instance of the class:
def change_color(self, color):
canvas.itemconfigure(self.tag, fill=color)
You can then create a red bubble like this:
bubble = Bubble()
bubble.change_color("red")
This also lets you change all bubbles at once using the "bubble" tag:
canvas.itemconfigure("bubble", outline="blue")
If you want the bubbles to blink, you should not create a while loop. Instead, take advantage of the loop that is already running.
Do this by creating a function that does whatever you want, and then have that function schedule itself to run again via after. For example:
def blink(color="red"):
canvas.itemconfigure("bubble", fill=color)
new_color = "red" if color == "white" else "white"
canvas.after(1000, blink, new_color)
This will cause all bubbles to blink every second as long as the program is running.
If you want to perform custom individual changes to each item (say, changing each item's color to a brand new random color), then you can't do any better than iterating through each one and calling itemconfig on them individually.
However, if you want to make the same change to each item, you can tag your items and call itemconfig a single time, using that tag as your specifier.
Example:
import Tkinter
import random
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
for i in range(1000):
x = random.randint(0, 400)
y = random.randint(0, 400)
canvas.create_oval((x-5,y-5,x+5,y+5), fill="white", tags=("bubble"))
current_color = "white"
def change_colors():
global current_color
current_color = "white" if current_color == "black" else "black"
canvas.itemconfig("bubble", fill = current_color)
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
Result:
However, as I indicated in an earlier comment, I'm still of the opinion that this is a premature optimization. Even if you have a thousand items, iterating through them and configuring them individually isn't noticeably slower than doing it with tags. Example:
import Tkinter
import random
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
items = []
for i in range(1000):
x = random.randint(0, 400)
y = random.randint(0, 400)
id = canvas.create_oval((x-5,y-5,x+5,y+5), fill="white")
items.append(id)
current_color = "white"
def change_colors():
global current_color
current_color = "white" if current_color == "black" else "black"
for id in items:
canvas.itemconfig(id, fill = current_color)
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
The Canvas.find_withtag() method will return a list of the IDs of the all the matching objects specified by first argument. You can use that in conjunction with a dictionary to map those back to the corresponding instances of your class. Once you have that, you can call any of its methods.
import Tkinter
import random
BUBBLE_TAG = 'Bubble'
current_color = 'white'
class Bubble(object):
def __init__(self, canvas, x, y, size, color):
self.canvas = canvas
self.id = canvas.create_oval((x-5,y-5,x+5,y+5), fill=color,
tags=BUBBLE_TAG)
def change_color(self, new_color):
self.canvas.itemconfigure(self.id, fill=new_color)
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
mapping = {}
for i in range(1000):
x, y = random.randint(0, 400), random.randint(0, 400)
color = 'black' if random.randint(0, 1) else 'white'
obj = Bubble(canvas, x, y, 5, color)
mapping[obj.id] = obj
def change_colors():
for id in canvas.find_withtag(BUBBLE_TAG):
current_color = canvas.itemcget(id, 'fill')
new_color = 'black' if current_color == 'white' else 'white'
mapping[id].change_color(new_color) # calls method of object
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
Here's an example of it running:

Categories

Resources