Issue
I'm trying to adapt this PyQt implementation of FlowLayout
to allow vertical flow as well as horizontal. This is my current implementation:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class FlowLayout(QLayout):
def __init__(self, orientation=Qt.Horizontal, parent=None, margin=0, spacing=-1):
super().__init__(parent)
self.orientation = orientation
if parent is not None:
self.setContentsMargins(margin, margin, margin, margin)
self.setSpacing(spacing)
self.itemList = []
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
self.itemList.append(item)
def count(self):
return len(self.itemList)
def itemAt(self, index):
if index >= 0 and index < len(self.itemList):
return self.itemList[index]
return None
def takeAt(self, index):
if index >= 0 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 self.orientation == Qt.Horizontal
def heightForWidth(self, width):
return self.doLayout(QRect(0, 0, width, 0), True)
def hasWidthForHeight(self):
return self.orientation == Qt.Vertical
def widthForHeight(self, height):
return self.doLayout(QRect(0, 0, 0, height), True)
def setGeometry(self, rect):
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())
margin, _, _, _ = self.getContentsMargins()
size += QSize(2 * margin, 2 * margin)
return size
def doLayout(self, rect, testOnly):
x = rect.x()
y = rect.y()
offset = 0
horizontal = self.orientation == Qt.Horizontal
for item in self.itemList:
wid = item.widget()
spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
if horizontal:
next = x + item.sizeHint().width() + spaceX
if next - spaceX > rect.right() and offset > 0:
x = rect.x()
y += offset + spaceY
next = x + item.sizeHint().width() + spaceX
offset = 0
else:
next = y + item.sizeHint().height() + spaceY
if next - spaceY > rect.bottom() and offset > 0:
x += offset + spaceX
y = rect.y()
next = y + item.sizeHint().height() + spaceY
offset = 0
if not testOnly:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
if horizontal:
x = next
offset = max(offset, item.sizeHint().height())
else:
y = next
offset = max(offset, item.sizeHint().width())
return y + offset - rect.y() if horizontal else x + offset - rect.x()
if __name__ == '__main__':
class Window(QWidget):
def __init__(self):
super().__init__()
#flowLayout = FlowLayout(orientation=Qt.Horizontal)
flowLayout = FlowLayout(orientation=Qt.Vertical)
flowLayout.addWidget(QPushButton("Short"))
flowLayout.addWidget(QPushButton("Longer"))
flowLayout.addWidget(QPushButton("Different text"))
flowLayout.addWidget(QPushButton("More text"))
flowLayout.addWidget(QPushButton("Even longer button text"))
self.setLayout(flowLayout)
self.setWindowTitle("Flow Layout")
import sys
app = QApplication(sys.argv)
mainWin = Window()
mainWin.show()
sys.exit(app.exec_())
This implementation has 2 (likely related) problems when handling vertical layouts:
QLayout
has thehasHeightForWidth
andheightForWidth
methods, but not their inverseshasWidthForHeight
andwidthForHeight
. I implemented the latter two methods regardless, but I doubt they're ever actually getting called.- When using the horizontal variant of the layout, the window is automatically appropriately sized to contain all the items. When using the vertical variant, this is not the case. However, the vertical layout does work properly if you manually resize the window.
How do I properly implement a vertical flow layout?
Solution
As you already found out, Qt layouts don't support widthForHeight, and, in general, these kinds of layouts are discouraged, mostly because they tend to behave erratically in complex situation with nested layouts and mixed widget size policies. Even when being very careful about their implementation, you might end up in recursive calls to size hints, policies etc.
That said, a partial solution is to still return a height for width, but position the widgets vertically instead of horizontally.
def doLayout(self, rect, testOnly):
x = rect.x()
y = rect.y()
lineHeight = columnWidth = heightForWidth = 0
for item in self.itemList:
wid = item.widget()
spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
if self.orientation == Qt.Horizontal:
nextX = x + item.sizeHint().width() + spaceX
if nextX - spaceX > rect.right() and lineHeight > 0:
x = rect.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())
else:
nextY = y + item.sizeHint().height() + spaceY
if nextY - spaceY > rect.bottom() and columnWidth > 0:
x = x + columnWidth + spaceX
y = rect.y()
nextY = y + item.sizeHint().height() + spaceY
columnWidth = 0
heightForWidth += item.sizeHint().height() + spaceY
if not testOnly:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
y = nextY
columnWidth = max(columnWidth, item.sizeHint().width())
if self.orientation == Qt.Horizontal:
return y + lineHeight - rect.y()
else:
return heightForWidth - rect.y()
This is how the widget appears as soon as it's shown (which is almost the same as the horizontal flow):
Now, resizing to allow less vertical space:
And even smaller height:
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.