Qt 在 boundingRect 中包装文本

Qt wrapping text in boundingRect

我正在用 PyQt 编写一个应用程序,我需要包装我正在绘制的文本。我使用boundingRect QFontMetrics class 的方法来定义尺寸然后QPainter.drawText 来绘制。我需要文本适合给定的矩形。这是我使用的行:

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

但是,由于标志 Qt.TextWordWrap,如果文本没有任何空格,它不会换行并且不适合矩形。我试过 Qt.TextWrapAnywhere 但这会把单词分开,即使有空格也是如此。 Qt.TextFlag 似乎没有任何标志,优先包装单词,只有在不可能时,才将单词分开,如 QTextOption.WrapAtWordBoundaryOrAnywhere。 有没有办法使用 boundingRectdrawText 来换行这样的文本?

对于这种情况,通常最好使用 QTextDocument,它允许使用更高级的选项。

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_())

这样的功能可以用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()