QLabel setMinimumHeight After Custom WordWrap Qt.TextWrapAnywhere PyQt5 ( Full responsive With/Without Emoji )

I want Qt.TextWrapAnywhere for my QLabel in a Layout.

I followed This instruction.My code is also same to give a minimal code

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle

class SuperQLabel(QLabel):
    def __init__(self, *args, **kwargs):
        super(SuperQLabel, self).__init__(*args, **kwargs)

        self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
        self.isTextLabel = True
        self.align = None

    def paintEvent(self, event):

        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)

        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)

        self.style().drawItemText(painter, self.rect(),
                                  self.textalignment, self.palette(), True, self.text())


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

        self.resize(100, 200)

        self.label = QLabel()
        self.label.setWordWrap(True)
        self.label.setText("11111111111111111111\n2222222211111111")

        self.slabel = SuperQLabel()
        self.slabel.setMinimumWidth(10)
        self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")

        self.centralwidget = QWidget()
        self.setCentralWidget(self.centralwidget)

        self.mainlayout = QVBoxLayout()
        self.mainlayout.addWidget(self.label)
        self.mainlayout.addWidget(self.slabel)

        self.centralwidget.setLayout(self.mainlayout)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

I changed little bit that code self.slabel.setMinimumWidth(10) otherwise resizing Label according to width wont work.

It is perfectly wrapping the text according to width.But the Problem is when height is considered self.label = QLabel() Normal QLabel auto adjust height according to content with layout.

For example if i add one \n with text that means Qlabel must show 2 lines.

But with this new Custom Label e.g.self.slabel = SuperQLabel() wrapping is good as long as there is space for height in layout. I think i have to use setminimumHeight() but dont know how to get proper height after custom wrapping.


Solution 1:

As long as the label is shown in a scroll area (which will not create issues with the top level layout), a better solution is to use a QTextEdit subclass, with the following configuration:

  • readOnly must be True;
  • scroll bars are disabled;
  • the vertical size policy must be Preferred (and not Expanding);
  • both minimumSizeHint() and sizeHint() should use the internal QTextDocument to return a proper height, with a minimum default width;
  • any change in size or contents must trigger updateGeometry() so that the parent layout will know that the hint has changed and geometries could be computed again;
  • the hint must include possible decorations of the scroll area (which is a QFrame);

This allows avoiding the paintEvent() override, and provide a better and easier implementation of the size mechanism due to the features provided by QTextDocument, while minimizing the possibility of recursion to an acceptable level.

class WrapLabel(QtWidgets.QTextEdit):
    def __init__(self, text=''):
        super().__init__(text)
        self.setStyleSheet('''
            WrapLabel {
                border: 1px outset palette(dark);
                border-radius: 8px;
                background: palette(light);
            }
        ''')
        self.setReadOnly(True)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, 
            QtWidgets.QSizePolicy.Maximum)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.textChanged.connect(self.updateGeometry)

    def minimumSizeHint(self):
        doc = self.document().clone()
        doc.setTextWidth(self.viewport().width())
        height = doc.size().height()
        height += self.frameWidth() * 2
        return QtCore.QSize(50, height)

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

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateGeometry()


class ChatTest(QtWidgets.QScrollArea):
    def __init__(self):
        super().__init__()
        self.messages = []

        container = QtWidgets.QWidget()
        self.setWidget(container)
        self.setWidgetResizable(True)

        layout = QtWidgets.QVBoxLayout(container)
        layout.addStretch()
        self.resize(480, 360)

        for i in range(1, 11):
            QtCore.QTimer.singleShot(1000 * i, lambda:
                self.addMessage('1' * randrange(100, 250)))

    def addMessage(self, text):
        self.widget().layout().addWidget(WrapLabel(text))
        QtCore.QTimer.singleShot(0, self.scrollToBottom)

    def scrollToBottom(self):
        QtWidgets.QApplication.processEvents()
        self.verticalScrollBar().setValue(
            self.verticalScrollBar().maximum())

Update: HTML and QTextDocument

When using setHtml() and setDocument(), the source could have pre formatted text that doesn't allow wrapping. To avoid that, it's necessary to iterate through all QTextBlocks of the document, get their QTextBlockFormat, check the nonBreakableLines() property and eventually set it to False and set the format back with a QTextCursor.

class WrapLabel(QtWidgets.QTextEdit):
    def __init__(self, text=None):
        super().__init__()
        if isinstance(text, str):
            if Qt.mightBeRichText(text):
                self.setHtml(text)
            else:
                self.setPlainText(text)
        elif isinstance(text, QtGui.QTextDocument):
            self.setDocument(text)
        # ...

    def setHtml(self, html):
        doc = QtGui.QTextDocument()
        doc.setHtml(html)
        self.setDocument(doc)

    def setDocument(self, doc):
        doc = doc.clone()
        tb = doc.begin() # start a QTextBlock iterator
        while tb.isValid():
            fmt = tb.blockFormat()
            if fmt.nonBreakableLines():
                fmt.setNonBreakableLines(False)
                # create a QTextCursor for the current text block,
                # then set the updated format to override the wrap
                tc = QtGui.QTextCursor(tb)
                tc.setBlockFormat(fmt)
            tb = tb.next()
        super().setDocument(doc)

Be aware, though, that this could not be enough whenever objects with predefined or minimum width are used: images and tables. The result will be that if the object is larger than the available space, it will be cropped on its right (or left for RightToLeft text layouts).

Solution 2:

After Some Research,I successfully fixed it. There is a trick

This is Full Responsive With/Without Emoji😅

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter,QFontMetrics,QFont
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
import math
class SuperQLabel(QLabel):
    def __init__(self, *args, **kwargs):
        super(SuperQLabel, self).__init__(*args, **kwargs)

        self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
        self.isTextLabel = True
        self.align = None

    def paintEvent(self, event):

        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)

        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)

        self.style().drawItemText(painter, self.rect(),
                                  self.textalignment, self.palette(), True, self.text())
        
        fm=QFontMetrics(self.font())
        #To get unicode in Text if using Emoji(Optional)
        string_unicode = self.text().encode("unicode_escape").decode()
        ##To remove emoji/unicode from text while calculating
        string_encode = self.text().encode("ascii", "ignore")
        string_decode = string_encode.decode()
        #If Unicode/Emoji is Used
        if string_unicode.count("\\U0001") > 0:
            height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()+1
            # +1 is varrying according to Different font .SO set different value and test.
        else:
            height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()
        row=math.ceil(fm.horizontalAdvance(self.text())/self.width())
        self.setMinimumHeight(row*height)
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.resize(100, 200)

        self.label = QLabel()
        self.label.setWordWrap(True)
        self.label.setStyleSheet("background:red;")
        self.label.setText("11111111111111111111\n2222222211111111")
        self.emoji_font = QFont("Segoe UI Emoji",15,0,False)
        self.emoji_font.setBold(True)
        self.slabel = SuperQLabel()
        self.slabel.setMinimumWidth(10)
        self.slabel.setStyleSheet("background:green;")
        self.slabel.setFont(self.emoji_font)
        ########### Plain Text ######################
        # self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
        ####################    Or Using Emoji ############
        self.slabel.setText("111111111111111111😉111ABCDDWAEQQ111111111111😅1111111111😉111111wqewqgdfgdfhyhtyhy1111111😅111111111111😉1111111111111111111")
        
        self.centralwidget = QWidget()
        self.setCentralWidget(self.centralwidget)

        self.mainlayout = QVBoxLayout()
        self.mainlayout.addWidget(self.label)
        self.mainlayout.addWidget(self.slabel)

        self.centralwidget.setLayout(self.mainlayout)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())