pyside qtreewidget限制拖放

时间:2013-12-16 18:24:12

标签: python drag-and-drop pyside maya

我正在尝试向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 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:1)

这是我的解决方案(最后的完整代码),子类化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)
...

table example

拖放重新实现

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

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

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

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

    next to drop target

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