В одном из приложений Qt, которое я поддерживаю, после недавнего обновления кода начала возникать странная проблема с макетом.
Наш диалог имеет несколько виджетов, включая текст и область прокрутки, и полностью игнорирует подсказку о размере нашего основного виджета, когда мы устанавливаем для одного из QLabels значение wordWrap.
Мне удалось воспроизвести два случая с очень минимальным кодом:
Случай 1: 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_())
Ожидаемый результат: 500x500, согласно подсказке по размеру.
Результат, когда установлен wordWrap:
Судя по отладке, которую я провел, мой макет теперь имеет «hasHeightForWidth», что приводит к тому, что он запускает эквивалент heightForWidth(sizeHint.width())
и отображается неправильно.
Случай 2: Метка и QScrollArea
Ближе к моему реальному приложению у нас есть QScrollArea. Мы обычно используем помощник ScrollArea, поскольку он обычно дает лучшие результаты, я включаю его сюда.
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)
Ожидал:
Результат, когда setWordWrap(True):
Это особенно плохо, поскольку я не могу уменьшить размер этого диалогового окна, оно ведет себя так, как если бы sizeHint ScrollArea имел минимальный размер. Вот что происходит в нашем приложении.
Примечание. Он работает лучше в PySide6, где он не ведет себя так, как если бы это был минимальный размер, но исходный размер по-прежнему показывает больше, чем желаемый sizeHint.
Я нашел несколько способов обойти sizeHint области прокрутки, который ведет себя как минимальный размер, но я не нашел способа одновременно иметь метку с переносом слов И соблюдать sizeHint моего виджета.
Я знаю, что в документации упоминается, что метки wordWrapped могут вызывать проблемы https://doc.qt.io/qt-6/layout.html#layout-issues и я нашел другие сообщения о переполнении стека с похожими темами, но они либо нет ответа, либо ответы не помогают найти обходной путь.
qlabel имеет неверный sizeHint(), когда включен перенос слов
Почему включение переноса слов для QLabel меняет макет?
Установка переноса слов в QLabel нарушает ограничения размера окна
Мои вопросы:
Спасибо за попытку. Я использую Windows 10 (то же самое поведение и в Windows 11). У меня не установлен отдельный Qt, только тот, который поставляется с PySide2 и использует PySide2 5.15.2.1. Что касается hasHeightForWidth
, для какого виджета вы бы его переопределили?
Как и в случае с PyQt, версия PySide не всегда соответствует версии Qt, которую он фактически использует. Для PySide используйте QtCore.__version__
. В любом случае это кажется ошибкой, и похоже, что minimumSizeHint()
(который по умолчанию вызывает minimumSize()
макета) вычисляется неправильно. Я тестировал с PyQt (но это ничего не изменит) с Qt 5.15.2, с MyWidget2
как окном верхнего уровня, так и дочерним, подсказка о размере по-прежнему соблюдается, так что это может быть проблема с платформой или стилем. . Можете ли вы попробовать, если добавление app.setStyle('fusion')
(сразу после создания QApplication) что-нибудь изменит?
В любом случае возможным обходным решением может быть переопределение minimumSizeHint()
родительского виджета (того, который содержит метку и область прокрутки), если этот виджет сам является дочерним, в противном случае попробуйте явно вызвать setMinimumHeight(500)
(или даже меньше, но все же больше 0 ). hasHeightForWidth()
должен быть установлен в виджете, для которого подсказка по макету создает проблемы, что в вашем случае должно быть MyWidget
(или MyWidget2
). Это потому, что по умолчанию он вызывает соответствующие функции макета (если они установлены, в противном случае используются функции, предусмотренные политикой размера IIRC).
Большое спасибо, что снова заглянули в это. Я тоже думаю, что где-то ошибка, хотя точно сказать не могу. Я попробовал на своем домашнем компьютере те же версии PySide (я проверил Qtcore.__version__, это действительно 5.15.2) и получил такое же поведение. Я попробовал ваше предложение попробовать стиль фьюжн и не увидел разницы. Я пытался реализовать minimumSizeHint()
, но, похоже, это проигнорировали. Переопределение hasHeightForWidth
в моем виджете и возврат False довольно хорошо справляются со своей задачей, исправляя все проблемы, которые у меня возникают.
Забыл спросить: вы используете масштабирование шрифтов или High DPI? Каково физическое разрешение (фактические пиксели) экрана? Вы можете проверить состояние DPI, используя функции QScreen, если хотите узнать больше. Возможно, мне не удалось воспроизвести вторую проблему из-за того, что я не использую высокое разрешение, хотя ошибка может быть связана с этим.
Я тоже не использую высокий DPI, однако мой экран необычно большой, я использую 42-дюймовый монитор 4K без какого-либо масштабирования. Я не знаю, может ли одного большого размера быть достаточно, чтобы вызвать ошибку, я стоит попробовать на одном из компьютеров с меньшими экранами, когда в следующий раз пойду в офис.
У меня такое же поведение с Qt 5.15.2 в Windows 11, обычный монитор 1920x1080 без масштабирования. Спасибо за обходной путь, Эрван. Это исправляет ограничение по высоте, но я заметил, что иногда длинная метка также может вызывать ограничение по ширине. Если я найду другой обходной путь, я опубликую его здесь как ответ.
Если у вас есть футляр для репродукции, мне было бы интересно посмотреть. В итоге я не сообщил об ошибке, потому что в Qt6 у меня все работает нормально, а это значит, что ошибка, кажется, уже исправлена.
Благодаря комментариям @musicamante я нашел работающее решение.
Кажется, что моя повторная реализация sizeHint
игнорируется, потому что виджет имеет высоту и ширину, поэтому повторная реализация hasSizeForWidth()
не позволяет родительскому макету пересчитывать высоту и правильно использует мою подсказку размера:
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
Проще говоря: в Qt 5.15.2 в Windows, если у вас есть QLabel в макете, установка для него wordWrap
на True
устанавливает минимальную высоту макета намного больше, чем она должна быть. Любая попытка изменить размер или переопределить sizeHint
для потомка метки или виджета, содержащего ее, не имеет никакого эффекта, если только вы не переопределите hasHeightForWidth
для возврата False
. Это не позволяет виджету динамически регулировать свою высоту, чтобы адаптироваться к изменяющемуся количеству строк метки.
Лучшее решение — обновить Qt.
Если ваша программа должна работать с версией 5.15.2, ниже представлен обходной путь, основанный на виджете WrapLabel musicamante в ответе на другой вопрос. Виджет сохраняет QTextDocument
, не отображая его, и использует его для расчета необходимой высоты.
class WrappedLabel(QWidget):
"""Workaround for word-wrapped QLabel size constraint bug.
Adapted from musicamante https://stackoverflow.com/a/70757504/18396947"""
def __init__(self, text, parent=None):
super().__init__(parent)
lyt = QHBoxLayout(self)
self.label = QLabel(text)
self.label.setWordWrap(True)
lyt.addWidget(self.label)
lyt.setContentsMargins(0, 0, 0, 0)
lyt.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.doc = QTextDocument(text, self)
self.doc.setDocumentMargin(0)
self.doc.setDefaultFont(self.label.font())
def minimumSizeHint(self):
self.doc.setTextWidth(self.label.width())
height = self.doc.size().height()
height += self.label.frameWidth() * 2
return QSize(50, height)
def sizeHint(self):
return self.minimumSizeHint()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateGeometry()
def hasHeightForWidth(self):
"""Without this, the sizeHint seems to be ignored"""
return False
Хорошо, это тоже хороший обходной путь. Я оставлю другой ответ как принятый, поскольку он делает больше, чем мне нужно, где высота метки не имеет значения.
Я не могу воспроизвести вторую проблему ни с Qt 5.15, ни с 6.6.1 в Linux. Какие версии OS/Qt (не PySide) вы используете? Первую проблему можно легко решить, переопределив
hasHeightForWidth()
и вернувFalse
.