Do I ever need to manually destroy objects (like pixmaps)?

Viewed 134

I am writing a PySide2-based applicatioon, which includes a QScrollArea that holds a lot of QPixmap images (or better: a list of QLabel's that in turn contain the pixmaps). That list of images can grow quite large over time, so when a certain number is reached I periodically remove some of these images from the scroll area - which works fine.

I do have the impression, however, that even after removing some of the images the memory consumption of my application is still the same. So removing the label widgets might not be sufficient. From the PySide2 docs on QLayout.removeWidget():

Removes the widget widget from the layout. After this call, it is the caller’s responsibility to give the widget a reasonable geometry or to put the widget back into a layout or to explicitly hide it if necessary.

In order to remove the widget I do the following:

while self.images_scroll_layout.count() > MAX_IMAGES:
    to_remove = self.images_scroll_layout.itemAt(self.images_scroll_layout.count() - 1)
    self.images_scroll_layout.removeItem(to_remove)
    to_remove.widget().deleteLater()

So my question is: Do I need to manually destroy the labels/pixmaps I removed from the layout, or should they be garbage-collected automatically?

1 Answers

To understand the operation you have to have the following clear concepts:

  • A QObject will not be removed by the GC if it has a parent.
  • When a widget is added to a layout, then the widget is set as a child of the widget where the layout was established.
  • When using removeWidget() then only the widget is removed from the widget list that handles the layout, so the parent of the widget is still the widget that handles the layout.

To verify you can use the following code where the destroyed signal that indicates when a QObject is deleted will not be emitted.

from PySide2 import QtWidgets


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.add_button = QtWidgets.QPushButton(self.tr("Add"), clicked=self.add_widget)
        self.remove_button = QtWidgets.QPushButton(
            self.tr("Remove"), clicked=self.remove_widget
        )

        scrollarea = QtWidgets.QScrollArea(widgetResizable=True)
        widget = QtWidgets.QWidget()
        scrollarea.setWidget(widget)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self.add_button)
        lay.addWidget(self.remove_button)
        lay.addWidget(scrollarea)

        self.resize(640, 480)

        self.label_layouts = QtWidgets.QVBoxLayout(widget)

        self.counter = 0

    def add_widget(self):
        label = QtWidgets.QLabel(f"label {self.counter}")
        self.label_layouts.addWidget(label)
        self.counter += 1

    def remove_widget(self):
        item = self.label_layouts.itemAt(0)
        if item is None:
            return
        widget = item.widget()
        if widget is None:
            return
        widget.destroyed.connect(print)
        print(f"widget: {widget} Parent: {widget.parentWidget()}")


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

In conclusion: removeWidget() is not used to remove the widget from memory but only makes the layout not handle that widget, if you want to remove a widget you must use deleteLater().

def remove_widget(self):
    item = self.label_layouts.itemAt(0)
    if item is None:
        return
    widget = item.widget()
    if widget is None:
        return
    widget.destroyed.connect(print)
    widget.deleteLater()
Related