[英]Drag and Drop a Widget in QTreeWidget
I have a QTreeWidget where I want to move around the items.我有一个 QTreeWidget,我想在其中移动项目。 This works fine with the cities (see example) but not with the buttons.
这适用于城市(参见示例),但不适用于按钮。
First Question: What do I need to do to make the buttons moveable like the cities?第一个问题:我需要做什么才能使按钮像城市一样可移动? Second Question: If I move the cities I get a copy, but I want to move the city only (delete from original place)
第二个问题:如果我移动城市我得到一个副本,但我只想移动城市(从原来的地方删除)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.setGeometry(300, 300, 550, 450)
self.cities = QTreeWidgetItem(self)
self.cities.setText(0,"Cities")
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,"Oslo")
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,"Bergen")
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0,"Stavanger")
button1 = QPushButton('Button1',self)
button2 = QPushButton("Button2",self)
label = QLabel("dragHandle")
container = QWidget()
containerLayout = QHBoxLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1)
containerLayout.addWidget(button2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1,0,container)
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
if __name__ == '__main__':
main()
An important aspect about index widgets (including persistent editors) is that the view takes complete ownership on the widget, and whenever index is removed for any reason, the widget associated to that widget gets automatically destroyed.关于索引小部件(包括持久编辑器)的一个重要方面是视图拥有小部件的完全所有权,并且无论何时出于任何原因删除索引,与该小部件关联的小部件都会自动销毁。 This also happens when calling again
setIndexWidget()
with another widget for the same index.这也发生在再次调用
setIndexWidget()
时使用另一个小部件以获得相同的索引。
There is absolutely no way to prevent that, the destruction is done internally (by calling deleteLater()
), and reparenting the widget won't change anything.绝对没有办法阻止这种情况,销毁是在内部完成的(通过调用
deleteLater()
),并且重新设置小部件不会改变任何内容。
The only way to "preserve" the widget is to set a "fake" container as the index widget, create a layout for it, and add the actual widget to it. “保留”小部件的唯一方法是将“假”容器设置为索引小部件,为其创建布局,然后将实际小部件添加到其中。
Then, the problem comes when using drag&drop, because item views always use serialization of items, even when the InternalMove
flag is set.然后,使用拖放时出现问题,因为项目视图总是使用项目的序列化,即使设置了
InternalMove
标志。
This means that when an item is moved , the original index gets removed (and the widget along with it, including widgets for any child item).这意味着当一个项目被移动时,原始索引被删除(以及随之而来的小部件,包括任何子项目的小部件)。
The solution, then, is to "capture" the drop operation before it's performed, reparent the contents of the container with a similar copy, proceed with the base implementation and then restore the widgets for the new target indexes.因此,解决方案是在执行删除操作之前“捕获”它,用类似的副本重新设置容器的内容,继续进行基本实现,然后为新的目标索引恢复小部件。
Since we are dealing with tree models, this automatically calls for recursive functions, both for reparenting and restoration.由于我们正在处理树模型,因此这会自动调用递归函数,用于重新父代和恢复。
In the following code I've created an implementation that should be compatible for all standard views (QTreeView, QTableView, QListView) and their higher level widgets.在下面的代码中,我创建了一个应该兼容所有标准视图(QTreeView、QTableView、QListView)及其更高级别小部件的实现。 I didn't consider QColumnView, as it's a quite peculiar view and rarely has such requirement.
我没有考虑 QColumnView,因为它是一个非常奇特的视图,很少有这样的要求。
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Handle(QWidget):
'''
A custom widget that shows a handle for item dragging whenever
the editor doesn't support it
'''
def __init__(self):
super().__init__()
self.setFixedWidth(self.style().pixelMetric(
QStyle.PM_ToolBarHandleExtent))
def paintEvent(self, event):
qp = QPainter(self)
opt = QStyleOption()
opt.initFrom(self)
style = self.style()
opt.state |= style.State_Horizontal
style.drawPrimitive(style.PE_IndicatorToolBarHandle, opt, qp)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setColumnCount(2)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.cities = QTreeWidgetItem(self)
self.cities.setText(0, 'Cities')
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,'Oslo')
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,'Bergen')
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0, 'Stavanger')
button1 = QPushButton('Button1', self)
button2 = QPushButton('Button2', self)
label = QLabel('dragHandle')
container = QWidget()
containerLayout = QGridLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1, 0, 1)
containerLayout.addWidget(button2, 0, 2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1, 0, container)
anotherItem = QTreeWidgetItem(self, ['Whatever'])
anotherChild = QTreeWidgetItem(anotherItem, ['Another child'])
grandChild = QTreeWidgetItem(anotherChild, ['a', 'b'])
self.setItemWidget(grandChild, 1, QPushButton('Whatever'))
self.expandAll()
height = self.header().sizeHint().height() + self.frameWidth() * 2
index = self.model().index(0, 0)
while index.isValid():
height += self.rowHeight(index)
index = self.indexBelow(index)
self.resize(500, height + 100)
def setItemWidget(self, item, column, widget, addHandle=False):
if widget and addHandle or widget.layout() is None:
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(Handle())
layout.addWidget(widget)
widget = container
super().setItemWidget(item, column, widget)
def createNewContainer(self, index):
'''
create a copy of the container and its layout, then reparent all
child widgets by adding them to the new layout
'''
oldWidget = self.indexWidget(index)
if oldWidget is None:
return
oldLayout = oldWidget.layout()
if oldLayout is None:
return
newContainer = oldWidget.__class__()
newLayout = oldLayout.__class__(newContainer)
newLayout.setContentsMargins(oldLayout.contentsMargins())
newLayout.setSpacing(oldLayout.spacing())
if isinstance(oldLayout, QGridLayout):
newLayout.setHorizontalSpacing(
oldLayout.horizontalSpacing())
newLayout.setVerticalSpacing(
oldLayout.verticalSpacing())
for r in range(oldLayout.rowCount()):
newLayout.setRowStretch(r,
oldLayout.rowStretch(r))
newLayout.setRowMinimumHeight(r,
oldLayout.rowMinimumHeight(r))
for c in range(oldLayout.columnCount()):
newLayout.setColumnStretch(c,
oldLayout.columnStretch(c))
newLayout.setColumnMinimumWidth(c,
oldLayout.columnMinimumWidth(c))
items = []
for i in range(oldLayout.count()):
layoutItem = oldLayout.itemAt(i)
if not layoutItem:
continue
if layoutItem.widget():
item = layoutItem.widget()
elif layoutItem.layout():
item = layoutItem.layout()
elif layoutItem.spacerItem():
item = layoutItem.spacerItem()
if isinstance(oldLayout, QBoxLayout):
items.append((item, oldLayout.stretch(i), layoutItem.alignment()))
else:
items.append((item, ) + oldLayout.getItemPosition(i))
for item, *args in items:
if isinstance(item, QWidget):
newLayout.addWidget(item, *args)
elif isinstance(item, QLayout):
newLayout.addLayout(item, *args)
else:
if isinstance(newLayout, QBoxLayout):
newLayout.addSpacerItem(item)
else:
newLayout.addItem(item, *args)
return newContainer
def getNewIndexWidgets(self, parent, row):
'''
A recursive function that returns a nested list of widgets and those of
child indexes, by creating new parent containers in the meantime to
avoid their destruction
'''
model = self.model()
rowItems = []
for column in range(model.columnCount()):
index = model.index(row, column, parent)
childItems = []
if column == 0:
for childRow in range(model.rowCount(index)):
childItems.append(self.getNewIndexWidgets(index, childRow))
rowItems.append((
self.createNewContainer(index),
childItems
))
return rowItems
def restoreIndexWidgets(self, containers, parent, startRow=0, startCol=0):
'''
Restore index widgets based on the previously created nested list of
widgets, based on the new parent and drop row (and column for tables)
'''
model = self.model()
for row, rowItems in enumerate(containers, startRow):
for column, (widget, childItems) in enumerate(rowItems, startCol):
index = model.index(row, column, parent)
if widget:
self.setIndexWidget(index, widget)
self.restoreIndexWidgets(childItems, index)
def dropEvent(self, event):
'''
Assume that the selected index is the source of the drag and drop
operation, then create a nested list of possible index widget that
are reparented *before* the drop is applied to avoid their destruction
and then restores them based on the drop index
'''
containers = []
if event.source() == self:
dropTarget = QPersistentModelIndex(self.indexAt(event.pos()))
dropPos = self.dropIndicatorPosition()
selection = self.selectedIndexes()
if selection and len(set(i.row() for i in selection)) == 1:
index = selection[0]
containers.append(
self.getNewIndexWidgets(index.parent(), index.row()))
super().dropEvent(event)
if containers:
model = self.model()
startCol = 0
if dropPos == self.OnViewport:
parent = QModelIndex()
dropRow = model.rowCount() - 1
else:
if dropPos == self.OnItem:
if isinstance(self, QTreeView):
# tree views move items as *children* of the drop
# target when the action is *on* an item
parent = model.index(
dropTarget.row(), dropTarget.column(),
dropTarget.parent())
dropRow = model.rowCount(parent) - 1
else:
# QTableView and QListView use the drop index as target
# for the operation, so the parent index is actually
# the root index of the view
parent = self.rootIndex()
dropRow = model.rowCount(dropTarget.parent()) - 1
dropRow = dropTarget.row()
startCol = dropTarget.column() - index.column()
else:
if isinstance(self, QTreeView):
parent = dropTarget.parent()
else:
parent = self.rootIndex()
if dropPos == self.AboveItem:
dropRow = dropTarget.row() - 1
else:
dropRow = dropTarget.row() + 1
# try to restore the widgets based on the above
self.restoreIndexWidgets(containers, parent, dropRow, startCol)
# ensure that all geometries are updated after the drop operation
self.updateGeometries()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
Notes:笔记:
setItemWidget
(which would be setIndexWidget()
for basic views or setCellWidget()
for QTableWidget, and with the relative argument differences;setItemWidget
的可选覆盖(对于基本视图将是setIndexWidget()
或对于 QTableWidget 是setCellWidget()
,并且具有相对参数差异;dragDropMode
);dragDropMode
开始); always consider the default behavior of those classes, and also the defaultDropAction
;defaultDropAction
;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.