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

2022-01-12 00:00:00 python pyqt pyqt5 qt

问题描述

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

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

我已经从 Qt 文档示例.

如果我将一些 FlowLayout 小部件(包装在带有 QVBoxLayout 的容器小部件中)放入 QScrollArea 并调整 QSrollArea 的大小,那么一切都会按照应有的方式流动和重新布局.

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.

然而,如果我增加滚动区域的宽度以减少它需要的高度,滚动区域仍然认为它的小部件需要其 minimumWidth 的原始高度:

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?

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

Below, you’ll find the (Python) implementation of the FlowLayout and in the __main__ block the actual example.

干杯,斯蒂芬

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


解决方案

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

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

这是一个工作示例:

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

相关文章