Emile Haas
Emile Haas

Reputation: 440

how to make an overriden QGraphicsTextItem editable & movable?

I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.

I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...

Here is a Minimal Reproducible Example :

import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem

from PyQt5.QtCore import *
from PyQt5.QtGui import  QPen
    

class NodeTag(QGraphicsTextItem):
    def __init__(self,text):
        QGraphicsTextItem.__init__(self,text)
        self.text = text
        self.setPos(0,0)
        self.setTextInteractionFlags(Qt.TextEditorInteraction)
        # self.setFlag(QGraphicsItem.ItemIsFocusable, True)   # All these flags are ignored...
        # self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)

    def boundingRect(self):
        return QRectF(0,0,80,25)

    def paint(self,painter,option,widget):
        painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
        painter.drawRect(self.boundingRect())
        painter.drawText(self.boundingRect(),self.text)
    
    def mousePressEvent(self, event):
        print("CLICK!")
        # self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
        # self.setFocus()


class GView(QGraphicsView):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.parent = parent
        self.setGeometry(100, 100, 700, 450)
        self.show()


class Scene(QGraphicsScene):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        tagItem = NodeTag("myText")   # create a NodeTag item
        self.addItem(tagItem)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()  # create default constructor for QWidget
        self.setGeometry(900, 70, 1000, 800)
        self.createGraphicView()
        self.show()

    def createGraphicView(self):
        self.scene = Scene(self)
        gView = GView(self)
        scene = Scene(gView)
        gView.setScene(scene)
        # Set the main window's central widget
        self.setCentralWidget(gView)

# Run program
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far. Any help appreciated!

Upvotes: 0

Views: 1238

Answers (2)

musicamante
musicamante

Reputation: 48399

All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.

The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.

Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.

Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.

Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.

Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).

Here's a full implementation of what described above:

class NodeTag(QGraphicsTextItem):
    def __init__(self, text):
        QGraphicsTextItem.__init__(self, text)
        self.startPos = None
        self.isMoving = False
        # the following is useless, not only because we are leaving the text
        # painting to the base implementation, but also because the text is 
        # already accessible using toPlainText() or toHtml()
        #self.text = text
        # this is unnecessary too as all new items always have a (0, 0) position
        #self.setPos(0, 0)

    def boundingRect(self):
        return super().boundingRect() | QRectF(0, 0, 80, 25)

    def paint(self, painter, option, widget):
        # draw the border *before* (as in "behind") the text
        painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
        painter.drawRect(self.boundingRect())
        super().paint(painter, option, widget)

    def shape(self):
        shape = QPainterPath()
        shape.addRect(self.boundingRect())
        return shape

    def focusOutEvent(self, event):
        # this is required in order to allow movement using the mouse
        self.setTextInteractionFlags(Qt.NoTextInteraction)

    def mousePressEvent(self, event):
        if (event.button() == Qt.LeftButton and 
            self.textInteractionFlags() != Qt.TextEditorInteraction):
                self.startPos = event.pos()
        else:
            super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.startPos:
            delta = event.pos() - self.startPos
            if (self.isMoving or 
                delta.manhattanLength() >= QApplication.startDragDistance()):
                    self.setPos(self.pos() + delta)
                    self.isMoving = True
                    return
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if (not self.isMoving and 
            self.textInteractionFlags() != Qt.TextEditorInteraction):
                self.setTextInteractionFlags(Qt.TextEditorInteraction)
                self.setFocus()
                # the following lines are used to correctly place the text 
                # cursor at the mouse cursor position
                cursorPos = self.document().documentLayout().hitTest(
                    event.pos(), Qt.FuzzyHit)
                textCursor = self.textCursor()
                textCursor.setPosition(cursorPos)
                self.setTextCursor(textCursor)

        super().mouseReleaseEvent(event)
        self.startPos = None
        self.isMoving = False

As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.

Upvotes: 2

S. Nick
S. Nick

Reputation: 13681

Try it:

...

        
class NodeTag(QGraphicsTextItem):
    def __init__(self, text, parent=None):
        super(NodeTag, self).__init__(parent)
        self.text = text
        self.setPlainText(text)

        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemIsSelectable)

    def focusOutEvent(self, event):
        self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
        super(NodeTag, self).focusOutEvent(event)

    def mouseDoubleClickEvent(self, event):
        if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
            self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
        super(NodeTag, self).mouseDoubleClickEvent(event)
       
    def paint(self,painter,option,widget):
        painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
        painter.drawRect(self.boundingRect())
#        painter.drawText(self.boundingRect(),self.text)
        super().paint(painter, option, widget)
 ...

enter image description here

Upvotes: 1

Related Questions