I have a GUI designed with QtDesigner and a QTreeWidget in it while programming the logic behind it with PySide2. Now I want the user to be able to swap the elements in the QTreeWidget by dragging and dropping, but without changing the hierarchy. So basically I don't want him to be able to insert an item into another item as a child or make a child item a top level item.
This is my QtreeWidget:
parent1
|child1
|child2
parent2
parent3
He should only be able to change the order of the parent items or the order of the child item, but not make one the child of an item or make one the parent of an item by drag and drop. I already tried experimenting with the settings in the QtDesigner and changing some values in the code for my QTreeWidget item, but nothing works. I would be really happy if someone could guide me on the right path about this.
EDIT: the answer has been updated, please ensure that you read all of it
Qt Designer doesn't allow to set such behavior, and while the item editor provides per item flags, it's not "fully" implemented: it does provide the flag for ItemIsDropEnabled
, but that's unchecked by default and even checking/unchecking it doesn't allow to "unset" that flag.
The result is that the tree widget will be created with the default QTreeWidgetItemFlags, which automatically sets that flag.
The simplest solution is to create a function that iters the top level items and disables that flag, but also calls a recursive one that disables the ItemIsDragEnabled
for child items.
That function must be called as soon as the tree widget is created if the structure already has items, and is also connected to the model's rowsInserted
signal so that it's updated everytime a new row is added, including for child items.
NOTE: this only works when the manual sorting is required amongst top level items, see below for an implementation that allows sorting for child items.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.checkTreeParents()
self.treeWidget.model().rowsInserted.connect(self.checkTreeParents)
def checkTreeParents(self):
disabledFlags = QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
def checkChildren(parent):
for row in range(parent.childCount()):
child = parent.child(row)
child.setFlags(child.flags() & ~disabledFlags)
checkChildren(child)
root = self.treeWidget.invisibleRootItem()
for row in range(root.childCount()):
child = root.child(row)
child.setFlags(child.flags() & ~QtCore.Qt.ItemIsDropEnabled)
checkChildren(child)
As stated, the above implementation works only because it's easy to make a distinction between a top level item and child one: the former always has an invalid QModelIndex. If sorting is required between child items, a different route has to be taken.
While the following can be achieved without subclassing (using "monkey patching") that path is not usually suggested, as it often leads to silent errors and bugs that are difficult to track.
The requirement is to use a promoted widget (I suggest to read my related answer and do some research about the subject), so that the tree widget can be correctly implemented.
The "trick" is to override the startDrag
function, get a list of the whole tree index, pair all items with their current flags, disable the ItemIsDropEnabled
flag for all items except for the parent of the dragged item(s); then restore the flags right after the drag operation. Since startDrag
is blocking (it starts its own "event loop" and returns after it exits), restoring flags after calling the default implementation is safe enough.
This ensures that drag events will only be accepted when hovering over the same parent as the selected items or between them, but not on them or on/between any other item or parent (including children).
This is probably the best method, as trying to do the same by overriding dragEnterEvent
, dragMoveEvent
and dropEvent
would be actually more complex (thus, prone to bugs), and would probably also require overriding paintEvent
in order to correctly display the drop indicator. By temporarily changing the drop flags of the items, we let the QTreeView take care of all of that.
Note: the following assumes that you promoted the tree widget using TreeView
as class name; please ensure that you've understood how widget promotion works.
class TreeView(QtWidgets.QTreeWidget):
def iterItems(self, parent=None):
# iter through **all** items in the tree model, recursively, and
# yield each item individually
if parent is None:
parent = self.invisibleRootItem()
# the root item *must* be yield! If not, the result is that the
# root will not have the ItemIsDropEnabled flag set, so it
# will accept drops even from child items
yield parent
for row in range(parent.childCount()):
childItem = parent.child(row)
yield childItem
for grandChild in self.iterItems(childItem):
# yield children recursively, including grandchildren
yield grandChild
def startDrag(self, actions):
selected = [i for i in self.selectedIndexes()
if i.flags() & QtCore.Qt.ItemIsDragEnabled]
parents = list(set(i.parent() for i in selected))
# we only accept drags from children of a single item
if len(parents) == 1:
parent = self.itemFromIndex(parents[0])
if not parent:
# required since itemFromIndex on the root *index* returns None
parent = self.invisibleRootItem()
else:
# no item will accept drops!
parent = None
itemFlags = []
for item in self.iterItems():
if item != parent:
# store all flags and disable the drop flag if set, UNLESS the
# item is the parent
flags = item.flags()
itemFlags.append((item, flags))
item.setFlags(flags & ~QtCore.Qt.ItemIsDropEnabled)
# call the default implementation and let the tree widget
# do all of its stuff
super().startDrag(actions)
# finally, restore the original flags
for item, flags in itemFlags:
item.setFlags(flags)
Notes:
iterItems()
and checking the parenthood of each item within the selection;setDragDropMode(InternalMove)
is still required; it can be set in Designer, anyway;