keeping track of own objects on tkinter canvas - python

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:

Related

How do I delete only one object from canvas and not both?

I have a code where every second I generate random circle on canvas. And when you click on the object you get +1 point. But count of your points gets deleted every time new circle is made. How can I do it so count is permanently displayed on canvas?
import tkinter
import random
canvas = tkinter.Canvas(width=300, height=300)
canvas.pack()
count=0
def motion(event):
x,y = event.x, event.y
global count
if q+20 > x > q-20 and w+20 > y > w-20:
count=count+1
canvas.create_text(150,20,text=count)
else:
count=0
canvas.create_text(150,20,text=count)
def game():
canvas.delete('all')
global q,w
q=random.randint(20,280)
w=random.randint(20,280)
canvas.create_oval(q-20,w-20,q+20,w+20)
canvas.after(1000,game)
game()
canvas.bind('<Button-1>', motion,)
If you only want to delete the circles while leaving everything else on the canvas, give the circles a common tag. You can then delete them all via the tag.
canvas.create_oval(q-20,w-20,q+20,w+20, tags=("circle",))
...
canvas.delete("circle")
Also, you probably shouldn't be creating a new text item every time the motion function is called. Instead, create it once and then just change the text. You can use the same tag concept for the text so that you don't have to remember the text id.
Just do this once, outside the function:
canvas.create_text(150,20,text=count, tags=("text",))
Then, inside the function you can change the text:
canvas.itemconfig("text", text=count)
Here's a complete example:
import tkinter
import random
root = tkinter.Tk()
canvas = tkinter.Canvas(root, width=300, height=300)
canvas.pack()
canvas.create_text(150, 20, text="0", tags=("text",))
count=0
def motion(event):
x,y = event.x, event.y
global count
if q+20 > x > q-20 and w+20 > y > w-20:
count=count+1
else:
count=0
canvas.itemconfig("text", text=count)
def game():
canvas.delete('circle')
global q,w
q=random.randint(20,280)
w=random.randint(20,280)
canvas.create_oval(q-20,w-20,q+20,w+20, tags=("circle",))
canvas.after(1000,game)
game()
canvas.bind('<Button-1>', motion,)
root.mainloop()
As a final optimization, you don't have to continuously delete and recreate the one circle. Like with the text object, you can create the one circle and then just reconfigure its coordinates much the same as I showed how to reconfigure the text.

Create an image in a canvas inside a class [duplicate]

This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 1 year ago.
I wanted to create a chess program using OOP. So I made a superclass Pieces, a subclass Bishop, and a UI class GameUI. I created a canvas in the class GameUI. I wanted, that when I instantiate an object bishop in the class GameUI, it shows an Image from a bishop, on the canvas.
The problem is, when I instantiate the Bishop, I don't see any image. So I tried to do the same with a text : instead of using the method create_image from the class Canvas, I used the method create_text, and it worked : I saw a text on the canvas. That means, the problem comes from the method create_image, and I don't understand it.
If I create an Image directly in the class GameUi, it works! but that's not what I want...
So I don't have any error message. I see the canvas (with a blue background), but no image on it.
Here's the code :
from tkinter import PhotoImage, Tk, Canvas
class Pieces:
def __init__(self, can, color, x_position, y_position):
self.color = color
self.x_position = x_position
self.y_position = y_position
class Bishop(Pieces):
def __init__(self, can, color, x_position, y_position):
super().__init__(can, color, x_position, y_position)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
icon = PhotoImage(file=icon_path) # doesn't see the image
can.create_image(x, y, image=icon)
class GameUI:
def __init__(self):
self.windows = Tk()
self.windows.title("My chess game")
self.windows.geometry("1080x720")
self.windows.minsize(300, 420)
self.can = Canvas(self.windows, width=1000, height=600, bg='skyblue')
icon = PhotoImage(file=icon_path) # here I create the image in this class, and
can.create_image(x, y, image=icon) # we can see it very well
self.bishop = Bishop(self.can, "black", 50, 50)
self.can.pack()
self.windows.mainloop()
app = GameUI()
To make your code work, I decided to sort of rewrite it based on this answer. It works now, but really the only thing that you needed to add was self.icon instead of icon. icon gets garbage collected since there is no further reference to it, while self.icon remains. Also, it's not entirely the same as yours was, so it probably needs a bit of rewriting too.
from tkinter import *
from random import randint
class Piece:
def __init__(self, canvas, x1, y1):
self.x1 = x1
self.y1 = y1
self.canvas = canvas
class Bishop(Piece):
def __init__(self, canvas, x1, y1, color):
super().__init__(canvas, x1, y1)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
self.icon = PhotoImage(file=icon_path)
self.ball = canvas.create_image(self.x1, self.y1, image=self.icon)
def move_piece(self):
deltax = randint(0,5)
deltay = randint(0,5)
self.canvas.move(self.ball, deltax, deltay)
self.canvas.after(50, self.move_piece)
class GameUI:
def __init__(self):
# initialize root Window and canvas
root = Tk()
root.title("Chess")
root.resizable(False,False)
canvas = Canvas(root, width = 300, height = 300)
canvas.pack()
# create two ball objects and animate them
bishop1 = Bishop(canvas, 10, 10, 'white')
bishop2 = Bishop(canvas, 60, 60, 'black')
bishop1.move_piece()
bishop2.move_piece()
root.mainloop()
app = GameUI()

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.

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
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.

tkinter Multiple Buttons Colour Change

I'm using tkinter to create a 8x8 button matrix, which when the individual buttons are pressed add to a final list (eg finalList = ((0,0),(5,7),(6,6), ...), allowing me to quickly create 8x8 (x,y) co-ordinate images. I have created the window with the buttons but now have issues trying to reference these buttons in a function to add to a list or even change the colour of the button
I have read that once the button is created and you create another it moves to that button reference. I suspect I need to use a dict or 2D array to store all these reference of buttons but am struggling to figure out a solution.
from tkinter import *
class App:
def updateChange(self):
'''
-Have the button change colour when pressed
-add coordinate to final list
'''
x , y = self.xY
self.buttons[x][y].configure(bg="#000000")
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.buttons = [] # Do I need to create a dict of button's so I can reference the particular button I wish to update?
for matrixColumn in range(8):
for matrixRow in range(8):
self.xY = (matrixColumn,matrixRow)
stringXY = str(self.xY)
self.button = Button(frame,text=stringXY, fg="#000000", bg="#ffffff", command = self.updateChange).grid(row=matrixRow,column=matrixColumn)
self.buttons[matrixColumn][matrixRow].append(self.button)
root = Tk()
app = App(root)
root.mainloop()
Example of the 8x8 Matrix
Below are 2 examples, the first is if you just want to change the colour and nothing else then you can do it without using a list. The second involves using a list and demonstrates what Delioth has pointed out
class App(object):
def __init__(self, master):
self._master = master
for col in range(8):
for row in range(8):
btn = tk.Button(master, text = '(%d, %d)' % (col, row), bg = 'white')
btn['command'] = lambda b = btn: b.config(bg = 'black')
btn.grid(row = row, column = col)
class App(object):
def __init__(self, master):
self._master = master
self._btn_matrix = []
for col in range(8):
row_matrix = []
for row in range(8):
btn = tk.Button(master, text = '(%d, %d)' % (col, row), bg = 'white',
command = lambda x = row, y = col: self.update(x, y))
btn.grid(row = row, column = col)
row_matrix.append(btn)
self._btn_matrix.append(row_matrix)
def update(self, row, col):
self._btn_matrix[col][row].config( bg = 'black' )
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
self.xY is set to 7,7 in your double for loop and never changed. If you want it to be different for each button, you may want to change updateChange to take two parameters (x,y), and pass them in as the command for the button using something like; lambda x=matrixColumn y=matrixRow: self.updateChange(x,y)
Example updateChange
def updateChange(self, x, y):
'''...'''
self.buttons[x][y].configure(bg="black")

Categories

Resources