As indicated in the following code, when you drag-drop an item (subclassed from QStandardItem
with a clone()
method) you get a QStandardItem
and not a subclass. Furthermore - data stored in the class or as part of the setData is lost. I suspect this is because of the inability to 'serialize' the data. But I am clueless how to 'save' the data - or the meta. How can I preserve the QObject
? The following code works fine, but once you move a branch node, all the nodes in the branch and the branch become QStandardItem
's and not myItem
and lose the data (if they had any).
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 4 09:10:16 2019
Test of Tree view with subclassed QStandardItem and Drag and Drop
enabled. When you move a parent the parent looses the subclass and thus
the meta - however, it also looses the data: This is likely because
the data cannot be serialized. How to fix?
@author: tcarnaha
"""
import sys
from PyQt5 import QtGui, QtWidgets, QtCore
class myData():
def __init__(self, title):
self._title = title
self._stuff = dict()
self._obj = QtCore.QObject()
@property
def obj(self):
return self._obj
@obj.setter
def obj(self, value):
self._obj = value
@property
def title(self):
return self._title
@title.setter
def title(self, value):
self._title = value
class myItem(QtGui.QStandardItem):
def __init__(self, parent=None):
super(myItem, self).__init__(parent)
self._meta = None
@property
def meta(self):
return self._meta
@meta.setter
def meta(self, value):
self._meta = value
def clone(self):
print "My cloning"
old_data = self.data()
print "Old data [{}]".format(old_data)
old_meta = self.meta
obj = myItem()
obj.setData(old_data)
print "New data [{}]".format(obj.data())
obj.meta = old_meta
print "Clone is a ", obj.__class__
return obj
class mainWidget(QtWidgets.QMainWindow):
def __init__(self):
super(mainWidget, self).__init__()
self.model = QtGui.QStandardItemModel()
self.model.setItemPrototype(myItem())
self.view = QtWidgets.QTreeView()
self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.view.customContextMenuRequested.connect(self.list_click)
self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.view.setDefaultDropAction(QtCore.Qt.MoveAction)
self.view.setDragDropOverwriteMode(False)
self.view.setAcceptDrops(True)
self.view.setDropIndicatorShown(True)
self.view.setDragEnabled(True)
self.view.setModel(self.model)
dataA = myData('A thing')
parentA = myItem()
parentA.setText('A')
parentA.setDragEnabled(True)
parentA.setDropEnabled(True)
parentA.setData(dataA)
parentA.meta = QtCore.QObject()
childa = myItem()
childa.setText('a')
childb = myItem()
childb.setText('b')
childc = myItem()
childc.setText('c')
parentA.appendRows([childa, childb, childc])
dataB = myData('B thing')
parentB = myItem()
parentB.setText('B')
parentB.setDragEnabled(True)
parentB.setDropEnabled(True)
parentB.setData(dataB)
parentB.meta = QtCore.QObject()
childd = myItem()
childd.setText('d')
childe = myItem()
childe.setText('e')
childf = myItem()
childf.setText('f')
parentB.appendRows([childd, childe, childf])
self.model.appendRow(parentA)
self.model.appendRow(parentB)
classAct = QtWidgets.QAction('Class', self)
classAct.triggered.connect(self.classIs)
dataAct = QtWidgets.QAction('Data', self)
dataAct.triggered.connect(self.dataIs)
metaAct = QtWidgets.QAction('Meta', self)
metaAct.triggered.connect(self.metaIs)
self.menu = QtWidgets.QMenu("Item info")
self.menu.addAction(classAct)
self.menu.addAction(dataAct)
self.menu.addAction(metaAct)
self.setCentralWidget(self.view)
@QtCore.pyqtSlot(QtCore.QPoint)
def list_click(self, position):
self.menu.popup(self.view.viewport().mapToGlobal(position))
def classIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
print "Item {} Class {} ".format(item.text(), item.__class__())
def dataIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
try:
print "Item {} data {} Object {}".format(item.text(),
item.data().title,
item.data().obj)
except Exception as exc:
print "Data exception ", exc
def metaIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
try:
print "Item {} meta {} ".format(item.text(), item.meta)
except Exception as exc:
print "Meta exception ", exc
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = mainWidget()
main.show()
app.exec_()
There are a couple of problems here related to how objects get serialized by Qt and also by PyQt. Firstly, when cloning a QStandardItem
, only the flags and data get copied - everything else is ignored (including dynamic python attributes). Secondly, there is no way to directly copy a QObject
. This is because it cannot be cast to a QVariant
(which Qt uses for serialization) and it cannot be pickled (which PyQt uses for serialization).
To solve the second problem, we need to keep separate references to all the QObject
instances, and then use indirect keys to access them again later. There are probably many different way to achieve this, but here's a very simple approach that illustrates the basic idea:
objects = {}
class MyObject(QtCore.QObject):
def __init__(self, parent=None):
super(MyObject, self).__init__(parent)
self.setProperty('key', max(objects.keys() or [0]) + 1)
objects[self.property('key')] = self
So this automatically adds each instance to a global cache and gives it a unique lookup key so that it can be easily found later on. With this in place, the myData
class now needs to be adapted to use the MyObject
class so that pickling is handled correctly:
class myData():
def __init__(self, title):
self._title = title
self._stuff = dict()
self._obj = MyObject()
def __setstate__(self, state):
self._obj = objects.get(state['obj'])
self._stuff = state['stuff']
self._title = state['title']
def __getstate__(self):
return {
'obj': self._obj and self._obj.property('key'),
'title': self._title,
'stuff': self._stuff,
}
Solving the first problem is much simpler: we just need to make sure any dynamic python properties store their underlying values in the item's data using custom data-roles. In this particular case, the value must be the key of the item's MyObject
instance, so that it can be retrieved after a drag and drop operation:
class myItem(QtGui.QStandardItem):
MetaRole = QtCore.Qt.UserRole + 1000
@property
def meta(self):
return objects.get(self.data(myItem.MetaRole))
@meta.setter
def meta(self, value):
self.setData(value.property('key'), myItem.MetaRole)
def clone(self):
print "My cloning"
obj = myItem(self)
print "Clone is a ", obj.__class__
return obj
Below is a working version of your original script that implements all the above. But please bear in mind that you will almost certainly need to adapt this to work properly with your real code. This is just a working proof-of-concept that shows how to deal with the two issues outlined above.
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtGui, QtWidgets, QtCore
objects = {}
class MyObject(QtCore.QObject):
def __init__(self, parent=None):
super(MyObject, self).__init__(parent)
self.setProperty('key', max(objects.keys() or [0]) + 1)
objects[self.property('key')] = self
class myData():
def __init__(self, title):
self._title = title
self._stuff = dict()
self._obj = MyObject()
def __setstate__(self, state):
self._obj = objects.get(state['obj'])
self._stuff = state['stuff']
self._title = state['title']
def __getstate__(self):
return {
'obj': self._obj.property('key'),
'title': self._title,
'stuff': self._stuff,
}
@property
def obj(self):
return self._obj
@obj.setter
def obj(self, value):
self._obj = value
@property
def title(self):
return self._title
@title.setter
def title(self, value):
self._title = value
class myItem(QtGui.QStandardItem):
MetaRole = QtCore.Qt.UserRole + 1000
@property
def meta(self):
return objects.get(self.data(myItem.MetaRole))
@meta.setter
def meta(self, value):
self.setData(value.property('key'), myItem.MetaRole)
def clone(self):
print "My cloning"
obj = myItem(self)
print "Clone is a ", obj.__class__
return obj
class mainWidget(QtWidgets.QMainWindow):
def __init__(self):
super(mainWidget, self).__init__()
self.model = QtGui.QStandardItemModel()
self.model.setItemPrototype(myItem())
self.view = QtWidgets.QTreeView()
self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.view.customContextMenuRequested.connect(self.list_click)
self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.view.setDefaultDropAction(QtCore.Qt.MoveAction)
self.view.setDragDropOverwriteMode(False)
self.view.setAcceptDrops(True)
self.view.setDropIndicatorShown(True)
self.view.setDragEnabled(True)
self.view.setModel(self.model)
dataA = myData('A thing')
parentA = myItem()
parentA.setText('A')
parentA.setDragEnabled(True)
parentA.setDropEnabled(True)
parentA.setData(dataA)
parentA.meta = MyObject()
childa = myItem()
childa.setText('a')
childb = myItem()
childb.setText('b')
childc = myItem()
childc.setText('c')
parentA.appendRows([childa, childb, childc])
dataB = myData('B thing')
parentB = myItem()
parentB.setText('B')
parentB.setDragEnabled(True)
parentB.setDropEnabled(True)
parentB.setData(dataB)
parentB.meta = MyObject()
childd = myItem()
childd.setText('d')
childe = myItem()
childe.setText('e')
childf = myItem()
childf.setText('f')
parentB.appendRows([childd, childe, childf])
self.model.appendRow(parentA)
self.model.appendRow(parentB)
classAct = QtWidgets.QAction('Class', self)
classAct.triggered.connect(self.classIs)
dataAct = QtWidgets.QAction('Data', self)
dataAct.triggered.connect(self.dataIs)
metaAct = QtWidgets.QAction('Meta', self)
metaAct.triggered.connect(self.metaIs)
self.menu = QtWidgets.QMenu("Item info")
self.menu.addAction(classAct)
self.menu.addAction(dataAct)
self.menu.addAction(metaAct)
self.setCentralWidget(self.view)
@QtCore.pyqtSlot(QtCore.QPoint)
def list_click(self, position):
self.menu.popup(self.view.viewport().mapToGlobal(position))
def classIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
print "Item {} Class {} ".format(item.text(), item.__class__())
def dataIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
try:
print "Item {} data {} Object {}".format(item.text(),
item.data().title,
item.data().obj)
except Exception as exc:
print "Data exception ", exc
def metaIs(self):
selected_indexes = self.view.selectedIndexes()
for index in selected_indexes:
item = self.model.itemFromIndex(index)
try:
print "Item {} meta {} ".format(item.text(), item.meta)
except Exception as exc:
print "Meta exception ", exc
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = mainWidget()
main.show()
app.exec_()