Python GUI多进程实现报错求助:无法序列化_tkinter.tkapp对象
解决Tkinter/CustomTkinter多进程中的Pickle与GUI更新问题
错误原因
- Tkinter对象无法序列化:将类方法(
self.loading、self.getGraph)作为多进程target时,Python会尝试序列化整个App实例(self),但Tkinter的tkapp对象不支持pickle,直接触发can't pickle _tkinter.tkapp objects错误。 - 子进程不能操作GUI:Tkinter的GUI必须在主线程中更新,子进程直接修改
self.tab_a1等元素会导致线程安全问题,进而引发EOFError(子进程异常退出后,主进程读取输出时无数据)。
解决方案
- 用**进程间队列(Queue)**实现子进程与主线程的数据传递,子进程只处理耗时计算,主线程负责GUI更新。
- 将耗时任务封装为独立函数,避免携带GUI实例引用。
- 用Tkinter的
after()方法定期监听队列,触发GUI更新。
修改后的完整代码
import tkinter import multiprocessing as mp from multiprocessing import Queue from tkinter import filedialog import customtkinter import matplotlib matplotlib.use("TkAgg") from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure from CV2_Frame_Extraction import * from Color_Extractor_CT import * from graph import * import os customtkinter.set_appearance_mode("System") customtkinter.set_default_color_theme("blue") # 独立的耗时任务函数,不引用GUI实例 def process_video(decision, queue): try: # 发送加载状态 queue.put({"type": "loading"}) # 帧提取 frame_extraction('\\'.join(decision)) # 颜色提取 colors = ["Red", "Green", "Blue"] red = [] green = [] blue = [] files = os.listdir('Frames') sorted_files = [file.replace('f_', '') for file in files] sorted_files = sorted(sorted_files, key=lambda x: int(os.path.splitext(x)[0])) for frame in sorted_files: rgb = color_extraction(f'Frames/f_{frame}') r, g, b = rgb[0] red.append(r) green.append(g) blue.append(b) print(f'Frame: {frame} RGB:{rgb[0]}') frames = [(int(os.path.splitext(x)[0]))*20 for x in sorted_files] # 生成图表数据(而非直接创建Canvas) figs = generate_graph(red, green, blue, frames) # 发送结果数据 queue.put({"type": "result", "figs": figs, "colors": colors}) except Exception as e: queue.put({"type": "error", "msg": str(e)}) class App(customtkinter.CTk): btn_list = [] def __init__(self): super().__init__() # Window Configuration self.title("Plasma Analysis") self.geometry("1100x580") # Layout Configuration self.grid_rowconfigure((0, 1, 2), weight=1) # Side Bar self.sb_frame = customtkinter.CTkFrame(self, width=140, corner_radius=0) self.sb_frame.grid(row=0, column=0, rowspan=4, sticky="nsew") self.sb_frame.grid_rowconfigure(4, weight=1) self.sb_new = customtkinter.CTkButton(self.sb_frame, text="Add Video", command=self.openfile, corner_radius=2) self.sb_new.grid(row=0, column=0, padx=20, pady=20) self.btn_frame = customtkinter.CTkFrame(self.sb_frame, width=140, height=400, corner_radius=0, fg_color="transparent", border_width=5) self.btn_frame.grid(row=1, column=0, rowspan=3) self.bf_label = customtkinter.CTkLabel(self.btn_frame, text="Analysis", font=customtkinter.CTkFont(size=20, weight="bold"), fg_color="transparent") self.bf_label.grid(row=0, column=0) self.appearance_mode_label = customtkinter.CTkLabel(self.sb_frame, text="Appearance Mode:", anchor="w") self.appearance_mode_label.grid(row=7, column=0, padx=20, pady=(10, 0)) self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sb_frame, values=["Light", "Dark", "System"], command=self.change_appearance_mode_event) self.appearance_mode_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 10)) self.scaling_label = customtkinter.CTkLabel(self.sb_frame, text="UI Scaling:", anchor="w") self.scaling_label.grid(row=9, column=0, padx=20, pady=(10, 0)) self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sb_frame, values=["80%", "90%", "100%", "110%", "120%"], command=self.change_scaling_event) self.scaling_optionemenu.grid(row=10, column=0, padx=20, pady=(10, 20)) # Creating Main Panel self.main_frame = customtkinter.CTkFrame(self, width=950, corner_radius=0, fg_color='transparent') self.main_frame.grid(row=0, column=1, rowspan=4, columnspan=4, sticky="nsew") self.tab_a1 = customtkinter.CTkTabview(self.main_frame, width=400, height=250, segmented_button_selected_color='gray', border_width=5) self.tab_a1.grid(row=0, column=0, padx=(30,50), pady=(15,15), sticky="nsew") self.tab_a1.grid_propagate(False) self.tab_a1.grid_columnconfigure(0, weight=0, uniform=1) self.tab_a1.grid_rowconfigure(0, weight=1, uniform=1) self.tab_a1.add("Color") self.tab_a2 = customtkinter.CTkTabview(self.main_frame, width=400, height=250, segmented_button_selected_color='gray', border_width=5) self.tab_a2.grid(row=0, column=1, padx=(0,50), pady=(15,15), sticky="nsew") self.tab_a2.add("Luminosity") self.tab_a3 = customtkinter.CTkTabview(self.main_frame, width=400, height=250, segmented_button_selected_color='gray', border_width=5) self.tab_a3.grid(row=1, column=0, padx=(30,50), pady=(15,15), sticky="nsew") self.tab_a3.add("Motion") self.tab_a4 = customtkinter.CTkTabview(self.main_frame, width=400, height=250, segmented_button_selected_color='gray', border_width=5) self.tab_a4.grid(row=1, column=1, padx=(0,50), pady=(15,15), sticky="nsew") self.tab_a4.add("Trend") # 初始化进程间队列 self.queue = Queue() # 启动队列监听 self.check_queue() def change_appearance_mode_event(self, new_appearance_mode: str): customtkinter.set_appearance_mode(new_appearance_mode) def change_scaling_event(self, new_scaling: str): new_scaling_float = int(new_scaling.replace("%", "")) / 100 customtkinter.set_widget_scaling(new_scaling_float) def openfile(self): decision = filedialog.askopenfilename().split('/') if not decision[0]: # 用户取消选择 return name = decision[-1].split('.')[0] # 添加视频按钮 self.btn_list.append(customtkinter.CTkButton(self.btn_frame, text=name)) for x, btn in enumerate(self.btn_list): btn.grid(row=x+1, column=0, padx=10, pady=10) # 启动子进程处理视频,传递队列 p = mp.Process(target=process_video, args=(decision, self.queue)) p.start() def show_loading(self): # 主线程显示加载提示 for tab in [self.tab_a2.tab('Luminosity'), self.tab_a3.tab('Motion'), self.tab_a4.tab('Trend')]: # 先清除原有内容 for widget in tab.winfo_children(): widget.destroy() lbl_load = customtkinter.CTkLabel(tab, text="Loading ...", font=customtkinter.CTkFont(size=20, weight="bold")) lbl_load.pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True, anchor='center') self.update() def update_graphs(self, figs, colors): # 主线程更新图表 # 清除原有Color标签页内容 self.tab_a1.delete("Color") # 添加新标签页并生成画布 for idx, f in enumerate(figs): self.tab_a1.add(colors[idx]) canvas = FigureCanvasTkAgg(f, self.tab_a1.tab(colors[idx])) f.tight_layout() canvas.draw() canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True, anchor='center') self.tab_a1.configure(fg_color="White") # 清除加载提示 for tab in [self.tab_a2.tab('Luminosity'), self.tab_a3.tab('Motion'), self.tab_a4.tab('Trend')]: for widget in tab.winfo_children(): widget.destroy() def check_queue(self): # 定期检查队列,处理子进程消息 while not self.queue.empty(): msg = self.queue.get() if msg["type"] == "loading": self.show_loading() elif msg["type"] == "result": self.update_graphs(msg["figs"], msg["colors"]) elif msg["type"] == "error": tkinter.messagebox.showerror("Error", msg["msg"]) # 每100ms检查一次队列 self.after(100, self.check_queue) if __name__ == "__main__": app = App() app.mainloop()
关键修改点
- 把
loading和getGraph中的GUI操作移到主线程,子进程只负责计算并通过Queue传递状态/结果。 - 使用独立函数
process_video作为多进程target,避免传递GUI实例。 - 用
after()方法实现非阻塞的队列监听,确保GUI响应流畅。 - 子进程中捕获异常并传递错误信息,避免静默崩溃。
内容的提问来源于stack exchange,提问作者NARAS
相关产品推荐
相关产品推荐

