如何避免Tkinter在形状数量增加时变慢?

huangapple go评论77阅读模式
英文:

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):
如何避免Tkinter在形状数量增加时变慢?

And after a few seconds:
如何避免Tkinter在形状数量增加时变慢?

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:
如何避免Tkinter在形状数量增加时变慢?

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:
如何避免Tkinter在形状数量增加时变慢?
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.

huangapple
  • 本文由 发表于 2023年5月20日 23:45:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/76296055.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定