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

Matplotlib MultiCursor在多子图场景下无法显示问题求助

问题:PySimpleGUI集成Matplotlib MultiCursor光标不显示

我正在开发一款包含多个动态子图的GUI应用,希望集成Matplotlib的MultiCursor组件。代码无报错,但光标无法显示。尝试过用matplotlib事件结合axvline实现(效果不佳),也试过将MultiCursor返回值设为全局变量、用plt.var_name存储,均未解决。注:mpl_connect和on_move回调的axvline代码是临时替代方案,可忽略。

以下是当前代码片段:

import numpy as np
import PySimpleGUI as sg
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
                                               NavigationToolbar2Tk)
from matplotlib.widgets import Cursor
from matplotlib.widgets import MultiCursor
from matplotlib.gridspec import GridSpec
from matplotlib.figure import Figure

class Toolbar(NavigationToolbar2Tk):

    def __init__(self, *args, **kwargs):
        super(Toolbar, self).__init__(*args, **kwargs)

def repack(widget, option):
    pack_info = widget.pack_info()
    pack_info.update(option)
    widget.pack(**pack_info)

def draw_figure_w_toolbar(canvas, fig, canvas_toolbar):
    if canvas.children:
        for child in canvas.winfo_children():
            child.destroy()
    if canvas_toolbar.children:
        for child in canvas_toolbar.winfo_children():
            child.destroy()
    figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
    figure_canvas_agg.draw_idle()
    toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
    toolbar.update()
    figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)

layout=[
        [
            sg.Frame(
                'Controls',
                expand_x=True,
                layout=[[
                    sg.Canvas(key='controls_cv')
                ]])
        ],
            [
                sg.Canvas(
                    key='fig_cv',
                    size=(1024, 525),  #(W,H)
                    expand_x=True,
                    expand_y=True,
                    background_color='black')
            ],
        ]

window = sg.Window('Data Visualization Tool',
                   layout=layout,
                   location=(0, 0),
                  
                   resizable=True,
                   margins=(0, 0),
                   finalize=True)

fig = Figure()

def get_data_ax(x_data, y_data, y_label, marker_sym=None):
    ax = fig.add_subplot()
    lines = ax.step(
        x_data,
        y_data,
    
        marker=marker_sym,
        where='post')
    
    return ax, lines
cursor_handle_list = []
axes_handle_list = []
def on_move(event, flag=False, c_flag=False):
    global cursor_present_flag, cursor_handle_list, axes_handle_list
    if event.dblclick:     
        def change_cursor_position(handle_obj):
            for handles in handle_obj:
                handles.set_xdata(float(event.xdata))   
        if event.inaxes is not None:
            ax = event.inaxes
            if ax not in axes_handle_list:
                cursor_handle = ax.axvline(event.xdata, color ='red', lw = .75, alpha = 0.65, visible=True)
                cursor_handle_list.append(cursor_handle)
                axes_handle_list.append(ax)
            else:
                change_cursor_position(cursor_handle_list)
            
            draw_figure_w_toolbar(window['fig_cv'].TKCanvas, fig,
                                        window['controls_cv'].TKCanvas)
        window.refresh()

fig.canvas.mpl_connect("button_press_event", on_move)
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t) 
num_rows = 2
gs = GridSpec(num_rows, 1, hspace=0)
ax0, lines = get_data_ax(t, s, 'test')

ax0.set_subplotspec(gs[0])
fig.add_subplot(ax0)
ax, lines = get_data_ax(t, s, 'test')
ax.set_subplotspec(gs[1])
fig.add_subplot(ax)
ax.get_shared_x_axes().join(ax0, ax)

graph = FigureCanvasTkAgg(fig, master=window['fig_cv'].TKCanvas)
cursor_multi = MultiCursor(graph, [ax,], color='r', lw=2.0, vertOn=True)
graph.draw()  
draw_figure_w_toolbar(window['fig_cv'].TKCanvas, fig,
                                      window['controls_cv'].TKCanvas)
while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break

解决方案

问题根源

  1. Canvas被重建导致MultiCursor失效:draw_figure_w_toolbar函数会销毁原有Canvas并创建新的,而你之前绑定MultiCursor的是旧Canvas实例,新Canvas没有绑定MultiCursor。
  2. MultiCursor未包含所有子图:初始化时只传入了[ax,],漏掉了ax0,即使正常工作也只会在一个子图显示光标。
  3. 重复添加子图:get_data_ax里已经用fig.add_subplot()创建了子图,后续又调用fig.add_subplot(ax0),导致子图重复添加,干扰布局和事件处理。
  4. 临时回调干扰:on_move里的绘图操作会触发Canvas重绘,可能覆盖MultiCursor的光标。

修改后的完整代码

import numpy as np
import PySimpleGUI as sg
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
                                               NavigationToolbar2Tk)
from matplotlib.widgets import MultiCursor
from matplotlib.gridspec import GridSpec
from matplotlib.figure import Figure

class Toolbar(NavigationToolbar2Tk):
    def __init__(self, *args, **kwargs):
        super(Toolbar, self).__init__(*args, **kwargs)

# 全局变量存储MultiCursor引用,防止被GC回收
multi_cursor = None

def draw_figure_w_toolbar(canvas, fig, canvas_toolbar, axes_list):
    global multi_cursor
    # 清理旧组件
    if canvas.children:
        for child in canvas.winfo_children():
            child.destroy()
    if canvas_toolbar.children:
        for child in canvas_toolbar.winfo_children():
            child.destroy()
    # 创建新Canvas
    figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
    figure_canvas_agg.draw_idle()
    # 创建工具栏
    toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
    toolbar.update()
    # 绑定MultiCursor到新Canvas,传入所有需要同步的子图
    multi_cursor = MultiCursor(figure_canvas_agg, axes_list, color='r', lw=2.0, vertOn=True)
    # 布局Canvas
    figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)

layout=[
    [sg.Frame('Controls', expand_x=True, layout=[[sg.Canvas(key='controls_cv')]])],
    [sg.Canvas(key='fig_cv', size=(1024, 525), expand_x=True, expand_y=True, background_color='black')],
]

window = sg.Window('Data Visualization Tool',
                   layout=layout,
                   location=(0, 0),
                   resizable=True,
                   margins=(0, 0),
                   finalize=True)

fig = Figure()

def get_data_ax(x_data, y_data, y_label, marker_sym=None):
    # 直接创建子图,避免重复添加
    ax = fig.add_subplot()
    ax.step(x_data, y_data, marker=marker_sym, where='post')
    ax.set_ylabel(y_label)
    return ax

t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t) 
num_rows = 2
gs = GridSpec(num_rows, 1, hspace=0)

# 创建子图并指定GridSpec位置
ax0 = get_data_ax(t, s, 'test 1')
ax0.set_subplotspec(gs[0])

ax1 = get_data_ax(t, s, 'test 2')
ax1.set_subplotspec(gs[1])

# 共享X轴
ax1.get_shared_x_axes().join(ax0, ax1)

# 初始绘制,传入所有子图列表
draw_figure_w_toolbar(window['fig_cv'].TKCanvas, fig, window['controls_cv'].TKCanvas, [ax0, ax1])

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break

window.close()

关键修改说明

  1. 将MultiCursor绑定移到绘图函数内部:每次调用draw_figure_w_toolbar创建新Canvas时,都重新绑定MultiCursor,确保关联的是当前活跃的Canvas。
  2. 传入所有子图到MultiCursor:初始化时传入[ax0, ax1],实现多子图光标同步。
  3. 全局存储MultiCursor引用:防止Python垃圾回收机制回收MultiCursor实例,导致光标失效。
  4. 修复子图重复添加问题:删除多余的fig.add_subplot(ax0)调用,get_data_ax里的fig.add_subplot()已经完成子图创建。
  5. 移除干扰性临时回调:删除on_move相关的代码(如果不需要该功能),避免干扰MultiCursor的事件处理逻辑。

内容的提问来源于stack exchange,提问作者Sreenath R

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 23:30:45