问题描述
我想创建一个类似于 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_())