QTextEdit 上可调整大小的内联图像

Resizable In-Line Image on QTextEdit

我正在为 QTextEdit 开发一个内联小部件,以通过 QSizeGrip 调整大小并通过上下文菜单实现 90 度旋转。我已经能够在里面渲染一个 QLabel,但它失去了它的常规功能。问题是如何使用 QTextObjectInterface 将 QWidget 正确地嵌入到 QTextEdit 中?

https://blog.rfox.eu/en/Programming/Python/Active_widget_in_PyQT5_-_QTextEdit.html

C++ 的相关问题 How to resize an image in a QTextEdit?

将“picture.jpg”替换为本地文件以启动示例。

from PyQt5.QtCore import QObject, QSizeF, QRectF, Qt
from PyQt5.QtGui import QTextObjectInterface, QTextFormat, QTextCharFormat, QTextDocument, QPainter, QPixmap
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton, QLabel, QSizeGrip


class InLineWidget(QLabel):

    def __init__(self, parent=None):
        super(InLineWidget, self).__init__(parent)
        w, h = self.width(), self.height()
        p = QPixmap(r"picture.jpg")
        self.setPixmap(p)

        self.setMinimumSize(1, 1)
        # self.setScaledContents(False)

        self.setWindowFlags(Qt.SubWindow)
        sizeGrip = QSizeGrip(self)
        # self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        # self.setScaledContents(True)
        # self.setFrameStyle(3)
        # self.setFixedSize(500, 600)
        # self.setMinimumSize(10, 10)
        # self.setStyleSheet("background-color: rgba(0, 0, 0, 40%)")
        # self.setAlignment(Qt.AlignCenter)


class ImageObject(QObject, QTextObjectInterface):
    IMAGE_TYPE = QTextFormat.UserObject + 2

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

    def drawObject(self, painter: QPainter, rect: QRectF, doc: QTextDocument, posInDocument: int,
                   format: QTextFormat) -> None:
        w = format.property(2)
        w.render(painter, rect.topLeft().toPoint())

    def intrinsicSize(self, doc: QTextDocument, posInDocument: int, format: QTextFormat) -> QSizeF:
        w = format.property(2)
        return QSizeF(w.size())


class TextEdit(QTextEdit):

    def __init__(self):
        super(TextEdit, self).__init__()
        self.img_interface = ImageObject()
        self.img_interface.setParent(self)

        layout = self.document().documentLayout()
        layout.registerHandler(ImageObject.IMAGE_TYPE, self.img_interface)


class TestWidget(QWidget):

    def __init__(self):
        super(TestWidget, self).__init__()
        layout = QVBoxLayout(self)
        self.te = TextEdit()
        layout.addWidget(self.te)
        self.btn_img = QPushButton('add picture', clicked=self.addPic)
        layout.addWidget(self.btn_img)

    def addPic(self):
        image = InLineWidget()
        image.setParent(self.te)

        imgCharFormat = QTextCharFormat()
        imgCharFormat.setObjectType(ImageObject.IMAGE_TYPE)
        imgCharFormat.setProperty(2, image)

        cursor = self.te.textCursor()
        cursor.insertText(chr(0xfffc), imgCharFormat)


if __name__ == '__main__':
    import sys
    from PyQt5.QtWidgets import QApplication
    import sys

    app = QApplication(sys.argv)
    widget = TestWidget()

    widget.show()

    sys.exit(app.exec_())

更新

由于这些问题,我更改了以前的代码。

1.We 在 mouseMoveEvent 中插入并重新插入 inlineWidget,它会导致溢出错误和最大递归错误,具体取决于 inlineWidget.In 我的情况,我嵌入 QTextEdit(我自己的应用程序的子类)并实现相同的操作,但这些错误已经发生。 所以我判断插入应该是一次。 我将用于插入新小部件的代码移至 mouseReleaseEvent.

2.For 执行 1 有一个问题。我们只在mouseReleaseEvent处插入了一次inlinewidget,在resize的过程中我们无法关注图片大小的图片。因此,我将操作授权给 ImageHandler paint func.

请将示例代码替换为此。

class ImageHandler(QGraphicsRectItem):
    def __init__(self, rect = QRectF(), view=None):
        super(ImageHandler, self).__init__(rect)
        self.view = view
        self._color = QColor(200, 100, 200, 100)
    def paint(self, painter, option, widget):
        if hasattr(self, "inlinewidget"):
            #pixmap = QPixmap(r"self.inlinewidget.pixmap()") is more abstractive.
            pixmap = QPixmap(r"picture.jpg")
            pixmap =  pixmap.scaled(self.rect().size().toSize())
            painter.drawPixmap(self.rect().toRect(), pixmap)
        painter.drawRect(self.rect())
    def boundingRect(self):
        return self.rect()
class ImageSizeGrip(ImageHandler):   
    def __init__(self, rect = QRectF(), view=None):
        super(ImageSizeGrip, self).__init__(rect, view)
        self.start_pos = QPointF()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setAcceptHoverEvents(True)
        self.color = QColor(120, 243, 80, 200)
        self.setFlags(QGraphicsRectItem.ItemIsSelectable|QGraphicsRectItem.ItemIsMovable)
        self.grab_textobject = False
        self.find_cursor = QTextCursor()
        self.find_position = 0
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.start_pos = event.scenePos()
        cur = self.parentItem().inlinewidget.parent().cursorForPosition(self.parentItem().inlinewidget.geometry().bottomRight())
        doc = self.parentItem().inlinewidget.parent().document()
        
        self.find_cursor = doc.find(chr(0xfffc), cur.position(), doc.FindBackward)
        self.find_position = self.find_cursor.position()
        self.find_cursor.clearSelection()             
        self.find_cursor.setPosition(self.find_position, self.find_cursor.MoveAnchor)
        self.find_cursor.movePosition(self.find_cursor.Left, self.find_cursor.KeepAnchor)
        return QGraphicsRectItem.mousePressEvent(self, event)
    def mouseMoveEvent(self, event):
        parentItem = self.parentItem()
        parentItem.prepareGeometryChange()
        rect = QRectF()
        rect.setCoords(parentItem.rect().topLeft().x(), parentItem.rect().topLeft().y(), event.scenePos().x(), event.scenePos().y())
        parentItem.setRect(rect)
        parentItem.update() 
        self.find_cursor.clearSelection()
        self.find_cursor.setPosition(self.find_position, self.find_cursor.MoveAnchor)
        return QGraphicsRectItem.mouseMoveEvent(self, event)
    def mouseReleaseEvent(self, event):
        parentItem = self.parentItem()
        self.find_cursor.movePosition(self.find_cursor.Left, self.find_cursor.KeepAnchor)
        if self.find_cursor.selectedText() == chr(0xfffc) and  len(self.find_cursor.selectedText()) == 1:
            self.find_cursor.setKeepPositionOnInsert(True)           
            self.find_cursor.deleteChar()
            inLineWidget = InLineWidget()
            inLineWidget.setGeometry(parentItem.rect().toRect())
            parentItem.inlinewidget.parent().insert_text_object(self.find_cursor, inLineWidget)        
            self.find_cursor.setKeepPositionOnInsert(False)
            parentItem.inlinewidget.parent()._trigger_obj_char_rescan()
        self.view.scene().removeItem(self.parentItem())
        self.view.scene().removeItem(self)
        self.view.scene().update()
        return QGraphicsRectItem.mouseReleaseEvent(self, event)
    def hoverEnterEvent(self, event):
        self.setCursor(Qt.PointingHandCursor)
        return QGraphicsRectItem.hoverEnterEvent(self, event)
    def hoverLeaveEvent(self, event):        
        return QGraphicsRectItem.hoverLeaveEvent(self, event)

旧答案

我判断问题的更新是最后一次,所以我想写我的答案。

至少,如果你想在小部件上嵌入相同大小的图片,你必须按父小部件的比例缩放它。

为此,您必须添加缩放代码。

w.setPixmap(w.pixmap().scaled(w.size()))

然后,您想将小部件的大小调整 QSizeGrip。 您还想调整嵌入图像的大小。

QTextObjectInterface 可以绘制 QTextObject 但它不是交互式的。 根据你的问题 url,你也必须渲染 QSizeGrip 小部件。

而您忘记在 def addPic(self):

中编写此代码
image.show()

但仅此而已,绘制了两个立体图像。一个是由drawObject绘制的,另一个是由'InlineWidget`嵌入的像素图对象。(一个= A,另一个= B)

现在,您绘制了一个小部件,但这是一个特例。本来QTextObjectInterface是准备把非交互对象当成character.Certainly来画的,随便你怎么画都可以draw.But正常,我们不用来画交互的widget . 你已经从特殊的case.It开始让我们很难识别问题出在哪里。

你会设法确认重复图片下发生了什么,因为没有意义的句子。 'A' 由 drawObject 绘制。 'B' 仅显示,但具有 QTextEdit 的父级,可以说,它只是一个 widget.Our 文本处理,对 'B'.

没有影响

如果添加一些文字,'A' 会被文字逐渐滑动,因为它确实是 TextObject。 但是'B'没有滑动,因为不是TextObject而是QWidget.

利用QTextObjectInterface的自动计算,我们必须使'B'的位置与'A'的位置重合。我们必须在 drawObject.

中添加此代码
w.move(rect.topLeft().toPoint())

通过这些步骤,您可以嵌入与小部件一样大的图片、显示交互式小部件、相对于图片和图片本身并行移动InLineLabel

当你显示小部件时,我认为 drawObject() 中的 inlined_widget.widget.render(painter, rect.topLeft().toPoint()) 已经不需要了,因为小部件代表它工作。


从现在开始,我将对您的代码进行大量更改。

即使添加了上述修复代码,如果要调整图像大小,仍然存在很多问题。

  1. QSizeGrip为了这个目的在这种情况下很难处理。我强烈建议使用 QGraphicsViewQGraphicsSceneQGraphicsItem 处理。(我不知道如何使用 QSizeGrip,就我看你的代码而言,这是在topLeft 角。我认为这应该放在 bottomRight corner.and 我无法捕捉坐标系 soon.I 假设发生这种情况是因为 InLineWidget 的几何形状尚未 decided.It当 inserted.But QSizeGrip 已经是它的子项时决定。)我尝试实现 QDialogQSizeGripEnabled(True),即使我将其设置为 False,我也可以调整对话框的大小...

  2. 如果我们调整小部件的大小,嵌入的像素图也将调整大小。但是 jpg 文件如果在同一内存上很容易崩溃,因为粗略累积的结果,近似 recalculating.We 需要重新加载新的同名像素图并缩放大小,然后交换。

  3. 即使我们通过删除、复制、剪切等方式删除了TextObject'A',widget'B'仍然会remained.We 需要实现同时删除widget和基础对象的方式

  4. 从'B'调整大小,当然可以调整,但是'A'不调整。因此,调整大小'B'对行space.We没有影响可以从这两种方法调整大小,但我采用'B'的方式。我们需要对[=151=进行更改] 对 'A' 有影响。为此,我们必须在第二个 URL.we 需要调整 'B' 大小、删除 'A' 并重新插入 'A' 同时.

要解决这些问题,我们至少需要执行URL的程序。 感谢小部件,接受鼠标操作要容易得多。它充当几何的代理和 getter。

这是混合代码,其中包含您介绍的 url,您的和我的。

from PyQt5.QtCore import QObject, QSizeF, QRectF, Qt, QPointF
from PyQt5.QtGui import QTextObjectInterface, QTextFormat, QTextCharFormat, QTextDocument, QPainter, QPixmap, QPageSize, QBrush, QColor, QTextCursor
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QPushButton, QLabel, QGraphicsView, QGraphicsScene, QGraphicsRectItem, QSizeGrip


class View(QGraphicsView):
    def __init__(self, parent=None):
        super(View, self).__init__(parent)       
        self.imageHandler = ImageHandler(QRectF(), self)
        self.sizeGripHandler = ImageSizeGrip(QRectF(), self)
        self.sizeGripHandler.setParentItem(self.imageHandler)
    def keyPressEvent(self, event):
        self.scene().removeItem(self.imageHandler)
        self.scene().removeItem(self.sizeGripHandler)
        return QGraphicsView.keyPressEvent(self, event)
class Scene(QGraphicsScene):
    def __init__(self, parent=None):
        super(Scene, self).__init__(parent)
        self.setBackgroundBrush(QBrush(Qt.gray, Qt.SolidPattern))
        
    def addItem(self, item):
        if item not in self.items():
            super(Scene, self).addItem(item)
    def removeItem(self, item):
        if item  in self.items():
            super(Scene, self).removeItem(item)
class ImageHandler(QGraphicsRectItem):
    def __init__(self, rect = QRectF(), view=None):
        super(ImageHandler, self).__init__(rect)
        self.view = view
        self._color = QColor(200, 100, 200, 100)
    def paint(self, painter, option, widget):
        painter.setBrush(QBrush(self._color, Qt.Dense1Pattern))
        painter.drawRect(self.rect())
    def boundingRect(self):
        return self.rect()
    
class ImageSizeGrip(ImageHandler):
   
    def __init__(self, rect = QRectF(), view=None):
        super(ImageSizeGrip, self).__init__(rect, view)
        self.start_pos = QPointF()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setAcceptHoverEvents(True)
        self.color = QColor(120, 243, 80, 200)
        self.setFlags(QGraphicsRectItem.ItemIsSelectable|QGraphicsRectItem.ItemIsMovable)
        self.grab_textobject = False
        self.find_cursor = QTextCursor()
        self.find_position = 0
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.start_pos = event.scenePos()
        cur = self.parentItem().inlinewidget.parent().cursorForPosition(self.parentItem().inlinewidget.geometry().bottomRight())
        doc = self.parentItem().inlinewidget.parent().document()
        #chr(0xfffc) is probably located in the previous positions because ImageSizeGrip is positioned at the rightbottom of the picture.
        self.find_cursor = doc.find(chr(0xfffc), cur.position(), doc.FindBackward)
        self.find_position = self.find_cursor.position()
        self.find_cursor.clearSelection()             
        self.find_cursor.setPosition(self.find_position, self.find_cursor.MoveAnchor)
        movable = self.find_cursor.movePosition(self.find_cursor.Left, self.find_cursor.KeepAnchor)
        return QGraphicsRectItem.mousePressEvent(self, event)
    def mouseMoveEvent(self, event):
        parentItem = self.parentItem()
        parentItem.prepareGeometryChange()
        rect = QRectF()
        rect.setCoords(parentItem.rect().topLeft().x(), parentItem.rect().topLeft().y(), event.scenePos().x(), event.scenePos().y())
        parentItem.setRect(rect)
        parentItem.inlinewidget.resize(rect.size().toSize())
        parentItem.update()
        #I think it is better you load initial pixmap item. Especially jpg is subject to be broken by size change.
        pixmap = QPixmap(r"picture.jpg")
        pixmap =  pixmap.scaled(rect.size().toSize())
        parentItem.inlinewidget.pixmap().swap(pixmap)
        self.find_cursor.clearSelection()
        self.find_cursor.setPosition(self.find_position, self.find_cursor.MoveAnchor)
        movable = self.find_cursor.movePosition(self.find_cursor.Left, self.find_cursor.KeepAnchor)
        if self.find_cursor.selectedText() == chr(0xfffc) and  len(self.find_cursor.selectedText()) == 1:
            self.find_cursor.setKeepPositionOnInsert(True)           
            self.find_cursor.deleteChar()
            inLineWidget = InLineWidget()
            inLineWidget.setGeometry(parentItem.rect().toRect())
            parentItem.inlinewidget.parent().insert_text_object(self.find_cursor, inLineWidget)        
            self.find_cursor.setKeepPositionOnInsert(False)
            parentItem.inlinewidget.parent()._trigger_obj_char_rescan()
        return QGraphicsRectItem.mouseMoveEvent(self, event)
    def mouseReleaseEvent(self, event):
        self.view.scene().removeItem(self.parentItem())
        self.view.scene().removeItem(self)
        self.view.scene().update()
        return QGraphicsRectItem.mouseReleaseEvent(self, event)
    def hoverEnterEvent(self, event):
        self.setCursor(Qt.PointingHandCursor)
        return QGraphicsRectItem.hoverEnterEvent(self, event)
    def hoverLeaveEvent(self, event):        
        return QGraphicsRectItem.hoverLeaveEvent(self, event)
class InLineWidget(QLabel):
    def __init__(self, parent=None):
        super(InLineWidget, self).__init__(parent)
        self.view = None
        #Now, you are using only one picture. but if you set it as variable, you can set any pixmap item.
        p = QPixmap(r"picture.jpg")
        self.setPixmap(p)
        self.setMinimumSize(1, 1)
        # self.setScaledContents(False)
        # self.setWindowFlags(Qt.SubWindow)
        sizeGrip = QSizeGrip(self)
        sizeGrip.setWindowFlags(Qt.SubWindow)
        # self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        # self.setScaledContents(True)
        self.setFrameStyle(3)
        # self.setFixedSize(500, 600)
        # self.setMinimumSize(10, 10)
        # self.setStyleSheet("background-color: rgba(0, 0, 0, 40%)")
        # self.setAlignment(Qt.AlignCenter)
    def mousePressEvent(self, event):
        self.view.imageHandler.setRect(QRectF(self.geometry()))        
        self.view.sizeGripHandler.setRect(QRectF(self.view.imageHandler.rect().bottomRight(), QSizeF(10, 10)))
        self.view.sizeGripHandler.setPos(0, 0)
        self.view.scene().addItem(self.view.imageHandler)
        self.view.scene().addItem(self.view.sizeGripHandler)        
        self.view.imageHandler.inlinewidget = self
        return QLabel.mousePressEvent(self, event)    

class InlinedWidgetInfo:
    #From your pasting URL
    object_replacement_character = chr(0xfffc)
    _instance_counter = 0
    def __init__(self, widget):
        self.widget = widget
        self.text_format_id = QTextFormat.UserObject + InlinedWidgetInfo._instance_counter
        self.char = self.object_replacement_character
        InlinedWidgetInfo._instance_counter += 1

class TextEdit(QTextEdit):
    
    def __init__(self):
        super(TextEdit, self).__init__()
        #From your pasting URL, but some valiables belong to QTextEdit.
        self.last_text_lenght = 0
        self.text_format_id_to_inlined_widget_map = {}
        self.currentCharFormatChanged.connect(self.on_character_format_change)
        self.selectionChanged.connect(self._trigger_obj_char_rescan)
        self.textChanged.connect(self.on_text_changed)
    
    def wrap_with_text_object(self, inlined_widget):       
        class ImageObject(QObject, QTextObjectInterface):       
            def __init__(self, parent=None):
                super(ImageObject, self).__init__(parent)        
            def drawObject(self, painter: QPainter, rect: QRectF, doc: QTextDocument, posInDocument: int,
                           format: QTextFormat) -> None:
                
                # inlined_widget.widget.render(painter, rect.topLeft().toPoint())
                inlined_widget.widget.setGeometry(rect.toRect())
            def intrinsicSize(self, doc: QTextDocument, posInDocument: int, format: QTextFormat) -> QSizeF:               
                return QSizeF(inlined_widget.widget.size())
        document_layout = self.document().documentLayout()
        document_layout.registerHandler(inlined_widget.text_format_id, ImageObject(self))
        self.text_format_id_to_inlined_widget_map[inlined_widget.text_format_id] = inlined_widget
        inlined_widget.widget.setPixmap(inlined_widget.widget.pixmap().scaled(inlined_widget.widget.size()))

    def insert_text_object(self, cursor, inlined_widget):
        inlined_widget = InlinedWidgetInfo(inlined_widget)        
        self.wrap_with_text_object(inlined_widget)
        inlined_widget.widget.view = self.vi
        inlined_widget.widget.setParent(self)

        char_format = QTextCharFormat()
        char_format.setObjectType(inlined_widget.text_format_id)
        cursor.insertText(inlined_widget.char, char_format)
        inlined_widget.widget.show()

    def on_character_format_change(self, qtextcharformat):
        text_format_id = qtextcharformat.objectType()
        
        # id 0 is used when the object is deselected - I don't really want the id
        # itself, I just want to know that there was some change AFTER it was done
        if text_format_id == 0:
            self._trigger_obj_char_rescan()

    def on_text_changed(self):
        current_text_lenght = len(self.toPlainText())
        if self.last_text_lenght > current_text_lenght:
            self._trigger_obj_char_rescan()

        self.last_text_lenght = current_text_lenght

    def _trigger_obj_char_rescan(self):
        text = self.toPlainText()
        character_indexes = [
            cnt for cnt, char in enumerate(text)
            if char == InlinedWidgetInfo.object_replacement_character
        ]

        # get text_format_id for all OBJECT REPLACEMENT CHARACTERs
        present_text_format_ids = set()
        for index in character_indexes:
            cursor = QTextCursor(self.document())

            # I have to create text selection in order to detect correct character
            cursor.setPosition(index)
            if index < len(text):
                cursor.setPosition(index + 1, QTextCursor.KeepAnchor)

            text_format_id = cursor.charFormat().objectType()

            present_text_format_ids.add(text_format_id)

        # diff for characters that are there and that should be there
        expected_text_format_ids = set(self.text_format_id_to_inlined_widget_map.keys())
        removed_text_ids = expected_text_format_ids - present_text_format_ids

        # hide widgets for characters that were removed
        for text_format_id in removed_text_ids:
            inlined_widget = self.text_format_id_to_inlined_widget_map[text_format_id]
            inlined_widget.widget.hide()
            del self.text_format_id_to_inlined_widget_map[text_format_id]
class TestWidget(QWidget):

    def __init__(self):
        super(TestWidget, self).__init__()
        layout = QVBoxLayout(self)
        self.vi = View()
        self.sc = Scene()
        self.te = TextEdit()
        self.te.vi = self.vi
        self.te.resize(int(QPageSize.size(QPageSize.A0, QPageSize.Point).width()), int(QPageSize.size(QPageSize.A0, QPageSize.Point).height()))
        self.sc.addWidget(self.te)
        self.vi.setScene(self.sc)
        self.vi.setSceneRect(QRectF(0, 0, int(QPageSize.size(QPageSize.A0, QPageSize.Point).width()), int(QPageSize.size(QPageSize.A0, QPageSize.Point).height())))
        layout.addWidget(self.vi)
        self.btn_img = QPushButton('add picture', clicked=lambda:self.te.insert_text_object(self.te.textCursor(), InLineWidget()))
        layout.addWidget(self.btn_img)
        self.vi.centerOn(0 ,0)
    def addPic(self):
        # is not used
        pass

if __name__ == '__main__':
    from PyQt5.QtWidgets import QApplication
    import sys

    app = QApplication(sys.argv)
    widget = TestWidget()

    widget.show()

    sys.exit(app.exec_())

变化点

我想你可能会明白要更改的地方。

  1. QGraphicsView新导入查看QGraphicsItem.
  2. QTextEdit 适合 QGraphicsView & QGraphicsScene
  3. 实现 URL 代码以彻底删除小部件。
  4. 实现第二​​种url交换QTextObject的方式,对齐QTextLine的动态底部。

如有疑问,请留言。

感谢您的 URL 易于理解和阅读的代码,停靠代码非常容易。

如果嵌入多张图片,每张图片的底线都会被强制对齐到当前QTextLine的最大底边。所以你在resizing.But的时候看起来会很奇怪只要图片是QTextInlineObject.

就很难解决