qtpyside2pyside6

Layout ignoring sizeHints when QLabel with text wrap is present


As part of one of the Qt applications I maintain, a weird layout issue started happening after a recent update to the code.

Our dialog has multiple widgets, including some text and scroll area, and completely ignores our main widget's size hint once we set one of the QLabels to wordWrap.

I have managed to reproduce 2 cases with very minimal code:

Case1: QLabel + AddStretch

import sys

from PySide2 import QtCore, QtWidgets  # Same results with PySide6


class MyWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent=parent)

        layout = QtWidgets.QVBoxLayout()

        self.label = QtWidgets.QLabel('This is a fairly long sentence, although not that long')
        self.label.setWordWrap(True)  # Comment out for expected results

        layout.addWidget(self.label)
        layout.addStretch()
        self.setLayout(layout)

    def sizeHint(self):
        return QtCore.QSize(500, 500)


if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)

    window = QtWidgets.QDialog()
    layout = QtWidgets.QVBoxLayout()
    window.setLayout(layout)

    notes_panel = MyWidget()
    layout.addWidget(notes_panel)

    window.show()

    sys.exit(app.exec_())

Expected Result: 500x500, as per the size hint

Expected result

Result, when wordWrap is set:

Result

From the debugging I've done, my layout now has "hasHeightForWidth", which causes it to run the equivalent of heightForWidth(sizeHint.width()), and display wrong.

Case 2: A Label and a QScrollArea

Closer to my real application, we have a QScrollArea. There is a ScrollArea helper we generally use as it usually gives us better results, I'm including it here.

class MyWidget2(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MyWidget2, self).__init__(parent=parent)

        layout = QtWidgets.QVBoxLayout()

        self.label = QtWidgets.QLabel('This is a fairly long sentence, although not that long')
        self.label.setWordWrap(True)

        layout.addWidget(self.label)

        # scroll area
        scroll = ScrollArea()
        layout.addWidget(scroll)

        for i in range(75):
            scroll.addWidget(QtWidgets.QCheckBox(str(i)))

        self.setLayout(layout)

    def sizeHint(self):
        return QtCore.QSize(500, 500)


class ScrollArea(QtWidgets.QScrollArea):
    """ Convenience class for setting up Scroll Areas"""
    def __init__(self, direction=QtCore.Qt.Vertical, parent=None):
        """
        Parameters
        ----------
        direction: QtCore.Qt.Vertical or QtCore.Qt.Horizontal, optional
        parent: QtWidgets.QWidget, optional
        """
        super(ScrollArea, self).__init__(parent=parent)

        layout_class = QtWidgets.QVBoxLayout if direction == QtCore.Qt.Vertical else QtWidgets.QHBoxLayout
        # Create components
        widget = QtWidgets.QWidget()
        widget_layout = layout_class()
        self.contents_layout = layout_class()
        # Assign components
        widget_layout.addLayout(self.contents_layout)
        widget_layout.addStretch()
        widget.setLayout(widget_layout)
        self.setWidget(widget)
        self.setWidgetResizable(True)

    def addWidget(self, widget):
        """ Add a widget to the scroll area """
        self.contents_layout.addWidget(widget)

    def addWidgets(self, widgets):
        """ Add multiple widgets to the scroll area"""
        for widget in widgets:
            self.addWidget(widget)

    def sizeHint(self):
        """ Overriden sizeHint to return the hint based on content, plus some margins to accommodate scroll bars. """
        return self.contents_layout.sizeHint() + QtCore.QSize(30, 20)

Expected:

expected result

Result when setWordWrap(True):

Result

This is particularly bad as I can't resize this dialog smaller, it's behaving as if the ScrollArea's sizeHint was a minimum size. This is what is happening in our application.

Note: It behaves better in PySide6, where it doesn't behave as if it was a minimum size, but the initial size still shows larger than the desired sizeHint.

I have found a few ways to get around the scroll area's sizeHint behaving as a minimum size, but I have not found any way to have both a word-wrapped label AND respect my widget's sizeHint.

I am aware that the docs mention wordWrapped labels can cause issues https://doc.qt.io/qt-6/layout.html#layout-issues and I have found other stack overflow posts with related topics, but they either have no answer or the answers do not help with finding a workaround.

qlabel has wrong sizeHint() when wordwrap is enabled

Why does enabling word wrap for a QLabel alter the layout?

Setting word wrap on QLabel breaks size constrains for the window

My questions are:


Solution

  • Thanks to the comments by @musicamante I have found a solution that works.

    It seems that my re-implemented sizeHint is ignored because the widget has height for width, so also re-implementing hasSizeForWidth() prevents the parent layout from re-calculating the height and properly uses my sizehint:

     class MyWidget2(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(MyWidget2, self).__init__(parent=parent)
    
            layout = QtWidgets.QVBoxLayout()
    
            self.label = QtWidgets.QLabel('This is a fairly long sentence, although not that long')
            self.label.setWordWrap(True)
    
            layout.addWidget(self.label)
    
            # scroll area
            scroll = ScrollArea()
            layout.addWidget(scroll)
    
            for i in range(75):
                scroll.addWidget(QtWidgets.QCheckBox(str(i)))
    
            self.setLayout(layout)
    
        def sizeHint(self):
            return QtCore.QSize(500, 500)
    
        # NEW METHOD
        def hasHeightForWidth(self):
            return False