如何避免 tkinter 应用程序中的多个线程

How to avoid many threads in a tkinter application

我目前正在构建一个 tkinter 应用程序。核心概念是用户必须点击方块。

正如您在图片中看到的那样,我们有一个正方形网格,用户可以从中选择一些。通过单击它们,用户应该会看到一个小动画,就像您在 gif 中看到的那样。

问题

在此 gif 中您可以看到问题所在。我的解决方案使用 python 的 multiprocessing 模块。但似乎由于我在动画过程中打开了许多线程,可视化速度变慢并停止运行我希望它运行的方式。

我的尝试很简单:

process = Process(target=self.anim,args=("someargs",))
process.run()

有没有一种方法可以将这些动画捆绑在一个进程中并避免使用多个线程,或者 python/tkinter 没有提供任何方法来解决我的问题?

感谢您的帮助。

试试这个:

import tkinter as tk

# Patially taken from: 
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()

这通过每 10 毫秒调度对 <Square>.fill 的调用来实现 tkinter 友好的 for 循环,直到半径为 20。然后它会填满整个正方形。

要测试代码,只需按 window 上的任意位置。也可以鼠标左右拖动


也用于清除方块:

import tkinter as tk

# Patially taken from: 
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()