Search code examples
pythonpyqtpyqtgraphqtextedit

Embed Pyqtgraph into Pyqt Textedit


I'd like to create a text editor where you can drag and drop Pyqtgraphs and interact with them real-time. I am having trouble understanding the behavior of TextEdit and how a Widget could be 'embedded' within the TextEdit itself. After review the API it seems that only text, html, and images can be added into the QTextEdit.

I might be grasping at straws here, but I was hoping that there was a void QTextEdit::insertWidget(QWidget*) or something of that nature.

To clarify here is an incomplete sample code:

import sys
import numpy as np

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pyqtgraph as pg


class PlotTextEditor(QMainWindow):
    def __init__(self):
        super().__init__()

        self.textEdit = QTextEdit(self)
        self.setCentralWidget(self.textEdit)

        toolbar = QToolBar()

        plotAction = QAction('Plot', self)
        plotAction.triggered.connect(self.addPlot)
        toolbar.addAction(plotAction)

        self.addToolBar(toolbar)

        self.setGeometry(300, 100, 800, 1000)

    def addPlot(self):
        x = np.linspace(0, 10, 100)
        y = np.sin(x)

        glWidget = pg.GraphicsLayoutWidget()
        plot = glWidget.addPlot()
        plot.plot(x, y, pen='r')

        # I'd like to be able to use a line such as this
        # self.textEdit.textCursor().insertWidget(glWidget)
        self.textEdit.textCursor().insertText('I want to insert a widget pointer instead.')


if __name__ == "__main__":
    app = QApplication(sys.argv)
    pte = PlotTextEditor()
    pte.show()
    sys.exit(app.exec_())

Some ideas I had were to overlay the pyqtgraph over a blank image, or try to find some kind of text widget I can insert and override paintevent to give it the pyqtgraph paint. Ultimately however, I am not sure if this is even possible with the current Qt backend with TextEdit.


Solution

  • Please try this Code.

    If you want to further information , please ask me by comment. I don't know if you like this result or not.

    UPDATE TRANSITION

    1. plot image can be rendered but the data is not rendered. I changed pen = 'r': plot.plot(x, y, pen) to plot.plot(x, y, pen = 'r')

    2. the data of plot image is dirty and not antialiased, intercepted on and off. I update intrinsicSize() and return image width and image height, You can look a bad result if you set return QSizeF() argument in intrinsicSize as you like.

    3. Interactively as possible as we can. As you can see, this image is drawn by QPainter. So , originally, this is uninteractive.I think this is the only way for rendering your result as QTextObject. QTextEdit can't accept a kind of widgets as text.

    4. Please click in front of the image. GraphicsLayoutWidget shows.and you change the graph contents, and you close it. And the next time, the image is redrawn and rendered again.

    5. I deleted needless code.

    Anyway, I tried to do until this point.

    Please ask me about further imformation by comment.

    import sys
    import numpy as np
    
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    import pyqtgraph as pg
    import pyqtgraph.exporters
    
    import os
    plot_image_dict = {}
    class PlotObjectInterface(QObject, QTextObjectInterface):
        def __init__(self, parent=None):
            super(PlotObjectInterface, self).__init__(parent=None)
        def drawObject(self, painter, rect, doc, posInDocument, format):      
            img = format.property(1)
            painter.save()        
            painter.setRenderHints(QPainter.Antialiasing)
            painter.drawImage(QRectF(rect), img)
            painter.restore()
        def intrinsicSize(self, doc, posInDocument, format):
            img = format.property(1)
            width = img.size().width()
            height = img.size().height()
            return QSizeF(width, height)
    
    class PlotTextEdit(QTextEdit):
        def __init__(self):
            super().__init__()        
    class PlotView(QGraphicsView):
        def __init__(self):
            super().__init__()
            self.plot_scene = QGraphicsScene()
            self.plot_tedit = PlotTextEdit()
            self.plot_tedit.resize(800, 1000)
            self.plot_scene.addWidget(self.plot_tedit)
            self.setScene(self.plot_scene)
    class PlotGraphicsLayoutWidget(pg.GraphicsLayoutWidget):
        def __init__(self):
            super().__init__()        
            self.current_edit_filename = ""
            self.current_edit_imagenum = 0
            self.current_edit_image = QImage()
            self.current_edit_position = 0
        def updateImage(self):
            #overwrite the image
            pg.QtGui.QApplication.processEvents()
            exporter = pg.exporters.ImageExporter(self.scene())
            filename = os.path.join(self.current_edit_filename)
            exporter.export(filename)     
            image = QImage()
            image.load(filename)
            image_info = plot_image_dict[self.current_edit_imagenum]  
            tc = QTextCursor(self.current_text_edit.document())
            tc.setPosition(image_info[3] - 1, tc.MoveAnchor)
            tc.movePosition(tc.Right, tc.KeepAnchor, 1)
            tc.setKeepPositionOnInsert(True)
            char = QTextCharFormat()
            char.setObjectType(QTextFormat.UserObject + 1)              
            char.setProperty(1, image)
            char.setProperty(2, image_info[1])
            char.setProperty(3, image_info[2])
            char.setProperty(4, image_info[3])        
            plot_image_dict[self.current_edit_imagenum] = [image, image_info[1], image_info[2], image_info[3]]
            tc.insertText("\ufffc", char)
            tc.setKeepPositionOnInsert(False)
        def closeEvent(self, event):
            self.updateImage()
            return pg.GraphicsLayoutWidget.closeEvent(self, event)
    class PlotTextEditor(QMainWindow):
        def __init__(self):
            super().__init__()
            self.plotview = PlotView()
            self.pyplotObjectInterface = PlotObjectInterface()
            self.plotview.plot_tedit.document().documentLayout().registerHandler(QTextFormat.UserObject+1,self.pyplotObjectInterface)    
            self.plotview.plot_tedit.viewport().installEventFilter(self)
            self.setCentralWidget(self.plotview)
            toolbar = QToolBar()
            plotAction = QAction('Plot', self)
            plotAction.triggered.connect(self.insertImage)
            toolbar.addAction(plotAction)
            self.addToolBar(toolbar)
            self.setGeometry(300, 100, 800, 1000)    
            self.test_glWidget = PlotGraphicsLayoutWidget()
            self.test_glWidget.current_text_edit = self.plotview.plot_tedit
    
            self.test_manipulation = False
            x = np.linspace(0, 10, 100)
            y = np.sin(x)               
            plot = self.test_glWidget.addPlot()       
            #PlotDataItem
            plot.plot(x, y, pen = 'b')
        def closeEvent(self, event):
            QApplication.closeAllWindows()
            return QMainWindow.closeEvent(self, event)
        def eventFilter(self, obj, event):    
            if event.type() == QMouseEvent.MouseButtonPress and  obj == self.plotview.plot_tedit.viewport():
                tc = self.plotview.plot_tedit.textCursor()
                position = tc.position()
                tc.movePosition(tc.Left, tc.KeepAnchor,1)
                if tc.selectedText() == "\ufffc":                
                    self.editImage(tc)                            
                tc.clearSelection()
                tc.setPosition(position)            
                p_next = position + 1
                tc.setPosition(p_next, tc.KeepAnchor)
                if tc.selectedText() == "\ufffc":
                    print("next character is \ufffc")
                    tc.clearSelection()
                    self.editImage(tc)                  
                return False
            return QMainWindow.eventFilter(self, obj, event)
        def editImage(self, tc):        
            tc = QTextCursor(tc)
            rect = self.plotview.plot_tedit.cursorRect(tc)
            topLeft = rect.topLeft()
            child_topLeft = self.plotview.mapToGlobal(topLeft)
            char = tc.charFormat()
            plot_img =  char.property(1)
            plot_num = char.property(2)
            filename = char.property(3)
            char.setProperty(4, tc.position())
            if plot_img  is not None:                    
                geometry = QRect(topLeft,QSize(plot_img .width(), plot_img .height()))
                self.test_glWidget.current_edit_filename = filename
                self.test_glWidget.current_edit_imagenum = plot_num
                self.test_glWidget.current_edit_image = plot_img
                self.test_glWidget.current_edit_position = tc.position()
                plot_image_dict[self.test_glWidget.current_edit_imagenum] = [plot_img, plot_num, filename,  tc.position()]
                self.test_glWidget.setGeometry(geometry)
                self.test_glWidget.show()    
        def insertImage(self):        
            pg.QtGui.QApplication.processEvents()
            exporter = pg.exporters.ImageExporter(self.test_glWidget.scene())
            filename = os.path.join(os.getcwd(), "plot.png")
            exporter.export(filename)
            plot_img = QImage()
            plot_img.load(filename)        
            plot_num = len(plot_image_dict)
            plot_char = QTextCharFormat()
            plot_char.setObjectType(QTextFormat.UserObject+1)        
            plot_char.setProperty(1, plot_img)     
            plot_char.setProperty(2, plot_num)
            plot_char.setProperty(3, filename)        
            plot_char.setProperty(4, 0)
            self.plotview.plot_tedit.textCursor().insertText("\ufffc", plot_char)
            plot_image_dict[plot_num] = [plot_img, plot_num, filename, 0]
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        pte = PlotTextEditor()
        pte.show()
        sys.exit(app.exec_())