问题描述
Qt的项目视图使用内部application / x-qabstractitemmodeldatalist MIME类型传递项目
我还阅读了this Question,该书主要关注Qlinewidget和QTreewidget中的字符串模型。
但是我如何获取包含多个QtWidget的qframe的QAbstractItemView.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及其子级直到树的第一层。
解决方法
前提
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