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'
问题原因
- Tkinter GUI操作必须在主线程:
ImageTk.PhotoImage依赖Tkinter的主循环上下文,在子线程中创建会导致资源访问异常,出现文件句柄(fp)变为None的错误。 事件频繁触发 :窗口resize时会连续触发数十次该事件,每次都启动新线程+重新打开图片文件,不仅加重线程开销,还可能导致文件资源未正确释放,进一步引发错误。- 重复加载图片:每次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
相关产品推荐
相关产品推荐

