Qt でウィンドウの幅によって列数がレスポンシブに変わる(自動で改行される)ようなレイアウトを組むには自分で作成する必要があるみたいです。
こういうレイアウトを Flow Layout というようです。
この Flow Layout を、QLayout
を継承して作成する方法が公式ドキュメントに載っています。
ただ、私の環境ではこのコードをそのまま PySide で実装しても細かい部分の挙動がおかしかったので、以下のページも参考にしつつ修正しました。
- https://discourse.techart.online/t/maya-flowlayout-within-a-scrolllayout/4710
- https://fereria.github.io/reincarnation_tech/11_PySide/02_Tips/11_custom_layout/
環境
- 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())