首页 文章

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

提问于
浏览
0

我想创建一个类似于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 回答

  • 0

    解决方案(令人惊讶)简单:使用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_())
    

相关问题