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

Tkinter+Pillow窗口缩放时图片卡顿,多线程实现报错求助

问题:Tkinter窗口Resize时背景图缩放卡顿+子线程报错

在Tkinter中实现带背景图的Frame,窗口调整大小、Frame随之改变时,图片缩放过程卡顿,尝试将缩放逻辑放到子线程执行后,出现AttributeError: 'NoneType' object has no attribute 'read'错误。

原代码

from tkinter import Tk, Button, Label, Frame
from tkinter import ttk, PhotoImage
import threading
import time
from PIL import Image, ImageTk

class MainWindow(Tk):
    def __init__(self):
        super().__init__()

        # 创建groupbox1背景图
        self.gb1_img = Image.open('imgs/groupbox-1.png')
        self.gb1_img = self.gb1_img.resize((260, 490), Image.Resampling.LANCZOS)
        self.gb1_img = ImageTk.PhotoImage(self.gb1_img)

        self.gb1_container = Frame(self, bg='#333333', width=280)
        self.gb1_container.pack(side='left', fill='y', padx=20, pady=(120, 20))
        self.gb1_container.bind('<Configure>', self.start_resize_gb1)

        self.gb1 = Label(self.gb1_container, image=self.gb1_img, bg='#333333')
        self.gb1.place(x=0, y=0)

    def start_resize_gb1(self, e):
        t = threading.Thread(target=self.resize_gb1, args=(e,), daemon=1)
        t.start()

    # 缩放groupbox1背景图
    def resize_gb1(self, e):
        self.gb1_img1 = Image.open('imgs/groupbox-1.png')
        self.gb1_img1 = self.gb1_img1.resize((e.width, e.height), Image.Resampling.LANCZOS)
        self.gb1_img1 = ImageTk.PhotoImage(self.gb1_img1)
        self.gb1.configure(image=self.gb1_img1)

if __name__ == '__main__':
    app = MainWindow()
    app.title('Main Window')
    app.configure(bg='#333333')

    # 窗口居中
    w = 1000  # 宽度
    h = 620  # 高度
    x = (app.winfo_screenwidth() - w) // 2
    y = (app.winfo_screenheight() - h) // 2
    app.geometry('%dx%d+%d+%d' % (w, h, x, y))
    app.mainloop()

报错信息

Exception in thread Thread-57 (resize_gb1):
Traceback (most recent call last):
  File "C:\Users\Aaron\AppData\Local\Programs\Python\Python310\lib\threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "C:\Users\Aaron\AppData\Local\Programs\Python\Python310\lib\threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Users\Aaron\Documents\Python\Project1\testing\MAIN.py", line 92, in resize_gb1
    self.gb1_img1 = ImageTk.PhotoImage(self.gb1_img1)
  File "C:\Users\Aaron\AppData\Roaming\Python\Python310\site-packages\PIL\ImageTk.py", line 143, in __init__
    self.paste(image)
  File "C:\Users\Aaron\AppData\Roaming\Python\Python310\site-packages\PIL\ImageTk.py", line 195, in paste
    im.load()
  File "C:\Users\Aaron\AppData\Roaming\Python\Python310\site-packages\PIL\ImageFile.py", line 268, in load
    self.load_end()
  File "C:\Users\Aaron\AppData\Roaming\Python\Python310\site-packages\PIL\PngImagePlugin.py", line 962, in load_end
    self.fp.read(4)  # CRC
AttributeError: 'NoneType' object has no attribute 'read'

问题原因

  1. Tkinter GUI操作必须在主线程:ImageTk.PhotoImage依赖Tkinter的主循环上下文,在子线程中创建会导致资源访问异常,出现文件句柄(fp)变为None的错误。
  2. 事件频繁触发:窗口resize时会连续触发数十次该事件,每次都启动新线程+重新打开图片文件,不仅加重线程开销,还可能导致文件资源未正确释放,进一步引发错误。
  3. 重复加载图片:每次resize都重新打开groupbox-1.png,增加了IO开销,加剧卡顿。

解决方案

方案1:主线程内优化(简单有效)

通过防抖机制减少触发频率,提前加载原始图片,避免重复IO操作:

from tkinter import Tk, Label, Frame
from PIL import Image, ImageTk

class MainWindow(Tk):
    def __init__(self):
        super().__init__()

        # 提前加载原始图片,避免重复打开
        self.original_gb1_img = Image.open('imgs/groupbox-1.png')
        # 初始化缩放后的图片
        self.gb1_img = ImageTk.PhotoImage(self.original_gb1_img.resize((260, 490), Image.Resampling.LANCZOS))

        self.gb1_container = Frame(self, bg='#333333', width=280)
        self.gb1_container.pack(side='left', fill='y', padx=20, pady=(120, 20))
        self.gb1_container.bind('<Configure>', self.on_resize)

        self.gb1 = Label(self.gb1_container, image=self.gb1_img, bg='#333333')
        self.gb1.place(x=0, y=0)

        # 防抖标记:避免连续触发resize
        self.resize_timer = None

    def on_resize(self, e):
        # 取消之前的延迟任务
        if self.resize_timer:
            self.after_cancel(self.resize_timer)
        # 延迟100ms执行缩放,只处理最后一次resize事件
        self.resize_timer = self.after(100, self.resize_gb1, e.width, e.height)

    def resize_gb1(self, width, height):
        # 用提前加载的原始图片缩放
        resized_img = self.original_gb1_img.resize((width, height), Image.Resampling.LANCZOS)
        self.gb1_img = ImageTk.PhotoImage(resized_img)
        self.gb1.configure(image=self.gb1_img)
        # 保留引用,避免图片被GC回收
        self.gb1.image = self.gb1_img

if __name__ == '__main__':
    app = MainWindow()
    app.title('Main Window')
    app.configure(bg='#333333')

    w = 1000
    h = 620
    x = (app.winfo_screenwidth() - w) // 2
    y = (app.winfo_screenheight() - h) // 2
    app.geometry('%dx%d+%d+%d' % (w, h, x, y))
    app.mainloop()

方案2:子线程处理缩放+主线程更新GUI(适合超高清大图)

如果图片分辨率极高,缩放计算耗时,可将缩放逻辑放到子线程,通过队列传递图片数据,主线程负责更新界面:

from tkinter import Tk, Label, Frame
from PIL import Image, ImageTk
import threading
import queue

class MainWindow(Tk):
    def __init__(self):
        super().__init__()

        self.original_gb1_img = Image.open('imgs/groupbox-1.png')
        self.gb1_img = ImageTk.PhotoImage(self.original_gb1_img.resize((260, 490), Image.Resampling.LANCZOS))

        self.gb1_container = Frame(self, bg='#333333', width=280)
        self.gb1_container.pack(side='left', fill='y', padx=20, pady=(120, 20))
        self.gb1_container.bind('<Configure>', self.on_resize)

        self.gb1 = Label(self.gb1_container, image=self.gb1_img, bg='#333333')
        self.gb1.place(x=0, y=0)

        # 用于传递缩放后图片的队列
        self.img_queue = queue.Queue()
        # 标记当前是否有缩放任务在执行,避免重复启动线程
        self.is_resizing = False
        # 启动队列监听
        self.listen_queue()

    def on_resize(self, e):
        if not self.is_resizing:
            self.is_resizing = True
            # 启动子线程执行缩放
            threading.Thread(target=self.thread_resize, args=(e.width, e.height), daemon=True).start()

    def thread_resize(self, width, height):
        resized_img = self.original_gb1_img.resize((width, height), Image.Resampling.LANCZOS)
        self.img_queue.put(resized_img)

    def listen_queue(self):
        try:
            # 从队列获取缩放后的图片
            resized_img = self.img_queue.get_nowait()
            self.gb1_img = ImageTk.PhotoImage(resized_img)
            self.gb1.configure(image=self.gb1_img)
            self.gb1.image = self.gb1_img
            self.is_resizing = False
        except queue.Empty:
            pass
        # 每隔50ms检查一次队列
        self.after(50, self.listen_queue)

if __name__ == '__main__':
    app = MainWindow()
    app.title('Main Window')
    app.configure(bg='#333333')

    w = 1000
    h = 620
    x = (app.winfo_screenwidth() - w) // 2
    y = (app.winfo_screenheight() - h) // 2
    app.geometry('%dx%d+%d+%d' % (w, h, x, y))
    app.mainloop()

关键优化点

  • 提前加载原始图片,避免重复IO操作
  • 防抖机制减少resize触发频率
  • 严格遵守Tkinter规则:所有GUI更新必须在主线程执行
  • 保留图片引用(self.gb1.image = self.gb1_img),防止被Python垃圾回收器回收导致图片消失

内容的提问来源于stack exchange,提问作者Aaron Catolico

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 06:40:28