在 QtreeWidget 中拖动 Qframe

例如

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

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

底线问题是:如何在 QTreeWidget 中移动包含多个 QtWidget 的 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)


陪伴而非守候
浏览 275回答 1
1回答

潇潇雨雨

前提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)
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python