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
解决方案
问题根源
- Canvas被重建导致MultiCursor失效:
draw_figure_w_toolbar函数会销毁原有Canvas并创建新的,而你之前绑定MultiCursor的是旧Canvas实例,新Canvas没有绑定MultiCursor。 - MultiCursor未包含所有子图:初始化时只传入了
[ax,],漏掉了ax0,即使正常工作也只会在一个子图显示光标。 - 重复添加子图:
get_data_ax里已经用fig.add_subplot()创建了子图,后续又调用fig.add_subplot(ax0),导致子图重复添加,干扰布局和事件处理。 - 临时回调干扰:
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()
关键修改说明
- 将MultiCursor绑定移到绘图函数内部:每次调用
draw_figure_w_toolbar创建新Canvas时,都重新绑定MultiCursor,确保关联的是当前活跃的Canvas。 - 传入所有子图到MultiCursor:初始化时传入
[ax0, ax1],实现多子图光标同步。 - 全局存储MultiCursor引用:防止Python垃圾回收机制回收MultiCursor实例,导致光标失效。
- 修复子图重复添加问题:删除多余的
fig.add_subplot(ax0)调用,get_data_ax里的fig.add_subplot()已经完成子图创建。 - 移除干扰性临时回调:删除
on_move相关的代码(如果不需要该功能),避免干扰MultiCursor的事件处理逻辑。
内容的提问来源于stack exchange,提问作者Sreenath R
相关产品推荐
相关产品推荐

