Qt wrapping text in boundingRect

1.6k Views Asked by At

I am writing an application in PyQt and I need to wrap the text that I am painting. I use boundingRect method of QFontMetrics class to define the size and then QPainter.drawText to draw. I need the text to fit in a given rectangle. This is the line I use:

rect = metrics.boundingRect(rect, Qt.TextWordWrap, text)

However, due to the flag Qt.TextWordWrap, if the text doesn't have any spaces it doesn't wrap and will not fit in the rectangle. I tried Qt.TextWrapAnywhere but this breaks the words apart even when there are spaces. The Qt.TextFlag doesn't seem to have any flag, that prioritize wrapping words and only if it is impossible, breaks the words apart, like QTextOption.WrapAtWordBoundaryOrAnywhere. Is there a way to wrap text like this using boundingRect and drawText?

2

There are 2 best solutions below

0
On BEST ANSWER

For situations like these, it's usually better to use a QTextDocument, which allows using more advanced options.

wrapped label

class WrapAnywhereLabel(QtWidgets.QWidget):
    def __init__(self, text=''):
        super().__init__()
        self.doc = QtGui.QTextDocument(text)
        self.doc.setDocumentMargin(0)
        opt = QtGui.QTextOption()
        opt.setWrapMode(opt.WrapAtWordBoundaryOrAnywhere)
        self.doc.setDefaultTextOption(opt)

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        self.doc.setTextWidth(width)
        return self.doc.size().height()

    def sizeHint(self):
        return self.doc.size().toSize()

    def resizeEvent(self, event):
        self.doc.setTextWidth(self.width())

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        self.doc.drawContents(qp, QtCore.QRectF(self.rect()))

import sys
app = QtWidgets.QApplication(sys.argv)
w = WrapAnywhereLabel('hellooooo hello hellooooooooooooooo')
w.show()
sys.exit(app.exec_())
0
On

Such function can be easily implemented with QFontMetricsF

from PyQt5 import QtCore, QtGui, QtWidgets
import math

def drawText(painter, rect, text):
    metrics = QtGui.QFontMetricsF(painter.font())
    space = metrics.horizontalAdvance(" ")
    width = rect.width()

    def lineWidth(line):
        return sum([word[1] for word in line]) + space * (len(line) - 1)

    def canFit(line, word):
        return lineWidth(line + [word]) < width

    def forceSplit(word):
        charSize = [metrics.horizontalAdvance(c) for c in word[0]]
        for i in reversed(range(1,len(charSize))):
            if sum(charSize[:i]) < width:
                return [(word, metrics.horizontalAdvance(word)) for word in [word[0][:i], word[0][i:]]]

    queue = [(word, metrics.horizontalAdvance(word)) for word in text.split(" ")]
    lines = []
    line = []

    while len(queue) > 0:
        word = queue.pop(0)
        if canFit(line, word):
            line.append(word)
        else:
            if len(line) == 0:
                word1, word2 = forceSplit(word)
                line.append(word1)
                lines.append(line)
                line = []
                queue.insert(0, word2)
            else:
                lines.append(line)
                line = []
                queue.insert(0, word)

    if len(line) > 0:
        lines.append(line)
        line = []

    painter.save()

    painter.setClipRect(rect)
    x = rect.x()
    y = rect.y() + metrics.height()
    for line in lines:
        text = " ".join([word[0] for word in line])
        painter.drawText(int(x), int(y), text)
        y += metrics.leading() + metrics.height()

    painter.restore()
    
def replaceSomeSpaces(text, n):
    res = []
    for i,word in enumerate(text.split(" ")):
        res.append(word)
        if (i % n) == 0:
            res.append(" ")
        else:
            res.append("_")
    return "".join(res)

class Widget(QtWidgets.QWidget):

    def __init__(self, parent = None):
        super().__init__(parent)
        self._text = None

    def setText(self, text):
        self._text = text

    def paintEvent(self, event):
        if self._text is None:
            return
        painter = QtGui.QPainter(self)
        rect = self.rect()
        # test clipping
        # rect.setHeight(rect.height() / 2) 
        drawText(painter, rect, self._text)

    def sizeHint(self):
        return QtCore.QSize(200,200)

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    lorem = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
    widget = Widget()
    widget.setFont(QtGui.QFont("Arial", 12))
    widget.setText(replaceSomeSpaces(lorem, 3))
    widget.show()
    app.exec()