【问题标题】:Drag Qframe within QtreeWidget在 QtreeWidget 中拖动 Qframe
【发布时间】:2020-09-09 18:37:31
【问题描述】:

您好,我正在阅读 this 文章关于如何拖放小部件项目,这很有意义,例如

Qt 的项目视图使用内部 application/x-qabstractitemmodeldatalist MIME 类型传递项目

我还阅读了this Question,它主要关注来自 Qlinewidget 和 QTreewidget 的字符串模型。

但是我怎样才能获得包含多个 QtWidgets 的 QFrame 的 QAbstractItemView.model()

底线问题是:如何在 QTreeWidget 中移动包含多个 QtWidgets 的 QFrame。请看下面的示例代码: 按下按钮添加子节点并尝试将它们拖到其他子节点或第一级树层次结构的父节点之间

from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QPushButton, QLabel, QDialog, QVBoxLayout, QApplication, QLineEdit)
from PyQt5.QtWidgets import (QPushButton, QDialog, QTreeWidget,
                             QTreeWidgetItem, QVBoxLayout,
                             QHBoxLayout, QFrame, QLabel, QComboBox,
                             QApplication)

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        self.index=0
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName("gridLayout")
        self.treeWidget = QtWidgets.QTreeWidget(self.centralwidget)
        self.treeWidget.setObjectName("treeWidget")
        self.treeWidget.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.treeWidget.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.treeWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.treeWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
        self.treeWidget.setAutoScrollMargin(10)
        
        self.treeWidget.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        
        self.treeWidget.setDragEnabled(1)
        self.treeWidget.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.treeWidget.setAnimated(True)
        self.treeWidget.setWordWrap(False)
        self.treeWidget.setExpandsOnDoubleClick(True)
        self.treeWidget.setObjectName("treeWidget")
        self.treeWidget.header().setVisible(False)
        self.treeWidget.header().setHighlightSections(False)
        self.treeWidget.header().setSortIndicatorShown(False)
        self.treeWidget.header().setStretchLastSection(True)

        self.gridLayout.addWidget(self.treeWidget, 0, 0, 1, 1)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        #initialize top level items
        self.topLevelItem1 = QTreeWidgetItem(self.treeWidget)
        
        
        #add those top level in treewidget
        self.treeWidget.addTopLevelItem(self.topLevelItem1)

        #create button
        self.ButtonWidget=QtWidgets.QPushButton("Press")
        self.ButtonWidget.clicked.connect(self.AddQFrame)
        #add button to tree widget
        self.treeWidget.setItemWidget(self.topLevelItem1, 0, self.ButtonWidget)
        
        
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)


    def AddQFrame(self):
        #add combo box on button press
        self.index=self.index+1

        #create child item
        self.ChildItem1 = QTreeWidgetItem()
        self.topLevelItem1.addChild(self.ChildItem1)

        #create frame & horizontal layout for that child item
        self.ChildWidgetFrame=QFrame(self.treeWidget)
        self.layoutChild=QHBoxLayout(self.ChildWidgetFrame)
        
        #layout in qframe
        self.QcomboWidget=QtWidgets.QComboBox()
        self.QcomboWidget.addItem("item "+str(self.index))
        self.QcomboWidget.addItem("CC")
        self.QcomboWidget.addItem("CV")
        self.spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
        #widgets in layout
        self.layoutChild.addWidget(QLabel("#"+str(self.index)))
        self.layoutChild.addWidget(self.QcomboWidget)
        self.layoutChild.addItem(self.spacerItem3)
        #display widget
        self.treeWidget.setItemWidget(self.ChildItem1, 0, self.ChildWidgetFrame)
        

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.headerItem().setText(0, _translate("MainWindow", "1"))
        __sortingEnabled = self.treeWidget.isSortingEnabled()
        self.treeWidget.setSortingEnabled(False)
        self.treeWidget.setSortingEnabled(__sortingEnabled)
        
    

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

编辑

就拖动哪个树级别而言,这个问题令人困惑。所以我编辑了这个问题:我只是在第一层级中重新排列/移动孩子/父母。感谢您的时间和努力!

EDIT2

另一个编辑以阐明 treewidget 层次结构的级别。

这就是我想要实现的目标:Dnd Parent Qframe 及其子级达到树的第一级。

【问题讨论】:

    标签: python pyqt5 qtreewidget


    【解决方案1】:

    前提

    OP 想要实现的目标不容易。索引小部件与底层模型无关(它们不应该!),因为它们仅与项目 view 相关。由于拖放操作作用于 QMimeData 对象及其内容(序列化为字节数据),因此没有直接的方法可以从拖放事件中访问索引小部件。这意味着 d&d 操作仅作用于项目 model,并且完全忽略索引小部件。

    但是,这还不够。
    即使您可以获得对索引小部件的字节引用,一旦索引小部件被替换或删除,这些小部件总是会被删除:setItemWidget() 的主要问题与setIndexWidget() 相同:

    如果索引小部件 A 被索引小部件 B 替换,则索引小部件 A 将被删除。

    source code 执行以下操作:

    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 的角度来看,我们对此有NO控制权,除非我们彻底实现相关的项目视图类(并且......祝你好运)。

    关于树模型的说明

    Qt 有自己的方式来支持树模型的InternalMove 标志。在下面的解释中,我假设拖放操作总是在为selectionMode() 属性设置的SingleSelection 模式下发生,并且dragDropMode() 设置为默认的InternalMove。 如果您想为高级拖放模式提供扩展选择的实现,您必须自己找到(可能通过研究 QAbstractItemView 和 QTreeView 的源代码)。

    [解决方法] 解决方案

    不过,有一个 hack。
    调用deleteLater() 的唯一小部件是使用setIndexWidget() 设置的实际小部件,而不是它的子小部件。
    因此,在这些情况下,要使用索引小部件添加对拖放的支持,唯一简单的解决方案是始终添加带有 容器 父小部件的索引小部件,并删除实际的容器中的小部件替换/删除索引小部件,然后在新索引/项目上使用setIndexWidget()(或setItemWidget())之前为实际小部件创建一个新容器,可能带有递归函数以确保保留子引用。

    这确保实际显示的(先前的)索引小部件不会被删除,因为只有它的容器会被删除,从而允许我们将该小部件设置为另一个索引。

    幸运的是,QTreeWidget 可以更轻松地访问项目,因为项目是实际的和持久的对象,即使在它们被移动后也可以被跟踪(与 QTreeView 中的 QModelIndex 发生的情况不同)。

    在以下示例中(使用 cmets 中提供的信息进行了更新),我正在创建顶级项目并仅允许在第一级放置。这是一个基本示例,您可能想要添加一些功能:例如,如果项目组合未设置为“重复”,则防止丢弃,甚至创建一个已设置为“重复”的新父项目并手动添加子项。

    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)
    

    【讨论】:

    • 非常感谢@musicamante。这又是一个非常愚蠢的问题。如何限制子元素在 toplevelitem 之外的拖动,或者换句话说,我如何禁用 toplevelitem 外部空白空间中的拖动。在您给定的答案中,当我将一个项目拖到外面并将其放置为顶层项目时,它会破坏。我需要禁用在空白处拖动。我会接受你的回答。谢谢楼主!
    • 2) 我可以限制拖入孩子的孩子吗?在您的代码中,如果我添加 5 个项目并将它们全部拖动到“项目 1”1 中,它将像嵌套树一样,我需要将其限制为仅 1 个嵌套。例如一个孩子不能被拖进另一个孩子。
    • 3) 我可以拖动而不使其成为另一个孩子的孩子吗?换句话说,我的意思是可以重新安排第一层次的孩子而不让他们相互进入?
    • 另外,请尽量少用 cmets,如果有疑问,停下来想一想,然后简洁明了,限制 cmets/requests 的数量。跨度>
    • @Elan 我不知道你有没有看我的消息,我已经更新了答案。
    猜你喜欢
    • 2013-03-14
    • 2016-01-23
    • 2014-10-02
    • 2018-04-15
    • 2021-08-07
    • 2017-05-13
    • 2014-01-04
    • 1970-01-01
    • 2014-02-12
    相关资源
    最近更新 更多