多线程绘制植物时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()
解决方案
核心问题分析
- 线程安全问题:Tkinter不是线程安全的,所有GUI操作必须在主线程执行。当前代码中子线程直接调用
canvas.create_image(),引发渲染冲突导致闪烁。 - 图像更新逻辑缺陷:每次更新都创建新图像对象,且未清理旧图像,导致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
相关产品推荐
相关产品推荐

