当 QGraphicsTextItem 超过定义的 QRect 时,删除它的文本

Elide the text of a QGraphicsTextItem when it exceeds a defined QRect

我有一个 QGraphicsTextItemQGraphicsPathItem 的子项,它绘制了一个方框。我希望 QGraphicsTextItem 仅显示适合框内的文本,如果它溢出,我希望该文本被省略。

我已经能够让它工作,但是使用硬编码值,这并不理想。 这是我的基本代码:

class Node(QtWidgets.QGraphicsPathItem):
    def __init__(self, scene, parent=None):
        super(Node, self).__init__(parent)

        scene.addItem(self)

        # Variables
        self.main_background_colour = QtGui.QColor("#575b5e")
        self.dialogue_background_colour = QtGui.QColor("#2B2B2B")
        self.dialogue_text_colour = QtGui.QColor("white")
        self.brush = QtGui.QBrush(self.main_background_colour)
        self.pen = QtGui.QPen(self.dialogue_text_colour, 2)

        self.dialogue_font = QtGui.QFont("Calibri", 12)
        self.dialogue_font.setBold(True)
        self.dialogue_font_metrics = QtGui.QFontMetrics(self.dialogue_font)

        self.dialogue_text = "To find out how fast you type, just start typing in the blank textbox on the right of the test prompt. You will see your progress, including errors on the left side as you type. You can fix errors as you go, or correct them at the end with the help of the spell checker. If you need to restart the test, delete the text in the text box. Interactive feedback shows you your current wpm and accuracy. Bring me all the biscuits, for I am hungry. They will be a fine meal for me and all the mice in town!"

        # Rects
        self.main_rect = QtCore.QRectF(0, -40, 600, 240)
        self.dialogue_rect = QtCore.QRectF(self.main_rect.x() + (self.main_rect.width() * 0.05), self.main_rect.top() + 10,
                          (self.main_rect.width() * 0.9), self.main_rect.height() - 20)

        self.dialogue_text_point = QtCore.QPointF(self.dialogue_rect.x() + (self.dialogue_rect.width() * 0.05), self.dialogue_rect.y() + 10)

        # Painter Paths
        self.main_path = QtGui.QPainterPath()
        self.main_path.addRoundedRect(self.main_rect, 4, 4)
        self.setPath(self.main_path)

        self.dialogue_path = QtGui.QPainterPath()
        self.dialogue_path.addRect(self.dialogue_rect)

        self.dialogue_text_item = QtWidgets.QGraphicsTextItem(self.dialogue_text, self)
        self.dialogue_text_item.setCacheMode(QtWidgets.QGraphicsPathItem.DeviceCoordinateCache)
        self.dialogue_text_item.setTextWidth(self.dialogue_rect.width() - 40)
        self.dialogue_text_item.setFont(self.dialogue_font)
        self.dialogue_text_item.setDefaultTextColor(self.dialogue_text_colour)
        self.dialogue_text_item.setPos(self.dialogue_text_point)

        # HARDCODED ELIDE
        elided = self.dialogue_font_metrics.elidedText(self.dialogue_text, QtCore.Qt.ElideRight, 3300)
        self.dialogue_text_item.setPlainText(self.dialogue_text) # elided

        # Flags
        self.setFlag(self.ItemIsMovable, True)
        self.setFlag(self.ItemSendsGeometryChanges, True)
        self.setFlag(self.ItemIsSelectable, True)
        self.setFlag(self.ItemIsFocusable, True)
        self.setCacheMode(QtWidgets.QGraphicsPathItem.DeviceCoordinateCache)

    def boundingRect(self):
        return self.main_rect

    def paint(self, painter, option, widget=None):
        # Background
        self.brush.setColor(self.main_background_colour)
        painter.setBrush(self.brush)

        painter.drawPath(self.path())

        # Dialogue
        self.brush.setColor(self.dialogue_background_colour)
        painter.setBrush(self.brush)
        self.pen.setColor(self.dialogue_background_colour.darker())
        painter.setPen(self.pen)

        painter.drawPath(self.dialogue_path)

这是我尝试使用的方法,但我的数学不太好。我认为我以错误的方式处理这个问题:

    # Dialogue
    text_length = self.dialogue_font_metrics.horizontalAdvance(self.dialogue_text)
    text_metric_rect = self.dialogue_font_metrics.boundingRect(QtCore.QRect(0, 0, self.dialogue_text_item.textWidth(), self.dialogue_font_metrics.capHeight()), QtCore.Qt.TextWordWrap, self.dialogue_text)
    
    elided_length = (text_length / text_metric_rect.height()) * (self.dialogue_rect.height() - 20)
    elided = self.dialogue_font_metrics.elidedText(self.dialogue_text, QtCore.Qt.ElideRight, 3300)

    self.dialogue_text_item.setPlainText(elided)

如有任何建议,我们将不胜感激!

QFontMetrics elide 函数仅适用于单行文本,不能用于layed out 文本,这是涉及自动换行或换行时发生的情况。
即使尝试根据任意大小设置 elide 函数的宽度,它也是无效的:每当换行时,用作该行参考的宽度就是“重置”。

假设您希望文本的宽度为 50 像素,因此您假设某些文本将分成两行,总共 100 像素。那么你在该文本中有三个单词,每个单词宽 40 像素,因此 elidedText() 和 100 像素的结果将是你将拥有所有三个单词,最后一个单词被省略。
然后设置该文本启用自动换行且最大宽度为 50 像素:结果将是您只会看到前两个单词,因为每行只能容纳一个单词。

唯一可行的解​​决方案是使用QTextLayout,并遍历它创建的所有文本行,然后,如果下一个行的高度超过最大值高度,您只为该行调用 elidedText()

不过请注意,这是假设格式(字体、字体大小和粗细)在整个文本中始终相同。更高级的布局是可能的,但它需要更高级地使用 QTextDocument 功能、QTextLayout 和 QTextFormat。

        textLayout = QtGui.QTextLayout(self.dialogue_text, dialogue_font)
        height = 0
        maxWidth = text_rect.width()
        maxHeight = text_rect.height()
        textLayout.beginLayout()
        text = ''
        while True:
            line = textLayout.createLine()
            if not line.isValid():
                break
            line.setLineWidth(maxWidth)
            text += self.dialogue_text[
                line.textStart():line.textStart() + line.textLength()]
            line.setPosition(QtCore.QPointF(0, height))
            height += line.height()
            if height + line.height() > maxHeight:
                line = textLayout.createLine()
                line.setLineWidth(maxWidth)
                line.setPosition(QtCore.QPointF(0, height))
                if line.isValid():
                    last = self.dialogue_text[line.textStart():]
                    fm = QtGui.QFontMetrics(dialogue_font)
                    text += fm.elidedText(last, QtCore.Qt.ElideRight, maxWidth)
                break

请注意,您的项目实现有点可疑:首先,您实际上没有使用 QGraphicsPathItem 的任何功能,因为您覆盖了两个 paint()boundingRect().

如果你想做类似的事情,只需使用一个基本的 QGraphicsItem,否则总是尝试使用现有的 类 和 Qt 提供的功能,这对于图形视图框架尤为重要,它依赖于C++ 优化:覆盖 paint() 强制绘图通过 python,这是一个 巨大的 瓶颈,尤其是当涉及许多项目时。

与其绘制所有内容,不如创建具有正确设置属性的子项。

最后,项目不应将自身添加到场景中。

考虑到以上所有因素,这是一个更好、更简单(也更易读)的实现:

class Node(QtWidgets.QGraphicsPathItem):
    def __init__(self, parent=None):
        super(Node, self).__init__(parent)

        self.setBrush(QtGui.QColor("#575b5e"))
        
        main_rect = QtCore.QRectF(0, -40, 600, 140)
        path = QtGui.QPainterPath()
        path.addRoundedRect(main_rect, 4, 4)
        self.setPath(path)

        hMargin = main_rect.width() * .05
        vMargin = 10
        dialogue_rect = main_rect.adjusted(hMargin, vMargin, -hMargin, -vMargin)

        dialogue_item = QtWidgets.QGraphicsRectItem(dialogue_rect, self)
        dialogue_color = QtGui.QColor("#2B2B2B")
        dialogue_item.setPen(QtGui.QPen(dialogue_color.darker(), 2))
        dialogue_item.setBrush(dialogue_color)

        text_rect = dialogue_rect.adjusted(hMargin, vMargin, -hMargin, -vMargin)
        dialogue_font = QtGui.QFont("Calibri", 12)
        dialogue_font.setBold(True)

        self.dialogue_text = "To find out how fast you type, just start typing "\
            "in the blank textbox on the right of the test prompt. You will see "\
            "your progress, including errors on the left side as you type. You "\
            "can fix errors as you go, or correct them at the end with the help "\
            "of the spell checker. If you need to restart the test, delete the "\
            "text in the text box. Interactive feedback shows you your current "\
            "wpm and accuracy. Bring me all the biscuits, for I am hungry. They "\
            "will be a fine meal for me and all the mice in town!"

        textLayout = QtGui.QTextLayout(self.dialogue_text, dialogue_font)
        height = 0
        maxWidth = text_rect.width()
        maxHeight = text_rect.height()
        textLayout.beginLayout()
        text = ''
        while True:
            line = textLayout.createLine()
            if not line.isValid():
                break
            line.setLineWidth(maxWidth)
            text += self.dialogue_text[
                line.textStart():line.textStart() + line.textLength()]
            line.setPosition(QtCore.QPointF(0, height))
            height += line.height()
            if height + line.height() > maxHeight:
                line = textLayout.createLine()
                line.setLineWidth(maxWidth)
                line.setPosition(QtCore.QPointF(0, height))
                if line.isValid():
                    last = self.dialogue_text[line.textStart():]
                    fm = QtGui.QFontMetrics(dialogue_font)
                    text += fm.elidedText(last, QtCore.Qt.ElideRight, maxWidth)
                break

        doc = QtGui.QTextDocument(text)
        doc.setDocumentMargin(0)
        doc.setDefaultFont(dialogue_font)
        doc.setTextWidth(text_rect.width())

        self.dialogue_text_item = QtWidgets.QGraphicsTextItem(self)
        self.dialogue_text_item.setDocument(doc)
        self.dialogue_text_item.setCacheMode(self.DeviceCoordinateCache)
        self.dialogue_text_item.setDefaultTextColor(QtCore.Qt.white)
        self.dialogue_text_item.setPos(text_rect.topLeft())

        # Flags
        self.setFlag(self.ItemIsMovable, True)
        self.setFlag(self.ItemSendsGeometryChanges, True)
        self.setFlag(self.ItemIsSelectable, True)
        self.setFlag(self.ItemIsFocusable, True)
        self.setCacheMode(self.DeviceCoordinateCache)