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

提升后的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()

问题分析与解决方案

问题产生的原因

  1. 主线程被阻塞:调用lift()后,主线程立刻进入循环创建大量GroupNoImage组件,每个组件还调用update_idletasks(),这一系列同步操作占据了主线程所有资源,导致tkinter的事件循环无法及时处理Label的重绘事件——lift()仅标记了Label需要重绘,但主线程没有空闲执行重绘,所以先显示灰色默认背景,直到后续有空才完成绘制。
  2. update_idletasks()的局限性:该方法仅处理“空闲”状态的任务(如布局调整、重绘标记),但如果主线程被同步任务持续占用,即使调用了它,重绘事件也无法得到及时执行,因为后续的同步任务会立刻抢占主线程。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 18:14:55