Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

QGraphicsScene changing objects when selected

Tags:

python

qt

pyside

I have a QGraphicsScene containing some simple objects (in this simplified example circles) that I want to change into other objects (here squares) when selected. More specifically I'd like to have parent objects which don't draw themselves, they are drawn by their child objects, and under various circumstances, but in particular when the parent objects are selected, I'd like the set of child objects to change. This is a nice conceptual framework for the overall app I am working on.

So I've implemented this in PySide and I thought it was working fine: the circles change nicely into squares when you click on them.

Until I use RubberBandDrag selection in the view. This causes an instant segfault when the rubber band selection reaches the parent object and the selection changes. Presumably this is being triggered because the rubber band selection in QT is somehow keeping a pointer to the child item which is disappearing before the rubber band selection action is complete.

Simplified code below - test it by first clicking on the object (it changes nicely) then dragging over the object - segfault:

from PySide import QtCore,QtGui

class SceneObject(QtGui.QGraphicsItem):
    def __init__(self, scene):
        QtGui.QGraphicsItem.__init__(self, scene = scene)
        self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QtGui.QGraphicsItem.ItemHasNoContents, True)
        self.updateContents()

    def updateContents(self):
        self.prepareGeometryChange()
        for c in self.childItems():
            self.scene().removeItem(c)

        if self.isSelected():
            shape_item = QtGui.QGraphicsRectItem()
        else:
            shape_item = QtGui.QGraphicsEllipseItem()
        shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)
        shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
        shape_item.setPen(QtGui.QPen("green"))
        shape_item.setRect(QtCore.QRectF(0,0,10,10))
        shape_item.setParentItem(self)

    def itemChange(self, change, value):
        if self.scene() != None:
            if change == QtGui.QGraphicsItem.ItemSelectedHasChanged:
                self.updateContents()
                return
        return super(SceneObject,self).itemChange(change, value)

    def boundingRect(self):
        return self.childrenBoundingRect()


class Visualiser(QtGui.QMainWindow):

    def __init__(self):
        super(Visualiser,self).__init__()

        self.viewer = QtGui.QGraphicsView(self)
        self.viewer.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
        self.setCentralWidget(self.viewer)
        self.viewer.setScene(QtGui.QGraphicsScene())

        parent_item = SceneObject(self.viewer.scene())
        parent_item.setPos(50,50)



app = QtGui.QApplication([])
mainwindow = Visualiser()
mainwindow.show()
app.exec_()

So questions:

Have I just made a mistake that can be straightforwardly fixed?

Or is removing objects from the scene not allowed when handling an ItemSelectedHasChanged event?

Is there a handy workaround? Or what's a good alternative approach? I could replace the QGraphicsRectItem with a custom item which can be drawn either as a square or a circle but that doesn't conveniently cover all my use cases. I can see that I could make that work but it will certainly not be as straightforward.

EDIT - Workaround:

It is possible to prevent this failing by preserving the about-to-be-deleted object for a while. This can be done by something like this:

def updateContents(self):
    self.prepareGeometryChange()
    self._temp_store = self.childItems()
    for c in self.childItems():
        self.scene().removeItem(c)

    ...

However, this is ugly code and increases the memory usage for no real benefit. Instead I have moved to using the QGraphicsScene.selectionChanged signal as suggested in this answer.

like image 482
strubbly Avatar asked Nov 08 '22 17:11

strubbly


1 Answers

I've debugged it. Reproduced on Lunix

1  qFatal(const char *, ...) *plt                                                                                                  0x7f05d4e81c40 
2  qt_assert                                                                                     qglobal.cpp                  2054 0x7f05d4ea197e 
3  QScopedPointer<QGraphicsItemPrivate, QScopedPointerDeleter<QGraphicsItemPrivate>>::operator-> qscopedpointer.h             112  0x7f05d2c767ec 
4  QGraphicsItem::flags                                                                          qgraphicsitem.cpp            1799 0x7f05d2c573b8 
5  QGraphicsScene::setSelectionArea                                                              qgraphicsscene.cpp           2381 0x7f05d2c94893 
6  QGraphicsView::mouseMoveEvent                                                                 qgraphicsview.cpp            3257 0x7f05d2cca553 
7  QGraphicsViewWrapper::mouseMoveEvent                                                          qgraphicsview_wrapper.cpp    1023 0x7f05d362be83 
8  QWidget::event                                                                                qwidget.cpp                  8374 0x7f05d2570371 

qt-everywhere-opensource-src-4.8.6/src/gui/graphicsview/qgraphicsscene.cpp:2381

void QGraphicsScene::setSelectionArea(const QPainterPath &path, Qt::ItemSelectionMode mode,
                                      const QTransform &deviceTransform)
{
...
    // Set all items in path to selected.
    foreach (QGraphicsItem *item, items(path, mode, Qt::DescendingOrder, deviceTransform)) {
        if (item->flags() & QGraphicsItem::ItemIsSelectable) { // item is invalid here
            if (!item->isSelected()) 
                changed = true;
            unselectItems.remove(item);
            item->setSelected(true);
        }
    }

They are using items() function to find a list of items under the rubber band selection. But if one item while processing deletes something the item pointer just becomes invalid. And next call to item->flags() causes the crash.

As alternative you could use QGraphicsScene::selectionChanged signal. It's emitted only once per selection change.

Looks like it's not expected by Qt to have some major changes in itemChange


Behind of this here is common mistake you have with prepareGeometryChange() call.

It's designed to be called right before changing boundingRect. Bounding rect should be the old one when prepareGeometryChange called and new one right after.

So that's could happen:

In updateContents:

self.prepareGeometryChange(); # calls boundingRect. old value returned
...
shape_item.setParentItem(self); # could call the boundingRect. but still old value returned!

After child added it calls boundingRect again but value unexpected different.

As a solution you can add a variable

def updateContents(self):
    for c in self.childItems():
        self.scene().removeItem(c)

    if self.isSelected():
        shape_item = QtGui.QGraphicsRectItem()
    else:
        shape_item = QtGui.QGraphicsEllipseItem()
    shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)

    shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
    shape_item.setPen(QtGui.QPen("green"))
    shape_item.setRect(QtCore.QRectF(0,0,10,10))
    shape_item.setParentItem(self) 

    self.prepareGeometryChange();
    self._childRect = self.childrenBoundingRect()

def boundingRect(self):
    return self._childRect
like image 161
Антон Сергунов Avatar answered Nov 15 '22 12:11

Антон Сергунов