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

动态选择绘图时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()

原因分析
  1. mplcursor实例被垃圾回收:在plot_data函数中创建的cursor是局部变量,函数执行完毕后没有被任何对象引用,Python的垃圾回收机制会直接销毁它,导致悬浮光标无法生效。
  2. 画布更新逻辑不兼容:直接通过canvas.figure = new_fig替换画布对象的方式,没有正确将新创建的mplcursor绑定到当前的Tkinter事件循环中,事件无法被触发。
  3. 闭包变量捕获问题:原代码中的lambda sel: sel.annotation.set_text(f"Data {plot_var-1}")会捕获函数外部的plot_var变量,当后续plot_var值变化时,lambda会使用最新值而非创建时的值,导致显示的信息错误。

修复方案

针对上述问题,调整代码如下:

  1. 将mplcursor实例保存为Figure对象的属性,避免被垃圾回收;
  2. 修复lambda的闭包变量捕获问题,使用默认参数固定创建时的plot_var值;
  3. 调整画布更新逻辑,每次切换视图时重新创建画布并替换旧的画布组件,确保事件绑定生效。

修复后的完整代码:

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:14:57