Макет игнорирует sizeHints, когда присутствует QLabel с переносом текста

В одном из приложений 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 нарушает ограничения размера окна

Мои вопросы:

  • Есть ли обходной путь, чтобы мой виджет изначально отображался в желаемом размере, но при этом его размер можно было изменить? (эквивалент SizePolicy.Preferred)
  • Это ошибка Qt, о которой я должен сообщить, или желаемое поведение?

Я не могу воспроизвести вторую проблему ни с Qt 5.15, ни с 6.6.1 в Linux. Какие версии OS/Qt (не PySide) вы используете? Первую проблему можно легко решить, переопределив hasHeightForWidth() и вернув False.

musicamante 05.04.2024 01:32

Спасибо за попытку. Я использую Windows 10 (то же самое поведение и в Windows 11). У меня не установлен отдельный Qt, только тот, который поставляется с PySide2 и использует PySide2 5.15.2.1. Что касается hasHeightForWidth, для какого виджета вы бы его переопределили?

Erwan Leroy 06.04.2024 02:29

Как и в случае с PyQt, версия PySide не всегда соответствует версии Qt, которую он фактически использует. Для PySide используйте QtCore.__version__. В любом случае это кажется ошибкой, и похоже, что minimumSizeHint() (который по умолчанию вызывает minimumSize() макета) вычисляется неправильно. Я тестировал с PyQt (но это ничего не изменит) с Qt 5.15.2, с MyWidget2 как окном верхнего уровня, так и дочерним, подсказка о размере по-прежнему соблюдается, так что это может быть проблема с платформой или стилем. . Можете ли вы попробовать, если добавление app.setStyle('fusion') (сразу после создания QApplication) что-нибудь изменит?

musicamante 06.04.2024 02:53

В любом случае возможным обходным решением может быть переопределение minimumSizeHint() родительского виджета (того, который содержит метку и область прокрутки), если этот виджет сам является дочерним, в противном случае попробуйте явно вызвать setMinimumHeight(500) (или даже меньше, но все же больше 0 ). hasHeightForWidth() должен быть установлен в виджете, для которого подсказка по макету создает проблемы, что в вашем случае должно быть MyWidget (или MyWidget2). Это потому, что по умолчанию он вызывает соответствующие функции макета (если они установлены, в противном случае используются функции, предусмотренные политикой размера IIRC).

musicamante 06.04.2024 02:57

Большое спасибо, что снова заглянули в это. Я тоже думаю, что где-то ошибка, хотя точно сказать не могу. Я попробовал на своем домашнем компьютере те же версии PySide (я проверил Qtcore.__version__, это действительно 5.15.2) и получил такое же поведение. Я попробовал ваше предложение попробовать стиль фьюжн и не увидел разницы. Я пытался реализовать minimumSizeHint(), но, похоже, это проигнорировали. Переопределение hasHeightForWidth в моем виджете и возврат False довольно хорошо справляются со своей задачей, исправляя все проблемы, которые у меня возникают.

Erwan Leroy 08.04.2024 01:02

Забыл спросить: вы используете масштабирование шрифтов или High DPI? Каково физическое разрешение (фактические пиксели) экрана? Вы можете проверить состояние DPI, используя функции QScreen, если хотите узнать больше. Возможно, мне не удалось воспроизвести вторую проблему из-за того, что я не использую высокое разрешение, хотя ошибка может быть связана с этим.

musicamante 08.04.2024 01:14

Я тоже не использую высокий DPI, однако мой экран необычно большой, я использую 42-дюймовый монитор 4K без какого-либо масштабирования. Я не знаю, может ли одного большого размера быть достаточно, чтобы вызвать ошибку, я стоит попробовать на одном из компьютеров с меньшими экранами, когда в следующий раз пойду в офис.

Erwan Leroy 08.04.2024 06:51

У меня такое же поведение с Qt 5.15.2 в Windows 11, обычный монитор 1920x1080 без масштабирования. Спасибо за обходной путь, Эрван. Это исправляет ограничение по высоте, но я заметил, что иногда длинная метка также может вызывать ограничение по ширине. Если я найду другой обходной путь, я опубликую его здесь как ответ.

Preminster 21.04.2024 09:30

Если у вас есть футляр для репродукции, мне было бы интересно посмотреть. В итоге я не сообщил об ошибке, потому что в Qt6 у меня все работает нормально, а это значит, что ошибка, кажется, уже исправлена.

Erwan Leroy 22.04.2024 02:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
94
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Благодаря комментариям @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

Хорошо, это тоже хороший обходной путь. Я оставлю другой ответ как принятый, поскольку он делает больше, чем мне нужно, где высота метки не имеет значения.

Erwan Leroy 23.04.2024 04:29

Другие вопросы по теме