首页 文章

pyside qtreewidget限制拖放

提问于
浏览
5

我正在尝试向QTreeWidget拖放功能添加约束,以防止分支进入另一个根中的另一个分支 .

这是一个让事情更清晰的例子:
我有4个物体 . 让我们称他们为苹果,香蕉,胡萝卜,榴莲 .

树看起来像这样:

isDelicious (Root)
|-- BackgroundObjects (Branch)
   |-- Durian
|-- ForgroundObjects (Branch)
   |-- Apple
   |-- Banana
   |-- Carrot
isSmelly (Root)
|-- BackgroundObjects (Branch)
   |-- Apple
   |-- Carrot
|-- ForgroundObjects (Branch)
   |-- Banana
   |-- Durian

因此,允许将对象从BackgroundObjects拖放到ForgroundObjects,反之亦然,但是不允许将它们拖放到不同根目录上的分支上 .

我已经尝试重新实现和子类化dragMoveEvent,dragEnterEvent和dropEvent,如果我在dragEnterEvent中对事件调用accept,它将调用dragMoveEvent(我期望) . 但是,只有当我退出QTreeWidget时才会调用dropEvent .

我想要做的是在移动之前检查所选对象的祖父母,以及建议的新祖父母以查看它们是否相同 . 如果是,那么接受此举 . 否则忽略此举 .

我已经搜索过是否有任何答案,到目前为止我还没有尝试过 . 可能最接近的是来自Stack Overflow的这两个问题:
https://stackoverflow.com/questions/17134289/managing-drag-and-drop-within-qtreewidgets-in-pyside
qt: QTreeView - limit drag and drop to only happen within a particlar grandparent (ancestor)

2 回答

  • 1

    Qt似乎没有让这种事情变得非常容易 .

    我能想到的最好的方法是在拖动输入和拖动移动事件期间暂时重置项目标志 . 下面的示例动态计算当前顶级项目以禁止拖放 . 但也可以通过使用 setData() 为每个项目添加标识符来完成 .

    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_())
    
  • 3

    这是我的解决方案(最后的完整代码),子类化 QTreeWidget . 我尝试过一些非常通用的东西,应该适用于很多情况 . 拖动时视觉提示仍然存在一个问题 . 以前的版本在Windows上不起作用,我希望这个版本能够 . 它在Linux上运行得非常好 .


    Defining categories

    树中的每个项目都有一个类别(一个字符串),我存储在 QtCore.Qt.ToolTipRole 中 . 您还可以将 QTreeWidgetItem 子类化为具有特定属性 category .

    我们在字典 settings 中定义所有类别,包括可以放入的类别列表和要设置的标志 . 例如:

    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)
    }
    

    类别“family”的每个项目都可以获得拖动,并且只能放入“root”(隐藏的根项) . “儿童”类别中的每一项都只能归入“家庭” .


    Adding items to the tree

    方法 addItem(strings,category,parent=None) 使用工具提示"category"和 setting 中的匹配标志创建 QTreeWidgetItem(strings,parent) . 它返回该项目 . 例:

    dupont=ex.addItem(["Dupont"],"family")
    robert=ex.addItem(["Robertsons"],"family")
    ex.addItem(["Laura"],"children",dupont)
    ex.addItem(["Matt"],"children",robert)
    ...
    

    table example


    Reimplementation of Drag and Drop

    被拖动的项目由 self.currentItem() 确定(不处理多项选择) . 可以删除此项目的类别列表是 okList=self.settings[itemBeingDragged.data(0,role)][0] .

    鼠标下的项目,即"drop target",是 self.itemAt(event.pos()) . 如果鼠标位于空白处,则将放置目标设置为根项目 .

    • dragMoveEvent (视觉提示是否接受/忽略掉落)
      如果放置目标在 okList 中,我们称之为常规 dragMoveEvent . 如果没有,我们必须检查"next to drop target" . 在下面的图像中,鼠标下的项目是Robertsons,但真正的下降目标是根项目(参见下面的Robertsons线?) . 为了解决这个问题,我们检查项目是否可以拖放到放置目标的父级 . 如果没有,我们称之为 event.ignore() .

    唯一剩下的问题是当鼠标实际上在“Robertsons”上时:接受拖动事件 . 视觉线索表示,当不是时,它将被接受 .

    next to drop target

    • dropEvent
      而不是接受或忽略掉落,这是因为"next to drop target"非常棘手,我们总是接受掉落,然后修复错误 .
      如果新父级与旧父级相同,或者它位于 okList 中,则我们不执行任何操作 . 否则,我们将拖动的项目放回旧父项中 .

    有时被删除的项目将被折叠,但这可以很容易地修复 itemBeingDragged.setExpanded()


    最后,完整代码有两个例子:

    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_())
    

相关问题