Search code examples
pythonpython-3.xpyside2

How to fix a QFocusEvent behavior


I'm making a widget to access and modify QGraphicsRectItem properties(scale, rotation, etc.). The main idea is to show a widget on clicking on an item. When you click on another item, this widget should be deleted and replaced by another(showing properties of another item.)

I've implemented this using QFocusEvent and unfortunately, it gives me a SIGSEGV error. I fully understand why it happens but can`t make out how to do it in another way.

Here's a python sketch:

from PySide2 import QtWidgets, QtCore, QtGui
import sys, shiboken2


class graphicsView(QtWidgets.QGraphicsView):
    def __init__(self, scene, parent=None):
        super(graphicsView, self).__init__(parent)
        self.scene = scene

        self.setScene(self.scene)

    def wheelEvent(self, event):
        factor = 1.41 ** (-event.delta() / 240)
        self.scale(factor, factor)

class boxItem(QtWidgets.QGraphicsRectItem):
    def __init__(self, parent):
        super(boxItem, self).__init__()

        self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable
                      | QtWidgets.QGraphicsItem.ItemIsMovable
                      | QtWidgets.QGraphicsItem.ItemIsFocusable)

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

        self.parent = parent


    def focusInEvent(self, event:QtGui.QFocusEvent):
        self.pBox = propBox()
        self.parent.layout.addWidget(self.pBox)
        self.pBox.r_box.setValue(self.rotation())
        self.pBox.r_box.valueChanged.connect(self.setRotationAngle)


    def focusOutEvent(self, event:QtGui.QFocusEvent):
        # Here's a shiboken(equivalent to sip in PyQt) deletes c++ object and a python wrapper
        # It works as expected but because of focusOutEvent it deletes the widget when i click on it
        # There's when the error appears.

        self.parent.layout.removeWidget(self.pBox)
        shiboken2.delete(self.pBox)

    def setRotationAngle(self, degrees):
        br = self.boundingRect()
        self.setTransformOriginPoint(QtCore.QPointF(br.width() / 2, br.height() / 2))
        self.setRotation(-degrees)
        self.update()



class propBox(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(propBox, self).__init__(parent)

        self.layout = QtWidgets.QVBoxLayout()
        self.layout.setAlignment(QtCore.Qt.AlignTop)
        self.setLayout(self.layout)

        self.r_label = QtWidgets.QLabel("Rotation:", self)
        self.r_box = QtWidgets.QSpinBox(self)
        self.r_layout = QtWidgets.QHBoxLayout()
        self.r_layout.addWidget(self.r_label)
        self.r_layout.addWidget(self.r_box)

        self.s_label = QtWidgets.QLabel("Scale:")
        self.s_box = QtWidgets.QSpinBox(self)
        self.s_layout = QtWidgets.QHBoxLayout()
        self.s_layout.addWidget(self.s_label)
        self.s_layout.addWidget(self.s_box)

        self.layout.addLayout(self.r_layout)
        self.layout.addLayout(self.s_layout)

class mainWidget(QtWidgets.QWidget):
    def __init__(self):
        super(mainWidget, self).__init__()

        self.layout = QtWidgets.QHBoxLayout()
        self.setLayout(self.layout)

        box1 = boxItem(self)
        box1.setRotation(45)
        box2 = boxItem(self)

        self.scene = QtWidgets.QGraphicsScene(self)
        self.scene.setSceneRect(0, 0, 500, 300)
        self.scene.addItem(box1)
        self.scene.addItem(box2)

        self.view = graphicsView(self.scene)

        self.pBox = propBox(self)

        self.layout.addWidget(self.view)



if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = mainWidget()
    window.show()
    sys.exit(app.exec_())

Solution

  • I have not analyzed because your program crashes so my answer will not focus on it, instead I will focus on the underlying problem.

    You just have to have a PropertyBox that you hide or show when necessary. I see a problem trying to use the focusInEvent and focusOutEvent methods because when you press the PropertyBox the item will lose its focus so the PropertyBox itself will be removed (maybe this is the reason for the crash). So instead of using the focus you should use the mousePressEvent and check if there is an item where it was clicked.

    Considering the above the solution is:

    import random
    from PySide2 import QtCore, QtGui, QtWidgets
    
    
    class BoxItem(QtWidgets.QGraphicsRectItem):
        def __init__(self, parent=None):
            super(BoxItem, self).__init__(QtCore.QRectF(0, 0, 200, 200), parent)
    
            self.setFlags(
                QtWidgets.QGraphicsItem.ItemIsSelectable
                | QtWidgets.QGraphicsItem.ItemIsMovable
                | QtWidgets.QGraphicsItem.ItemIsFocusable
            )
            br = self.boundingRect()
            self.setTransformOriginPoint(
                QtCore.QPointF(br.width() / 2, br.height() / 2)
            )
    
    
    class PropertyBox(QtWidgets.QWidget):
        rotationChanged = QtCore.Signal(float)
        scaleChanged = QtCore.Signal(float)
    
        def __init__(self, parent=None):
            super(PropertyBox, self).__init__(parent)
            self.m_rotation_spinbox = QtWidgets.QDoubleSpinBox(
                minimum=-360, maximum=360
            )
            self.m_rotation_spinbox.valueChanged.connect(self.rotationChanged)
            self.m_scale_spinbox = QtWidgets.QDoubleSpinBox(
                minimum=0, maximum=100, singleStep=0.1
            )
            self.m_scale_spinbox.valueChanged.connect(self.scaleChanged)
    
            lay = QtWidgets.QFormLayout(self)
            lay.addRow("Rotation:", self.m_rotation_spinbox)
            lay.addRow("Scale:", self.m_scale_spinbox)
    
        @property
        def rotation(self):
            return self.m_rotation_spinbox.value()
    
        @rotation.setter
        def rotation(self, value):
            self.m_rotation_spinbox.setValue(value)
    
        @property
        def scale(self):
            return self.m_scale_spinbox.value()
    
        @scale.setter
        def scale(self, value):
            self.m_scale_spinbox.setValue(value)
    
    
    class GraphicsView(QtWidgets.QGraphicsView):
        currentItemChanged = QtCore.Signal(QtWidgets.QGraphicsItem)
    
        def mousePressEvent(self, event):
            super(GraphicsView, self).mousePressEvent(event)
            it = self.itemAt(event.pos())
            self.currentItem = it
    
        @property
        def currentItem(self):
            if not hasattr(self, "_currentItem"):
                self._currentItem = None
            return self._currentItem
    
        @currentItem.setter
        def currentItem(self, it):
            if self.currentItem != it:
                self._currentItem = it
                self.currentItemChanged.emit(it)
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
    
            self.m_scene = QtWidgets.QGraphicsScene()
            self.m_view = GraphicsView(self.m_scene)
            self.m_view.currentItemChanged.connect(self.onCurrentItemChanged)
    
            for i in range(4):
                it = BoxItem()
                self.m_scene.addItem(it)
                it.setPos(QtCore.QPointF(100 * i, 100 * i))
                it.setBrush(QtGui.QColor(*random.sample(range(255), 3)))
    
            self.m_property_box = PropertyBox()
            self.m_property_box.rotationChanged.connect(self.onRotationChanged)
            self.m_property_box.scaleChanged.connect(self.onScaleChanged)
            self.m_property_box.hide()
    
            lay = QtWidgets.QHBoxLayout(self)
            lay.addWidget(self.m_view)
            lay.addWidget(self.m_property_box)
    
        @QtCore.Slot(QtWidgets.QGraphicsItem)
        def onCurrentItemChanged(self, item):
            self.m_property_box.setVisible(item is not None)
            if item is not None:
                self.m_property_box.rotation = item.rotation()
                self.m_property_box.scale = item.scale()
    
        @QtCore.Slot(float)
        def onRotationChanged(self, rotation):
            it = self.m_view.currentItem
            if it is not None:
                it.setRotation(rotation)
    
        @QtCore.Slot(float)
        def onScaleChanged(self, scale):
            it = self.m_view.currentItem
            if it is not None:
                it.setScale(scale)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = Widget()
        w.resize(640, 480)
        w.show()
        sys.exit(app.exec_())