为了账号安全,请及时绑定邮箱和手机立即绑定

在 QtreeWidget 中拖动 Qframe

在 QtreeWidget 中拖动 Qframe

陪伴而非守候 2023-07-27 14:12:32
例如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)
查看完整描述

1 回答

?
潇潇雨雨

TA贡献1833条经验 获得超4个赞

前提

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)


查看完整回答
反对 回复 2023-07-27
  • 1 回答
  • 0 关注
  • 274 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号