PySide QtTreeWidget约束拖放

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

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

你提到可以使用setData来完成。你能演示一下如何做吗? - JokerMartini
@JokerMartini。我在示例中修复了一个错误,但我认为整体解决方案并不是非常可靠的,现在我可能不会推荐它。使用setData不会有任何区别。目前,恐怕我没有更好的想法,并且没有时间进一步研究它。 - ekhumoro
你能帮我解决一下我的问题吗?我已经在这里更新了我的帖子。虽然它几乎可以工作,但还有一些错误。http://stackoverflow.com/questions/34133789/controlling-drag-n-drop-disable-enable-of-qtreewidget-items-python?noredirect=1#comment56017728_34133789 - JokerMartini

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”(不可见的根项目)中。 类别为“children”的每个项目只能被放置在“family”中。


向树中添加项目

方法addItem(strings,category,parent=None)会创建一个带有工具提示“category”和匹配标志的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


重新实现拖放功能

被拖动的项目可以通过 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_())

我在Windows上找到了一些类似问题的链接,这是其中一个错误报告:https://bugreports.qt.io/browse/QTBUG-46642。 - Mel
@JokerMartini 我已经编辑了一个新版本。你能试一下吗?在我的Linux系统上完美运行。 - Mel
你遇到了我也遇到的问题。用户可以绕过所有检查,将物品放在其他物品之间并破坏系统。这就是我刚才在你最新的代码中所做的。 - JokerMartini
很奇怪。无论如何,您都可以删除此“if”,如果它评估为True,则“elif”也应该评估为True。 - Mel
NotimplementedError是PySide中的bug。我已经编辑过了,删除了有问题的那一行,因为我们不需要它。 - Mel
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接