簡體   English   中英

在 QTreeWidget 中拖放一個 Widget

[英]Drag and Drop a Widget in QTreeWidget

我有一個 QTreeWidget,我想在其中移動項目。 這適用於城市(參見示例),但不適用於按鈕。

第一個問題:我需要做什么才能使按鈕像城市一樣可移動? 第二個問題:如果我移動城市我得到一個副本,但我只想移動城市(從原來的地方刪除)

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()

關於索引小部件(包括持久編輯器)的一個重要方面是視圖擁有小部件的完全所有權,並且無​​論何時出於任何原因刪除索引,與該小部件關聯的小部件都會自動銷毀。 這也發生在再次調用setIndexWidget()時使用另一個小部件以獲得相同的索引。

絕對沒有辦法阻止這種情況,銷毀是在內部完成的(通過調用deleteLater() ),並且重新設置小部件不會改變任何內容。

“保留”小部件的唯一方法是將“假”容器設置為索引小部件,為其創建布局,然后將實際小部件添加到其中。

然后,使用拖放時出現問題,因為項目視圖總是使用項目的序列化,即使設置了InternalMove標志。

這意味着當一個項目被移動時,原始索引被刪除(以及隨之而來的小部件,包括任何子項目的小部件)。

因此,解決方案是在執行刪除操作之前“捕獲”它,用類似的副本重新設置容器的內容,繼續進行基本實現,然后為新的目標索引恢復小部件。

由於我們正在處理樹模型,因此這會自動調用遞歸函數,用於重新父代和恢復。

在下面的代碼中,我創建了一個應該兼容所有標准視圖(QTreeView、QTableView、QListView)及其更高級別小部件的實現。 我沒有考慮 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_()

筆記:

  • 雖然上面是為 QTreeWidget 完成的,但它可以很容易地移植到其他視圖的通用用法; 唯一的區別是setItemWidget的可選覆蓋(對於基本視圖將是setIndexWidget()或對於 QTableWidget 是setCellWidget() ,並且具有相對參數差異;
  • 項目視圖(和“項目小部件視圖”)和模型在拖放操作中可能具有不同的行為,並且取決於它們的設置(從dragDropMode開始); 始終考慮這些類的默認行為,以及defaultDropAction
  • 請仔細研究上面的所有代碼:這是一個通用示例,拖放操作非常復雜,您可能需要自定義行為; 我強烈建議您在需要對 d&d 進行更高級控制時,尤其是在處理樹結構時,耐心而仔細地閱讀該代碼以了解其方面;
  • 請記住,索引小部件雖然有用,但通常會出現問題; 大多數時候,使用自定義委托可能是更好的選擇,即使對於交互式小部件(如按鈕或復雜小部件)也是如此; 請記住,項目視圖的基本來源是模型數據,包括最終的“小部件”(甚至是編輯器及其內容);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM