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
Result, when wordWrap is set:
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:
Result when setWordWrap(True):
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:
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 hasHeightForWidth
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