从 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
.
中创建 QMimeData
和 QDrag
对象
在下方 link,您可以找到在源列表小部件 mouseMoveEvent
中创建 QMimeData
和 QDrag
对象的一些帮助。 (代码是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
我有两个 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
.
QMimeData
和 QDrag
对象
在下方 link,您可以找到在源列表小部件 mouseMoveEvent
中创建 QMimeData
和 QDrag
对象的一些帮助。 (代码是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