CustomTkinter中Matplotlib图表再生后无法更新的问题
动态图表再生后无法更新数据的问题解决
我需要创建一个可控制更新时机与频率的动态图表,要求按指定时间间隔输出数据,避免GUI卡顿且保证低性能消耗。使用customtkinter搭建界面,通过线程定时更新Matplotlib图表数据,依赖plt.ion()实现动态显示。但调用regen_graph()再生图表框架后,图表仅显示初始坐标,后续添加的数据无法展示。
相关代码
import customtkinter as ct from matplotlib import pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import threading class App(ct.CTk): def __init__(self, **kwargs): super().__init__(**kwargs) # Configure window self.geometry("800x600") # Variables self.loop_count = 0 self.line_dict_ = {} self.data_store = ([1, 2], [3, 2]) # Widgets self.regen_button = ct.CTkButton(self, text="Regen", command=self.regen_graph) self.regen_button.pack() self.graph_frame = ct.CTkFrame(self) self.graph_frame.pack() # Functions self.gen_graph(self.graph_frame) # Initial def gen_graph(self, master): # Figure Declaration self.fig, self.ax1 = plt.subplots(layout='constrained') self.ax1.set_xlim(0, 100) self.ax1.set_ylim(0, 100) # Set interactive plt.ion() # Create Canvas with figure in frame canvas = FigureCanvasTkAgg(self.fig, master=master) canvas.get_tk_widget().pack() # Create Plots for name in ["1", "2"]: self.line_dict_[name], = self.ax1.plot(*self.data(), scaley=True, scalex=True, label=name) self.loop_count += 1 self._update_loop(self.loop_count) # Loop def _update_loop(self, num): if self.loop_count == num: print("Thread: ", threading.current_thread()) # Prep Next Loop self.graph_thread = threading.Timer(1, lambda x=num: self._update_loop(x)) self.graph_thread.daemon = True self.graph_thread.start() # Apply new data to lines for key in self.line_dict_: # Apply the data from the key to the keys line self.line_dict_[key].set_data(*self.data(True)) # Functions def regen_graph(self): print("resetting graph") # Clear Graph Frame plt.ioff() self.fig.clf() self.graph_frame.destroy() # New Graph Frame self.graph_frame = ct.CTkFrame(self) self.graph_frame.pack() self.gen_graph(self.graph_frame) def data(self, add=False): if add: self.data_store[0].append(self.data_store[0][-1] + 1) self.data_store[1].append(self.data_store[1][-1] + 1) return self.data_store app = App() app.mainloop()
已尝试的方法
- 替换customtkinter为tkinter,无效果;
- 将线程定时器替换为
self.after(),结果一致; - 调整
plt.ion()与plt.ioff()调用时机:移除regen_graph()中的plt.ioff()后图表可更新但弹出额外窗口;添加plt.close()后图表闪显,存在性能顾虑。
问题根源与修复方案
核心问题
- 再生图表时未清空
line_dict_,旧线程仍操作失效的图表对象; - 依赖
plt.ion()的交互模式与FigureCanvasTkAgg的刷新机制冲突,导致额外窗口或无法刷新; - 更新数据后未触发
canvas的重绘操作,GUI无法感知数据变化。
修复后的代码
import customtkinter as ct from matplotlib import pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import threading class App(ct.CTk): def __init__(self, **kwargs): super().__init__(**kwargs) self.geometry("800x600") self.loop_count = 0 self.line_dict_ = {} self.data_store = ([1, 2], [3, 2]) self.canvas = None # 保存canvas引用,用于后续重绘 self.graph_thread = None # 保存线程引用,用于再生时取消旧线程 self.regen_button = ct.CTkButton(self, text="Regen", command=self.regen_graph) self.regen_button.pack() self.graph_frame = ct.CTkFrame(self) self.graph_frame.pack() self.gen_graph(self.graph_frame) def gen_graph(self, master): # 清空旧线条字典,避免引用失效对象 self.line_dict_.clear() # 创建新的figure和axes,指定尺寸适配界面 self.fig, self.ax1 = plt.subplots(layout='constrained', figsize=(6,4)) self.ax1.set_xlim(0, 100) self.ax1.set_ylim(0, 100) # 创建canvas并保存引用 self.canvas = FigureCanvasTkAgg(self.fig, master=master) self.canvas.get_tk_widget().pack() # 初始化图表线条 for name in ["1", "2"]: self.line_dict_[name], = self.ax1.plot(*self.data(), label=name) self.ax1.legend() # 添加图例,提升可读性 self.loop_count += 1 # 取消旧线程(如果存在),避免重复更新 if self.graph_thread: self.graph_thread.cancel() self._update_loop(self.loop_count) def _update_loop(self, num): if self.loop_count != num: return # 若已再生图表,终止旧线程的更新逻辑 # 定时启动下一次更新 self.graph_thread = threading.Timer(1, lambda x=num: self._update_loop(x)) self.graph_thread.daemon = True self.graph_thread.start() # 更新线条数据 for key in self.line_dict_: self.line_dict_[key].set_data(*self.data(True)) # 必须在主线程触发重绘,避免GUI线程安全问题 self.after(0, self.canvas.draw) def regen_graph(self): # 停止当前更新线程 if self.graph_thread: self.graph_thread.cancel() # 销毁旧框架并关闭旧figure,释放资源 self.graph_frame.destroy() plt.close(self.fig) # 创建新框架并生成图表 self.graph_frame = ct.CTkFrame(self) self.graph_frame.pack() self.gen_graph(self.graph_frame) def data(self, add=False): if add: self.data_store[0].append(self.data_store[0][-1] + 1) self.data_store[1].append(self.data_store[1][-1] + 1) return self.data_store app = App() app.mainloop()
关键修复点
- 线程生命周期管理:再生图表前取消旧定时器线程,防止旧线程操作已失效的图表对象;
- 清空线条字典:每次生成新图表时清空
line_dict_,确保新线条引用正确; - 主线程触发重绘:通过
self.after(0, self.canvas.draw)在GUI主线程中调用canvas.draw(),保证线程安全且正确刷新界面; - 关闭旧figure:用
plt.close(self.fig)关闭旧Matplotlib figure,避免残留窗口和内存泄漏; - 移除交互模式依赖:不再使用
plt.ion()/plt.ioff(),直接通过canvas的draw()方法控制刷新,解决额外窗口问题。
内容的提问来源于stack exchange,提问作者Josh R
相关产品推荐
相关产品推荐

