Home > Net >  How to avoid many threads in a tkinter application
How to avoid many threads in a tkinter application

Time:09-17

I am currently building a tkinter application. The core concept is that the user has to click on squares.

enter image description here

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.

enter image description here

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()
  • Related