I am trying to add items to a FlowLayout dynamically and I have stumbled upon a bug that is really annoying.
When I add items before calling the show() method of the parent widget, they do not have the same dimensions as when they are added after calling the show() method:
All the buttons should have the same size except for the width which obviously is dependent on the text lenght.
Here's the code I used to achieve this (unwanted) result.
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()
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()
if __name__ == '__main__':
class Window(QWidget):
def __init__(self):
super().__init__()
#flowLayout = FlowLayout(orientation=Qt.Horizontal)
self.flowLayout = FlowLayout(orientation=Qt.Vertical)
self.setLayout(self.flowLayout)
self.setWindowTitle("Flow Layout")
def addItems(self):
self.flowLayout.addWidget(QPushButton("Short"))
self.flowLayout.addWidget(QPushButton("Longer"))
self.flowLayout.addWidget(QPushButton("Different text"))
self.flowLayout.addWidget(QPushButton("More text"))
self.flowLayout.addWidget(QPushButton("Even longer button text"))
import sys
app = QApplication(sys.argv)
mainWin = Window()
mainWin.addItems() #Size is correct
mainWin.show()
mainWin.addItems() # Size is not correct (items are too big)
sys.exit(app.exec_())
Does anyone have an idea on how to avoid this behavior ?
Edit : It seems that the issues is coming from Qt's framework when using Windows 11 and two displays.
With the same code:
- The issue doesn't happen when only one screen is active
- The issue only happen on the secondary screen when using two different displays.
CodePudding user response:
self.itemList
contains QWidgetItem
s not widgets, so when you call item.sizeHint()
it does not return widgets' size hint, it returns something else (need more digging to understand). You can verify that by adding this code to the top of doLayout
function:
for i,item in enumerate(self.itemList):
widget = item.widget()
print("{} item size hint {} widget size hint {}".format(i, item.sizeHint(), widget.sizeHint()))
print("")
It prints
0 item size hint PyQt5.QtCore.QSize(75, 23) widget size hint PyQt5.QtCore.QSize(75, 23)
1 item size hint PyQt5.QtCore.QSize(75, 23) widget size hint PyQt5.QtCore.QSize(75, 23)
2 item size hint PyQt5.QtCore.QSize(75, 23) widget size hint PyQt5.QtCore.QSize(75, 23)
3 item size hint PyQt5.QtCore.QSize(75, 23) widget size hint PyQt5.QtCore.QSize(75, 23)
4 item size hint PyQt5.QtCore.QSize(123, 23) widget size hint PyQt5.QtCore.QSize(123, 23)
5 item size hint PyQt5.QtCore.QSize(75, 23) widget size hint PyQt5.QtCore.QSize(75, 23)
6 item size hint PyQt5.QtCore.QSize() widget size hint PyQt5.QtCore.QSize(75, 23)
7 item size hint PyQt5.QtCore.QSize() widget size hint PyQt5.QtCore.QSize(75, 23)
8 item size hint PyQt5.QtCore.QSize() widget size hint PyQt5.QtCore.QSize(75, 23)
9 item size hint PyQt5.QtCore.QSize() widget size hint PyQt5.QtCore.QSize(123, 23)
PyQt5.QtCore.QSize()
represents 0x0 size here
But strangely enough QWidgetItem.sizeHint()
returns empty size only few times and then falls back to returning QWidget.sizeHint()
So obvious fix would be repacing item.sizeHint()
everywhere in code with something like
widget = item.widget()
itemOrWidget = item if widget is None else widget
itemOrWidget.sizeHint()
or with function call
def sizeHint(item):
widget = item.widget()
if widget:
return widget.sizeHint()
return item.sizeHint()
Also, I have PyQt5-5.12.3
and I cannot reproduce behaviour on attached image, code just works as it is, all buttons have expected size, the only issue is that layout reports zero size hint and window got shrinked to minimum size, which I fixed with mainWin.resize(QSize(400, 400))
CodePudding user response:
It seems that the issues is coming from PyQt5.
With the same code:
- The issue doesn't happen when only one screen is active.
- The issue only happen on the secondary screen when using two different displays.
Migrating the code to PyQt6 resolve the issue, everything behaves as expected.