【问题标题】:pyside qtreewidget constrain drag and droppyside qtreewidget 约束拖放
【发布时间】:2014-01-04 06:20:32
【问题描述】:

我正在尝试为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)

【问题讨论】:

    标签: python drag-and-drop pyside qtreewidget


    【解决方案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_())
    

    【讨论】:

    • 你提到它可以通过使用 setData 来完成。你能展示一下怎么做吗?
    • @JokerMartini。我修复了示例中的一个错误,但我认为整体解决方案不是很可靠,我现在可能不会推荐它。使用setData 不会有任何区别。目前恐怕没有更好的想法,也没有时间再研究了。
    • 你能帮我解决我的情况吗?我已经更新了我在这里看到的帖子。我几乎可以使用它,但它有一些错误stackoverflow.com/questions/34133789/…
    • @JokerMartini。正如我所说:解决方案不是很可靠,我没有更好的解决方案。对不起。
    【解决方案2】:

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


    定义类别

    树中的每个项目都有一个类别(一个字符串),我将其存储在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”(不可见的根项目)中。 “儿童”类别的每一项都只能归入“家庭”。


    向树中添加项目

    addItem(strings,category,parent=None) 方法创建一个带有工具提示“类别”的QTreeWidgetItem(strings,parent)setting 中的匹配标志。它返回项目。示例:

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


    重新实现拖放

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

    鼠标下的项目,也就是“放置目标”,是self.itemAt(event.pos())。如果鼠标在空白处,则放置目标设置为根项目。

    • dragMoveEvent(是否接受/忽略丢弃的视觉提示)
      如果放置目标在okList 中,我们称其为常规dragMoveEvent。 如果没有,我们必须检查“下一个放置目标”。在下图中,鼠标下方的项目是 Robertsons,但真正的放置目标是根项目(参见下面的行 Robertsons ?)。为了解决这个问题,我们检查它是否可以将项目拖动到放置目标的父级上。如果没有,我们打电话给event.ignore()

      剩下的唯一问题是当鼠标实际上在“罗伯逊”上时:拖动事件被接受。视觉提示说,如果不接受,则将接受丢弃。

    • dropEvent
      我们总是接受丢弃,然后修复错误,而不是接受或忽略丢弃(因为“放置目标旁边”而非常棘手)。
      如果新父级与旧父级相同,或者在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_())
    

    【讨论】:

    • 我不太确定这是否能正常工作。当我拖放一个项目时,它会永远消失......?
    • 它在 Linux 上运行良好,但我刚刚在家里的 Windows 上进行了测试,确实项目消失了。也可能是 python 版本,或者我以某种方式进行了“微不足道”的更改并破坏了代码......
    • 我在 windows 上发现了一些类似问题的链接,这里有一个错误报告:bugreports.qt.io/browse/QTBUG-46642
    • @JokerMartini 我已经编辑了一个新版本。你能试试吗?它在我的 Linux 上完美运行。
    • 你遇到了和我一样的问题。用户可以偷偷通过所有检查并在其他人之间丢弃物品并破坏系统。这就是我刚刚对你的最新代码所做的。
    猜你喜欢
    • 2012-05-04
    • 2018-04-15
    • 2021-08-07
    • 1970-01-01
    • 2017-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多