【问题标题】:How to avoid many threads in a tkinter application如何避免 tkinter 应用程序中的多个线程
【发布时间】:2021-11-02 13:12:09
【问题描述】:

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

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

问题

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

我的尝试很简单:

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

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

感谢您的帮助。

【问题讨论】:

  • 查看.after 脚本。此外,如果您将多个线程与tkinter 一起使用,则可能会导致python 崩溃而没有错误回溯。 This 是制作 tkinter for 循环的正确方法,有延迟
  • 似乎您可以在一个线程中执行此操作,而不是每平方一个线程。诀窍是让每个动画在等待下一帧时产生 - 你没有分享你的 anim 代码所以我不得不猜测,但我假设你目前在帧之间拥有每个线程 sleep(),这意味着在动画完成之前不会释放线程(将同时动画的数量限制为进程池的大小,这基于您的屏幕截图看起来像 1)。
  • @Samwise 你是对的,我正在使用sleep() 方法。我也会试试的。你知道如何在 tkinter 中获取帧吗?
  • 多处理和多线程对于这个问题来说太过分了。
  • @PucciLaCanton:after 方法不会更新画布。通过after 调用函数之间的事件循环发生更新。

标签: python multithreading animation tkinter


【解决方案1】:

试试这个:

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

这实现了一个 tkinter 友好的 for 循环,每 10 毫秒调度一次对 &lt;Square&gt;.fill 的调用,直到半径为 20。然后它会填满整个正方形。

要测试代码,只需按窗口上的任意位置。您也可以拖动鼠标。


也用于清除方块:

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

【讨论】:

  • @PucciLaCanton 我更新了我的答案,解释了它是如何工作的。有关更多详细信息,请查看代码中的 cmets。此外,我没有实现应该分隔不同方块的线条。
  • 非常感谢。我现在完全理解了循环后的 tkinter。我尝试使用 after 方法非常慢的原因是因为我在更改正方形后更新了画布,这不是很有效,因为 after 方法已经更新了画布。
  • 顺便说一句,App 甚至需要__slots__ 吗? (第一次了解它们(从您的回答中了解它们))据我了解,它们为这些属性在内存中保留空间,因此如果创建多个实例会很有用,但 App() 通常只会创建一次所以节省的空间不会那么多?
  • @Matiiss 我最近开始使用__slots__。从技术上讲它是无用的,但它让读者知道我将使用什么变量。它还可以防止我出现一些拼写错误,因为 python 会引发错误。此外,如果有人想将此代码移植到 C/C++ 之类的语言中,您需要在其中指定类属性,这样会更容易。我不使用__slots__ 来减少内存。
  • @PucciLaCanton .after 实际上并没有更新画布。 .mainloop() 处理 .after 预定调用和重绘小部件。当.after 完成后,它返回到.mainloop,它像往常一样重绘画布。但实际上,只要 .after 中的超时时间不为 0,您所说的内容与实际发生的情况相同。尝试将其更改为 0,将 20 更改为 20000,看看会发生什么。
猜你喜欢
  • 2017-09-10
  • 1970-01-01
  • 1970-01-01
  • 2021-06-24
  • 1970-01-01
  • 1970-01-01
  • 2014-01-31
  • 1970-01-01
相关资源
最近更新 更多