从 QListWidget 中拖动多个项目时,不可拖动的项目将被删除

When dragging multiple items from QListWidget, non-draggable items get removed

我有两个 QListWidget。用户可以 select 一个列表中的多个项目并将它们拖到另一个列表中。但是在每个列表中,有些项目是可拖动的,有些则不是。如果 selection 同时包含可拖动和不可拖动的项目,则会出现问题。只有可拖动的项目出现在第二个列表中,这是正确的。但是 all 项从第一个列表中消失了。

在上面的动画图像中,项目 00、01 和 02 selected。只有项目 00 和 02 启用了拖动。拖放后,第一个列表中的所有三个项目都消失了。我该如何解决这个问题?

这里是重现问题的一些代码:

import random
import sys
from PySide import QtCore, QtGui

class TestMultiDragDrop(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(TestMultiDragDrop, self).__init__(parent)

        centralWidget = QtGui.QWidget()
        self.setCentralWidget(centralWidget)

        layout = QtGui.QHBoxLayout(centralWidget)
        self.list1 = QtGui.QListWidget()
        self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list1.setSelectionMode(QtGui.QListWidget.ExtendedSelection)

        self.list2 = QtGui.QListWidget()
        self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list2.setSelectionMode(QtGui.QListWidget.ExtendedSelection)

        layout.addWidget(self.list1)
        layout.addWidget(self.list2)

        self.fillListWidget(self.list1, 8, 'someItem')
        self.fillListWidget(self.list2, 4, 'anotherItem')

    def fillListWidget(self, listWidget, numItems, txt):
        for i in range(numItems):
            item = QtGui.QListWidgetItem()
            newTxt = '{0}{1:02d}'.format(txt, i)
            if random.randint(0, 1):
                item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
            else:
                # If the item is draggable, indicate it with a *
                newTxt += ' *'
                item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
            item.setText(newTxt)
            listWidget.addItem(item)

def openMultiDragDrop():
    global multiDragDropUI
    try:
        multiDragDropUI.close()
    except:
        pass
    multiDragDropUI = TestMultiDragDrop()
    multiDragDropUI.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    multiDragDropUI.show()
    return multiDragDropUI

if __name__ == '__main__':
    app = QtGui.QApplication([])
    openMultiDragDrop()
    sys.exit(app.exec_())

这里我有点怀疑setDefaultDropAction(QtCore.Qt.MoveAction)

阅读文档中的以下段落:特别是粗体行

在最简单的情况下,拖放操作的 target 接收被拖动数据的副本,而 source 决定是否删除原件。这由 CopyAction 操作描述。目标也可以选择处理其他动作,特别是 MoveAction 和 LinkAction 动作。 如果源调用 QDrag::exec(),并且它 returns MoveAction,则源负责删除任何原始数据(如果它选择这样做)。不应删除由源小部件创建的 QMimeData 和 QDrag 对象 - 它们将被 Qt.

销毁

(http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions)

先尝试 QtCore.Qt.CopyAction

其次,如果 MoveAction 是必需的,请尝试在源列表小部件的 mouseMoveEvent.

中创建 QMimeDataQDrag 对象

在下方 link,您可以找到在源列表小部件 mouseMoveEvent 中创建 QMimeDataQDrag 对象的一些帮助。 (代码是C++,我的目的是获得概念性的想法)。

http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions

我认为 Kuba Ober 是正确的,这是一个 Qt 错误。在C++源代码中,有一个函数void QAbstractItemViewPrivate::clearOrRemove()。它会删除所有选定的行,但不会查看每个项目是否已启用拖动。

既然如此,我想出了一些解决方法:


方法 1:使所有不可拖动的项目也不可选择

这是最简单的方法。只需从所有不可拖动的项目中删除 QtCore.Qt.ItemIsEnabled 标志。当然,如果你想让你所有的项目都可以选择,这是行不通的。


方法 2:重新创建 "startDrag" 函数

由于 clearOrRemove 函数属于私有 class,我无法覆盖它。但是该函数是由 startDrag 函数调用的,它可以被覆盖。所以我基本上复制了 Python 中的函数,并用我自己的函数 removeSelectedDraggableItems.

替换了对 clearOrRemove 的调用

此方法的问题在于 startDrag 包含对属于私有 class 的一些其他函数的调用。这些函数调用其他私有 class 函数。具体来说,这些函数负责控制项目在拖动事件期间的绘制方式。因为我不想重新创建 all 函数,所以我忽略了这些函数。结果是此方法产生了正确的功能,但它丢失了正在拖动哪些项目的图形指示。

class DragListWidget(QtGui.QListWidget):
    def __init__(self):
        super(DragListWidget, self).__init__()

    def startDrag(self, supportedDragActions):
        indexes = self.getSelectedDraggableIndexes()
        if not indexes:
            return

        mimeData = self.model().mimeData(indexes)
        if not mimeData:
            return

        drag = QtGui.QDrag(self)
        rect = QtCore.QRect()
        # "renderToPixmap" is from a private class in the C++ code, so I can't use it.
        #pixmap = renderToPixmap(indexes, rect)
        #drag.setPixmap(pixmap)
        drag.setMimeData(mimeData)
        # "pressedPosition" is from a private class in the C++ code, so I can't use it.
        #drag.setHotSpot(pressedPostion() - rect.topLeft())
        defaultDropAction = self.defaultDropAction()
        dropAction = QtCore.Qt.IgnoreAction
        if ((defaultDropAction != QtCore.Qt.IgnoreAction) and 
            (supportedDragActions & defaultDropAction)):
            dropAction = defaultDropAction
        elif ((supportedDragActions & QtCore.Qt.CopyAction) and
              (self.dragDropMode() != self.InternalMove)):
            dropAction = QtCore.Qt.CopyAction

        dragResult = drag.exec_(supportedDragActions, dropAction)
        if dragResult == QtCore.Qt.MoveAction:
            self.removeSelectedDraggableItems()

    def getSelectedDraggableIndexes(self):
        """ Get a list of indexes for selected items that are drag-enabled. """
        indexes = []
        for index in self.selectedIndexes():
            item = self.itemFromIndex(index)
            if item.flags() & QtCore.Qt.ItemIsDragEnabled:
                indexes.append(index)
        return indexes

    def removeSelectedDraggableItems(self):
        selectedDraggableIndexes = self.getSelectedDraggableIndexes()
        # Use persistent indices so we don't lose track of the correct rows as
        # we are deleting things.
        root = self.rootIndex()
        model = self.model()
        persistentIndices = [QtCore.QPersistentModelIndex(i) for i in selectedDraggableIndexes]
        for pIndex in persistentIndices:
            model.removeRows(pIndex.row(), 1, root)

方法 3:破解 "startDrag"

此方法在调用内置 "startDrag" 方法之前将放置操作从 "MoveAction" 更改为 "CopyAction"。然后它调用一个自定义函数来删除选定的启用拖动的项目。这样就解决了图形拖动动画丢失的问题。

这是一个非常简单的 hack,但它有自己的问题。假设用户安装了一个事件过滤器,在某些情况下将放置操作从 "MoveAction" 更改为 "IgnoreAction"。此黑客代码未获得更新值。它仍然会删除项目,就像操作是 "MoveAction" 一样。 (方法2没有这个问题。)这个问题有解决方法,但我不会在这里详细介绍。

class DragListWidget2(QtGui.QListWidget):
    def startDrag(self, supportedDragActions):
        dropAction = self.defaultDropAction()
        if dropAction == QtCore.Qt.MoveAction:
            self.setDefaultDropAction(QtCore.Qt.CopyAction)
        super(DragListWidget2, self).startDrag(supportedDragActions)
        if dropAction == QtCore.Qt.MoveAction:
            self.setDefaultDropAction(dropAction)
            self.removeSelectedDraggableItems()

    def removeSelectedDraggableItems(self):
        # Same code from Method 2.  Removed here for brevity.
        pass