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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With