You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程绘制植物时Tkinter Canvas严重闪烁问题求助

植物同步绘制的Tkinter卡顿与闪烁问题解决

问题背景

正在实现植物同步绘制功能:Plant类包含get_circles()方法(返回生成器对象,每次迭代输出若干圆形)与is_growing()方法(返回植物生长状态)。

阶段1:after()方法的卡顿问题

最初使用after()实现绘制,但同时绘制两个植物时,程序会卡顿至其中一个植物绘制完成。核心代码如下:

def start_drawing(self):
    """
    Start drawing and generation of plant

    1. Stop all previous drawing (if exist)
    2. Get new plant
    3. Start drawing and generating new plant
    """
    try:
        if self.current_drawing:
            self.after_cancel(self.current_drawing)
        self.clear_canvas()
        self.progress_var.set(0)
        plant = self.controller.user_frame.get_plant()
        self.current_drawing = self.after(1, self.draw, plant)
    except:
        ...

def draw(self, plant: Plant):
    """
    Draw plant while it is growing
    """
    for circle in plant.get_circles():
        self.draw_circle(circle)
    self.update_progress(plant.drawed / plant.total * 100)
    self.update_canvas()

    if plant.is_growing():
        self.current_drawing = self.after(1, self.draw, plant)
    else:
        self.update_progress(100.0)
        self.current_drawing = None

阶段2:多线程改造后的闪烁问题

改用threading模块解决了卡顿,但Canvas组件出现严重闪烁。尝试过用Label替代Canvas、重新配置Canvas图片,均未解决问题。当前多线程实现代码如下:

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageDraw, ImageTk

import time
from threading import Thread, Event

from tools import Color, Circle, Vec2
from plant_generator import Plant


class StoppableThread(Thread):
    def __init__(self):
        super().__init__()
        self._stop_event = Event()

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()


class ThreadPainter(StoppableThread):
    def __init__(self, canvas, image, draw, plant):
        super().__init__()
        self.canvas = canvas
        self.image = image
        self.draw = draw
        self.plant = plant
        self.delay = 0.3

    def clear_canvas(self):
        """
        Clear image with plant and update canvas
        """
        self.image = Image.new("RGB",
                               (600, 600),
                               (255, 255, 255)) 
        self.draw = ImageDraw.Draw(self.image)
        self.update_canvas()

    def update_canvas(self):
        """
        Show image on canvas
        """
        self.canvas.image = ImageTk.PhotoImage(self.image)
        self.canvas.create_image(600 // 2, 600 // 2,
                                 anchor=tk.CENTER, image=self.canvas.image)

    def draw_circle(self, circle: Circle):
        """
        Draw circle on image

        Draw 3 circles, main, darker and lighter
        for 3D effect
        """
        width = 600
        height = 600
        x, y = circle.pos + Vec2(width // 2, height // 2)
        if x < 0 or x > width or y < 0 or y > height:
            return
        r = abs(circle.radius) + 1

        x0, y0 = x - r, y - r
        x1, y1 = x + r, y + r

        default_color = circle.color
        dark_color = circle.color + Color(20, 20, 20)
        light_color = circle.color - Color(20, 20, 20)
        self.draw.ellipse((x0, y0, x1, y1),
                                fill=light_color.rgb)
        self.draw.ellipse((x0 - 1, y0 - 1, x1 - 1, y1 - 1),
                                fill=default_color.rgb)
        self.draw.ellipse((x0 + 1, y0 + 1, x1 + 1, y1 + 1),
                                fill=dark_color.rgb)

    def run(self):
        self.clear_canvas()
        while self.plant.is_growing() and not self.stopped():
            for circle in self.plant.get_circles():
                self.draw_circle(circle)
            time.sleep(self.delay) 
            self.update_canvas()


class DrawPlantFrame(ttk.Frame):
    def __init__(self, container):
        super().__init__(container)
        self.canvas_width = 600
        self.canvas_height = 600

        self.canvas = tk.Canvas(self, 
                                width=self.canvas_width,
                                height=self.canvas_height,
                                bg="white")
        self.start_button = tk.Button(self,
                                      text="Start", height=2,
                                      command=self.start_drawing)

        self.background = (250, 250, 250)
        self.plant_image = Image.new("RGBA",
                                     (self.canvas_width, self.canvas_height),
                                     self.background)
        self.plant_draw = ImageDraw.Draw(self.plant_image)
        self.current_drawing = None

        self.canvas.grid(row=0, column=0)
        self.start_button.grid(row=1, column=0, sticky="ew")
    
    def start_drawing(self):
        if self.current_drawing:
            self.current_drawing.stop()

        self.current_drawing = ThreadPainter(self.canvas,
                                            self.plant_image,
                                            self.plant_draw,
                                            Plant.random())
        self.current_drawing.start()


class MainWindow(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setup_window()
        self.setup_widgets()

    def setup_window(self):
        """
        Set title ans size of window
        """
        self.title("Threading Drawing")

        self.geometry("1280x800")

    def setup_widgets(self):
        """
        Setup frames with plants 
        """
        self.plant1 = DrawPlantFrame(self) 
        self.plant2 = DrawPlantFrame(self)

        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.rowconfigure(0, weight=1)

        self.plant1.grid(row=0, column=0, padx=10, pady=10)
        self.plant2.grid(row=0, column=1, padx=10, pady=10)


if __name__ == "__main__":
    app = MainWindow()
    app.mainloop()

解决方案

核心问题分析

  1. 线程安全问题:Tkinter不是线程安全的,所有GUI操作必须在主线程执行。当前代码中子线程直接调用canvas.create_image(),引发渲染冲突导致闪烁。
  2. 图像更新逻辑缺陷:每次更新都创建新图像对象,且未清理旧图像,导致Canvas反复重绘时出现闪烁。

修复步骤

1. 将GUI操作移回主线程

使用Tkinter的after()方法,在子线程中调度主线程执行GUI更新:

def update_canvas(self):
    # 调度主线程执行更新
    self.canvas.after(0, self._update_canvas_in_main)

def _update_canvas_in_main(self):
    # 仅在主线程执行GUI操作
    self.canvas.image = ImageTk.PhotoImage(self.image)
    # 先清除旧图像再绘制新图像
    self.canvas.delete("all")
    self.canvas.create_image(600 // 2, 600 // 2,
                             anchor=tk.CENTER, image=self.canvas.image)

2. 优化图像清空逻辑

复用现有图像对象,避免频繁创建新图像:

def clear_canvas(self):
    # 填充白色清空图像
    self.draw.rectangle((0, 0, 600, 600), fill=(255,255,255))
    self.update_canvas()

3. 线程安全的植物状态访问

给Plant类的方法调用添加锁,避免多线程数据访问冲突:

from threading import Lock

class ThreadPainter(StoppableThread):
    def __init__(self, canvas, image, draw, plant):
        super().__init__()
        # ... 其他初始化代码
        self.plant_lock = Lock()

    def run(self):
        self.clear_canvas()
        while not self.stopped():
            with self.plant_lock:
                if not self.plant.is_growing():
                    break
                circles = list(self.plant.get_circles())
            for circle in circles:
                self.draw_circle(circle)
            time.sleep(self.delay) 
            self.update_canvas()

4. 启用Canvas双缓冲

创建Canvas时添加配置,减少闪烁:

self.canvas = tk.Canvas(self, 
                        width=self.canvas_width,
                        height=self.canvas_height,
                        bg="white",
                        bd=0,
                        highlightthickness=0)

修复后的核心代码片段

class ThreadPainter(StoppableThread):
    def __init__(self, canvas, image, draw, plant):
        super().__init__()
        self.canvas = canvas
        self.image = image
        self.draw = draw
        self.plant = plant
        self.delay = 0.3
        self.plant_lock = Lock()

    def clear_canvas(self):
        self.draw.rectangle((0, 0, 600, 600), fill=(255,255,255))
        self.update_canvas()

    def update_canvas(self):
        self.canvas.after(0, self._update_canvas_in_main)

    def _update_canvas_in_main(self):
        self.canvas.image = ImageTk.PhotoImage(self.image)
        self.canvas.delete("all")
        self.canvas.create_image(600 // 2, 600 // 2,
                                 anchor=tk.CENTER, image=self.canvas.image)

    # draw_circle方法保持不变

    def run(self):
        self.clear_canvas()
        while not self.stopped():
            with self.plant_lock:
                if not self.plant.is_growing():
                    break
                circles = list(self.plant.get_circles())
            for circle in circles:
                self.draw_circle(circle)
            time.sleep(self.delay) 
            self.update_canvas()

内容的提问来源于stack exchange,提问作者killer whale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 15:30:54