1 回答

TA贡献1833条经验 获得超4个赞
前提
OP 想要实现的目标并不容易。索引小部件与底层模型无关(它们不应该!),因为它们仅与项目视图相关。由于拖放操作作用于 QMimeData 对象及其内容(序列化为字节数据),因此没有直接方法从放置事件访问索引小部件。这意味着 d&d 操作仅作用于项目模型,而索引小部件将被完全忽略。
但是,这还不够。
即使您可以获得对索引小部件的字节引用,一旦替换或删除索引小部件,这些小部件总是会被删除:主要问题与以下setItemWidget()
相同setIndexWidget()
:
如果将索引小部件 A 替换为索引小部件 B,则索引小部件 A 将被删除。
源代码执行以下操作:
void QAbstractItemView::setIndexWidget(const QModelIndex &index, QWidget *widget)
{
# ...
if (QWidget *oldWidget = indexWidget(index)) {
d->persistent.remove(oldWidget);
d->removeEditor(oldWidget);
oldWidget->removeEventFilter(this);
oldWidget->deleteLater();
}
# ...
}
结果是,每当设置索引小部件(或删除索引)时,相关索引小部件就会被删除。从 PyQt 方面来看,我们无法控制这一点,除非我们彻底实现相关项目视图类(并且......祝你好运)。
关于树模型的注意事项
Qt 有自己的方式来支持InternalMove
树模型的标志。在下面的解释中,我假设拖/放操作总是在SingleSelection
为属性设置的模式下发生selectionMode()
,并且dragDropMode()
设置为默认值InternalMove
。如果您想提供具有扩展选择功能的高级拖放模式的实现,您必须找到自己的实现(可能通过研究 QAbstractItemView 和 QTreeView 的源代码)。
[解决办法] 解决方案
不过,有一个黑客。
唯一被deleteLater()
调用的小部件是使用 集设置的实际小部件setIndexWidget()
,而不是其子部件。
因此,在这些情况下,要添加对索引小部件拖放的支持,唯一简单的解决方案是始终添加带有容器父小部件的索引小部件,并在替换/删除索引小部件之前从容器中删除实际小部件,然后创建在新索引/项目上使用setIndexWidget()
(或)之前,实际小部件的新容器setItemWidget()
,可能使用递归函数来确保保留子引用。
这确保了实际显示的(先前的)索引小部件不会被删除,因为只有它的容器会被删除,从而允许我们为另一个索引设置该小部件。
幸运的是,QTreeWidget 可以更轻松地访问这些项目,因为这些项目是实际且持久的对象,即使在移动后也可以对其进行跟踪(与 QTreeView 中的 QModelIndex 发生的情况不同)。
在下面的示例中(使用评论中提供的信息进行更新),我正在创建顶级项目并仅允许在第一级上放置。这是一个基本示例,您可能想要添加一些功能:例如,如果项目组合未设置为“重复”,则防止掉落,甚至创建一个已设置为“重复”的新父项目并手动添加子项目。
class WidgetDragTree(QtWidgets.QTreeWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.header().hide()
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.setDragEnabled(True)
self.setDefaultDropAction(QtCore.Qt.MoveAction)
def addFrame(self):
item = QtWidgets.QTreeWidgetItem()
self.addTopLevelItem(item)
item.setExpanded(True)
# create the "virtual" container; use 0 contents margins for the layout
# to avoid unnecessary padding around the widget
container = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
# the *actual* widget that we want to add
widget = QtWidgets.QFrame()
layout.addWidget(widget)
frameLayout = QtWidgets.QHBoxLayout(widget)
widget.label = QtWidgets.QLabel('#{}'.format(self.topLevelItemCount()))
frameLayout.addWidget(widget.label)
combo = QtWidgets.QComboBox()
frameLayout.addWidget(combo)
combo.addItems(['Select process', 'CC', 'VV', 'Repeat'])
# add a spacer at the end to keep widgets at their minimum required size
frameLayout.addStretch()
# the widget has to be added AT THE END, otherwise its sizeHint won't be
# correctly considered for the index
self.setItemWidget(item, 0, container)
def delFrame(self):
for index in self.selectedIndexes():
item = self.itemFromIndex(index)
if item.parent():
item.parent().takeChild(item)
else:
self.takeTopLevelItem(index.row())
def updateLabels(self, parent=None):
if parent is None:
parent = self.rootIndex()
for row in range(self.model().rowCount(parent)):
index = self.model().index(row, 0, parent)
container = self.indexWidget(index)
if container and container.layout():
widget = container.layout().itemAt(0).widget()
try:
widget.label.setText('#{}'.format(row + 1))
except Exception as e:
print(e)
# if the index has children, call updateLabels recursively
if self.model().rowCount(index):
self.updateLabels(index)
def dragMoveEvent(self, event):
super().dragMoveEvent(event)
if self.dropIndicatorPosition() == self.OnViewport:
# do not accept drop on the viewport
event.ignore()
elif self.dropIndicatorPosition() == self.OnItem:
# do not accept drop beyond the first level
target = self.indexAt(event.pos())
if target.parent().isValid():
event.ignore()
def getIndexes(self, indexList):
# get indexes recursively using a set (to get unique indexes only)
indexes = set(indexList)
for index in indexList:
childIndexes = []
for row in range(self.model().rowCount(index)):
childIndexes.append(self.model().index(row, 0, index))
if childIndexes:
indexes |= self.getIndexes(childIndexes)
return indexes
def dropEvent(self, event):
widgets = []
# remove the actual widget from the container layout and store it along
# with the tree item
for index in self.getIndexes(self.selectedIndexes()):
item = self.itemFromIndex(index)
container = self.indexWidget(index)
if container and container.layout():
widget = container.layout().itemAt(0).widget()
if widget:
container.layout().removeWidget(widget)
widgets.append((item, widget))
super().dropEvent(event)
# restore the widgets in a new container
for item, widget in widgets:
container = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widget)
self.setItemWidget(item, 0, container)
index = self.indexFromItem(item)
if index.parent().isValid():
self.expand(index.parent())
# force the update of the item layouts
self.updateGeometries()
# update the widget labels
self.updateLabels()
class Test(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QVBoxLayout(self)
btnLayout = QtWidgets.QHBoxLayout()
layout.addLayout(btnLayout)
self.addBtn = QtWidgets.QPushButton('+')
btnLayout.addWidget(self.addBtn)
self.delBtn = QtWidgets.QPushButton('-')
btnLayout.addWidget(self.delBtn)
self.tree = WidgetDragTree()
layout.addWidget(self.tree)
self.addBtn.clicked.connect(self.tree.addFrame)
self.delBtn.clicked.connect(self.tree.delFrame)
更新(Windows 修复)
似乎存在一个可能的错误,该错误发生在 Windows 中(至少在 Qt 5.13 和 Windows 10 中):单击某个项目然后单击组合框后,树小部件会收到一堆mouseMoveEvent触发拖动的信息。不幸的是,我无法进行进一步的测试,但这是一个可能的解决方法:
class WidgetDragTree(QtWidgets.QTreeWidget):
# ...
def mousePressEvent(self, event):
# fix for [unknown] bug on windows where clicking on a combo child of an
# item widget also sends back some mouseMoveEvents
item = self.itemAt(event.pos())
if item and self.itemWidget(item, 0):
# if the item has a widget, make a list of child combo boxes
combos = self.itemWidget(item, 0).findChildren(QtWidgets.QComboBox)
underMouseWidget = QtWidgets.QApplication.widgetAt(event.globalPos())
if underMouseWidget in combos:
return
super().mousePressEvent(event)
添加回答
举报