动态选择绘图时mplcursor悬浮光标不显示的问题排查
问题:mplcursor悬浮光标在CustomTkinter+Matplotlib单组数据视图中不显示
我尝试在绘图中展示多组数据,用户可选择单独查看某一组数据,此时希望通过mplcursor悬浮光标显示该数据的信息,但悬浮光标始终不显示。以下是最小可复现示例:
import customtkinter as ctk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import mplcursors def plot_data(x_data, y_data, plot_var): x_plot = [] # data to draw on the plot y_plot = [] if plot_var in [1,2]: x_plot.append(x_data[0]) y_plot.append(y_data[0]) if plot_var in [1,3]: x_plot.append(x_data[1]) y_plot.append(y_data[1]) if plot_var in [1,4]: x_plot.append(x_data[2]) y_plot.append(y_data[2]) if plot_var in [1,5]: x_plot.append(x_data[3]) y_plot.append(y_data[3]) fig = Figure(figsize=(5, 5)) ax = fig.add_subplot(111) for i in range(len(x_plot)): ax.plot(x_plot[i], y_plot[i], marker='o', label=f"Data {i+1}") if plot_var != 1: cursor = mplcursors.cursor(ax, hover=True) cursor.connect("add", lambda sel: sel.annotation.set_text(f"Data {plot_var-1}")) return fig def draw_plot_from_rb(canvas, x_data, y_data, plot_var): canvas.figure = plot_data(x_data, y_data, plot_var) canvas.draw() canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True) return canvas # Create the main window window = ctk.CTk() window.title("Custom Window") # Create the options frame options_frame = ctk.CTkFrame(window) options_frame.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True) # Create the data frame data_frame = ctk.CTkFrame(window) data_frame.pack(side=ctk.RIGHT, fill=ctk.BOTH, expand=True) # Add widgets to the options frame options_label = ctk.CTkLabel(options_frame, text="Options", font=("Arial", 16)) options_label.pack() # Add widgets to the data frame data_label = ctk.CTkLabel(data_frame, text="Data", font=("Arial", 16)) data_label.pack() # Create four separate x, y datasets x1 = [1, 2, 3, 4, 5] y1 = [1, 4, 9, 16, 25] x2 = [1, 2, 3, 4, 5] y2 = [36, 49, 64, 81, 100] x3 = [1, 2, 3, 4, 5] y3 = [121, 144, 169, 196, 225] x4 = [1, 2, 3, 4, 5] y4 = [256, 289, 324, 361, 400] x = [x1, x2, x3, x4] y = [y1, y2, y3, y4] # Create four radio button options plot_var = ctk.IntVar() plot_var.set(1) radio1 = ctk.CTkRadioButton( options_frame, text="View All", variable=plot_var, value=1, command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get()) ) radio1.pack() radio2 = ctk.CTkRadioButton( options_frame, text="Data 1", variable=plot_var, value=2, command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get()) ) radio2.pack() radio3 = ctk.CTkRadioButton( options_frame, text="Data 2", variable=plot_var, value=3, command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get()) ) radio3.pack() radio4 = ctk.CTkRadioButton( options_frame, text="Data 3", variable=plot_var, value=4, command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get()) ) radio4.pack() radio5 = ctk.CTkRadioButton( options_frame, text="Data 4", variable=plot_var, value=5, command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get()) ) radio5.pack() fig = plot_data(x, y, plot_var.get()) # Create the canvas for the plot canvas = FigureCanvasTkAgg(fig, master=data_frame) canvas.draw() canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True) # Start the main loop window.mainloop()
原因分析
- mplcursor实例被垃圾回收:在
plot_data函数中创建的cursor是局部变量,函数执行完毕后没有被任何对象引用,Python的垃圾回收机制会直接销毁它,导致悬浮光标无法生效。 - 画布更新逻辑不兼容:直接通过
canvas.figure = new_fig替换画布对象的方式,没有正确将新创建的mplcursor绑定到当前的Tkinter事件循环中,事件无法被触发。 - 闭包变量捕获问题:原代码中的
lambda sel: sel.annotation.set_text(f"Data {plot_var-1}")会捕获函数外部的plot_var变量,当后续plot_var值变化时,lambda会使用最新值而非创建时的值,导致显示的信息错误。
修复方案
针对上述问题,调整代码如下:
- 将mplcursor实例保存为Figure对象的属性,避免被垃圾回收;
- 修复lambda的闭包变量捕获问题,使用默认参数固定创建时的
plot_var值; - 调整画布更新逻辑,每次切换视图时重新创建画布并替换旧的画布组件,确保事件绑定生效。
修复后的完整代码:
import customtkinter as ctk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import mplcursors def plot_data(x_data, y_data, plot_var): x_plot = [] y_plot = [] if plot_var in [1,2]: x_plot.append(x_data[0]) y_plot.append(y_data[0]) if plot_var in [1,3]: x_plot.append(x_data[1]) y_plot.append(y_data[1]) if plot_var in [1,4]: x_plot.append(x_data[2]) y_plot.append(y_data[2]) if plot_var in [1,5]: x_plot.append(x_data[3]) y_plot.append(y_data[3]) fig = Figure(figsize=(5, 5)) ax = fig.add_subplot(111) for i in range(len(x_plot)): ax.plot(x_plot[i], y_plot[i], marker='o', label=f"Data {i+1}") if plot_var != 1: # 将cursor保存为fig的属性,避免被垃圾回收 fig.cursor = mplcursors.cursor(ax, hover=True) # 使用默认参数捕获当前的plot_var值,避免闭包问题 fig.cursor.connect("add", lambda sel, pv=plot_var: sel.annotation.set_text(f"Data {pv-1}")) return fig def draw_plot_from_rb(canvas_container, x_data, y_data, plot_var): # 移除旧画布 for widget in canvas_container.winfo_children(): if isinstance(widget, ctk.CTkCanvas): widget.destroy() # 创建新画布 fig = plot_data(x_data, y_data, plot_var) canvas = FigureCanvasTkAgg(fig, master=canvas_container) canvas.draw() canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True) return canvas # 主窗口创建 window = ctk.CTk() window.title("Custom Window") # 框架创建 options_frame = ctk.CTkFrame(window) options_frame.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True) data_frame = ctk.CTkFrame(window) data_frame.pack(side=ctk.RIGHT, fill=ctk.BOTH, expand=True) # 标签 options_label = ctk.CTkLabel(options_frame, text="选项", font=("Arial", 16)) options_label.pack() data_label = ctk.CTkLabel(data_frame, text="数据", font=("Arial", 16)) data_label.pack() # 数据集 x1 = [1, 2, 3, 4, 5] y1 = [1, 4, 9, 16, 25] x2 = [1, 2, 3, 4, 5] y2 = [36, 49, 64, 81, 100] x3 = [1, 2, 3, 4, 5] y3 = [121, 144, 169, 196, 225] x4 = [1, 2, 3, 4, 5] y4 = [256, 289, 324, 361, 400] x = [x1, x2, x3, x4] y = [y1, y2, y3, y4] # 单选按钮 plot_var = ctk.IntVar() plot_var.set(1) radio1 = ctk.CTkRadioButton( options_frame, text="查看全部", variable=plot_var, value=1, command=lambda: draw_plot_from_rb(data_frame, x, y, plot_var.get()) ) radio1.pack() radio2 = ctk.CTkRadioButton( options_frame, text="数据1", variable=plot_var, value=2, command=lambda: draw_plot_from_rb(data_frame, x, y, plot_var.get()) ) radio2.pack() radio3 = ctk.CTkRadioButton( options_frame, text="数据2", variable=plot_var, value=3, command=lambda: draw_plot_from_rb(data_frame, x, y, plot_var.get()) ) radio3.pack() radio4 = ctk.CTkRadioButton( options_frame, text="数据3", variable=plot_var, value=4, command=lambda: draw_plot_from_rb(data_frame, x, y, plot_var.get()) ) radio4.pack() radio5 = ctk.CTkRadioButton( options_frame, text="数据4", variable=plot_var, value=5, command=lambda: draw_plot_from_rb(data_frame, x, y, plot_var.get()) ) radio5.pack() # 初始化画布 draw_plot_from_rb(data_frame, x, y, plot_var.get()) window.mainloop()
内容的提问来源于stack exchange,提问作者ohshitgorillas
相关产品推荐
相关产品推荐

