简体   繁体   English

在 QTreeWidget 中拖放一个 Widget

[英]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:笔记:

  • while the above is done for QTreeWidget, it can be easily ported for generic usage of other views;虽然上面是为 QTreeWidget 完成的,但它可以很容易地移植到其他视图的通用用法; the only difference is the optional override of setItemWidget (which would be setIndexWidget() for basic views or setCellWidget() for QTableWidget, and with the relative argument differences;唯一的区别是setItemWidget的可选覆盖(对于基本视图将是setIndexWidget()或对于 QTableWidget 是setCellWidget() ,并且具有相对参数差异;
  • item views (and "item widget views") and models might have different behaviors on drag&drop operations, and depending on their settings (starting with dragDropMode );项目视图(和“项目小部件视图”)和模型在拖放操作中可能具有不同的行为,并且取决于它们的设置(从dragDropMode开始); always consider the default behavior of those classes, and also the defaultDropAction ;始终考虑这些类的默认行为,以及defaultDropAction
  • please study all the code above with extreme care: it's a generic example, drag and drop operations are quite complex, and you might need custom behavior;请仔细研究上面的所有代码:这是一个通用示例,拖放操作非常复杂,您可能需要自定义行为; I strongly suggest to patiently and carefully read that code to understand its aspects whenever you need more advanced control over d&d, especially when dealing with tree structures;我强烈建议您在需要对 d&d 进行更高级控制时,尤其是在处理树结构时,耐心而仔细地阅读该代码以了解其方面;
  • remember that index widgets, while useful, are often problematic;请记住,索引小部件虽然有用,但通常会出现问题; most of the times, using a custom delegate might be a better choice, even for interactive widgets such as buttons or complex widgets;大多数时候,使用自定义委托可能是更好的选择,即使对于交互式小部件(如按钮或复杂小部件)也是如此; remember that the base source of item views is the model data, including eventual "widgets" (or even editors and their contents);请记住,项目视图的基本来源是模型数据,包括最终的“小部件”(甚至是编辑器及其内容);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM