Qt小组件 - 2(布局)瀑布流布局,GridLayout,FlowLayout

分享两个布局

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无法拉伸控件高度的缺点,以及无法自适应列数,一个仿VUEGridLayout

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())
相关推荐
Kiri霧7 分钟前
细谈kotlin中缀表达式
开发语言·微信·kotlin
想要成为祖国的花朵12 分钟前
Java_Springboot技术框架讲解部分(二)
java·开发语言·spring boot·spring
Q_Q51100828518 分钟前
python的小学课外综合管理系统
开发语言·spring boot·python·django·flask·node.js
勤奋的知更鸟30 分钟前
JavaScript 性能优化实战:深入性能瓶颈,精炼优化技巧与最佳实践
开发语言·javascript·性能优化
hqxstudying1 小时前
Java行为型模式---模板方法模式
java·开发语言·设计模式·代码规范·模板方法模式
weixin_443290691 小时前
【脚本系列】如何使用 Python 脚本对同一文件夹中表头相同的 Excel 文件进行合并
开发语言·python·excel
暮色驶过苍茫1 小时前
H2 与高斯数据库兼容性解决方案:虚拟表与类型处理
数据库·oracle
zm1 小时前
多客户端-服务器(select,poll)
网络·数据库
荷蒲2 小时前
【小白量化智能体】应用5:编写通达信股票交易指标及生成QMT自动交易Python策略程序
开发语言·python
ljh5746491192 小时前
PHP password_verify() 函数
开发语言·php