RuntimeError: Internal C++ object (FloorPlotWidget) already deleted.

84 views
Skip to first unread message

Arnold Kyeza

unread,
Nov 8, 2024, 12:06:40 PM11/8/24
to pyqtgraph
I have this simple code, but every time I try to close one of my proxy widgets, I get this error below:

import sys
import numpy as np
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QGraphicsProxyWidget, QGridLayout
)
from PySide6.QtCore import Signal, Qt
import pyqtgraph as pg


class YawSweepPlot(pg.PlotWidget):
    """
    Represents Plot A: Yaw Sweep Plot.    Displays Clf vs Yaw with a movable vertical cursor.    Emits a yawChanged signal when the cursor is moved.    """
    yawChanged = Signal(float)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setBackground('w')
        self.setTitle("Clf vs Yaw")
        self.setLabel('left', "Clf")
        self.setLabel('bottom', "Yaw")
        self.setXRange(-12, 10)
        self.setYRange(-2.5, -1.5)

        # Simulated Clf data
        yaw = np.linspace(-12, 10, 500)
        clf = -2 + 0.5 * np.sin(0.5 * yaw)
        self.plot(yaw, clf, pen=pg.mkPen('b', width=2))

        # Add Vertical Cursor
        self.cursor = pg.InfiniteLine(angle=90, movable=True, pen=pg.mkPen('r', width=2))
        self.addItem(self.cursor)
        self.cursor.setPos(0)  # Initial Yaw position

        # Connect cursor movement to signal        self.cursor.sigPositionChanged.connect(self.emit_yaw_changed)

    def emit_yaw_changed(self):
        current_yaw = self.cursor.value()
        self.yawChanged.emit(current_yaw)


class FloorPlot(pg.PlotWidget):
    """
    Represents each Floor Plot (Plot B).    Displays pressure data based on yaw.    """

    def __init__(self, yaw=0.0, parent=None):
        super().__init__(parent)
        self.setBackground('w')
        self.setContentsMargins(0, 0, 0, 0)
        self.setTitle(f"Floor Plot at Yaw = {yaw:.2f}")
        self.setXRange(-1, 1)
        self.setYRange(-1, 1)
        self.yaw = yaw
        self.pressure_data = self.generate_pressure_data(yaw)
        self.plot_pressure()

    def generate_pressure_data(self, yaw):
        """
        Generates simulated pressure data based on yaw.        Ensures the seed is within [0, 2**32 - 1].        """
        seed = int(yaw * 10)
        seed = abs(seed) % (2 ** 32)
        rng = np.random.default_rng(seed)
        num_points = 50
        x = rng.uniform(-1, 1, num_points)
        y = rng.uniform(-1, 1, num_points)
        pressure = rng.uniform(0, 100, num_points)
        return np.column_stack((x, y, pressure))

    def plot_pressure(self):
        """
        Plots the pressure data on the PlotWidget.        """
        self.clear()
        x = self.pressure_data[:, 0]
        y = self.pressure_data[:, 1]
        pressure = self.pressure_data[:, 2]

        # Normalize pressure for color mapping
        norm_pressure = (pressure - pressure.min()) / (pressure.max() - pressure.min())
        colors = [pg.intColor(int(p * 255)) for p in norm_pressure]

        scatter = pg.ScatterPlotItem(x, y, size=10, brush=colors)
        self.addItem(scatter)

    def update_data(self, yaw):
        """
        Updates the pressure plot based on the new yaw value.        """
        self.yaw = yaw
        self.setTitle(f"Floor Plot at Yaw = {yaw:.2f}")
        self.pressure_data = self.generate_pressure_data(yaw)
        self.plot_pressure()


class FloorPlotWidget(QWidget):
    """
    Encapsulates a FloorPlot along with control buttons.    """
    duplicateRequested = Signal()
    closeRequested = Signal(QWidget)

    def __init__(self, yawChanged_signal, parent=None):
        super().__init__(parent)
        self.is_frozen = False
        self.yawChanged_signal = yawChanged_signal

        # Layout Setup
        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(main_layout)

        # Initialize FloorPlot
        self.floor_plot = FloorPlot(yaw=0.0)
        main_layout.addWidget(self.floor_plot)

        # Buttons Layout
        buttons_layout = QHBoxLayout()
        main_layout.addLayout(buttons_layout)

        # Freeze Button
        self.freeze_button = QPushButton("Freeze")
        self.freeze_button.clicked.connect(self.toggle_freeze)
        buttons_layout.addWidget(self.freeze_button)

        # Duplicate Button
        self.duplicate_button = QPushButton("Duplicate")
        self.duplicate_button.clicked.connect(self.duplicateRequested.emit)
        buttons_layout.addWidget(self.duplicate_button)

        # Close Button
        self.close_button = QPushButton("Close")
        self.close_button.clicked.connect(self.close_plot)
        buttons_layout.addWidget(self.close_button)

        # Connect to yawChanged_signal
        self.yawChanged_signal.connect(self.handle_yaw_changed)

    def handle_yaw_changed(self, yaw):
        if not self.is_frozen:
            self.floor_plot.update_data(yaw)

    def toggle_freeze(self):
        if self.is_frozen:
            # Unfreeze: Start listening
            self.is_frozen = False
            self.freeze_button.setText("Freeze")
            self.yawChanged_signal.connect(self.handle_yaw_changed)
        else:
            # Freeze: Stop listening
            self.is_frozen = True
            self.freeze_button.setText("Unfreeze")
            self.yawChanged_signal.disconnect(self.handle_yaw_changed)

    def close_plot(self):
        """
        Emits a signal to request closing this plot.        """
        self.closeRequested.emit(self)


class FloorPlotGrid(pg.GraphicsLayoutWidget):
    """
    Manages a grid layout of FloorPlotWidget instances.    """

    def __init__(self, yawChanged_signal, parent=None):
        super().__init__(parent)
        self.yawChanged_signal = yawChanged_signal
        self.setLayout(QGridLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)
        self.color_bar = pg.ColorBarItem(interactive=False)
        color_map = pg.colormap.get('CET-D9')
        self.color_bar.setColorMap(color_map)
        self.color_bar.setLevels((-0.2, 0.2))
        self.max_columns = 3
        self.floor_plots = []
        self.proxies = []

        self.setStyleSheet("background: red")

        # Initialize with one FloorPlotWidget
        self.add_floor_plot()

    def update_color_bar(self):
        if len(self.floor_plots) == 0:
            return

        if self.ci.getItem(0, self.max_columns) is not None:
            self.removeItem(self.color_bar)

        rows_span, _ = self.get_row_col()
        col = self.max_columns if len(self.floor_plots) < self.max_columns else len(self.floor_plots)
        self.addItem(self.color_bar, 0, col, rowspan=rows_span + 1)



    def add_floor_plot(self, floor_plot_widget=None):
        """
        Adds a new FloorPlotWidget to the grid.        If floor_plot_widget is None, creates a new instance.        """
        if floor_plot_widget is None:
            floor_plot_widget = FloorPlotWidget(self.yawChanged_signal)

            # Connect signals
        floor_plot_widget.duplicateRequested.connect(self.add_floor_plot)
        floor_plot_widget.closeRequested.connect(self.remove_floor_plot)

        row, col =  self.get_row_col()

        # Create QGraphicsProxyWidget to embed FloorPlotWidget
        proxy = QGraphicsProxyWidget()
        proxy.setContentsMargins(0, 0, 0, 0)
        proxy.setWidget(floor_plot_widget)
        self.addItem(proxy, row, col)

        # Track the FloorPlotWidget and its proxy
        self.floor_plots.append(floor_plot_widget)
        self.proxies.append(proxy)
        self.update_color_bar()

    def get_row_col(self):
        # Calculate row and column
        index = len(self.floor_plots)
        return  divmod(index, self.max_columns)

    def remove_floor_plot(self, floor_plot_widget):
        """
        Removes a FloorPlotWidget from the grid.        """
        if floor_plot_widget in self.floor_plots:
            index = self.floor_plots.index(floor_plot_widget)
            proxy = self.proxies[index]

            # Remove from GraphicsLayoutWidget
            self.removeItem(proxy)

            # Remove references
            self.floor_plots.pop(index)
            self.proxies.pop(index)

            # Delete the proxy and widget
            floor_plot_widget.setParent(None)
            del floor_plot_widget
            del proxy
            # floor_plot_widget.deleteLater()
            # proxy.deleteLater()

            # Rearrange the remaining plots
            self.rearrange_plots()
            self.update_color_bar()

    def rearrange_plots(self):
        """
        Rearranges the plots in the grid after removal.        """
        if len(self.ci.items) > 1:
            self.ci.clear()

            # Reset proxies list
        self.proxies = []


        # Re-add FloorPlotWidgets
        for idx, floor_plot_widget in enumerate(self.floor_plots):
            row, col = divmod(idx, self.max_columns)
            proxy = QGraphicsProxyWidget()
            proxy.setWidget(floor_plot_widget)
            self.addItem(proxy, row, col)
            self.proxies.append(proxy)


class MainLayoutWidget(QWidget):
    """
    Main layout widget containing YawSweepPlot on the left and FloorPlotGrid on the right.    """

    def __init__(self, parent=None):
        super().__init__(parent)

        # Main Layout
        main_layout = QHBoxLayout()
        self.setLayout(main_layout)

        # Initialize YawSweepPlot
        self.yaw_sweep_plot = YawSweepPlot()
        main_layout.addWidget(self.yaw_sweep_plot, stretch=1)

        # Initialize FloorPlotGrid
        self.floor_plot_grid = FloorPlotGrid(self.yaw_sweep_plot.yawChanged)
        main_layout.addWidget(self.floor_plot_grid, stretch=2)


class MainWindow(QMainWindow):
    """
    The main application window.    """

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Yaw Sweep and Floor Plots")
        self.resize(1600, 800)

        # Set Central Widget
        self.main_layout_widget = MainLayoutWidget()
        self.setCentralWidget(self.main_layout_widget)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

I'm getting this error:

Traceback (most recent call last):
  File "C:\dev\personal\LearningPyQT\final_floor_prototype.py", line 243, in remove_floor_plot
    self.rearrange_plots()
  File "C:\dev\personal\LearningPyQT\final_floor_prototype.py", line 260, in rearrange_plots
    proxy.setWidget(floor_plot_widget)
RuntimeError: Internal C++ object (FloorPlotWidget) already deleted.

I'm unable to figure out the exact issue, anyone faced this before

Johan Rauchfuss

unread,
Nov 12, 2024, 12:28:02 PM11/12/24
to pyqt...@googlegroups.com

the FloorPlotWidget you are trying to access has already been deleted, but you are still trying to interact with it, specifically when you call proxy.setWidget(floor_plot_widget).

Problem

In your remove_floor_plot() method, you are explicitly deleting the floor_plot_widget and its associated QGraphicsProxyWidget:

floor_plot_widget.setParent(None)
del floor_plot_widget
del proxy

After this, the widget is completely destroyed, but when you call rearrange_plots(), you attempt to access this deleted widget:

proxy.setWidget(floor_plot_widget)

This leads to the RuntimeError because the object no longer exists.

Solution

Instead of manually deleting the widget with del floor_plot_widget and del proxy, use deleteLater() to safely schedule the deletion of the widget and proxy. This ensures that they are deleted at an appropriate time after the event loop has completed any pending operations.

Fix:

Replace this part of your remove_floor_plot() method:

floor_plot_widget.setParent(None)
del floor_plot_widget
del proxy
# floor_plot_widget.deleteLater()
# proxy.deleteLater()

With:

floor_plot_widget.setParent(None)
floor_plot_widget.deleteLater()
proxy.deleteLater()

Explanation

deleteLater() tells Qt to delete the object when it is safe to do so, after all pending events have been processed.

This avoids accessing a deleted object during the rearrangement of your plots.

Additional Consideration

In your rearrange_plots() method, ensure that you are not trying to use already-deleted objects:

if len(self.ci.items) > 1:
    self.ci.clear()

Instead of manually clearing and resetting, it may be safer to use a check like this:

if not self.floor_plots:
    return

This will skip the rearrangement if there are no plots left.

Summary

Replace manual deletions with deleteLater() to avoid accessing deleted objects, and add additional checks to ensure you are not working with empty lists or invalid references.


--
You received this message because you are subscribed to the Google Groups "pyqtgraph" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pyqtgraph+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/pyqtgraph/8911a4c2-2030-44c1-94cf-88f516d202f6n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages