最近研究瀑布流布局,发现都是收费的,所以只能自己写算法写布局。
所以啥都不说直接上代码
ImageLabel
参考 pyqt5 QLabel显示网络图片或qfluentwidgets官网
代码
import math
import sys
from pathlib import Pathfrom PyQt5.Qt import *
from qfluentwidgets import ImageLabelclass FlowLayout(QLayout):""" 流布局 """def __init__(self, parent=None, needAni=False, isTight=False):"""Parameters----------parent:parent window or layoutneedAni: boolwhether to add moving animationisTight: boolwhether 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 = 10self._horizontalSpacing = 10self.duration = 300self.ease = QEasingCurve.Linearself.needAni = needAniself.isTight = isTightself._deBounceTimer = QTimer(self)self._deBounceTimer.setSingleShot(True)self._deBounceTimer.timeout.connect(lambda: self._doLayout(self.geometry(), True))self._wParent = Noneself._isInstalledEventFilter = Falsedef 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:returnani = 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 animationParameters----------duration: intthe duration of animation in millisecondsease: QEasingCurvethe easing curve of animation"""if not self.needAni:returnself.duration = durationself.ease = easefor 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 Nonedef takeAt(self, index: int):if 0 <= index < len(self._items):item = self._items[index] # type: QLayoutItemani = item.widget().property('flowAni')if ani:self._anis.remove(ani)self._aniGroup.removeAnimation(ani)ani.deleteLater()return self._items.pop(index).widget()return Nonedef 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 Truedef 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 sizedef setVerticalSpacing(self, spacing: int):""" set vertical spacing between widgets """self._verticalSpacing = spacingdef verticalSpacing(self):""" get vertical spacing between widgets """return self._verticalSpacingdef setHorizontalSpacing(self, spacing: int):""" set horizontal spacing between widgets """self._horizontalSpacing = spacingdef horizontalSpacing(self):""" get horizontal spacing between widgets """return self._horizontalSpacingdef 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 = Trueif obj == self._wParent and event.type() == QEvent.Type.Show:self._doLayout(self.geometry(), True)self._isInstalledEventFilter = Truereturn super().eventFilter(obj, event)def _doLayout(self, rect: QRect, move: bool):""" adjust widgets position according to the window size """aniRestart = Falsemargin = self.contentsMargins()x = rect.x() + margin.left()y = rect.y() + margin.top()rowHeight = 0spaceX = self.horizontalSpacing()spaceY = self.verticalSpacing()for i, item in enumerate(self._items):if item.widget() and not item.widget().isVisible() and self.isTight:continuenextX = x + item.sizeHint().width() + spaceXif nextX - spaceX > rect.right() - margin.right() and rowHeight > 0:x = rect.x() + margin.left()y = y + rowHeight + spaceYnextX = x + item.sizeHint().width() + spaceXrowHeight = 0if 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 = Truex = nextXrowHeight = max(rowHeight, item.sizeHint().height())if self.needAni and aniRestart:self._aniGroup.stop()self._aniGroup.start()return y + rowHeight + margin.bottom() - rect.y()class WaterFallFlowLayout(FlowLayout):""" 瀑布流布局 """def _doLayout(self, rect: QRect, move: bool):""" 重写布局函数 """aniRestart = False# 获取item的宽度margin = self.contentsMargins()item_width = self._items[0].sizeHint().width()# 计算列数columnCount = math.floor(rect.width() / item_width)# 获取spacingspaceX = self.horizontalSpacing()spaceY = self.verticalSpacing()# 布局,根据布局调整左边宽度,默认是居中的,若靠左left=0,靠右2 * leftleft = (rect.width() - item_width * columnCount - spaceX * (columnCount - 1)) // 2hrr = []for index, item in enumerate(self._items):item_size = item.sizeHint()if index < columnCount:target = QRect(QPoint(left + index * (item_width + spaceX), spaceY), item_size)hrr.append(item_size.height() + spaceY)else:minHeight = min(hrr)i = hrr.index(minHeight)target = QRect(QPoint(left + i * (item_width + spaceX), minHeight + spaceY), item_size)hrr[i] += item_size.height() + spaceYif not self.needAni:item.setGeometry(target)elif target != self._anis[index].endValue():self._anis[index].stop()self._anis[index].setEndValue(target)aniRestart = Trueif self.needAni and aniRestart:self._aniGroup.stop()self._aniGroup.start()return max(hrr)class Window(QScrollArea):def __init__(self, parent=None):super().__init__(parent)self.widget = QWidget()self.waterfallLayout = WaterFallFlowLayout(self.widget)pngs = Path(r"G:\手机\壁纸\手机壁纸").glob("**/*.png")for i, file in enumerate(pngs):pixmap = QPixmap(str(file))if not pixmap.isNull():imageLabel = ImageLabel()imageLabel.setBorderRadius(8, 8, 8, 8)imageLabel.setPixmap(pixmap)imageLabel.scaledToWidth(300)self.waterfallLayout.addWidget(imageLabel)if i == 30:breakself.__initWidgets()def __initWidgets(self):self.widget.setAutoFillBackground(False)self.setWidget(self.widget)self.setWidgetResizable(True)if __name__ == '__main__':QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)app = QApplication(sys.argv)app.setQuitOnLastWindowClosed(True)demo = Window()demo.resize(800, 600)demo.show()sys.exit(app.exec_())