如何避免 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()
我目前正在构建一个 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()