带有 FlowLayout 小部件的 QScrollArea 未正确调整大小
问题描述
我想创建一个类似于 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_())
相关文章