Как я могу реализовать адаптивный QPlainTextEdit?

Вот MRE:

import sys, random
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        main_splitter = MainSplitter(self)
        self.setCentralWidget(main_splitter)
        main_splitter.setOrientation(QtCore.Qt.Vertical)
        main_splitter.setStyleSheet('background-color: red; border: 1px solid pink;');
        
        # top component of vertical splitter: a QFrame to hold horizontal splitter and breadcrumbs QPlainTextEdit
        top_frame = QtWidgets.QFrame()
        top_frame_layout = QtWidgets.QVBoxLayout()
        top_frame.setLayout(top_frame_layout)
        top_frame.setStyleSheet('background-color: green; border: 1px solid green;');
        main_splitter.addWidget(top_frame)
        
        # top component of top_frame: horizontal splitter
        h_splitter = HorizontalSplitter()
        h_splitter.setStyleSheet('background-color: cyan; border: 1px solid orange;')
        top_frame_layout.addWidget(h_splitter)

        # bottom component of top_frame: QPlainTextEdit (for "breadcrumbs")
        self.breadcrumbs_pte = BreadcrumbsPTE(top_frame)
        top_frame_layout.addWidget(self.breadcrumbs_pte)
        self.text = 'some plain text '
        self.breadcrumbs_pte.setPlainText(self.text * 50)
        self.breadcrumbs_pte.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
        self.breadcrumbs_pte.setStyleSheet('background-color: orange; border: 1px solid blue;');
        self.breadcrumbs_pte.setMinimumWidth(300)
        self.breadcrumbs_pte.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)

        # bottom component of vertical splitter: a QFrame
        bottom_panel = BottomPanel(main_splitter)
        bottom_panel.setStyleSheet('background-color: magenta; border: 1px solid cyan;')
        main_splitter.addWidget(bottom_panel)

    def resizeEvent(self, *args):
        print('resize event...')
        n_repeat = random.randint(10, 50)
        self.breadcrumbs_pte.setPlainText(self.text * n_repeat)
        super().resizeEvent(*args)

class BreadcrumbsPTE(QtWidgets.QPlainTextEdit):
    def sizeHint(self):
        return QtCore.QSize(500, 100)

class MainSplitter(QtWidgets.QSplitter):
    def sizeHint(self):
        return QtCore.QSize(40, 150)

class HorizontalSplitter(QtWidgets.QSplitter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setOrientation(QtCore.Qt.Horizontal)

        self.left_panel = LeftPanel()
        self.left_panel.setStyleSheet('background-color: yellow; border: 1px solid black;');
        self.addWidget(self.left_panel)
        self.left_panel.setMinimumHeight(150)

        right_panel = QtWidgets.QFrame()
        right_panel.setStyleSheet('background-color: black; border: 1px solid blue;');
        self.addWidget(right_panel)

        # to achieve 66%-33% widths ratio
        self.setStretchFactor(0, 20)
        self.setStretchFactor(1, 10)
         
    def sizeHint(self):
        return self.left_panel.sizeHint()
    
class LeftPanel(QtWidgets.QFrame):
    def sizeHint(self):
        return QtCore.QSize(150, 250)

class BottomPanel(QtWidgets.QFrame):
    def sizeHint(self):
        return QtCore.QSize(30, 180)

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

Если вы запустите его, вы увидите, что если вы отрегулируете размер окна, текст в QPlainTextEdit изменится каждый раз.

Чего я пытаюсь достичь: я хочу, чтобы текст в QPlainTextEdit настраивал размер этого компонента и компонента над ним (HorizontalSplitter), чтобы QPlainTextEdit просто идеально содержал текст, не оставляя места внизу.

Я хочу, чтобы это произошло так, чтобы не происходило изменения размера главного окна (очевидно, если бы это произошло, это, как написано в коде, в настоящее время привело бы к бесконечному срабатыванию MainWindow.resizeEvent()).

Учебники по Qt/PyQt, похоже, просто не дают исчерпывающего технического объяснения того, как работают все различные механизмы и как они взаимодействуют. Например, я знаю, что sizeHint играет решающую роль в определении размеров и макетов, но, если не считать попыток углубленного изучения исходного кода, я не знаю, как я могу улучшить свое понимание.

Например, я пробовал бесконечное количество вариантов комментирования sizeHint различных классов здесь, в том числе комментирование их всех: но self.top_pte.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents), похоже, никогда не работает (то есть так, как я хочу!).

Обратите внимание, что при необходимости для BottomPanel (т. е. второго дочернего элемента по вертикали QSplitter) можно установить минимальную и максимальную высоту (т. е. фиксированную высоту), чтобы упростить задачу. Основная цель состоит в том, чтобы настроить горизонтальную QSplitter и QPlainTextEdit так, чтобы высота последней идеально соответствовала содержащемуся в ней тексту...

Подробного объяснения нет, потому что каждый макет различен, некоторые виджеты довольно сложны и ведут себя немного иначе, чем другие, поэтому на самом деле невозможно предоставить общее руководство. Теперь возникает первая проблема: насколько динамично это должно работать: фиксирована/жестко запрограммирована длина текста? Изменяется ли оно во время выполнения одного и того же виджета? Или он определяется при инициализации виджета и остается неизменным до тех пор, пока не будет уничтожен? Обратите внимание, что политика корректировки размера соблюдается только в тех областях прокрутки, которые правильно реализуют viewportSizeHint(): подклассы QScrollArea и QAbstractItemView.

musicamante 06.07.2024 02:00

Вышесказанное вполне логично: 1. текст не имеет определенного макета, поскольку его геометрия может зависеть от доступной ширины и режима переноса; 2. текстовый контент по своей природе очень динамичен, и реализация правильно настроенной подсказки по размеру, которая может меняться каждый раз, когда в тексте изменяется даже один символ, может быть очень сложной (тот факт, что редактирование текста может быть доступно только для чтения, не имеет значения). Затем возникает еще одна проблема: если изменение размера окна возможно, родительский элемент будет изменять возможную подсказку по-разному, в зависимости от текста, и, возможно, заставит полосы прокрутки отображаться или скрываться, поэтому »

musicamante 06.07.2024 02:08

» вызывая возможную рекурсию (даже бесконечную, в очень специфических случаях). Вот почему в подобных ситуациях обычно предпочтительнее всегда отображать полосы прокрутки (если только временные полосы прокрутки не доступны, но у них также есть свои недостатки), даже если они не нужны, потому что прокручивать нечего. Пожалуйста, уточните, что было задано в моем первом комментарии, и учтите все остальное, что я написал.

musicamante 06.07.2024 02:12

Спасибо, как всегда полезные пояснения. Длина текста постоянно меняется, поэтому реализованный MRE, если будет найдено решение, решит проблему в моем приложении. На самом деле TopLeftPanel содержит QTreeView, а QPlainTextEdit будет содержать строку «хлебные крошки», показывающую тексты предков QStandardItem, ведущие к текущему. Таким образом, это будет меняться каждый раз при изменении текущего индекса. В свете ваших комментариев я теперь предполагаю, что для выполнения того, что я хочу, потребуется код, возможно, сигнал и т. д.

mike rodent 06.07.2024 08:18

Если подумать, возможно, было бы лучше разместить «хлебные крошки» под TopSplitter, чтобы пользователи меньше раздражались: таким образом, когда они выберут новый QStandardItem и хлебные крошки изменятся, по крайней мере QStandardItem не будет изменить положение. В этом случае высоту TopSplitter все равно придется изменить. (Что-то может быть откусано снизу... но это можно исправить с помощью «прокрутки до видимого» на QStandardItem).

mike rodent 06.07.2024 09:09

Изменено... также, с учетом ваших комментариев, добавлено значение «всегда включено» для вертикальной полосы прокрутки QPlainTextEdit. Но на самом деле я думаю, что полоса прокрутки V никогда не появится, если высота отрегулирована правильно.

mike rodent 06.07.2024 09:51

Обратите внимание: этот ответ выглядит интересно: stackoverflow.com/a/21467596/595305 ... будем работать над этим подходом

mike rodent 06.07.2024 10:33
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
7
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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

import sys, random
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        main_splitter = MainSplitter(self)
        self.setCentralWidget(main_splitter)
        main_splitter.setOrientation(QtCore.Qt.Vertical)
        main_splitter.setStyleSheet('background-color: red; border: 1px solid pink;');
        
        # top component of vertical splitter: a QFrame to hold horizontal splitter and breadcrumbs QPlainTextEdit
        top_frame = TopFrame(self)
        top_frame_layout = QtWidgets.QVBoxLayout()
        top_frame.setLayout(top_frame_layout)
        top_frame.setStyleSheet('background-color: green; border: 1px solid green;');
        main_splitter.addWidget(top_frame)
        
        # top component of top_frame: horizontal splitter
        h_splitter = HorizontalSplitter()
        h_splitter.setStyleSheet('background-color: cyan; border: 1px solid orange;')
        top_frame_layout.addWidget(h_splitter, stretch=1)

        # bottom component of top_frame: QPlainTextEdit (for "breadcrumbs")
        self.breadcrumbs_pte = BreadcrumbsPTE(top_frame)
        top_frame_layout.addWidget(self.breadcrumbs_pte)
        self.text = 'some plain text '
        self.breadcrumbs_pte.setPlainText(self.text * 50 + '... END')
        self.breadcrumbs_pte.setStyleSheet('background-color: orange; border: 1px solid blue;');
        self.breadcrumbs_pte.setMinimumWidth(300)

        # bottom component of vertical splitter: a QFrame
        bottom_panel = BottomPanel(main_splitter)
        bottom_panel.setStyleSheet('background-color: magenta; border: 1px solid cyan;')
        main_splitter.addWidget(bottom_panel)

    def resizeEvent(self, *args):
        print('WINDOW resize event...')
        n_repeat = random.randint(10, 50)
        self.breadcrumbs_pte.setPlainText(self.text * n_repeat + '... END')
        super().resizeEvent(*args)
        self.breadcrumbs_pte.resizeEvent(*args)

class TopFrame(QtWidgets.QFrame):
    pass

class BreadcrumbsPTE(QtWidgets.QPlainTextEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        # print(f'+++ self.wordWrapMode() {self.wordWrapMode()}') # 4 (QTextOption::WrapAtWordBoundaryOrAnywhere)

    def resizeEvent(self, *args):
        print('BPTE resize event...')
        plain_text = self.document().toPlainText()
        print(f'+++ RESIZE len(plain_text) {len(plain_text)}')
        super().resizeEvent(*args)
        # fortunately it seemed possible to avoid this sort of technique:
        # QtCore.QTimer.singleShot(0, self.updateGeometry)
        self.updateGeometry()

    def sizeHint(self):
        super_hint = super().sizeHint()
        actual_size = self.size()
        print(f'+++ super_hint {super_hint} actual_size {actual_size}') # 
        document = QtGui.QTextDocument()
        plain_text = self.document().toPlainText()
        # print(f'+++ len(plain_text) {len(plain_text)}')
        document.setPlainText(plain_text)
        document.setTextWidth(float(actual_size.width()))
        document_float_size = document.size()
        document_int_size = QtCore.QSize(int(document_float_size.width()), int(document_float_size.height()))
        # print(f'+++ document_int_size {document_int_size}')
        self.updateGeometry()
        return document_int_size

    def minimumSizeHint(self):
        return self.sizeHint()


class MainSplitter(QtWidgets.QSplitter):
    pass

class HorizontalSplitter(QtWidgets.QSplitter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setOrientation(QtCore.Qt.Horizontal)

        self.left_panel = LeftPanel()
        self.left_panel.setStyleSheet('background-color: yellow; border: 1px solid black;');
        self.addWidget(self.left_panel)
        self.left_panel.setMinimumHeight(150)

        right_panel = QtWidgets.QFrame()
        right_panel.setStyleSheet('background-color: black; border: 1px solid blue;');
        self.addWidget(right_panel)

        # to achieve 66%-33% widths ratio
        self.setStretchFactor(0, 20)
        self.setStretchFactor(1, 10)
         
    def sizeHint(self):
        return self.left_panel.sizeHint()

class LeftPanel(QtWidgets.QFrame):
    def sizeHint(self):
        return QtCore.QSize(150, 500)

class BottomPanel(QtWidgets.QFrame):
    def sizeHint(self):
        return QtCore.QSize(30, 180)

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

Я заметил, что довольно часто можно получить событие изменения размера главного окна, при котором ничего больше (например, sizeHint на других компонентах) не происходит... поэтому решил, что лучше всего передать это resizeEvent непосредственно на BreadcrumbsPTE. Я не знаю, есть ли лучший способ заставить виджеты-потомки запускаться resizeEvent. Позже да, у PTE появился сигнал textChanged ...

sizeHint запускает updateGeometry, и minimumSizeHint, очевидно, просто запускается sizeHint, поэтому он тоже это делает. Но, похоже, BreadcrumbsPTE.resizeEvent также необходимо активировать updateGeometry, чтобы все заработало.

Меня озадачил QTextDocument, доставленный BreadcrumbsPTE.document в BreadcrumbsPTE.sizeHint: мне не удалось заставить это изменить размер: возможно, он ограничен своим родителем (или контейнером), то есть BreadcrumbsPTE: по этой причине мне, похоже, пришлось создать отдельный QTextDocument заполните его текстом и установите его ширину... чтобы он мог сообщить мне, какой тогда будет высота обернутого текста...

Во-первых, я бы не стал менять текст на resizeEvent(), так как это может вызвать некоторый уровень рекурсии, которая затруднит отладку, особенно при использовании непрозрачного изменения размера. В конце концов рассмотрите возможность использования QTimer с разумным интервалом, возможно, перезапущенного в течение resizeEvent(), чтобы вы могли правильно видеть результат в текущем положении вещей. Это очень важно: некоторые виджеты вызывают более одного события изменения размера, иногда используя опубликованные события, чтобы избежать рекурсии. В результате менеджер макета может инициировать дальнейшее обновление макета после изменения размеров всех виджетов.

musicamante 07.07.2024 04:48

Другие примечания: sizeHint() не следует вызывать updateGeometry(), поскольку именно updateGeometry() обычно вызывает вызовы sizeHint() из менеджера(ов) компоновки. Это, косвенно, причина, по которой практически невозможно заставить виджет (или даже окно) всегда иметь определенное соотношение сторон при использовании менеджеров макетов и других виджетов с переменными размерами, а также по этой же причине возникают известные проблемы. с переносом слов QLabels в определенных макетах. Как бы это ни досадно (и другие подходы могут это допускать), именно так работают макеты Qt.

musicamante 07.07.2024 04:58

Учитывая поведение менеджеров макетов и некоторых сложных виджетов (в частности, областей прокрутки), правильное достижение этого на 100% надежным и эффективным способом может оказаться невозможным: даже если мы «кэшируем» какое-то событие изменения размера и используем его для правильного отложенного вычисления Если возможный намек на размер, всегда будет некоторый важный уровень неопределенности относительно: 1. того, что вызвало изменение размера; 2. как/когда можно выполнять изменение размера и вызовы sizeHint(). В любом случае, один из способов решения этой проблемы — рассмотреть hasHeightForWidth() и связанную с ней функцию heightForWidth(), но имейте в виду: это может »

musicamante 07.07.2024 05:03

» все еще недостаточно. Внутренний QAbstractTextDocumentLayout (который является QPlainTextDocumentLayout для QPlainTextEdit) может самостоятельно задерживать вычисление макета и давать неожиданные результаты из-за результатов реализованного resizeEvent(). Вы можете создать клон документа и установить для него соответствующий макет, но тогда вы всегда должны делать это, включая расчет всего макета (блок за блоком, строка за строкой) при каждом вызове подсказки по изменению размера и размеру. Это может сработать, но вам нужно достаточно знать весь интерфейс QTextDocument, чтобы делать это надежно и эффективно.

musicamante 07.07.2024 05:07

Спасибо. Отличные идеи и объяснения. Вы когда-нибудь думали написать книгу о Qt?

mike rodent 07.07.2024 08:48

Пожалуйста. Да, я обдумывал это, но решил, что мне это неинтересно (написание целой книги, правильно написанной и организованной, потребует много времени и усилий) и сомневаюсь, что когда-нибудь поменяю идею. Тем не менее, я намеревался создать небольшой веб-сайт, собрав некоторые из моих кодов и приемов, несколько руководств или воспоминаний о моем опыте, включая исправленные/расширенные версии некоторых ответов, которые я давал здесь на SO в течение многих лет. Это также займет некоторое время, но, возможно, это более осуществимо. Посмотрим :-)

musicamante 08.07.2024 00:50

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