0

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.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241

1 Answers1

1

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)

UPDATE

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:

  1. the above code doesn't consider the possibility of trying to drag items that have different parent items (as explained in the comment); doing it is possible, but would require a much more complex implementation of both iterItems() and checking the parenthood of each item within the selection;
  2. drop from external sources is obviously not considered here;
  3. setDragDropMode(InternalMove) is still required; it can be set in Designer, anyway;
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Wow, it works perfectly for the parent items, thanks a lot! Sadly, I can't drag or drop the child items of the parent items, how could I extend the code to make it work for them to? –  Jul 06 '21 at 19:44
  • Thank you so much for the update and your time! It works now just fine. :) –  Jul 07 '21 at 16:38