带有FlowLayout小部件的QScrollArea没有正确调整大小

我想创建一个类似于KDE(或Gnome或MacOS)系统设置的小部件(例如,像这张图片)

我已经从Qt docs exampe实现了一个FlowLayout .

如果我将一些FlowLayout小部件(包含在带有QVBoxLayout的容器小部件中)放入QScrollArea并调整QSrollArea的大小,那么所有内容都会流动并重新布局 .

但是,如果我增加滚动区域的宽度以使其需要更少的高度,滚动区域仍然认为其小部件需要其最小宽度的原始高度:

0_1507630989642_flow_layout_in_scrollarea.gif

如何使用其子项的实际高度更新滚动区域,以便在不再需要时垂直滚动条消失?

下面,您将找到FlowLayout的(Python)实现,并在 __main__ 块中找到实际示例 .

干杯,斯特凡

"""
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)

2 years ago

解决方案(令人惊讶)简单:使用FlowLayout的 heightChanged 信号来更新容器的最小高度(ScrollArea的小部件) .

这是一个工作示例:

"""
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_())