提升后的ttk.Label组件无法及时重绘?求替代update()的方案
tkinter中ttk.Label提升后无法及时重绘的问题
问题描述
我用多线程创建带图片的ttk.Button组件时遇到一个问题:每当self.label被lift()提升后,它无法及时重绘,会先显示一块灰色区域,之后才完全加载出内容。调用self.update_idletasks()无法解决该问题,只有调用self.update()能修复,但有观点认为update()存在风险。想知道能否不使用self.update()解决问题,以及该问题产生的原因。
问题演示

预期效果

最小可复现示例(MRE)
将任意.jpg文件保存到脚本同一目录,命名为testimage.jpg。GUI操作方式:点击Run按钮启动多线程;如需重新运行,需先点击Reset按钮,再点击Run按钮。请勿在线程运行时点击Reset,反之亦然。
# Python modules import tkinter as tk import tkinter.ttk as ttk import concurrent.futures as cf import queue import threading from itertools import repeat import random from time import sleep # External modules from PIL import Image, ImageTk def get_thumbnail_c(gid: str, fid: str, fpath: str, psize=(100, 100)): # print(f"{threading.main_thread()=} {threading.current_thread()=}") with Image.open(fpath) as img: img.load() img.thumbnail(psize) return gid, fid, img def get_thumbnails_concurrently_with_queue( g_ids: list, f_ids: list, f_paths: list, rqueue: queue.Queue, size: tuple): futures = [] job_fn = get_thumbnail_c with cf.ThreadPoolExecutor() as vp_executor: for gid, fids, fpath in zip(g_ids, f_ids, f_paths): for gg, ff, pp in zip(repeat(gid, len(fids)), fids, repeat(fpath, len(fids))): job_args = gg, ff, pp, size futures.append(vp_executor.submit(job_fn, *job_args)) for future in cf.as_completed(futures): rqueue.put(("thumbnail", future.result())) futures.remove(future) if not futures: print(f'get_thumbnails_concurrently has completed!') rqueue.put(("completed", ())) class GroupNoImage(ttk.Frame): def __init__(self, master, gid, fids): super().__init__(master, style='gframe.TFrame') self.bns = {} self.imgs = {} for i, fid in enumerate(fids): self.bns[fid] = ttk.Button(self, text=f"{gid}-P{i}", compound="top", style="imgbns.TButton") self.bns[fid].grid(row=0, column=i, stick="nsew") class App(ttk.PanedWindow): def __init__(self, master, **options): super().__init__(master, **options) self.master = master self.groups = {} self.rqueue = queue.Queue() self.vsf = ttk.Frame(self) self.add(self.vsf) self.label = ttk.Label( self, style="label.TLabel", width=7, anchor="c", text="ttk.Label", font=('Times', '70', '')) self.label.place( relx=0.5, rely=0.5, relwidth=.8, relheight=.8, anchor="center", in_=self.vsf) self.label.lower(self.vsf) def create_grpsframe(self): self.grpsframe = ttk.Frame(self.vsf, style='grpsframe.TFrame') self.grpsframe.grid(row=0, column=0, sticky="nsew") def run(self, event): self.create_grpsframe() gids = [f"G{i}" for i in range(50)] random.seed() fids = [] for gid in gids: f_ids = [] total = random.randint(2,10) for i in range(total): f_ids.append(f"{gid}-P{i}" ) fids.append(f_ids) fpaths = ["testimage.jpg" for i in range(len(gids))] self.create_groups_concurrently(gids, fids, fpaths) def reset(self, event): self.grpsframe.destroy() self.groups.clear() def create_groups_concurrently(self, gids, fids, fpaths): print(f"\ncreate_groups_concurrently") self.label.lift(self.vsf) # self.update_idletasks() # Can't fix self.label appearance issue # self.update() # Fixed self.label appearance issue for i, (gid, f_ids) in enumerate(zip(gids, fids)): self.groups[gid] = GroupNoImage(self.grpsframe, gid, f_ids) self.groups[gid].grid(row=i, column=0, sticky="nsew") self.update_idletasks() # sleep(3) print(f"\nStart thread-queue") jthread = threading.Thread( target=get_thumbnails_concurrently_with_queue, args=(gids, fids, fpaths, self.rqueue, (100,100)), name="jobthread") jthread.start() self.check_rqueue() def check_rqueue(self): # print(f"\ndef _check_thread(self, thread, start0):") duration = 1 # millisecond try: info = self.rqueue.get(block=False) # print(f"{info=}") except queue.Empty: self.after(1, lambda: self.check_rqueue()) else: match info[0]: case "thumbnail": gid, fid, img = info[1] print(f"{gid=} {fid=}") grps = self.groups grps[gid].imgs[fid] = ImageTk.PhotoImage(img) grps[gid].bns[fid]["image"] = grps[gid].imgs[fid] self.update_idletasks() self.after(duration, lambda: self.check_rqueue()) case "completed": print(f'Completed') self.label.lower(self.vsf) class ButtonGroups(ttk.Frame): def __init__(self, master, **options): super().__init__(master, style='bnframe.TFrame', **options) self.master = master self.bnrun = ttk.Button( self, text="Run", width=10, style='bnrun.TButton') self.bnreset = ttk.Button( self, text="Reset", width=10, style='bnreset.TButton') self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.bnrun.grid(row=0, column=0, sticky="nsew") self.bnreset.grid(row=0, column=1, sticky="nsew") if __name__ == "__main__": root = tk.Tk() root.geometry('1300x600') root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) ss = ttk.Style() ss.theme_use('default') ss.configure(".", background="gold") ss.configure("TPanedwindow", background="red") ss.configure('grpsframe.TFrame', background='green') ss.configure('gframe.TFrame', background='yellow') ss.configure('imgbns.TButton', background='orange') ss.configure("label.TLabel", background="cyan") ss.configure('bnframe.TFrame', background='white') ss.configure('bnrun.TButton', background='violet') ss.configure('bnreset.TButton', background='green') app = App(root) bns = ButtonGroups(root) app.grid(row=0, column=0, sticky="nsew") bns.grid(row=1, column=0, sticky="nsew") bns.bnrun.bind("<B1-ButtonRelease>", app.run) bns.bnreset.bind("<B1-ButtonRelease>", app.reset) root.mainloop()
问题分析与解决方案
问题产生的原因
- 主线程被阻塞:调用
lift()后,主线程立刻进入循环创建大量GroupNoImage组件,每个组件还调用update_idletasks(),这一系列同步操作占据了主线程所有资源,导致tkinter的事件循环无法及时处理Label的重绘事件——lift()仅标记了Label需要重绘,但主线程没有空闲执行重绘,所以先显示灰色默认背景,直到后续有空才完成绘制。 update_idletasks()的局限性:该方法仅处理“空闲”状态的任务(如布局调整、重绘标记),但如果主线程被同步任务持续占用,即使调用了它,重绘事件也无法得到及时执行,因为后续的同步任务会立刻抢占主线程。update()的风险:update()会强制处理所有待处理事件(包括重绘、用户输入等),因此能立刻让Label重绘,但它可能打乱正常的事件队列顺序,甚至触发无限循环,引发不可预测的GUI行为。
不用update()的解决方案
核心思路是将耗时的同步任务拆分,让事件循环有时间处理重绘,以下是两种可行方案:
方案1:用after()分批创建组件
把循环创建组件的逻辑拆分成多个小任务,通过after()让每个任务在事件循环的下一个周期执行,给主线程留出处理重绘的时间:
def create_groups_concurrently(self, gids, fids, fpaths): print(f"\ncreate_groups_concurrently") self.label.lift(self.vsf) # 分批创建组件,给事件循环留处理重绘的时间 def create_next_group(index=0): if index < len(gids): gid, f_ids = gids[index], fids[index] self.groups[gid] = GroupNoImage(self.grpsframe, gid, f_ids) self.groups[gid].grid(row=index, column=0, sticky="nsew") # 1ms后执行下一个组件创建任务 self.after(1, create_next_group, index+1) else: print(f"\nStart thread-queue") jthread = threading.Thread( target=get_thumbnails_concurrently_with_queue, args=(gids, fids, fpaths, self.rqueue, (100,100)), name="jobthread") jthread.start() self.check_rqueue() create_next_group()
方案2:用after_idle()确保重绘完成后再执行任务
利用after_idle()等待事件循环空闲(即Label重绘完成)后,再执行组件创建任务:
def create_groups_concurrently(self, gids, fids, fpaths): print(f"\ncreate_groups_concurrently") self.label.lift(self.vsf) # 等待事件循环空闲(Label重绘完成)后执行后续任务 def start_after_redraw(): for i, (gid, f_ids) in enumerate(zip(gids, fids)): self.groups[gid] = GroupNoImage(self.grpsframe, gid, f_ids) self.groups[gid].grid(row=i, column=0, sticky="nsew") self.update_idletasks() print(f"\nStart thread-queue") jthread = threading.Thread( target=get_thumbnails_concurrently_with_queue, args=(gids, fids, fpaths, self.rqueue, (100,100)), name="jobthread") jthread.start() self.check_rqueue() self.after_idle(start_after_redraw)
方案说明
- 方案1适合大量组件的创建场景,拆分任务后GUI不会卡顿,Label也能及时显示。
- 方案2适合组件数量较少的场景,确保Label完全重绘后再执行同步任务,避免主线程被立刻占用。
两种方案都基于tkinter的事件循环机制,无需调用update()即可解决Label显示灰色的问题。
内容的提问来源于stack exchange,提问作者Sun Bear
相关产品推荐
相关产品推荐

