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

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()后图表闪显,存在性能顾虑。

问题根源与修复方案

核心问题

  1. 再生图表时未清空line_dict_,旧线程仍操作失效的图表对象;
  2. 依赖plt.ion()的交互模式与FigureCanvasTkAgg的刷新机制冲突,导致额外窗口或无法刷新;
  3. 更新数据后未触发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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 00:33:10