pyside qtreewidget constrain drag and drop

只愿长相守 提交于 2019-12-03 00:36:42

Qt does not seem to make this sort of thing very easy.

The best I could come up with was to temporarily reset the item flags during the drag-enter and drag-move events. The example below calculates the current top-level item dynamically in order to contrain drag and drop. But it could also be done by using setData() to add an identifier to each item.

from PyQt4 import QtCore, QtGui

class TreeWidget(QtGui.QTreeWidget):
    def __init__(self, parent=None):
        QtGui.QTreeWidget.__init__(self, parent)
        self.setDragDropMode(self.InternalMove)
        self.setDragEnabled(True)
        self.setDropIndicatorShown(True)
        self._dragroot = self.itemRootIndex()

    def itemRootIndex(self, item=None):
        root = self.invisibleRootItem()
        while item is not None:
            item = item.parent()
            if item is not None:
                root = item
        return QtCore.QPersistentModelIndex(
            self.indexFromItem(root))

    def startDrag(self, actions):
        items = self.selectedItems()
        self._dragroot = self.itemRootIndex(items and items[0])
        QtGui.QTreeWidget.startDrag(self, actions)

    def dragEnterEvent(self, event):
        self._drag_event(event, True)

    def dragMoveEvent(self, event):
        self._drag_event(event, False)

    def _drag_event(self, event, enter=True):
        items = []
        disable = False
        item = self.itemAt(event.pos())
        if item is not None:
            disable = self._dragroot != self.itemRootIndex(item)
            if not disable:
                rect = self.visualItemRect(item)
                if event.pos().x() < rect.x():
                    disable = True
        if disable:
            for item in item, item.parent():
                if item is not None:
                    flags = item.flags()
                    item.setFlags(flags & ~QtCore.Qt.ItemIsDropEnabled)
                    items.append((item, flags))
        if enter:
            QtGui.QTreeWidget.dragEnterEvent(self, event)
        else:
            QtGui.QTreeWidget.dragMoveEvent(self, event)
        for item, flags in items:
            item.setFlags(flags)

class Window(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.tree = TreeWidget(self)
        self.tree.header().hide()
        def add(root, *labels):
            item = QtGui.QTreeWidgetItem(self.tree, [root])
            item.setFlags(item.flags() &
                          ~(QtCore.Qt.ItemIsDragEnabled |
                            QtCore.Qt.ItemIsDropEnabled))
            for index, title in enumerate(
                ('BackgroundObjects', 'ForegroundObjects')):
                subitem = QtGui.QTreeWidgetItem(item, [title])
                subitem.setFlags(
                    subitem.flags() & ~QtCore.Qt.ItemIsDragEnabled)
                for text in labels[index].split():
                    child = QtGui.QTreeWidgetItem(subitem, [text])
                    child.setFlags(
                        child.flags() & ~QtCore.Qt.ItemIsDropEnabled)
        add('isDelicious', 'Durian', 'Apple Banana Carrot')
        add('isSmelly', 'Apple Carrot', 'Banana Durian')
        root = self.tree.invisibleRootItem()
        root.setFlags(root.flags() & ~QtCore.Qt.ItemIsDropEnabled)
        self.tree.expandAll()
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.tree)

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(500, 300, 300, 300)
    window.show()
    sys.exit(app.exec_())

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_())
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!