在QtreeWidget中拖动Qframe

问题描述

您好,我正在阅读this文章内容涉及如何拖放小部件项目,

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

我还阅读了this Question,该书主要关注Qlinewidget和QTreewidget中的字符串模型。

但是我如何获取包含多个QtWidget的qframeQAbstractItemView.model()

底线问题是:如何在QTreeWidget中移动包含多个QtWidgets的qframe。请参见下面的示例代码: 按下按钮添加子项,然后尝试将其拖到其他子项或第一级树层次结构的父级之间

from PyQt5.QtWidgets import (QTreeWidget,QTreeWidgetItem,QPushButton,QLabel,QDialog,QVBoxLayout,QApplication,QLineEdit)
from PyQt5.QtWidgets import (QPushButton,QTreeWidget,QHBoxLayout,qframe,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,1,1)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(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,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,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

另一种澄清树小工具层次结构的编辑。

这是我要实现的目标:父级qframe及其子级直到树的第一层。

Example Image

解决方法

前提

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

关于树模型的注意事项

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

[解决方法]

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

这可确保不会删除实际显示的(上一个)索引窗口小部件,因为只有它的容器将进入,因此我们可以为另一个索引设置该窗口小部件。

幸运的是,QTreeWidget可以更轻松地访问这些项目,因为这些项目是实际的和持久的对象,即使它们已经移动,也可以进行跟踪(与QTreeView中的QModelIndex不同)。

在下面的示例中(使用注释中提供的信息进行了更新),我正在创建顶级项目,并且仅允许在第一级放置。这是一个基本示例,您可能需要添加一些功能:例如,如果某个项目组合未设置为“重复”,则防止删除,或者甚至创建一个已经设置为“重复”的新父项并手动添加子项。 / p>

setItemWidget()

更新(Windows修复)

似乎有一个可能的错误,发生在Windows(至少在Qt 5.13和Windows 10上):单击某项并然后单击组合框后,树小部件会收到一个错误。一堆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) # 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,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,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,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) layout.addWidget(widget) self.setItemWidget(item,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) 触发拖动。不幸的是,我无法进行进一步的测试,但这是一个可能的解决方法:

mouseMoveEvent

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...