プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2022/03/08

[PySide] FlowLayout を作成する

event_note2022/03/08 6:07

Qt でウィンドウの幅によって列数がレスポンシブに変わる(自動で改行される)ようなレイアウトを組むには自分で作成する必要があるみたいです。

こういうレイアウトを Flow Layout というようです。

この Flow Layout を、QLayout を継承して作成する方法が公式ドキュメントに載っています。

ただ、私の環境ではこのコードをそのまま PySide で実装しても細かい部分の挙動がおかしかったので、以下のページも参考にしつつ修正しました。

環境

  • Python 3.8.10
  • PySide 6.2.2.1

サンプルコード

以下が作成した FlowLayout クラスです。
解説などは上記のページに載っているので割愛します。

import sys
from PySide6.QtWidgets import QApplication, QSizePolicy, QWidget, QLayout, QLayoutItem, QPushButton, QScrollArea
from PySide6.QtCore import (Qt, QSize, QRect, QPoint)

class FlowLayout(QLayout):

    def __init__(self, parent=None, margin:int=0, hspacing:int=-1, vspacing:int=-1):
        super().__init__(parent)
        self.setContentsMargins(margin, margin, margin, margin)

        self.__itemlist = []
        self.__hSpacing = hspacing
        self.__vSpacing = vspacing

    def __del__(self):
        item = self.takeAt(0)
        while item:
            del item
            item = self.takeAt(0)

    def addItem(self, item:QLayoutItem):
        self.__itemlist.append(item)

    def count(self):
        return len(self.__itemlist)

    def itemAt(self, index:int):
        if 0 <= index and index < len(self.__itemlist):
            return self.__itemlist[index]
        return None

    def takeAt(self, index:int):
        if 0 <= index and index < len(self.__itemlist):
            return self.__itemlist.pop(index)
        return None

    def expandingDirections(self):
        return Qt.Orientations(Qt.Orientation(0))

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width:int):
        height = self.doLayout(QRect(0, 0, width, 0), True)
        return height

    def setGeometry(self, rect:QRect):
        super().setGeometry(rect)
        self.doLayout(rect, False)

    def sizeHint(self):
        return self.minimumSize()

    def minimumSize(self):
        size = QSize()
        for item in self.__itemlist:
            size = size.expandedTo(item.minimumSize())
        margins = self.contentsMargins()
        size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
        return size

    def doLayout(self, rect:QRect, testOnly:bool):
        left, top, right, bottom = self.getContentsMargins()
        effectiveRect = rect.adjusted(+left, +top, -right, -bottom)
        x = effectiveRect.x()
        y = effectiveRect.y()
        lineHeight = 0

        for item in self.__itemlist:
            wid = item.widget()
            spaceX = self.__hSpacing
            if spaceX == -1:
                spaceX = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.__vSpacing
            if spaceY == -1:
                spaceY = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)

            nextX = x + item.sizeHint().width() + spaceX
            if nextX - spaceX > effectiveRect.right() and lineHeight > 0:
                x = effectiveRect.x()
                y = y + lineHeight + spaceY
                nextX = x + item.sizeHint().width() + spaceX
                lineHeight = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            x = nextX
            lineHeight = max(lineHeight, item.sizeHint().height())

        return y + lineHeight - effectiveRect.y() + top + bottom

if __name__ == '__main__':

    class MainWindow(QScrollArea):

        def __init__(self, parent=None):
            super().__init__(parent)

            widget = QWidget()
            flowlayout = FlowLayout(widget, 10, 5)
            for i in range(100):
                button = QPushButton(f"Sample {i}")
                flowlayout.addWidget(button)
            self.setWidgetResizable(True)
            self.setWidget(widget)

    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())