QScrollArea 中的冻结小部件
Frozen widgets in QScrollArea
我正在尝试创建一个可滚动的方形按钮网格,如果 window 太小而无法显示所有按钮。我希望在最左边的列和最上面的行上有标签显示按钮索引。
有没有办法创建一个 QScrollArea,其中的小部件(标签)在最顶行和最左边的列“冻结”。类似于在 Excel Sheet 中冻结行和列的方式,它们会在您滚动时跟随视图。
在此处查看模型:
欢迎使用 Qt 和 PyQt。
我使用 this answer 中概述的方法解决了多个 QScrollAreas 的问题。这个想法是让冻结区域 QScrollArea
禁用滚动,而未冻结的 QScrollArea
滚动条信号连接到冻结的 QScrollArea
滚动条槽。
这是我的模型的代码,最上面的行和最左边的列被冻结。特别相关的部分是 FrozenScrollArea
class 和 Window
class.
中的连接
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QPushButton,
QWidget,
QScrollArea,
QGridLayout,
QLabel,
QFrame,
QSpacerItem,
QSizePolicy,
)
ROWS = 10
COLS = 20
SIZE = 35
style = """
Button {
padding: 0;
margin: 0;
border: 1px solid black;
}
Button::checked {
background-color: lightgreen;
}
"""
class Button(QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(SIZE, SIZE)
self.setCheckable(True)
self.setStyleSheet(style)
class Label(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAlignment(Qt.AlignCenter)
self.setFixedSize(SIZE, SIZE)
class Labels(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class FrozenScrollArea(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.verticalScrollBar().setEnabled(False)
self.horizontalScrollBar().setEnabled(False)
class FrozenRow(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for c in range(COLS):
label = Label(self, text = str(c))
labels.layout().addWidget(label, 0, c, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, COLS, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedHeight(SIZE)
self.setWidget(labels)
class FrozenColumn(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for r in range(ROWS):
label = Label(self, text = str(r))
labels.layout().addWidget(label, r, 0, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), ROWS, 0, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedWidth(SIZE)
self.setWidget(labels)
class ButtonGroup(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
for r in range(ROWS):
for c in range(COLS):
button = Button(self)
layout.addWidget(button, r, c, 1, 1)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class Buttons(QScrollArea):
def __init__(self, parent):
super().__init__()
self.setFrameShape(QFrame.NoFrame)
self.setWidget(ButtonGroup(parent))
class Window(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# layout
layout = QGridLayout()
self.setLayout(layout)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# frozen row (top)
self.frozenRow = FrozenRow(self)
layout.addWidget(self.frozenRow, 0, 1, 1, 1)
# frozen column (left)
self.frozenColumn = FrozenColumn(self)
layout.addWidget(self.frozenColumn, 1, 0, 1, 1)
# button grid
self.buttons = Buttons(self)
layout.addWidget(self.buttons, 1, 1, 1, 1)
# scrollbar connections
self.buttons.horizontalScrollBar().valueChanged.connect(self.frozenRow.horizontalScrollBar().setValue) # horizontal scroll affects frozen row only
self.buttons.verticalScrollBar().valueChanged.connect(self.frozenColumn.verticalScrollBar().setValue) # vertical scroll affects frozemn column only
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec())
虽然冻结滚动区域的方法很有效,但它也有一些缺点;最重要的是,它:
- 不是动态的;
- 不考虑基本的框布局;
- 不支持不同方向(对于盒装布局)或原点(对于网格布局);
虽然这更像是一个“边缘案例”,但我想建议一个替代方案,它基于 QHeaderView 和一个使用 header 尺寸的布局管理器的“私有”模型。
它并不像标准 QHeaderView 所期望的那样直接支持调整大小,但这几乎是不可能的:对于盒装布局,不可能设置布局项目大小(如果不是通过完全覆盖布局设置几何图形的方式) ),并且对于网格布局,没有办法知道行或列是否被“实际”删除,因为 rowCount()
和 columnCount()
永远不会在网格大小更改时动态更新。
该概念基于覆盖滚动区域的事件过滤器,并检查是否正在发生几何变化和 如果布局必须再次布置项目。然后,实现使用布局信息来更新基础模型并为 SizeHintRole
为 headerData()
.
提供适当的值
子类 QScrollArea 创建两个 QHeaderView,并在需要时使用 ResizeToContents
部分调整大小模式(查询 headerData()
)更新它们,并根据 setViewportMargins
的大小提示使用 headers.
class LayoutModel(QtCore.QAbstractTableModel):
reverse = {
QtCore.Qt.Horizontal: False,
QtCore.Qt.Vertical: False
}
def __init__(self, rows=None, columns=None):
super().__init__()
self.rows = rows or []
self.columns = columns or []
def setLayoutData(self, hSizes, vSizes, reverseH=False, reverseV=False):
self.beginResetModel()
self.reverse = {
QtCore.Qt.Horizontal: reverseH,
QtCore.Qt.Vertical: reverseV
}
self.rows = vSizes
self.columns = hSizes
opt = QtWidgets.QStyleOptionHeader()
opt.text = str(len(vSizes))
style = QtWidgets.QApplication.style()
self.headerSizeHint = style.sizeFromContents(style.CT_HeaderSection, opt, QtCore.QSize())
self.endResetModel()
def rowCount(self, parent=None):
return len(self.rows)
def columnCount(self, parent=None):
return len(self.columns)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if self.reverse[orientation]:
if orientation == QtCore.Qt.Horizontal:
section = len(self.columns) - 1 - section
else:
section = len(self.rows) - 1 - section
# here you can add support for custom header labels
return str(section + 1)
elif role == QtCore.Qt.SizeHintRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QSize(self.columns[section], self.headerSizeHint.height())
return QtCore.QSize(self.headerSizeHint.width(), self.rows[section])
def data(self, *args, **kwargs):
pass # not really required, but provided for consistency
class ScrollAreaLayoutHeaders(QtWidgets.QScrollArea):
_initialized = False
def __init__(self):
super().__init__()
self.hHeader = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self)
self.vHeader = QtWidgets.QHeaderView(QtCore.Qt.Vertical, self)
self.layoutModel = LayoutModel()
for header in self.hHeader, self.vHeader:
header.setModel(self.layoutModel)
header.setSectionResizeMode(header.Fixed)
self.updateTimer = QtCore.QTimer(
interval=0, timeout=self.updateHeaderSizes, singleShot=True)
def layout(self):
try:
return self.widget().layout()
except AttributeError:
pass
def eventFilter(self, obj, event):
if obj == self.widget() and obj.layout() is not None:
if event.type() in (event.Resize, event.Move):
if self.sender() in (self.verticalScrollBar(), self.horizontalScrollBar()):
self.updateGeometries()
else:
self.updateHeaderSizes()
elif event.type() == event.LayoutRequest:
self.widget().adjustSize()
self.updateTimer.start()
return super().eventFilter(obj, event)
def updateHeaderSizes(self):
layout = self.layout()
if layout is None:
self.layoutModel.setLayoutData([], [])
self.updateGeometries()
return
self._initialized = True
hSizes = []
vSizes = []
layGeo = self.widget().rect()
reverseH = reverseV = False
if isinstance(layout, QtWidgets.QBoxLayout):
count = layout.count()
direction = layout.direction()
geometries = [layout.itemAt(i).geometry() for i in range(count)]
# LeftToRight and BottomToTop layouts always have a first bit set
reverse = direction & 1
if reverse:
geometries.reverse()
lastPos = 0
lastGeo = geometries[0]
if layout.direction() in (layout.LeftToRight, layout.RightToLeft):
if reverse:
reverseH = True
vSizes.append(layGeo.bottom())
lastExt = lastGeo.x() + lastGeo.width()
for geo in geometries[1:]:
newPos = lastExt + (geo.x() - lastExt) / 2
hSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.x() + geo.width()
hSizes.append(layGeo.right() - lastPos - 1)
else:
if reverse:
reverseV = True
hSizes.append(layGeo.right())
lastExt = lastGeo.y() + lastGeo.height()
for geo in geometries[1:]:
newPos = lastExt + (geo.y() - lastExt) / 2
vSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.y() + geo.height()
vSizes.append(layGeo.bottom() - lastPos + 1)
else:
# assume a grid layout
origin = layout.originCorner()
if origin & 1:
reverseH = True
if origin & 2:
reverseV = True
first = layout.cellRect(0, 0)
lastX = lastY = 0
lastRight = first.x() + first.width()
lastBottom = first.y() + first.height()
for c in range(1, layout.columnCount()):
cr = layout.cellRect(0, c)
newX = lastRight + (cr.x() - lastRight) / 2
hSizes.append(newX - lastX)
lastX = newX
lastRight = cr.x() + cr.width()
hSizes.append(layGeo.right() - lastX)
for r in range(1, layout.rowCount()):
cr = layout.cellRect(r, 0)
newY = lastBottom + (cr.y() - lastBottom) / 2
vSizes.append(newY - lastY)
lastY = newY
lastBottom = cr.y() + cr.height()
vSizes.append(layGeo.bottom() - lastY)
hSizes[0] += 2
vSizes[0] += 2
self.layoutModel.setLayoutData(hSizes, vSizes, reverseH, reverseV)
self.updateGeometries()
def updateGeometries(self):
self.hHeader.resizeSections(self.hHeader.ResizeToContents)
self.vHeader.resizeSections(self.vHeader.ResizeToContents)
left = self.vHeader.sizeHint().width()
top = self.hHeader.sizeHint().height()
self.setViewportMargins(left, top, 0, 0)
vg = self.viewport().geometry()
self.hHeader.setGeometry(vg.x(), 0,
self.viewport().width(), top)
self.vHeader.setGeometry(0, vg.y(),
left, self.viewport().height())
self.hHeader.setOffset(self.horizontalScrollBar().value())
self.vHeader.setOffset(self.verticalScrollBar().value())
def sizeHint(self):
if not self._initialized and self.layout():
self.updateHeaderSizes()
hint = super().sizeHint()
if self.widget():
viewHint = self.viewportSizeHint()
if self.horizontalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.width() > hint.width():
hint.setHeight(hint.height() + self.horizontalScrollBar().sizeHint().height())
if self.verticalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.height() > hint.height():
hint.setWidth(hint.width() + self.verticalScrollBar().sizeHint().width())
hint += QtCore.QSize(
self.viewportMargins().left(), self.viewportMargins().top())
return hint
def resizeEvent(self, event):
super().resizeEvent(event)
QtCore.QTimer.singleShot(0, self.updateGeometries)
备注:
- 上面的代码会导致某种程度的递归;这是预料之中的,因为调整视口的大小显然会触发
resizeEvent
,但 Qt 足够聪明,可以在大小不变时忽略它们;
- 这仅适用于基本的 QBoxLayouts 和 QGridLayout;它未经 QFormLayout 测试,其他自定义 QLayout 子类的行为完全出乎意料;
我正在尝试创建一个可滚动的方形按钮网格,如果 window 太小而无法显示所有按钮。我希望在最左边的列和最上面的行上有标签显示按钮索引。
有没有办法创建一个 QScrollArea,其中的小部件(标签)在最顶行和最左边的列“冻结”。类似于在 Excel Sheet 中冻结行和列的方式,它们会在您滚动时跟随视图。
在此处查看模型:
欢迎使用 Qt 和 PyQt。
我使用 this answer 中概述的方法解决了多个 QScrollAreas 的问题。这个想法是让冻结区域 QScrollArea
禁用滚动,而未冻结的 QScrollArea
滚动条信号连接到冻结的 QScrollArea
滚动条槽。
这是我的模型的代码,最上面的行和最左边的列被冻结。特别相关的部分是 FrozenScrollArea
class 和 Window
class.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QPushButton,
QWidget,
QScrollArea,
QGridLayout,
QLabel,
QFrame,
QSpacerItem,
QSizePolicy,
)
ROWS = 10
COLS = 20
SIZE = 35
style = """
Button {
padding: 0;
margin: 0;
border: 1px solid black;
}
Button::checked {
background-color: lightgreen;
}
"""
class Button(QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedSize(SIZE, SIZE)
self.setCheckable(True)
self.setStyleSheet(style)
class Label(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAlignment(Qt.AlignCenter)
self.setFixedSize(SIZE, SIZE)
class Labels(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class FrozenScrollArea(QScrollArea):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.verticalScrollBar().setEnabled(False)
self.horizontalScrollBar().setEnabled(False)
class FrozenRow(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for c in range(COLS):
label = Label(self, text = str(c))
labels.layout().addWidget(label, 0, c, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, COLS, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedHeight(SIZE)
self.setWidget(labels)
class FrozenColumn(FrozenScrollArea):
def __init__(self, parent):
super().__init__()
labels = Labels(parent)
for r in range(ROWS):
label = Label(self, text = str(r))
labels.layout().addWidget(label, r, 0, 1, 1, Qt.AlignCenter)
labels.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), ROWS, 0, 1, 1)
self.setFrameShape(QFrame.NoFrame)
self.setFixedWidth(SIZE)
self.setWidget(labels)
class ButtonGroup(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QGridLayout()
for r in range(ROWS):
for c in range(COLS):
button = Button(self)
layout.addWidget(button, r, c, 1, 1)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
class Buttons(QScrollArea):
def __init__(self, parent):
super().__init__()
self.setFrameShape(QFrame.NoFrame)
self.setWidget(ButtonGroup(parent))
class Window(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# layout
layout = QGridLayout()
self.setLayout(layout)
layout.setHorizontalSpacing(0)
layout.setVerticalSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# frozen row (top)
self.frozenRow = FrozenRow(self)
layout.addWidget(self.frozenRow, 0, 1, 1, 1)
# frozen column (left)
self.frozenColumn = FrozenColumn(self)
layout.addWidget(self.frozenColumn, 1, 0, 1, 1)
# button grid
self.buttons = Buttons(self)
layout.addWidget(self.buttons, 1, 1, 1, 1)
# scrollbar connections
self.buttons.horizontalScrollBar().valueChanged.connect(self.frozenRow.horizontalScrollBar().setValue) # horizontal scroll affects frozen row only
self.buttons.verticalScrollBar().valueChanged.connect(self.frozenColumn.verticalScrollBar().setValue) # vertical scroll affects frozemn column only
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec())
虽然冻结滚动区域的方法很有效,但它也有一些缺点;最重要的是,它:
- 不是动态的;
- 不考虑基本的框布局;
- 不支持不同方向(对于盒装布局)或原点(对于网格布局);
虽然这更像是一个“边缘案例”,但我想建议一个替代方案,它基于 QHeaderView 和一个使用 header 尺寸的布局管理器的“私有”模型。
它并不像标准 QHeaderView 所期望的那样直接支持调整大小,但这几乎是不可能的:对于盒装布局,不可能设置布局项目大小(如果不是通过完全覆盖布局设置几何图形的方式) ),并且对于网格布局,没有办法知道行或列是否被“实际”删除,因为 rowCount()
和 columnCount()
永远不会在网格大小更改时动态更新。
该概念基于覆盖滚动区域的事件过滤器,并检查是否正在发生几何变化和 如果布局必须再次布置项目。然后,实现使用布局信息来更新基础模型并为 SizeHintRole
为 headerData()
.
子类 QScrollArea 创建两个 QHeaderView,并在需要时使用 ResizeToContents
部分调整大小模式(查询 headerData()
)更新它们,并根据 setViewportMargins
的大小提示使用 headers.
class LayoutModel(QtCore.QAbstractTableModel):
reverse = {
QtCore.Qt.Horizontal: False,
QtCore.Qt.Vertical: False
}
def __init__(self, rows=None, columns=None):
super().__init__()
self.rows = rows or []
self.columns = columns or []
def setLayoutData(self, hSizes, vSizes, reverseH=False, reverseV=False):
self.beginResetModel()
self.reverse = {
QtCore.Qt.Horizontal: reverseH,
QtCore.Qt.Vertical: reverseV
}
self.rows = vSizes
self.columns = hSizes
opt = QtWidgets.QStyleOptionHeader()
opt.text = str(len(vSizes))
style = QtWidgets.QApplication.style()
self.headerSizeHint = style.sizeFromContents(style.CT_HeaderSection, opt, QtCore.QSize())
self.endResetModel()
def rowCount(self, parent=None):
return len(self.rows)
def columnCount(self, parent=None):
return len(self.columns)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if self.reverse[orientation]:
if orientation == QtCore.Qt.Horizontal:
section = len(self.columns) - 1 - section
else:
section = len(self.rows) - 1 - section
# here you can add support for custom header labels
return str(section + 1)
elif role == QtCore.Qt.SizeHintRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QSize(self.columns[section], self.headerSizeHint.height())
return QtCore.QSize(self.headerSizeHint.width(), self.rows[section])
def data(self, *args, **kwargs):
pass # not really required, but provided for consistency
class ScrollAreaLayoutHeaders(QtWidgets.QScrollArea):
_initialized = False
def __init__(self):
super().__init__()
self.hHeader = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self)
self.vHeader = QtWidgets.QHeaderView(QtCore.Qt.Vertical, self)
self.layoutModel = LayoutModel()
for header in self.hHeader, self.vHeader:
header.setModel(self.layoutModel)
header.setSectionResizeMode(header.Fixed)
self.updateTimer = QtCore.QTimer(
interval=0, timeout=self.updateHeaderSizes, singleShot=True)
def layout(self):
try:
return self.widget().layout()
except AttributeError:
pass
def eventFilter(self, obj, event):
if obj == self.widget() and obj.layout() is not None:
if event.type() in (event.Resize, event.Move):
if self.sender() in (self.verticalScrollBar(), self.horizontalScrollBar()):
self.updateGeometries()
else:
self.updateHeaderSizes()
elif event.type() == event.LayoutRequest:
self.widget().adjustSize()
self.updateTimer.start()
return super().eventFilter(obj, event)
def updateHeaderSizes(self):
layout = self.layout()
if layout is None:
self.layoutModel.setLayoutData([], [])
self.updateGeometries()
return
self._initialized = True
hSizes = []
vSizes = []
layGeo = self.widget().rect()
reverseH = reverseV = False
if isinstance(layout, QtWidgets.QBoxLayout):
count = layout.count()
direction = layout.direction()
geometries = [layout.itemAt(i).geometry() for i in range(count)]
# LeftToRight and BottomToTop layouts always have a first bit set
reverse = direction & 1
if reverse:
geometries.reverse()
lastPos = 0
lastGeo = geometries[0]
if layout.direction() in (layout.LeftToRight, layout.RightToLeft):
if reverse:
reverseH = True
vSizes.append(layGeo.bottom())
lastExt = lastGeo.x() + lastGeo.width()
for geo in geometries[1:]:
newPos = lastExt + (geo.x() - lastExt) / 2
hSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.x() + geo.width()
hSizes.append(layGeo.right() - lastPos - 1)
else:
if reverse:
reverseV = True
hSizes.append(layGeo.right())
lastExt = lastGeo.y() + lastGeo.height()
for geo in geometries[1:]:
newPos = lastExt + (geo.y() - lastExt) / 2
vSizes.append(newPos - lastPos)
lastPos = newPos
lastExt = geo.y() + geo.height()
vSizes.append(layGeo.bottom() - lastPos + 1)
else:
# assume a grid layout
origin = layout.originCorner()
if origin & 1:
reverseH = True
if origin & 2:
reverseV = True
first = layout.cellRect(0, 0)
lastX = lastY = 0
lastRight = first.x() + first.width()
lastBottom = first.y() + first.height()
for c in range(1, layout.columnCount()):
cr = layout.cellRect(0, c)
newX = lastRight + (cr.x() - lastRight) / 2
hSizes.append(newX - lastX)
lastX = newX
lastRight = cr.x() + cr.width()
hSizes.append(layGeo.right() - lastX)
for r in range(1, layout.rowCount()):
cr = layout.cellRect(r, 0)
newY = lastBottom + (cr.y() - lastBottom) / 2
vSizes.append(newY - lastY)
lastY = newY
lastBottom = cr.y() + cr.height()
vSizes.append(layGeo.bottom() - lastY)
hSizes[0] += 2
vSizes[0] += 2
self.layoutModel.setLayoutData(hSizes, vSizes, reverseH, reverseV)
self.updateGeometries()
def updateGeometries(self):
self.hHeader.resizeSections(self.hHeader.ResizeToContents)
self.vHeader.resizeSections(self.vHeader.ResizeToContents)
left = self.vHeader.sizeHint().width()
top = self.hHeader.sizeHint().height()
self.setViewportMargins(left, top, 0, 0)
vg = self.viewport().geometry()
self.hHeader.setGeometry(vg.x(), 0,
self.viewport().width(), top)
self.vHeader.setGeometry(0, vg.y(),
left, self.viewport().height())
self.hHeader.setOffset(self.horizontalScrollBar().value())
self.vHeader.setOffset(self.verticalScrollBar().value())
def sizeHint(self):
if not self._initialized and self.layout():
self.updateHeaderSizes()
hint = super().sizeHint()
if self.widget():
viewHint = self.viewportSizeHint()
if self.horizontalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.width() > hint.width():
hint.setHeight(hint.height() + self.horizontalScrollBar().sizeHint().height())
if self.verticalScrollBarPolicy() == QtCore.Qt.ScrollBarAsNeeded:
if viewHint.height() > hint.height():
hint.setWidth(hint.width() + self.verticalScrollBar().sizeHint().width())
hint += QtCore.QSize(
self.viewportMargins().left(), self.viewportMargins().top())
return hint
def resizeEvent(self, event):
super().resizeEvent(event)
QtCore.QTimer.singleShot(0, self.updateGeometries)
备注:
- 上面的代码会导致某种程度的递归;这是预料之中的,因为调整视口的大小显然会触发
resizeEvent
,但 Qt 足够聪明,可以在大小不变时忽略它们; - 这仅适用于基本的 QBoxLayouts 和 QGridLayout;它未经 QFormLayout 测试,其他自定义 QLayout 子类的行为完全出乎意料;