PyQt5. Адаптация QPlainTextEdit по высоте в зависимости от количества строк текста в нем

Постигаю тут PyQt5, пишу чат. Мне нужно оформить виджет сообщения.

В чем проблема: пользователь сужает окно чата, виджет сообщения сужается вместе с ним, соответственно текст, расположенный в нередактируемом QPlainTextEdit, сообщения переносится по словам на строку ниже, то есть растет в высоту, сам виджет сообщения не расширяется, а у QPlainTextEdit появляется скролл, а надо сделать так, чтобы скролл не появлялся, а QPlainTextEdit увеличивал (и уменьшал) свою высоту в зависимости от высоты (или количества строк) текста в нем.

На картинках:

  • начальное положение окна (Как мы видим, высота сообщения не адаптируется): Как мы видим, высота сообщения не адаптируется

  • сужение окна (Как мы видим, последняя строка сообщения скрывается): Как мы видим, последняя строка сообщения скрывается

Что пробовал:

  • Подцепился к сигналу documentSizeChanged у documentLayout и увеличивал или уменьшал высоту сообщения, в зависимости от высоты документа. Однако, этот сигнал возникает при редактировании в QPlainTextEdit, а у меня он только отображает текст. Подключение к documentSizeChanged: txtMessageText.document().documentLayout().documentSizeChanged.connect(self.on_documentSize_changed)

Смысла код слота вставлять не вижу, так как все равно этот сигнал не работает.

  • Подцепился к сигналу rangeChanged у verticalScrollBar и увеличивал или уменьшал высоту сообщения в зависимости от значения максимума скроллбара, однако, при таком подходе в случае развертывания окна на весь экран сообщение не меняет свой размер.

Подключение к rangeChanged: txtMessageText.verticalScrollBar().rangeChanged.connect(self.on_range_changed)

Слот on_range_changed:

    @pyqtSlot(int, int)
    def on_mod_changed(self, min_, max_):
        # нашел виджет сообщения
        parent = self.sender().parent().parent().parent().parent().parent().parent()
        # изменяю размер пока так: 
        # (к сообщению добавляется пустая строка, которая будет скрыта, 
        #  то есть постоянно пытаюсь сохранить максимальное значение равным 1 (max_==1))
        if max_>1:
            parent.setMinimumSize(parent.minimumWidth(), parent.minimumHeight() + 25)
            parent.setMaximumSize(parent.maximumWidth(), parent.minimumHeight() + 25)
        elif max_<1:
            parent.setMinimumSize(parent.minimumWidth(), parent.minimumHeight() - 25)
            parent.setMaximumSize(parent.maximumWidth(), parent.minimumHeight() - 25)

Как бы вы сделали адаптацию QPlainTextEdit по высоте на PyQt5?

UPD. Вот минимальный пример:

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class Ui_Client(object):
        def setupUi(self, Client):
            Client.setObjectName("Client")
            Client.resize(522, 228)
            Client.setStyleSheet("QMainWindow{\n"
    "background-color: #29334A;\n"
    "}\n"
    "\n"
    "QFrame {\n"
    "background-color: #29334A;\n"
    "}\n"
    "\n"
    "QLabel {\n"
    "color: #E8EAF1;\n"
    "font-size:24px\n"
    "}\n"
    "\n"
    "QPlainTextEdit {\n"
    "color: #E8EAF1;\n"
    "}\n"
    "\n"
    "")
            self.centralwidget = QtWidgets.QWidget(Client)
            self.centralwidget.setMinimumSize(QtCore.QSize(0, 6))
            self.centralwidget.setObjectName("centralwidget")
            self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
            self.verticalLayout_2.setObjectName("verticalLayout_2")
            self.scrollArea = QtWidgets.QScrollArea(self.centralwidget)
            self.scrollArea.setWidgetResizable(True)
            self.scrollArea.setObjectName("scrollArea")
            self.scrollAreaWidgetContents = QtWidgets.QWidget()
            self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 502, 408))
            self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
            self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
            self.verticalLayout_3.setObjectName("verticalLayout_3")
            self.frame_2 = QtWidgets.QFrame(self.scrollAreaWidgetContents)
            sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
            sizePolicy.setHorizontalStretch(0)
            sizePolicy.setVerticalStretch(0)
            sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth())
            self.frame_2.setSizePolicy(sizePolicy)
            self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)
            self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised)
            self.frame_2.setObjectName("frame_2")
            self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2)
            self.horizontalLayout.setObjectName("horizontalLayout")
            self.frame = QtWidgets.QFrame(self.frame_2)
            self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
            self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
            self.frame.setObjectName("frame")
            self.verticalLayout = QtWidgets.QVBoxLayout(self.frame)
            self.verticalLayout.setContentsMargins(0, 0, 0, 0)
            self.verticalLayout.setObjectName("verticalLayout")
            self.label = QtWidgets.QLabel(self.frame)
            self.label.setObjectName("label")
            self.verticalLayout.addWidget(self.label)
            self.plainTextEdit = QtWidgets.QPlainTextEdit(self.frame)
            self.plainTextEdit.setReadOnly(True)
            self.plainTextEdit.setObjectName("plainTextEdit")
            self.verticalLayout.addWidget(self.plainTextEdit)
            self.label_2 = QtWidgets.QLabel(self.frame)
            self.label_2.setObjectName("label_2")
            self.verticalLayout.addWidget(self.label_2)
            self.horizontalLayout.addWidget(self.frame)
            spacerItem = QtWidgets.QSpacerItem(197, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
            self.horizontalLayout.addItem(spacerItem)
            self.verticalLayout_3.addWidget(self.frame_2)
            spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
            self.verticalLayout_3.addItem(spacerItem1)
            self.scrollArea.setWidget(self.scrollAreaWidgetContents)
            self.verticalLayout_2.addWidget(self.scrollArea)
            Client.setCentralWidget(self.centralwidget)
    
            self.retranslateUi(Client)
            QtCore.QMetaObject.connectSlotsByName(Client)
    
        def retranslateUi(self, Client):
            _translate = QtCore.QCoreApplication.translate
            Client.setWindowTitle(_translate("Client", "Client"))
            self.label.setText(_translate("Client", "Имя Пользователя"))
            self.plainTextEdit.setPlainText(_translate("Client", "11111111111111111111111111111111111111111222222222222222222222222222222222222222223333333333333333333333333333333333333333344444444444444444444444444444444444444444555555555555555555555555555555555555555556666666666666666666666666666666666666666677777777777777777777777777777777777777777888888888888888888888888888888888888888889999999999999999999999999999999999999999910101010101010101010101010101010101010101"))
            self.label_2.setText(_translate("Client", "Дата  Время"))
    
    
    if __name__ == "__main__":
        import sys
        app = QtWidgets.QApplication(sys.argv)
        Client = QtWidgets.QMainWindow()
        ui = Ui_Client()
        ui.setupUi(Client)
        Client.show()
        sys.exit(app.exec_())

Что в нем происходит: при сужении окна приложения, у QPlainTextEdit появляется скролл, и часть текста скрывается, надо же сделать так, чтобы высота сообщения увеличивалась с увеличением строк в QPlainTextEdit.


Ответы (1 шт):

Автор решения: S. Nick

void QPlainTextEdit::updateRequest(const QRect &rect, int dy)

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

Обратите внимание, что я убрал self.plainTextEdit.setReadOnly(True), чтобы вы могли проверить что происходит.

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Client(object):
    def setupUi(self, Client):
        Client.setObjectName("Client")
        Client.resize(522, 228)
        Client.setStyleSheet("QMainWindow{\n"
"background-color: #29334A;\n"
"}\n"
"\n"
"QFrame {\n"
"background-color: #29334A;\n"
"}\n"
"\n"
"QLabel {\n"
"color: #E8EAF1;\n"
"font-size:24px\n"
"}\n"
"\n"
"")
        self.centralwidget = QtWidgets.QWidget(Client)
        self.centralwidget.setMinimumSize(QtCore.QSize(0, 6))
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.scrollArea = QtWidgets.QScrollArea(self.centralwidget)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollAreaWidgetContents = QtWidgets.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 502, 408))
        self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.frame_2 = QtWidgets.QFrame(self.scrollAreaWidgetContents)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth())
        self.frame_2.setSizePolicy(sizePolicy)
        self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame_2.setObjectName("frame_2")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.frame = QtWidgets.QFrame(self.frame_2)
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.frame)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = QtWidgets.QLabel(self.frame)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)

        
        self.plainTextEdit = QtWidgets.QPlainTextEdit(self.frame)
#        self.plainTextEdit.setReadOnly(True)
        self.plainTextEdit.setObjectName("plainTextEdit")
        self.verticalLayout.addWidget(self.plainTextEdit)
        
        
        self.label_2 = QtWidgets.QLabel(self.frame)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.horizontalLayout.addWidget(self.frame)
        spacerItem = QtWidgets.QSpacerItem(197, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.verticalLayout_3.addWidget(self.frame_2)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout_3.addItem(spacerItem1)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.verticalLayout_2.addWidget(self.scrollArea)
        Client.setCentralWidget(self.centralwidget)

        self.retranslateUi(Client)
        QtCore.QMetaObject.connectSlotsByName(Client)

    def retranslateUi(self, Client):
        _translate = QtCore.QCoreApplication.translate
        Client.setWindowTitle(_translate("Client", "Client"))
        self.label.setText(_translate("Client", "Имя Пользователя"))
        self.plainTextEdit.setPlainText(_translate("Client", "11111111111111111111111111111111111111111222222222222222222222222222222222222222223333333333333333333333333333333333333333344444444444444444444444444444444444444444555555555555555555555555555555555555555556666666666666666666666666666666666666666677777777777777777777777777777777777777777888888888888888888888888888888888888888889999999999999999999999999999999999999999910101010101010101010101010101010101010101"))
        self.label_2.setText(_translate("Client", "Дата  Время"))


class Window(QtWidgets.QMainWindow, Ui_Client):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        
        self.plainTextEdit.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.plainTextEdit.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        self.plainTextEdit.appendHtml(""" <h3 style='color: red;'> Hello World </h3>
Подцепился к сигналу documentSizeChanged у documentLayout и увеличивал или 
уменьшал высоту сообщения, в зависимости от высоты документа. 
Однако, этот сигнал возникает при редактировании в QPlainTextEdit, 
а у меня он только отображает текст. Подключение к documentSizeChanged:   ...
        """)     
        self.plainTextEdit.updateRequest.connect(self.handle_updateRequest)         # !!!
        self.handle_updateRequest(QtCore.QRect(), 0)                           

    def handle_updateRequest(self, rect, dy):
        doc = self.plainTextEdit.document()
        tb = doc.findBlockByNumber(doc.blockCount() - 1)
        h = self.plainTextEdit.blockBoundingGeometry(tb).bottom() + 2 * doc.documentMargin()
        self.plainTextEdit.setFixedHeight(h)
        
    
if __name__ == '__main__':
    import sys
    
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())

введите сюда описание изображения

введите сюда описание изображения

→ Ссылка