Skip to content

Commit eec2dba

Browse files
authored
Merge pull request #152 from sot/hope
Stop RuntimeError FigureCanvasQTAgg has been deleted
2 parents 39c0f58 + 791f0ee commit eec2dba

File tree

2 files changed

+58
-24
lines changed

2 files changed

+58
-24
lines changed

xija/gui_fit/app.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from xija.get_model_spec import get_xija_model_spec
2424

2525
from .fitter import FitWorker, fit_logger
26-
from .plots import FitStatWindow, HistogramWindow, PlotsBox
26+
from .plots import FitStatWindow, HistogramWindow, PlotsPanel
2727

2828
gui_config = {}
2929

@@ -956,10 +956,10 @@ class MainLeftPanel(Panel):
956956
def __init__(self, model, main_window):
957957
Panel.__init__(self, orient="v")
958958
self.control_buttons_panel = ControlButtonsPanel(model)
959-
self.plots_box = PlotsBox(model, main_window)
959+
self.plots_panel = PlotsPanel(model, main_window)
960+
self.plots_box = self.plots_panel.plots_box
960961
self.pack_start(self.control_buttons_panel)
961-
# This specialized code is for the PlotsBox because we
962-
# want it to be scrollable
962+
# Make PlotsPanel scrollable
963963
self.scroll = QtWidgets.QScrollArea()
964964
self.scroll.setWidgetResizable(True)
965965
self.scroll.setSizePolicy(
@@ -968,12 +968,7 @@ def __init__(self, model, main_window):
968968
self.scroll.setFrameShape(
969969
QtWidgets.QFrame.NoFrame
970970
) # optional, just looks nicer
971-
container = QtWidgets.QWidget()
972-
container.setLayout(self.plots_box)
973-
container.setSizePolicy(
974-
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred
975-
)
976-
self.scroll.setWidget(container)
971+
self.scroll.setWidget(self.plots_panel)
977972
self.box.addWidget(self.scroll, 1)
978973

979974

@@ -1026,9 +1021,10 @@ def __init__(self, model, fit_worker): # noqa: PLR0915
10261021

10271022
self.main_left_panel = MainLeftPanel(model, self)
10281023
mlp = self.main_left_panel
1024+
self.plots_panel = self.main_left_panel.plots_panel
10291025
self.plots_box = self.main_left_panel.plots_box
10301026

1031-
self.main_right_panel = MainRightPanel(model, mlp.plots_box)
1027+
self.main_right_panel = MainRightPanel(model, self.plots_panel)
10321028
mrp = self.main_right_panel
10331029

10341030
self.show_radzones = False

xija/gui_fit/plots.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def __init__(self, main_window):
209209
self.fig = Figure()
210210

211211
canvas = FigureCanvas(self.fig)
212+
self.canvas = canvas
212213
toolbar = NavigationToolbar(canvas, parent=None)
213214

214215
toolbar_box = QtWidgets.QHBoxLayout()
@@ -269,6 +270,8 @@ def update_plot(self):
269270
self.fig.canvas.flush_events()
270271

271272
def close_window(self, *args):
273+
if hasattr(self, "canvas"):
274+
self.canvas = None
272275
self.close()
273276

274277

@@ -291,6 +294,7 @@ def __init__(self, model, hist_msids): # noqa: PLR0915
291294
self.fig = Figure()
292295

293296
canvas = FigureCanvas(self.fig)
297+
self.canvas = canvas
294298
toolbar = NavigationToolbar(canvas, parent=None)
295299

296300
msid_select = QtWidgets.QComboBox()
@@ -387,6 +391,8 @@ def emax_edited(self):
387391
self.update_plots()
388392

389393
def close_window(self, *args):
394+
if hasattr(self, "canvas"):
395+
self.canvas = None
390396
self.close()
391397

392398
_rz_mask = None
@@ -649,15 +655,38 @@ def update_plots(self): # noqa: PLR0915
649655
self.fig.canvas.flush_events()
650656

651657

652-
class PlotBox(QtWidgets.QVBoxLayout):
653-
def __init__(self, plot_name, plots_box):
654-
super().__init__()
658+
class PlotsPanel(QtWidgets.QWidget):
659+
"""
660+
QWidget wrapper for PlotsBox (QVBoxLayout), ensures persistent ownership and correct
661+
parent/child relationship. Also exposes main_window for compatibility with code
662+
expecting PlotsBox.main_window.
663+
"""
655664

665+
def __init__(self, model, main_window):
666+
super().__init__()
667+
self.main_window = main_window
668+
self.model = model
669+
self.plots_box = PlotsBox(model, main_window, parent_widget=self)
670+
# Keep explicit reference to plot_boxes to prevent GC
671+
self._plot_boxes_ref = self.plots_box.plot_boxes
672+
layout = QtWidgets.QVBoxLayout()
673+
layout.setContentsMargins(0, 0, 0, 0)
674+
layout.addLayout(self.plots_box)
675+
self.setLayout(layout)
676+
677+
678+
class PlotBox(QtWidgets.QWidget):
679+
def __init__(self, plot_name, plots_box, parent=None):
680+
super().__init__(parent)
681+
self.plot_name = plot_name
656682
comp_name, plot_method = plot_name.split() # E.g. "tephin fit_resid"
657683
self.comp = plots_box.model.comp[comp_name]
658684
self.plot_method = plot_method
659685
self.comp_name = comp_name
660-
self.plot_name = plot_name
686+
687+
# Layout for this widget
688+
vbox = QtWidgets.QVBoxLayout()
689+
self.setLayout(vbox)
661690

662691
self.fig = Figure(constrained_layout=True)
663692
canvas = FigureCanvas(self.fig)
@@ -681,8 +710,8 @@ def __init__(self, plot_name, plots_box):
681710
toolbar_box.addStretch(1)
682711
toolbar_box.addWidget(delete_plot_button)
683712

684-
self.addWidget(canvas)
685-
self.addLayout(toolbar_box)
713+
vbox.addWidget(canvas)
714+
vbox.addLayout(toolbar_box)
686715

687716
# Add shared x-axes for plots with time on the x-axis
688717
xaxis = plot_method.split("__")
@@ -824,11 +853,14 @@ def update(self, first=False):
824853

825854

826855
class PlotsBox(QtWidgets.QVBoxLayout):
827-
def __init__(self, model, main_window):
856+
def __init__(self, model, main_window, parent_widget=None):
828857
super().__init__()
829858
self.main_window = main_window
830859
self.model = model
831-
self.plot_boxes = []
860+
self.parent_widget = (
861+
parent_widget # Store the parent widget for proper Qt ownership
862+
)
863+
self.plot_boxes = [] # Keep strong references to PlotBox objects
832864
self.plot_names = []
833865

834866
self.set_times()
@@ -868,19 +900,25 @@ def add_plot_box(self, plot_name):
868900
if plot_name == "Add plot..." or plot_name in self.plot_names:
869901
return
870902
print("Adding plot ", plot_name)
871-
plot_box = PlotBox(plot_name, self)
872-
self.addLayout(plot_box)
873-
plot_box.update(first=True)
903+
# Pass parent_widget during PlotBox construction for proper Qt ownership
904+
plot_box = PlotBox(plot_name, self, parent=self.parent_widget)
905+
# Keep strong references BEFORE any other operations
874906
self.plot_boxes.append(plot_box)
875907
self.plot_names.append(plot_name)
908+
# Now add to layout - layout takes ownership but we keep Python ref
909+
self.addWidget(plot_box)
910+
# Defer update to allow Qt to fully establish ownership
911+
# This prevents premature garbage collection of the PlotBox
912+
QtCore.QTimer.singleShot(0, lambda: plot_box.update(first=True))
876913

877914
def delete_plot_box(self, plot_name):
878915
for i, pb in enumerate(list(self.plot_boxes)):
879916
if pb.plot_name == plot_name:
880917
self.plot_boxes.pop(i)
881918
self.plot_names.pop(i)
882-
self.removeItem(pb)
883-
clear_layout(pb)
919+
self.removeWidget(pb)
920+
pb.setParent(None)
921+
pb.deleteLater()
884922
break
885923
self.update()
886924

0 commit comments

Comments
 (0)