2

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?

Matthias
  • 9,817
  • 14
  • 66
  • 125

1 Answers1

2

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • That is very helpful, thanks a lot. So I will call `deleteLater()` on the widget that has been removed from the layout. One more question though: As said I am using a `QPixmap` placed on to a `QLabel`, and that label is added to the scroll area layout. The pixmap does not have a `deleteLater()` method. Is it sufficient to call that method on the label object only, or do I need to delete the pixmap somehow as well? – Matthias May 08 '20 at 14:57
  • @Matthias When you delete the QLabel all content in your memory is removed including the QPixmap – eyllanesc May 08 '20 at 14:58
  • Ok, now when I call `deleteLater()` on that removed widget I get the following error: `RuntimeError: Internal C++ object (PySide2.QtWidgets.QWidgetItem) already deleted.`. Any ideas? – Matthias May 08 '20 at 15:08
  • @Matthias I think that somewhere in your code you are accessing the widget that has already been deleted, without a [mre] it is impossible to help you. – eyllanesc May 08 '20 at 15:10
  • I added the code sample to my original question. It is quite simple: get the last item from the layout, remove that via `layout.removeItem()`, then call `removed_item.widget().deleteLater()`. And that last call leads to the aforementioned `RuntimeError` message. – Matthias May 08 '20 at 15:18
  • @Matthias remove `self.images_scroll_layout.removeItem(to_remove)` – eyllanesc May 08 '20 at 15:25
  • If I do remove that call then the `while` loop becomes and endless loop. Don't I need to remove the widget from the layout? – Matthias May 08 '20 at 15:31
  • 1
    @Matthias change to `for i in range(MAX, self.images_scroll_layout.count()): item = self.images_scroll_layout.itemAt(i) item.widget().deleteLater()` – eyllanesc May 08 '20 at 15:40
  • Ok that worked, thank you so much. Just one more question for my understanding: When I call `deleteLater()` I don't need to manually remove that widget from the layout, as it is being automatically removed from the layout as part of the deletion process? – Matthias May 08 '20 at 17:01
  • @Matthias Yes, that is what happens. – eyllanesc May 08 '20 at 17:02