I am currently building a tkinter
application. The core concept is that the user has to click on squares.
Like you can see in the picture we have a grid of squares from which the user can choose some. By clicking on them the user should see a small animation like you can see in the gif.
Problem
In this gif you can see the problem. My solution uses the multiprocessing
module of python. But it seems because of the many threads I open in the animation process the visualization slows down and stops functioning how i would like it to function.
My attempt is quite simple:
process = Process(target=self.anim,args=("someargs",))
process.run()
Is there a way to bundle those animations in one process and avoid many threads or is python
/tkinter
not providing any way to solve my issue?
Thank you for your help.
CodePudding user response:
Try this:
import tkinter as tk
# Patially taken from: https://stackoverflow.com/a/17985217/11106801
def create_circle(self, x:int, y:int, r:int, **kwargs) -> int:
return self.create_oval(x-r, y-r, x r, y r, **kwargs)
def resize_circle(self, id:int, x:int, y:int, r:int) -> None:
self.coords(id, x-r, y-r, x r, y r)
tk.Canvas.create_circle = create_circle
tk.Canvas.resize_circle = resize_circle
# Defining constants:
WIDTH:int = 400
HEIGHT:int = 400
SQUARES_WIDTH:int = 40
SQUARES_HEIGHT:int = 40
# Each square will be it's own class to make it easier to work with
class Square:
# This can cause problems for people that don't know `__slots__`
# __slots__ = ("canvas", "id", "x", "y", "filled")
def __init__(self, canvas:tk.Canvas, x:int, y:int):
self.canvas:tk.Canvas = canvas
self.x:int = x
self.y:int = y
self.id:int = None
self.filled:bool = False
def fill(self, for_loop_counter:int=0) -> None:
"""
This implements a tkinter friendly for loop with a delay of
10 milliseconds. It creates a grows a circle to `radius = 20`
"""
# If the square is already filled jsut return
if self.filled:
return None
x:int = self.x SQUARES_WIDTH // 2
y:int = self.y SQUARES_WIDTH // 2
# If this is the first time, create the circle
if for_loop_counter == 0:
self.id:int = self.canvas.create_circle(x, y, 0, outline="", fill="black")
# Grow the cicle
else:
self.canvas.resize_circle(self.id, x, y, for_loop_counter)
# If we reach the highest radius:
if for_loop_counter == 20:
self.fill_square()
# Otherwise call `self.fill` in 10 milliseconds with
# `for_loop_counter 1` as a parameter
else:
self.canvas.after(10, self.fill, for_loop_counter 1)
def fill_square(self) -> None:
"""
Removed the circle and fills in the square
"""
self.canvas.delete(self.id)
x2:int = self.x SQUARES_WIDTH
y2:int = self.y SQUARES_HEIGHT
self.id = self.canvas.create_rectangle(self.x, self.y, x2, y2, fill="black", outline="")
self.filled:bool = True
class App:
# This can cause problems for people that don't know `__slots__`
# __slots__ = ("root", "canvas", "squares")
def __init__(self):
self.root:tk.Tk = tk.Tk()
self.canvas:tk.Canvas = tk.Canvas(self.root, width=WIDTH, height=HEIGHT)
self.canvas.pack()
# Create the squares:
self.squares:list[Square] = []
for x in range(0, WIDTH, SQUARES_WIDTH):
for y in range(0, HEIGHT, SQUARES_HEIGHT):
square:Square = Square(self.canvas, x, y)
self.squares.append(square)
self.canvas.bind("<Button-1>", self.on_mouse_clicked)
self.canvas.bind("<B1-Motion>", self.on_mouse_clicked)
def on_mouse_clicked(self, event:tk.Event) -> None:
# Search for the square that was pressed
mouse_x:int = event.x
mouse_y:int = event.y
for square in self.squares:
if 0 < mouse_x - square.x < SQUARES_WIDTH:
if 0 < mouse_y - square.y < SQUARES_HEIGHT:
# Tell that square that it should fill itself
square.fill()
return None
def mainloop(self) -> None:
self.root.mainloop()
if __name__ == "__main__":
app = App()
app.mainloop()
This implements a tkinter
friendly for loop by scheduling calls to <Square>.fill
every 10 milliseconds until the radius is 20. Then it fills the whole square.
To test the code, just press anywhere on the window. You can also drag the mouse around.
For clearing squares as well:
import tkinter as tk
# Patially taken from: https://stackoverflow.com/a/17985217/11106801
def create_circle(self, x:int, y:int, r:int, **kwargs) -> int:
return self.create_oval(x-r, y-r, x r, y r, **kwargs)
def resize_circle(self, id:int, x:int, y:int, r:int) -> None:
self.coords(id, x-r, y-r, x r, y r)
tk.Canvas.create_circle = create_circle
tk.Canvas.resize_circle = resize_circle
# Defining constants:
WIDTH:int = 400
HEIGHT:int = 400
SQUARES_WIDTH:int = 40
SQUARES_HEIGHT:int = 40
# Each square will be it's own class to make it easier to work with
class Square:
# This can cause problems for people that don't know `__slots__`
# __slots__ = ("canvas", "id", "x", "y", "filled")
def __init__(self, canvas:tk.Canvas, x:int, y:int):
self.canvas:tk.Canvas = canvas
self.x:int = x
self.y:int = y
self.id:int = None
self.filled:bool = False
def fill(self, for_loop_counter:int=0) -> None:
"""
This implements a tkinter friendly for loop with a delay of
10 milliseconds. It creates a grows a circle to `radius = 20`
"""
x:int = self.x SQUARES_WIDTH // 2
y:int = self.y SQUARES_WIDTH // 2
# If this is the first time, create the circle
if for_loop_counter == 0:
# If the square is already filled just return
if self.filled:
return None
self.filled:bool = True
self.id:int = self.canvas.create_circle(x, y, 0, outline="", fill="black")
# User wants to clear the square
elif self.id is None:
return None
# Grow the cicle
else:
self.canvas.resize_circle(self.id, x, y, for_loop_counter)
# If we reach the highest radius:
if for_loop_counter == 20:
self.fill_square()
# Otherwise call `self.fill` in 10 milliseconds with
# `for_loop_counter 1` as a parameter
else:
self.canvas.after(10, self.fill, for_loop_counter 1)
def fill_square(self) -> None:
"""
Removed the circle and fills in the square
"""
x2:int = self.x SQUARES_WIDTH
y2:int = self.y SQUARES_HEIGHT
self.canvas.delete(self.id)
self.id = self.canvas.create_rectangle(self.x, self.y, x2, y2, fill="black", outline="")
def clear(self) -> None:
"""
Clears the square
"""
self.filled:bool = False
self.canvas.delete(self.id)
self.id:int = None
class App:
# This can cause problems for people that don't know `__slots__`
__slots__ = ("root", "canvas", "squares")
def __init__(self):
self.root:tk.Tk = tk.Tk()
self.canvas:tk.Canvas = tk.Canvas(self.root, width=WIDTH, height=HEIGHT)
self.canvas.pack()
# Create the squares:
self.squares:list[Square] = []
for x in range(0, WIDTH, SQUARES_WIDTH):
for y in range(0, HEIGHT, SQUARES_HEIGHT):
square:Square = Square(self.canvas, x, y)
self.squares.append(square)
self.canvas.bind("<Button-1>", self.on_mouse_clicked)
self.canvas.bind("<B1-Motion>", self.on_mouse_clicked)
self.canvas.bind("<Button-3>", self.on_mouse_clicked)
self.canvas.bind("<B3-Motion>", self.on_mouse_clicked)
def on_mouse_clicked(self, event:tk.Event) -> None:
# Search for the square that was pressed
mouse_x:int = event.x
mouse_y:int = event.y
for square in self.squares:
if 0 < mouse_x - square.x < SQUARES_WIDTH:
if 0 < mouse_y - square.y < SQUARES_HEIGHT:
# If the right mouse button is pressed
if (event.state & 1024 != 0) or (event.num == 3):
# Tell that square that it should clear itself
square.clear()
else:
# Tell that square that it should fill itself
square.fill()
return None
def mainloop(self) -> None:
self.root.mainloop()
if __name__ == "__main__":
app = App()
app.mainloop()