Python Tkinter结合CV2运行报PhotoImage无_PhotoImage__photo属性如何解决
错误根因
- Tkinter为非线程安全库,所有GUI更新、
ImageTk.PhotoImage实例创建、Tkinter组件属性修改操作,必须在运行mainloop()的主线程中执行。你当前代码将LoadIntro方法放在子线程运行,WhileIntro内创建图片对象、更新Label显示的操作均在子线程执行,直接触发RuntimeError: main thread is not in main loop。 - 第一个报错触发后,PhotoImage实例创建失败,内部
__photo私有属性未初始化,实例销毁时就会抛出第二个AttributeError: 'PhotoImage' object has no attribute '_PhotoImage__photo'错误。 - 额外代码问题:
self.destroy == True是判断语句,赋值需用单等号self.destroy = True;DestroyGui方法第一个参数缺少self,调用时会报错。
修复方案
将视频帧读取、格式转换等计算逻辑放在子线程执行,GUI更新操作调度回主线程执行,修改后可运行代码如下:
import tkinter as tk import cv2 from PIL import ImageTk, Image from pynput import keyboard from threading import Thread, Lock from time import sleep from pyautogui import position class IntroApp: def __init__(self, master=None): # build ui self.intro = tk.Tk() if master is None else tk.Toplevel(master) self.IntroVideo = tk.Label(self.intro) self.IntroVideo.place(anchor='center', relx='0.5', rely='0.5', x='0', y='0') self.enter = tk.Label(self.intro) self.enter.configure( background='#e74c3c', font='{Arial} 25 {}', foreground='#c0392b', justify='center') self.enter.configure(padx='10', pady='10', text='Waiting') self.enter.place(anchor='nw', relx='0.025', rely='0.025', x='0', y='0') self.screen_width = self.intro.winfo_screenwidth() self.screen_height = self.intro.winfo_screenheight() self.intro.configure(background='#f6f6f6') self.intro.geometry(f'{self.screen_width}x{self.screen_height}+0+0') self.intro.resizable(False, False) self.intro.title('Locked - RED LINE') self.destroy_flag = False self.intro.protocol("WM_DELETE_WINDOW", self.DestroyGui) self.intro.attributes('-fullscreen', True) # 新增:帧缓存和锁 self.current_frame = None self.frame_lock = Lock() self.video = None def DestroyGui(self): pass def PressEnter(self, key): if key == keyboard.Key.enter: self.destroy_flag = True if self.video: self.video.release() self.intro.destroy() def CheckEnter(self): sleep(1) # 这里configure是GUI操作,用after调度回主线程 self.intro.after(0, lambda: self.enter.configure(text='Press ENTER to continue')) self.listener = keyboard.Listener(on_press=self.PressEnter) self.listener.start() # 新增:子线程单独读视频处理帧 def read_video(self): self.video = cv2.VideoCapture('D:\\Lock\\RedLine.mp4') while not self.destroy_flag: ret, frame = self.video.read() if ret: frame = cv2.resize(frame, (self.screen_width, self.screen_height)) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 加锁存帧 with self.frame_lock: self.current_frame = frame.copy() else: # 视频播放完重新加载 self.video.release() self.video = cv2.VideoCapture('D:\\Lock\\RedLine.mp4') sleep(1) self.video.release() # 新增:主线程更新UI def update_frame(self): if self.destroy_flag: return # 加锁取帧 with self.frame_lock: if self.current_frame is not None: image = ImageTk.PhotoImage(Image.fromarray(self.current_frame)) self.IntroVideo.configure(image=image) self.IntroVideo.image = image self.intro.after(10, self.update_frame) def Afk(self): OldX, OldY = position() while True: sleep(30) NewX, NewY = position() if OldX == NewX and OldY == NewY: print('Detected') self.listener.stop() self.destroy_flag = True break OldX, OldY = NewX, NewY IntroApp().run() def run(self): Thread(target=self.CheckEnter).start() # 子线程读视频 Thread(target=self.read_video, daemon=True).start() Thread(target=self.Afk).start() # 主线程启动UI更新 self.update_frame() self.intro.mainloop() if __name__ == '__main__': app = IntroApp() app.run()
关键修改说明
- 把视频读取、帧处理逻辑完全移到子线程,仅将处理后的帧缓存到实例变量,不涉及任何GUI操作
- 主线程通过
after方法定时从缓存中取帧,执行图片创建、Label更新操作,符合Tkinter的线程安全要求 - 增加线程锁避免帧数据读写冲突,修改了原有代码的逻辑错误
- 所有GUI属性修改操作都通过
after调度回主线程执行,避免跨线程操作报错
内容的提问来源于stack exchange,提问作者KevinTran
相关产品推荐
相关产品推荐

