Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. QScrollArea with FlowLayout widgets not resizing properly

QScrollArea with FlowLayout widgets not resizing properly

Scheduled Pinned Locked Moved Solved General and Desktop
qscrollareaflowlayout
2 Posts 1 Posters 2.0k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • Stefan ScherfkeS Offline
    Stefan ScherfkeS Offline
    Stefan Scherfke
    wrote on last edited by Stefan Scherfke
    #1

    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:

    0_1507630989642_flow_layout_in_scrollarea.gif

    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_())
    
    1 Reply Last reply
    0
    • Stefan ScherfkeS Offline
      Stefan ScherfkeS Offline
      Stefan Scherfke
      wrote on last edited by Stefan Scherfke
      #2

      The solution was (surprisingly) simple: Use the FlowLayout’s heightChanged signal to update the minimum height of the container (the ScrollArea’s widget).

      Here is a working example:

      """
      PyQt5 port of the `layouts/flowlayout
      <https://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-example.html>`_ example
      from Qt5.
      
      """
      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, QGroupBox
      
          app = QApplication(sys.argv)
      
          container = QWidget()
          container_layout = QVBoxLayout()
          for i in range(2):
              g = QGroupBox(f'Group {i}')
              l = FlowLayout(margin=10)
              l.heightChanged.connect(container.setMinimumHeight)
              g.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_layout.addWidget(g)
          container_layout.addStretch()
          container.setLayout(container_layout)
      
          w = QScrollArea()
          w.setWindowTitle('Flow Layout')
          w.setWidgetResizable(True)
          w.setWidget(container)
          w.show()
      
          sys.exit(app.exec_())
      
      
      1 Reply Last reply
      2

      • Login

      • Login or register to search.
      • First post
        Last post
      0
      • Categories
      • Recent
      • Tags
      • Popular
      • Users
      • Groups
      • Search
      • Get Qt Extensions
      • Unsolved