How to avoid "jump" when removing a widget from QSrollArea

97 Views Asked by At

I have a QScollarea where widgets (rows) can be added or removed dynamically. I would like to avoid jumping around when removing a row (by adding blank space at the bottom if necessary).

For example: I have 5 rows and have scrolled/resized such that rows 2,3,4,5 are shown. Right now, when I remove row 4, rows 1,2,3,5 will be shown. However, I'd like to keep row 2 at the top and add some white space at the bottom so that rows 2,3,5 + a blank row are shown.

I have tried to play around with stretch but could not achieve this behaviour. Any ideas or help would be greatly appreciated!

Here is my code:

from PySide6.QtGui import Qt
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLineEdit,
    QPushButton,
    QScrollArea,
    QVBoxLayout,
    QWidget,
)


class CustomWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.counter = 0
        self.widget = QWidget()
        self._layout = QVBoxLayout()
        self.setLayout(self._layout)
        for _ in range(5):
            self.add_row()

    def add_row(self):
        self.counter += 1
        add_button = QPushButton("+", None)
        add_button.clicked.connect(lambda _: self.add_row())
        remove_button = QPushButton("-", None)
        remove_button.clicked.connect(
            lambda _: self.remove_row_by_button(remove_button)
        )
        line_edit = QLineEdit(f"{self.counter}")

        row_layout = QHBoxLayout()
        row_layout.addWidget(add_button, 0, Qt.AlignTop)
        row_layout.addWidget(remove_button, 0, Qt.AlignTop)
        row_layout.addWidget(line_edit, 0, Qt.AlignTop)
        row_layout.addStretch()

        self._layout.addLayout(row_layout)

    def remove_row_by_button(self, remove_button):
        for i in range(self._layout.count()):
            row_layout = self._layout.itemAt(i)
            if row_layout.itemAt(1).widget() is remove_button:
                self.remove_row(i)
                return

    def remove_row(self, index):
        row_layout = self._layout.takeAt(index)
        QWidget().setLayout(row_layout)  # remove layout by setting it to another widget


class CustomScrollArea(QScrollArea):
    def __init__(self):
        super().__init__()
        self.setWidgetResizable(True)
        widget = QWidget()

        layout = QVBoxLayout()
        layout.addWidget(CustomWidget())

        widget.setLayout(layout)
        self.setWidget(widget)


if __name__ == "__main__":
    app = QApplication([])
    sa = CustomScrollArea()
    sa.show()
    app.exec()
1

There are 1 best solutions below

5
Alexander On

Well one really simple way to do this would be to add some fixed spacing using layout.addSpacing to the end of the layout each time a row is removed from the scroll area.

You will also want to make sure you properly delete the row in your remove_row method.

I made some inline notes where I made changes in the example below.

For example:

    def remove_row_by_button(self, remove_button):

        # add button height plus layout gaps to get the size
        size = remove_button.height() + self._layout.spacing()

        for i in range(self._layout.count()):
            row_layout = self._layout.itemAt(i)
            if row_layout.itemAt(1).widget() is remove_button:
                self.remove_row(i)
                
                # add the spacing after removing the row
                self._layout.addSpacing(size)  

                return


    def remove_row(self, index):
        row_layout = self._layout.takeAt(index)
        row_layout.deleteLater()