I want to create a widget similar to the KDE (or Gnome or MacOS) system settings (e.g., like this picture)

I already implemented a FlowLayout from the Qt docs exampe.
If I put some FlowLayout widgets (wrapped in a container widget with a QVBoxLayout) into a QScrollArea and resize the QSrollArea, everything flows and re-layouts as it shoulds.
However, if I increase the scroll area’s width so that it needs less height, the scroll area’s still thinks that its widgets require the orginal height for their minimumWidth:

How can I can I update the scroll area with the actual height of its child so that the vertical scroll bar disappears when it’s no longer needed?
Below, you’ll find the (Python) implementation of the FlowLayout and in the __main__ block the actual example.
Cheers, Stefan
"""
PyQt5 port of the `layouts/flowlayout
<https://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-example.html>`_ example
from Qt5.
Usage:
python3 -m pip install pyqt5
python3 flow_layout.py
"""
from PyQt5.QtCore import pyqtSignal, QPoint, QRect, QSize, Qt
from PyQt5.QtWidgets import QLayout, QSizePolicy, QSpacerItem
class FlowLayout(QLayout):
"""A ``QLayout`` that aranges its child widgets horizontally and
vertically.
If enough horizontal space is available, it looks like an ``HBoxLayout``,
but if enough space is lacking, it automatically wraps its children into
multiple rows.
"""
heightChanged = pyqtSignal(int)
def __init__(self, parent=None, margin=0, spacing=-1):
super().__init__(parent)
if parent is not None:
self.setContentsMargins(margin, margin, margin, margin)
self.setSpacing(spacing)
self._item_list = []
def __del__(self):
while self.count():
self.takeAt(0)
def addItem(self, item): # pylint: disable=invalid-name
self._item_list.append(item)
def addSpacing(self, size): # pylint: disable=invalid-name
self.addItem(QSpacerItem(size, 0, QSizePolicy.Fixed, QSizePolicy.Minimum))
def count(self):
return len(self._item_list)
def itemAt(self, index): # pylint: disable=invalid-name
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
def takeAt(self, index): # pylint: disable=invalid-name
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
def expandingDirections(self): # pylint: disable=invalid-name,no-self-use
return Qt.Orientations(Qt.Orientation(0))
def hasHeightForWidth(self): # pylint: disable=invalid-name,no-self-use
return True
def heightForWidth(self, width): # pylint: disable=invalid-name
height = self._do_layout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect): # pylint: disable=invalid-name
super().setGeometry(rect)
self._do_layout(rect, False)
def sizeHint(self): # pylint: disable=invalid-name
return self.minimumSize()
def minimumSize(self): # pylint: disable=invalid-name
size = QSize()
for item in self._item_list:
minsize = item.minimumSize()
extent = item.geometry().bottomRight()
size = size.expandedTo(QSize(minsize.width(), extent.y()))
margin = self.contentsMargins().left()
size += QSize(2 * margin, 2 * margin)
return size
def _do_layout(self, rect, test_only=False):
m = self.contentsMargins()
effective_rect = rect.adjusted(+m.left(), +m.top(), -m.right(), -m.bottom())
x = effective_rect.x()
y = effective_rect.y()
line_height = 0
for item in self._item_list:
wid = item.widget()
space_x = self.spacing()
space_y = self.spacing()
if wid is not None:
space_x += wid.style().layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
space_y += wid.style().layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
next_x = x + item.sizeHint().width() + space_x
if next_x - space_x > effective_rect.right() and line_height > 0:
x = effective_rect.x()
y = y + line_height + space_y
next_x = x + item.sizeHint().width() + space_x
line_height = 0
if not test_only:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = next_x
line_height = max(line_height, item.sizeHint().height())
new_height = y + line_height - rect.y()
self.heightChanged.emit(new_height)
return new_height
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication, QPushButton, QScrollArea, QVBoxLayout, QWidget
class Container(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout())
self._widgets = []
def sizeHint(self):
w = self.size().width()
h = 0
for widget in self._widgets:
h += widget.layout().heightForWidth(w)
sh = super().sizeHint()
print(sh)
print(w, h)
return sh
def add_widget(self, widget):
self._widgets.append(widget)
self.layout().addWidget(widget)
def add_stretch(self):
self.layout().addStretch()
app = QApplication(sys.argv) # pylint: disable=invalid-name
container = Container()
for i in range(2):
w = QWidget()
w.setWindowTitle('Flow Layout')
l = FlowLayout(w, 10)
w.setLayout(l)
l.addWidget(QPushButton('Short'))
l.addWidget(QPushButton('Longer'))
l.addWidget(QPushButton('Different text'))
l.addWidget(QPushButton('More text'))
l.addWidget(QPushButton('Even longer button text'))
container.add_widget(w)
container.add_stretch()
sa = QScrollArea()
sa.setWidgetResizable(True)
sa.setWidget(container)
sa.show()
sys.exit(app.exec_())
QListViewalready has this functionality. The kde settings program looks like it uses multiple list-views in a scroll-area. - ekhumoro