Qt小组件 - 2(布局)瀑布流布局,GridLayout,FlowLayout
分享两个布局
FlowLayout
流式布局,从左到右排序
# coding:utf-8
from typing import Listfrom PySide6.QtCore import QSize, QPoint, Qt, QRect, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QEvent, QTimer, QObject
from PySide6.QtWidgets import QLayout, QWidgetItem, QLayoutItemclass FlowLayout(QLayout):""" Flow layout """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()
例子
from random import randintfrom PySide6.QtWidgets import QWidget, QPushButton, QApplication
from flowLayout import FlowLayoutclass 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
# coding: utf-8
from pathlib import Pathfrom PySide6.QtCore import QRect, QPoint, QSize, Property
from PySide6.QtWidgets import QWidget, QScrollArea
from flowLayout import FlowLayoutclass WaterfallLayout(FlowLayout):def __init__(self, parent=None):super().__init__(parent, False, False)self._itemMinWidth = 200self._geometry = self.geometry()def setItemMinimumWidth(self, width: int):self._itemMinWidth = widthself._doLayout(self.geometry(), True)def getItemMinimumWidth(self):return self._itemMinWidthdef _doLayout(self, rect: QRect, move: bool):aniRestart = Falsemargin = 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] * columnsfor i, item in enumerate(self._items):if item.widget() and not item.widget().isVisible() and self.isTight:continuewidget = 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 = TruecolumnHeights[column] += height + spaceYif 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
的组件
# coding: utf-8
from pathlib import Pathfrom PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget, QScrollAreafrom components import ImageLabel
from waterfallLayout import WaterfallLayoutclass 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 sysfrom PySide6.QtWidgets import QApplicationapp = QApplication(sys.argv)w = MyWidget()w.resize(867, 628)w.show()sys.exit(app.exec())
没有 scaledToWidth
组件
setItemMinimumWidth
的值必须大于item.width()
,需要设置setMinimumSize
否则无法换行,找半天,但是没找到原因
from pathlib import Pathfrom PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget, QScrollAreafrom components import ImageLabel
from waterfallLayout import WaterfallLayoutclass 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 sysfrom PySide6.QtWidgets import QApplicationapp = QApplication(sys.argv)w = MyWidget()w.resize(867, 628)w.show()sys.exit(app.exec())