pythonpyqtpyqt5qgraphicsitemqgraphicsrectitem

PyQt issue with resizing and repainting of QGraphicsRectItem


I'm trying to create a resizable QGraphicsRectItem with an option to choose different draw styles.

If I create a simple rect with a resize feature only, then it works as expected:

class Rectangle(QtWidgets.QGraphicsRectItem):
    def __init__(self, x, y, w, h):
        super(Rectangle, self).__init__(0, 0, w, h)
        self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
        self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
            | QtWidgets.QGraphicsItem.ItemIsMovable
            | QtWidgets.QGraphicsItem.ItemIsFocusable
            | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
            | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setPos(QtCore.QPointF(x, y))

        self.rect = rect = QtCore.QRectF(0, 0, 200, 200)

    def boundingRect(self):
        return self.rect.adjusted(-10, -10, 10, 10)

    def mouseMoveEvent(self, event):
        if event.buttons() & QtCore.Qt.LeftButton:
            super(Rectangle, self).mouseMoveEvent(event)
        if event.buttons() & QtCore.Qt.RightButton:
            self.rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
            self.prepareGeometryChange()
            self.setRect(self.rect)

When i try to modify it to change pen styles and color if active, it becomes unselectable and unfocusable. More than that, the bounding rect disappears unexpectedly. Here's a modified version:

class Rectangle(QtWidgets.QGraphicsRectItem):
    def __init__(self, position, scene, style=QtCore.Qt.SolidLine,
                 rect=None, matrix=QtGui.QMatrix()):
        super(Rectangle, self).__init__()

        # self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
        self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
                      | QtWidgets.QGraphicsItem.ItemIsMovable
                      | QtWidgets.QGraphicsItem.ItemIsFocusable
                      | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
                      | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)

        if rect is None:
            rect = QtCore.QRectF(0, 0, 200, 200)

        self.size = QtCore.QPointF(200, 200)
        self.rect = rect
        self.style = style
        self.setPos(position)
        self.setMatrix(matrix)

        scene.clearSelection()
        scene.addItem(self)
        self.setSelected(True)
        self.setFocus()
        global RAW
        RAW = True

        self.pen = QtGui.QPen(self.style)
        self.pen.setColor(QtCore.Qt.black)
        self.pen.setWidth(1)

    def parentWidget(self):
        return self.scene().views()[0]

    def boundingRect(self):
        return self.rect.adjusted(-10, -10, 10, 10)

    def paint(self, painter, option, widget):
        if option.state & QtWidgets.QStyle.State_Selected:
            self.pen.setColor(QtCore.Qt.blue)

        painter.setPen(self.pen)
        painter.drawRect(self.rect)

    def itemChange(self, change, variant):
        if change != QtWidgets.QGraphicsItem.ItemSelectedChange:
            global RAW
            RAW = True
        return QtWidgets.QGraphicsItem.itemChange(self, change, variant)

    def contextMenuEvent(self, event):
        wrapped = []
        menu = QtWidgets.QMenu(self.parentWidget())
        for text, param in (("&Solid", QtCore.Qt.SolidLine),
                            ("&Dashed", QtCore.Qt.DashLine),
                            ("D&otted", QtCore.Qt.DotLine),
                            ("D&ashDotted", QtCore.Qt.DashDotLine),
                            ("DashDo&tDotten", QtCore.Qt.DashDotDotLine)):
            wrapper = functools.partial(self.setStyle, param)
            wrapped.append(wrapper)
            menu.addAction(text, wrapper)
        menu.exec_(event.screenPos())

    def setStyle(self, style):
        #self.prepareGeometryChange()
        self.style = style
        self.update()
        global RAW
        RAW = True

    def mousePressEvent(self, event):
        if event.buttons() & QtCore.Qt.LeftButton:
            super(Rectangle, self).mouseMoveEvent(event)
        if event.buttons() & QtCore.Qt.MiddleButton:
            if self.isSelected():
                self.rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
                self.prepareGeometryChange()
                self.setRect(self.rect)

            global RAW
            RAW = True

I guess that the main problem lays in reimplemented paint() function but I still haven't come up with any idea where exactly...

Could anyone explain what am I doing wrong? Where's a mistake and how to make this thing work properly?


Solution

  • I do not have the middle button on my touchpad so I have implemented the logic with the right button but I have given a small edge of 10px where you can change the size of the rectangle.

    To change the style you just have to change the QPen of the QGraphicsItem.

    import functools
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Rectangle(QtWidgets.QGraphicsRectItem):
        def __init__(self, x, y, w, h):
            super(Rectangle, self).__init__(0, 0, w, h)
            self.setPen(QtGui.QPen(QtCore.Qt.red, 2))
            self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
                | QtWidgets.QGraphicsItem.ItemIsMovable
                | QtWidgets.QGraphicsItem.ItemIsFocusable
                | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
                | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
            self.setPos(QtCore.QPointF(x, y))
    
        def mouseMoveEvent(self, event):
            if event.buttons() & QtCore.Qt.LeftButton:
                super(Rectangle, self).mouseMoveEvent(event)
            if event.buttons() & QtCore.Qt.RightButton:
                rect = QtCore.QRectF(QtCore.QPoint(), event.pos()).normalized()
                self.prepareGeometryChange()
                self.setRect(rect)
    
        def contextMenuEvent(self, event):
            super(Rectangle, self).contextMenuEvent(event)
            delta = 10
            r = self.boundingRect()
            r.adjust(delta, delta, -delta, -delta)
            if not r.contains(event.pos()):
                return
            self.setSelected(True)
            wrapped = []
            menu = QtWidgets.QMenu(self.parentWidget())
            for text, param in (("&Solid", QtCore.Qt.SolidLine),
                                ("&Dashed", QtCore.Qt.DashLine),
                                ("D&otted", QtCore.Qt.DotLine),
                                ("D&ashDotted", QtCore.Qt.DashDotLine),
                                ("DashDo&tDotten", QtCore.Qt.DashDotDotLine)):
                wrapper = functools.partial(self.setStyle, param)
                wrapped.append(wrapper)
                menu.addAction(text, wrapper)
            menu.exec_(event.screenPos())
    
        def paint(self, painter, option, widget):
            painter.setPen(self.pen())
            painter.setBrush(self.brush())
            if option.state & QtWidgets.QStyle.State_Selected:
                pen = self.pen()
                pen.setColor(QtCore.Qt.blue)
                painter.setPen(pen)
                painter.setBrush(QtCore.Qt.NoBrush)
            painter.drawRect(self.boundingRect())
    
        def setStyle(self, style):
            pen = self.pen()
            pen.setStyle(style)
            self.setPen(pen)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        scene = QtWidgets.QGraphicsScene(-400, -400, 800, 800)
        w = QtWidgets.QGraphicsView(scene)
        scene.addItem(Rectangle(100, 100, 100, 100))
        w.show()
        sys.exit(app.exec_())