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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 04:06:04