分享两个布局
FlowLayout
流式布局,从左到右排序
python
# coding:utf-8
from typing import List
from PySide6.QtCore import QSize, QPoint, Qt, QRect, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QEvent, QTimer, QObject
from PySide6.QtWidgets import QLayout, QWidgetItem, QLayoutItem
class FlowLayout(QLayout):
""" Flow layout """
def __init__(self, parent=None, needAni=False, isTight=False):
"""
Parameters
----------
parent:
parent window or layout
needAni: bool
whether to add moving animation
isTight: bool
whether to use the tight layout when widgets are hidden
"""
super().__init__(parent)
self._items = [] # type: List[QLayoutItem]
self._anis = [] # type: List[QPropertyAnimation]
self._aniGroup = QParallelAnimationGroup(self)
self._verticalSpacing = 10
self._horizontalSpacing = 10
self.duration = 300
self.ease = QEasingCurve.Linear
self.needAni = needAni
self.isTight = isTight
self._deBounceTimer = QTimer(self)
self._deBounceTimer.setSingleShot(True)
self._deBounceTimer.timeout.connect(lambda: self._doLayout(self.geometry(), True))
self._wParent = None
self._isInstalledEventFilter = False
def addItem(self, item):
self._items.append(item)
def insertItem(self, index, item):
self._items.insert(index, item)
def addWidget(self, w):
super().addWidget(w)
self._onWidgetAdded(w)
def insertWidget(self, index, w):
self.insertItem(index, QWidgetItem(w))
self.addChildWidget(w)
self._onWidgetAdded(w, index)
def _onWidgetAdded(self, w, index=-1):
if not self._isInstalledEventFilter:
if w.parent():
self._wParent = w.parent()
w.parent().installEventFilter(self)
else:
w.installEventFilter(self)
if not self.needAni:
return
ani = QPropertyAnimation(w, b'geometry')
ani.setEndValue(QRect(QPoint(0, 0), w.size()))
ani.setDuration(self.duration)
ani.setEasingCurve(self.ease)
w.setProperty('flowAni', ani)
self._aniGroup.addAnimation(ani)
if index == -1:
self._anis.append(ani)
else:
self._anis.insert(index, ani)
def setAnimation(self, duration, ease=QEasingCurve.Linear):
""" set the moving animation
Parameters
----------
duration: int
the duration of animation in milliseconds
ease: QEasingCurve
the easing curve of animation
"""
if not self.needAni:
return
self.duration = duration
self.ease = ease
for ani in self._anis:
ani.setDuration(duration)
ani.setEasingCurve(ease)
def count(self):
return len(self._items)
def itemAt(self, index: int):
if 0 <= index < len(self._items):
return self._items[index]
return None
def takeAt(self, index: int):
if 0 <= index < len(self._items):
item = self._items[index] # type: QLayoutItem
ani = item.widget().property('flowAni')
if ani:
self._anis.remove(ani)
self._aniGroup.removeAnimation(ani)
ani.deleteLater()
return self._items.pop(index).widget()
return None
def removeWidget(self, widget):
for i, item in enumerate(self._items):
if item.widget() is widget:
return self.takeAt(i)
def removeAllWidgets(self):
""" remove all widgets from layout """
while self._items:
self.takeAt(0)
def takeAllWidgets(self):
""" remove all widgets from layout and delete them """
while self._items:
w = self.takeAt(0)
if w:
w.deleteLater()
def expandingDirections(self):
return Qt.Orientation(0)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width: int):
""" get the minimal height according to width """
return self._doLayout(QRect(0, 0, width, 0), False)
def setGeometry(self, rect: QRect):
super().setGeometry(rect)
if self.needAni:
self._deBounceTimer.start(80)
else:
self._doLayout(rect, True)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self._items:
size = size.expandedTo(item.minimumSize())
m = self.contentsMargins()
size += QSize(m.left()+m.right(), m.top()+m.bottom())
return size
def setVerticalSpacing(self, spacing: int):
""" set vertical spacing between widgets """
self._verticalSpacing = spacing
def verticalSpacing(self):
""" get vertical spacing between widgets """
return self._verticalSpacing
def setHorizontalSpacing(self, spacing: int):
""" set horizontal spacing between widgets """
self._horizontalSpacing = spacing
def horizontalSpacing(self):
""" get horizontal spacing between widgets """
return self._horizontalSpacing
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if obj in [w.widget() for w in self._items] and event.type() == QEvent.Type.ParentChange:
self._wParent = obj.parent()
obj.parent().installEventFilter(self)
self._isInstalledEventFilter = True
if obj == self._wParent and event.type() == QEvent.Type.Show:
self._doLayout(self.geometry(), True)
self._isInstalledEventFilter = True
return super().eventFilter(obj, event)
def _doLayout(self, rect: QRect, move: bool):
""" adjust widgets position according to the window size """
aniRestart = False
margin = self.contentsMargins()
x = rect.x() + margin.left()
y = rect.y() + margin.top()
rowHeight = 0
spaceX = self.horizontalSpacing()
spaceY = self.verticalSpacing()
for i, item in enumerate(self._items):
if item.widget() and not item.widget().isVisible() and self.isTight:
continue
nextX = x + item.sizeHint().width() + spaceX
if nextX - spaceX > rect.right() - margin.right() and rowHeight > 0:
x = rect.x() + margin.left()
y = y + rowHeight + spaceY
nextX = x + item.sizeHint().width() + spaceX
rowHeight = 0
if move:
target = QRect(QPoint(x, y), item.sizeHint())
if not self.needAni:
item.setGeometry(target)
elif target != self._anis[i].endValue():
self._anis[i].stop()
self._anis[i].setEndValue(target)
aniRestart = True
x = nextX
rowHeight = max(rowHeight, item.sizeHint().height())
if self.needAni and aniRestart:
self._aniGroup.stop()
self._aniGroup.start()
return y + rowHeight + margin.bottom() - rect.y()
例子
python
from random import randint
from PySide6.QtWidgets import QWidget, QPushButton, QApplication
from flowLayout import FlowLayout
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.flowLayout = FlowLayout(self)
for i in range(20):
btn = QPushButton(f"Button {i + 1}")
btn.setMinimumWidth(randint(100, 300))
self.flowLayout.addWidget(btn)
if __name__ == '__main__':
app = QApplication()
w = MyWidget()
w.show()
app.exec()

WaterfallLayout
瀑布流布局,布局内部使用
scaledToWidth
函数来设置高度,如果不设置scaledToWidth
函数,默认保持原有的控件比例进行拉伸,弥补QGridLayout
无法拉伸控件高度的缺点,以及无法自适应列数,一个仿VUE
的GridLayout
python
# coding: utf-8
from pathlib import Path
from PySide6.QtCore import QRect, QPoint, QSize, Property
from PySide6.QtWidgets import QWidget, QScrollArea
from flowLayout import FlowLayout
class WaterfallLayout(FlowLayout):
def __init__(self, parent=None):
super().__init__(parent, False, False)
self._itemMinWidth = 200
self._geometry = self.geometry()
def setItemMinimumWidth(self, width: int):
self._itemMinWidth = width
self._doLayout(self.geometry(), True)
def getItemMinimumWidth(self):
return self._itemMinWidth
def _doLayout(self, rect: QRect, move: bool):
aniRestart = False
margin = self.contentsMargins()
left = rect.x() + margin.left()
top = rect.y() + margin.top()
spaceX = self.horizontalSpacing()
spaceY = self.verticalSpacing()
availableWidth = rect.width() - left - margin.right()
columns = max(1, (availableWidth + spaceX) // (self.itemMinimumWidth + spaceX))
itemWidth = int((availableWidth - (columns - 1) * spaceX) / columns)
columnHeights = [top] * columns
for i, item in enumerate(self._items):
if item.widget() and not item.widget().isVisible() and self.isTight:
continue
widget = item.widget()
if hasattr(widget, 'scaledToWidth'):
widget.scaledToWidth(itemWidth)
height = widget.height()
else:
than = widget.height() / widget.width() # 宽高比
height = int(itemWidth * than)
column = min(columnHeights.index(min(columnHeights)), columns - 1)
x = left + column * (itemWidth + spaceX)
y = columnHeights[column]
if move:
target = QRect(QPoint(x, y), QSize(itemWidth, height))
if not self.needAni:
item.setGeometry(target)
elif target != self._anis[i].endValue():
self._anis[i].stop()
self._anis[i].setEndValue(target)
aniRestart = True
columnHeights[column] += height + spaceY
if self.needAni and aniRestart:
self._aniGroup.stop()
self._aniGroup.start()
return top + max(columnHeights) + margin.bottom() - rect.y()
itemMinimumWidth = Property(int, getItemMinimumWidth, setItemMinimumWidth)
例子
ImageLabel
可参考https://blog.csdn.net/weixin_54217201/article/details/149336017?spm=1011.2415.3001.5331
有scaledToWidth
的组件
python
# coding: utf-8
from pathlib import Path
from PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget, QScrollArea
from components import ImageLabel
from waterfallLayout import WaterfallLayout
class MyWidget(QScrollArea):
def __init__(self, parent=None):
super().__init__(parent)
self.setWidget(QWidget())
self.setWidgetResizable(True)
self.flowLayout = WaterfallLayout(self.widget())
self.flowLayout.setItemMinimumWidth(350)
self.widget().setLayout(self.flowLayout)
for file in list(Path(r'G:\手机\壁纸').glob('*.*'))[:5]:
item = ImageLabel()
item.setRadius(5)
item.setIsCenter(True)
item.setImage(file)
item.setMinimumSize(QSize(300, 200))
self.flowLayout.addWidget(item)
if __name__ == '__main__':
import sys
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
w = MyWidget()
w.resize(867, 628)
w.show()
sys.exit(app.exec())

没有 scaledToWidth
组件
setItemMinimumWidth
的值必须大于item.width()
,需要设置setMinimumSize
否则无法换行,找半天,但是没找到原因
python
from pathlib import Path
from PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget, QScrollArea
from components import ImageLabel
from waterfallLayout import WaterfallLayout
class MyWidget(QScrollArea):
def __init__(self, parent=None):
super().__init__(parent)
self.setWidget(QWidget())
self.setWidgetResizable(True)
self.flowLayout = WaterfallLayout(self.widget())
self.flowLayout.setItemMinimumWidth(350)
self.widget().setLayout(self.flowLayout)
for file in list(Path(r'G:\手机\壁纸').glob('*.*'))[:10]:
item = QLabel()
item.setPixmap(QPixmap(file))
item.setMinimumSize(QSize(300, 200))
item.setScaledContents(True)
self.flowLayout.addWidget(item)
if __name__ == '__main__':
import sys
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
w = MyWidget()
w.resize(867, 628)
w.show()
sys.exit(app.exec())
