Here's my solution (full code at the end), subclassing a QTreeWidget
. I tried to have something very general that should work for a lot of cases. One issue remains with the visual cues when dragging. The previous version didn't work on windows, I hope this one will. It works absolutely fine on Linux.
Defining categories
Every item in the tree has a category (a string), that I stored in QtCore.Qt.ToolTipRole
. You could also subclass QTreeWidgetItem
to have a specific attribute category
.
We define in a dictionary settings
all the categories, with the list of the categories they can be drop into and the flag to set. For example:
default=QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled
drag=QtCore.Qt.ItemIsDragEnabled
drop=QtCore.Qt.ItemIsDropEnabled
settings={
"family":(["root"],default|drag|drop),
"children":(["family"],default|drag)
}
Every item of category "family" can receive drag, and can only be drop in "root" (the invisible root item).
Every item of category "children" can only be drop into a "family".
Adding items to the tree
The method addItem(strings,category,parent=None)
creates a QTreeWidgetItem(strings,parent)
with a tool tip "category" and the matching flags in setting
. It returns the item. Example:
dupont=ex.addItem(["Dupont"],"family")
robert=ex.addItem(["Robertsons"],"family")
ex.addItem(["Laura"],"children",dupont)
ex.addItem(["Matt"],"children",robert)
...
Reimplementation of Drag and Drop
The item being dragged is determined with self.currentItem()
(multiple selection is not handled). The list of categories where this item can be dropped is okList=self.settings[itemBeingDragged.data(0,role)][0]
.
The item under the mouse, aka "drop target", is self.itemAt(event.pos())
. If the mouse in on blank space, the drop target is set to the root item.
dragMoveEvent
(visual cue for whether the drop will be accepted/ignored)
If the drop target is in okList
, we call the regular dragMoveEvent
.
If not, we have to check for "next to drop target". In the image bellow, the item under the mouse is Robertsons, but the real drop target is the root item (see the line bellow Robertsons ?). To fix this, we check it the item can be dragged on the parent of the drop target. If not, we call event.ignore()
.
The only remaining issue is when the mouse is actually on "Robertsons": the drag event is accepted. The visual cue says the drop will be accepted when it's not.
dropEvent
Instead of accepting or ignoring the drop, which is very tricky because of "next to drop target", we always accept the drop, and then fix mistakes.
If the new parent is the same as the old parent, or if it is in okList
, we do nothing. Otherwise, we put back the dragged item in the old parent.
Sometimes the dropped item will be collapsed, but this could easily be fixed with itemBeingDragged.setExpanded()
Finally, the full code with two examples:
import sys
from PyQt4 import QtCore, QtGui
class CustomTreeWidget( QtGui.QTreeWidget ):
def __init__(self,settings, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
#self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setItemsExpandable(True)
self.setAnimated(True)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.settings=settings
root=self.invisibleRootItem()
root.setData(0,QtCore.Qt.ToolTipRole,"root")
def dragMoveEvent(self, event):
role=QtCore.Qt.ToolTipRole
itemToDropIn = self.itemAt(event.pos())
itemBeingDragged=self.currentItem()
okList=self.settings[itemBeingDragged.data(0,role)][0]
if itemToDropIn is None:
itemToDropIn=self.invisibleRootItem()
if itemToDropIn.data(0,role) in okList:
super(CustomTreeWidget, self).dragMoveEvent(event)
return
else:
# possible "next to drop target" case
parent=itemToDropIn.parent()
if parent is None:
parent=self.invisibleRootItem()
if parent.data(0,role) in okList:
super(CustomTreeWidget, self).dragMoveEvent(event)
return
event.ignore()
def dropEvent(self, event):
role=QtCore.Qt.ToolTipRole
#item being dragged
itemBeingDragged=self.currentItem()
okList=self.settings[itemBeingDragged.data(0,role)][0]
#parent before the drag
oldParent=itemBeingDragged.parent()
if oldParent is None:
oldParent=self.invisibleRootItem()
oldIndex=oldParent.indexOfChild(itemBeingDragged)
#accept any drop
super(CustomTreeWidget,self).dropEvent(event)
#look at where itemBeingDragged end up
newParent=itemBeingDragged.parent()
if newParent is None:
newParent=self.invisibleRootItem()
if newParent.data(0,role) in okList:
# drop was ok
return
else:
# drop was not ok, put back the item
newParent.removeChild(itemBeingDragged)
oldParent.insertChild(oldIndex,itemBeingDragged)
def addItem(self,strings,category,parent=None):
if category not in self.settings:
print("unknown categorie" +str(category))
return False
if parent is None:
parent=self.invisibleRootItem()
item=QtGui.QTreeWidgetItem(parent,strings)
item.setData(0,QtCore.Qt.ToolTipRole,category)
item.setExpanded(True)
item.setFlags(self.settings[category][1])
return item
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
default=QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable
drag=QtCore.Qt.ItemIsDragEnabled
drop=QtCore.Qt.ItemIsDropEnabled
#family example
settings={
"family":(["root"],default|drag|drop),
"children":(["family"],default|drag)
}
ex = CustomTreeWidget(settings)
dupont=ex.addItem(["Dupont"],"family")
robert=ex.addItem(["Robertsons"],"family")
smith=ex.addItem(["Smith"],"family")
ex.addItem(["Laura"],"children",dupont)
ex.addItem(["Matt"],"children",dupont)
ex.addItem(["Kim"],"children",robert)
ex.addItem(["Stephanie"],"children",robert)
ex.addItem(["John"],"children",smith)
ex.show()
sys.exit(app.exec_())
#food example: issue with "in between"
settings={
"food":([],default|drop),
"allVegetable":(["food"],default|drag|drop),
"allFruit":(["food"],default|drag|drop),
"fruit":(["allFruit","fruit"],default|drag|drop),
"veggie":(["allVegetable","veggie"],default|drag|drop),
}
ex = CustomTreeWidget(settings)
top=ex.addItem(["Food"],"food")
fruits=ex.addItem(["Fruits"],"allFruit",top)
ex.addItem(["apple"],"fruit",fruits)
ex.addItem(["orange"],"fruit",fruits)
vegetable=ex.addItem(["Vegetables"],"allVegetable",top)
ex.addItem(["carrots"],"veggie",vegetable)
ex.addItem(["lettuce"],"veggie",vegetable)
ex.addItem(["leek"],"veggie",vegetable)
ex.show()
sys.exit(app.exec_())