I’ve been beating my head against a wall for three days now trying to figure this one out, so I’m hoping someone out there has managed to get this working at some point.
I’m trying to implement a tree model that supports reordering of items via drag and drop. I’ve got it working, thanks to Yasin’s Model View tutorials and a bunch of Stack Overflow threads – but there’s a bug that I can’t seem to wrap my head around. Incidentally, there’s an example on stack overflow that exhibits the same exact issue.
I wrote up a simpler version of what I’m working with for example purposes, it’s very similar to the example on Stack Overflow here.
from PyQt4 import QtCore, QtGui
import sys
import cPickle as pickle
import cStringIO
import copy
class Item(object):
def __init__(self, name, parent=None):
self.name = name
self.children = []
self.parent = parent
if parent is not None:
self.parent.addChild( self )
def addChild(self, child):
self.children.append(child)
child.parent = self
def removeChild(self, row):
self.children.pop(row)
def child(self, row):
return self.children[row]
def __len__(self):
return len(self.children)
def row(self):
if self.parent is not None:
return self.parent.children.index(self)
#====================================================================
class PyObjMime(QtCore.QMimeData):
MIMETYPE = QtCore.QString('application/x-pyobj')
def __init__(self, data=None):
super(PyObjMime, self).__init__()
self.data = data
if data is not None:
# Try to pickle data
try:
pdata = pickle.dumps(data)
except:
return
self.setData(self.MIMETYPE, pickle.dumps(data.__class__) + pdata)
def itemInstance(self):
if self.data is not None:
return self.data
io = cStringIO.StringIO(str(self.data(self.MIMETYPE)))
try:
# Skip the type.
pickle.load(io)
# Recreate the data.
return pickle.load(io)
except:
pass
return None
#====================================================================
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super(TreeModel, self).__init__(parent)
self.root = root
def itemFromIndex(self, index):
if index.isValid():
return index.internalPointer()
return self.root
def rowCount(self, index):
item = self.itemFromIndex(index)
return len(item)
def columnCount(self, index):
return 1
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDropEnabled
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsSelectable
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
item = self.itemFromIndex(index)
return item.name
def index(self, row, column, parentIndex):
parent = self.itemFromIndex(parentIndex)
return self.createIndex( row, column, parent.child(row) )
def parent(self, index):
item = self.itemFromIndex(index)
parent = item.parent
if parent == self.root:
return QtCore.QModelIndex()
return self.createIndex(parent.row(), 0, parent)
def insertRows(self, row, count, parentIndex):
self.beginInsertRows(parentIndex, row, row+count-1)
self.endInsertRows()
return True
def removeRows(self, row, count, parentIndex):
self.beginRemoveRows(parentIndex, row, row+count-1)
parent = self.itemFromIndex(parentIndex)
parent.removeChild(row)
self.endRemoveRows()
return True
def mimeTypes(self):
types = QtCore.QStringList()
types.append('application/x-pyobj')
return types
def mimeData(self, index):
item = self.itemFromIndex(index[0])
mimedata = PyObjMime(item)
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
item = mimedata.itemInstance()
dropParent = self.itemFromIndex(parentIndex)
itemCopy = copy.deepcopy(item)
dropParent.addChild(itemCopy)
self.insertRows(len(dropParent)-1, 1, parentIndex)
self.dataChanged.emit(parentIndex, parentIndex)
return True
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
root = Item( 'root' )
itemA = Item( 'ItemA', root )
itemB = Item( 'ItemB', root )
itemC = Item( 'ItemC', root )
itemD = Item( 'ItemD', itemA )
itemE = Item( 'ItemE', itemB )
itemF = Item( 'ItemF', itemC )
model = TreeModel(root)
tree = QtGui.QTreeView()
tree.setModel( model )
tree.setDragEnabled(True)
tree.setAcceptDrops(True)
tree.setDragDropMode( QtGui.QAbstractItemView.InternalMove )
tree.show()
tree.expandAll()
sys.exit(app.exec_())
To see the issue that I’m talking about, drag ‘ItemF’ onto ‘ItemB’, and then drag ‘ItemF’ back onto ‘ItemC’. It should throw a list index error.
This seems to happen any time you drag an item with siblings onto another item… items without siblings seem to be fine (hence the first drag and drop doesn’t have an issue).
I’ve stepped through the code and as far as I can tell, the error is raised when the ‘removeRows’ function (which is automatically called at the end of the move operation) calls the internal ‘beginRemoveRows’ function. Somewhere in that function, a parent node is queried for a child from its children list using an index that’s out of range. I don’t know why the internal function is doing that – I’ve even poked through the C++ QT source code trying to figure it out - no dice.
The last thing I’ve figured out is that if I replace the model’s ‘index’ function with this one that catches the out of range index:
def index(self, row, column, parent):
item = self.itemFromIndex(parent)
if row < len(item):
return self.createIndex(row, column, item.child(row))
else:
return QtCore.QModelIndex()
it works just fine.
In a pinch, I can use that alternate index function – but I’d sure like to know why the heck this whole issue is happening in the first place.