Home > Mobile >  PyQt5 - Items added to custom layout have different size if they are added after QWidget.show() has
PyQt5 - Items added to custom layout have different size if they are added after QWidget.show() has

Time:07-26

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:

enter image description here

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 QWidgetItems 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.

  • Related