英文:
How to avoid Tkinter slowing down as number of shapes increases?
问题
这是一个使用Python和Tkinter库创建的项目,它在每次迭代中绘制小的红色正方形。随着正方形数量的增加,您注意到Tkinter的速度会下降。您想知道是否Tkinter在每次迭代时都重新绘制整个画布,以及是否有方法可以避免这种减速。
这个问题需要深入了解Tkinter的内部工作机制,以及如何优化性能。建议查看Tkinter的文档以获取更多信息和优化建议。
英文:
I have a python project with tkinter. On this project I draw small squares over time.
I noticed tkinter is slowing down as the number of square increases.
Here is a simple example that draws 200 red squares on each iteration:
import tkinter as tk
import random
import time
WIDTH = 900
CELL_SIZE = 2
GRID_WIDTH = int(WIDTH / CELL_SIZE)
CELL_PER_ITERATION = 200
SLEEP_MS = 50
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=WIDTH, bg="black")
canvas.pack()
current_iteration = 0
cell_count = 0
previous_iteration_end = time.time()
text = tk.Label(root, text=f"iteration {current_iteration}")
text.pack()
def draw_cell(x_grid, y_grid):
x = x_grid * CELL_SIZE
y = y_grid * CELL_SIZE
canvas.create_rectangle(x, y, x + CELL_SIZE, y + CELL_SIZE, fill="red")
def iteration():
global current_iteration
global previous_iteration_end
global cell_count
current_iteration_start = time.time()
for _ in range(CELL_PER_ITERATION):
draw_cell(
x_grid=random.randint(0, GRID_WIDTH),
y_grid=random.randint(0, GRID_WIDTH),
)
cell_count += 1
current_iteration_end = time.time()
# duration of this iteration
current_iteration_duration = current_iteration_end - current_iteration_start
# duration between start of this iteration and end of previous iteration
between_iteration_duration = current_iteration_start - previous_iteration_end
current_iteration += 1
text.config(text=f"iteration {current_iteration} | cell_count: {cell_count} | iter duration: {int(current_iteration_duration*1000)} ms | between iter duration: {int(between_iteration_duration*1000)} ms")
previous_iteration_end = current_iteration_end
def main_loop():
iteration()
root.after(ms=SLEEP_MS, func=main_loop)
root.after(func=main_loop, ms=SLEEP_MS)
root.mainloop()
Which gives (time data is written at the bottom of picture):
So the time to execute an iteration stays constant. But between two iterations, the duration keeps increasing over time. I don't understand why tkinter is slowing down.
Is it redrawing the entire canvas (so all already drawn squares) at each iteration ? Is there a way to avoid this slow down ?
Note: This is an example, the real project i am working on looks like this: Slime Mold Simulation
答案1
得分: 2
当添加形状时,你不仅仅是给像素上色。你要创建带有上下文的图形,并将它们存储到内存中。不要滥用Canvas
来绘制图片,然后将这个图片显示在Canvas
上,这样会更快速,而且会给你更多选择来着色你想要绘制的图像。
英文:
When adding shapes, you are not just colorize pixels. You create graphics with contexts and store them into memory. Instead of abusing the Canvas
paint a picture and show this picture in the Canvas
this will be a lot faster and will give you more options to colorize your image that you rather want to draw.
答案2
得分: 2
以下是您提供的代码的翻译部分:
如@Thingamabobs所提到的,使用`tkinter.PhotoImage`会更好:
import tkinter as tk
import random
import time
WIDTH = 900
CELL_SIZE = 2
GRID_WIDTH = int(WIDTH / CELL_SIZE)
CELL_PER_ITERATION = 200
SLEEP_MS = 50
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=WIDTH, bg="black")
canvas.pack()
offscreen_buffer = tk.PhotoImage(width=WIDTH, height=WIDTH)
canvas.create_image((WIDTH // 2, WIDTH // 2), image=offscreen_buffer, state="normal")
current_iteration = 0
cell_count = 0
previous_iteration_end = time.time()
text = tk.Label(root, text=f"iteration {current_iteration}")
text.pack()
def draw_cell(x_grid, y_grid):
x = x_grid * CELL_SIZE
y = y_grid * CELL_SIZE
offscreen_buffer.put("red", to=(x, y, x + CELL_SIZE, y + CELL_SIZE))
def iteration():
global current_iteration
global previous_iteration_end
global cell_count
current_iteration_start = time.time()
for _ in range(CELL_PER_ITERATION):
draw_cell(
x_grid=random.randint(0, GRID_WIDTH),
y_grid=random.randint(0, GRID_WIDTH),
)
cell_count += 1
current_iteration_end = time.time()
# 这次迭代的持续时间
current_iteration_duration = current_iteration_end - current_iteration_start
# 上一次迭代结束后到这次迭代开始之间的持续时间
between_iteration_duration = current_iteration_start - previous_iteration_end
current_iteration += 1
text.config(
text=f"iteration {current_iteration} | cell_count: {cell_count} | iter duration: {int(current_iteration_duration * 1000)} ms | between iter duration: {int(between_iteration_duration * 1000)} ms"
)
previous_iteration_end = current_iteration_end
def main_loop():
iteration()
root.after(ms=SLEEP_MS, func=main_loop)
root.after(func=main_loop, ms=SLEEP_MS)
root.mainloop()
请注意,代码中的注释也已被翻译。
英文:
As @Thingamabobs mentioned, using tkinter.PhotoImage
would be better:
import tkinter as tk
import random
import time
WIDTH = 900
CELL_SIZE = 2
GRID_WIDTH = int(WIDTH / CELL_SIZE)
CELL_PER_ITERATION = 200
SLEEP_MS = 50
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=WIDTH, bg="black")
canvas.pack()
offscreen_buffer = tk.PhotoImage(width=WIDTH, height=WIDTH)
canvas.create_image((WIDTH // 2, WIDTH // 2), image=offscreen_buffer, state="normal")
current_iteration = 0
cell_count = 0
previous_iteration_end = time.time()
text = tk.Label(root, text=f"iteration {current_iteration}")
text.pack()
def draw_cell(x_grid, y_grid):
x = x_grid * CELL_SIZE
y = y_grid * CELL_SIZE
offscreen_buffer.put("red", to=(x, y, x + CELL_SIZE, y + CELL_SIZE))
def iteration():
global current_iteration
global previous_iteration_end
global cell_count
current_iteration_start = time.time()
for _ in range(CELL_PER_ITERATION):
draw_cell(
x_grid=random.randint(0, GRID_WIDTH),
y_grid=random.randint(0, GRID_WIDTH),
)
cell_count += 1
current_iteration_end = time.time()
# duration of this iteration
current_iteration_duration = current_iteration_end - current_iteration_start
# duration between start of this iteration and end of previous iteration
between_iteration_duration = current_iteration_start - previous_iteration_end
current_iteration += 1
text.config(
text=f"iteration {current_iteration} | cell_count: {cell_count} | iter duration: {int(current_iteration_duration * 1000)} ms | between iter duration: {int(between_iteration_duration * 1000)} ms")
previous_iteration_end = current_iteration_end
def main_loop():
iteration()
root.after(ms=SLEEP_MS, func=main_loop)
root.after(func=main_loop, ms=SLEEP_MS)
root.mainloop()
The above have a higher iter duration
than your implementation, but it is constant, and does not increase much. Same for the between iter duration
.
To get more insight than manual testing, I made two separate plot using matplotlib
, for your version and mine:
import tkinter as tk
import random
import time
import matplotlib.pyplot as plt
import psutil
WIDTH = 900
CELL_SIZE = 2
GRID_WIDTH = int(WIDTH / CELL_SIZE)
CELL_PER_ITERATION = 200
SLEEP_MS = 50
DURATION_MINUTES = 1
# Implementation 1 timing data
implementation1_between_iter_time = []
implementation1_iteration_duration = []
implementation1_ram_usage = []
root1 = tk.Tk()
canvas1 = tk.Canvas(root1, width=WIDTH, height=WIDTH, bg="black")
canvas1.pack()
current_iteration1 = 0
cell_count1 = 0
previous_iteration_end1 = time.time()
start_time1 = time.time()
text1 = tk.Label(root1, text=f"iteration {current_iteration1}")
text1.pack()
def draw_cell1(x_grid, y_grid):
x = x_grid * CELL_SIZE
y = y_grid * CELL_SIZE
canvas1.create_rectangle(x, y, x + CELL_SIZE, y + CELL_SIZE, fill="red")
def iteration1():
global current_iteration1
global previous_iteration_end1
global cell_count1
current_iteration_start = time.time()
for _ in range(CELL_PER_ITERATION):
draw_cell1(
x_grid=random.randint(0, GRID_WIDTH),
y_grid=random.randint(0, GRID_WIDTH),
)
cell_count1 += 1
current_iteration_end = time.time()
# duration of this iteration
current_iteration_duration = current_iteration_end - current_iteration_start
# duration between start of this iteration and end of previous iteration
between_iteration_duration = current_iteration_start - previous_iteration_end1
implementation1_between_iter_time.append(between_iteration_duration)
implementation1_iteration_duration.append(current_iteration_duration)
# RAM usage of the process
ram_usage = psutil.Process().memory_info().rss / 1024 ** 2 # in megabytes
implementation1_ram_usage.append(ram_usage)
current_iteration1 += 1
text1.config(
text=f"iteration {current_iteration1} | cell_count: {cell_count1} | iter duration: {int(current_iteration_duration*1000)} ms | between iter duration: {int(between_iteration_duration*1000)} ms"
)
previous_iteration_end1 = current_iteration_end
elapsed_time = time.time() - start_time1
if elapsed_time >= DURATION_MINUTES * 60:
root1.destroy()
def main_loop1():
iteration1()
root1.after(ms=SLEEP_MS, func=main_loop1)
root1.after(func=main_loop1, ms=SLEEP_MS)
root1.mainloop()
# Implementation 2 timing data
implementation2_between_iter_time = []
implementation2_iteration_duration = []
implementation2_ram_usage = []
root2 = tk.Tk()
canvas2 = tk.Canvas(root2, width=WIDTH, height=WIDTH, bg="black")
canvas2.pack()
offscreen_buffer2 = tk.PhotoImage(width=WIDTH, height=WIDTH)
canvas2.create_image((WIDTH // 2, WIDTH // 2), image=offscreen_buffer2, state="normal")
current_iteration2 = 0
cell_count2 = 0
previous_iteration_end2 = time.time()
start_time2 = time.time()
text2 = tk.Label(root2, text=f"iteration {current_iteration2}")
text2.pack()
def draw_cell2(x_grid, y_grid):
x = x_grid * CELL_SIZE
y = y_grid * CELL_SIZE
offscreen_buffer2.put("red", to=(x, y, x + CELL_SIZE, y + CELL_SIZE))
def iteration2():
global current_iteration2
global previous_iteration_end2
global cell_count2
current_iteration_start = time.time()
for _ in range(CELL_PER_ITERATION):
draw_cell2(
x_grid=random.randint(0, GRID_WIDTH),
y_grid=random.randint(0, GRID_WIDTH),
)
cell_count2 += 1
current_iteration_end = time.time()
# duration of this iteration
current_iteration_duration = current_iteration_end - current_iteration_start
# duration between start of this iteration and end of previous iteration
between_iteration_duration = current_iteration_start - previous_iteration_end2
implementation2_between_iter_time.append(between_iteration_duration)
implementation2_iteration_duration.append(current_iteration_duration)
# RAM usage of the process
ram_usage = psutil.Process().memory_info().rss / 1024 ** 2 # in megabytes
implementation2_ram_usage.append(ram_usage)
current_iteration2 += 1
text2.config(
text=f"iteration {current_iteration2} | cell_count: {cell_count2} | iter duration: {int(current_iteration_duration*1000)} ms | between iter duration: {int(between_iteration_duration*1000)} ms"
)
previous_iteration_end2 = current_iteration_end
elapsed_time = time.time() - start_time2
if elapsed_time >= DURATION_MINUTES * 60:
root2.destroy()
def main_loop2():
iteration2()
root2.after(ms=SLEEP_MS, func=main_loop2)
root2.after(func=main_loop2, ms=SLEEP_MS)
root2.mainloop()
# Plotting the data
plt.figure(figsize=(10, 5))
# Plotting "Between Iteration Time" for implementation 1
plt.subplot(211)
plt.plot([t * 1000 for t in implementation1_between_iter_time], label="Between Iteration - Implementation 1")
plt.plot([t * 1000 for t in implementation1_iteration_duration], label="Iteration Duration - Implementation 1")
plt.xlabel("Iteration")
plt.ylabel("Time (ms)")
plt.title("Timing - Implementation 1")
plt.legend()
# Plotting RAM Usage for implementation 1
ax2 = plt.twinx()
ax2.plot([t for t in implementation1_ram_usage], label="RAM Usage - Implementation 1", color="red")
ax2.set_ylabel("RAM Usage (MB)")
ax2.yaxis.label.set_color("red")
ax2.legend(loc="upper right")
# Plotting "Between Iteration Time" for implementation 2
plt.subplot(212)
plt.plot([t * 1000 for t in implementation2_between_iter_time], label="Between Iteration - Implementation 2")
plt.plot([t * 1000 for t in implementation2_iteration_duration], label="Iteration Duration - Implementation 2")
plt.xlabel("Iteration")
plt.ylabel("Time (ms)")
plt.title("Timing - Implementation 2")
plt.legend(loc="upper left")
# Plotting RAM Usage for implementation 2
ax3 = plt.twinx()
ax3.plot([t for t in implementation2_ram_usage], label="RAM Usage - Implementation 2", color="red")
ax3.set_ylabel("RAM Usage (MB)")
ax3.yaxis.label.set_color("red")
ax3.legend(loc="upper right")
# Display the plot
plt.tight_layout()
plt.show()
This has three axis, one for iter duration
and between iter duration
, and a third one for ram usage. I set the maximum duration of each implementation to 1 minute.
Here is the image of the dual plot:
As you can see in the image (and by manual testing), the ram usage is lower as time advance on the second implementation compared to yours. It is slightly lower when starting for the first implementation, however...
The between iter duration
and iter duration
are constantly staying at a minimum over time on the second implementation, while the first implementation have it's between iter duration
and ram usage increasing exponentially over time, and it's iter duration
staying around 0/1ms at all time.
EDIT: Following the suggestion of @Thingamabobs in the comment, here is a slightly better version of implementation2:
def iteration2():
global current_iteration2
global previous_iteration_end2
global cell_count2
current_iteration_start = time.time()
cells_to_draw = []
for _ in range(CELL_PER_ITERATION):
x_grid = random.randint(0, GRID_WIDTH)
y_grid = random.randint(0, GRID_WIDTH)
cells_to_draw.append((x_grid, y_grid))
for x_grid, y_grid in cells_to_draw:
draw_cell2(x_grid, y_grid)
cell_count2 += CELL_PER_ITERATION
current_iteration_end = time.time()
# duration of this iteration
current_iteration_duration = current_iteration_end - current_iteration_start
# duration between start of this iteration and end of previous iteration
between_iteration_duration = current_iteration_start - previous_iteration_end2
implementation2_between_iter_time.append(between_iteration_duration)
implementation2_iteration_duration.append(current_iteration_duration)
# RAM usage of the process
ram_usage = psutil.Process().memory_info().rss / 1024 ** 2 # in megabytes
implementation2_ram_usage.append(ram_usage)
current_iteration2 += 1
text2.config(
text=f"iteration {current_iteration2} | cell_count: {cell_count2} | iter duration: {int(current_iteration_duration*1000)} ms | between iter duration: {int(between_iteration_duration*1000)} ms"
)
previous_iteration_end2 = current_iteration_end
elapsed_time = time.time() - start_time2
if elapsed_time >= DURATION_MINUTES * 60:
root2.destroy()
And the resulting plot:
The ram usage seems to be lower or a bit more stable in it's increase.
答案3
得分: 1
"有很多事情你无法做。当画布上有数万个对象时,画布已知存在性能问题。"
英文:
There's not much you can do. The canvas has known performance problems when there are tens of thousands of objects on the canvas.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论