1 回答

TA貢獻(xiàn)1833條經(jīng)驗(yàn) 獲得超4個(gè)贊
前提
OP 想要實(shí)現(xiàn)的目標(biāo)并不容易。索引小部件與底層模型無關(guān)(它們不應(yīng)該?。?yàn)樗鼈儍H與項(xiàng)目視圖相關(guān)。由于拖放操作作用于 QMimeData 對象及其內(nèi)容(序列化為字節(jié)數(shù)據(jù)),因此沒有直接方法從放置事件訪問索引小部件。這意味著 d&d 操作僅作用于項(xiàng)目模型,而索引小部件將被完全忽略。
但是,這還不夠。
即使您可以獲得對索引小部件的字節(jié)引用,一旦替換或刪除索引小部件,這些小部件總是會(huì)被刪除:主要問題與以下setItemWidget()
相同setIndexWidget()
:
如果將索引小部件 A 替換為索引小部件 B,則索引小部件 A 將被刪除。
源代碼執(zhí)行以下操作:
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();
? ? }
? ? # ...
}
結(jié)果是,每當(dāng)設(shè)置索引小部件(或刪除索引)時(shí),相關(guān)索引小部件就會(huì)被刪除。從 PyQt 方面來看,我們無法控制這一點(diǎn),除非我們徹底實(shí)現(xiàn)相關(guān)項(xiàng)目視圖類(并且......祝你好運(yùn))。
關(guān)于樹模型的注意事項(xiàng)
Qt 有自己的方式來支持InternalMove
樹模型的標(biāo)志。在下面的解釋中,我假設(shè)拖/放操作總是在SingleSelection
為屬性設(shè)置的模式下發(fā)生selectionMode()
,并且dragDropMode()
設(shè)置為默認(rèn)值InternalMove
。如果您想提供具有擴(kuò)展選擇功能的高級拖放模式的實(shí)現(xiàn),您必須找到自己的實(shí)現(xiàn)(可能通過研究 QAbstractItemView 和 QTreeView 的源代碼)。
[解決辦法] 解決方案
不過,有一個(gè)黑客。
唯一被deleteLater()
調(diào)用的小部件是使用 集設(shè)置的實(shí)際小部件setIndexWidget()
,而不是其子部件。
因此,在這些情況下,要添加對索引小部件拖放的支持,唯一簡單的解決方案是始終添加帶有容器父小部件的索引小部件,并在替換/刪除索引小部件之前從容器中刪除實(shí)際小部件,然后創(chuàng)建在新索引/項(xiàng)目上使用setIndexWidget()
(或)之前,實(shí)際小部件的新容器setItemWidget()
,可能使用遞歸函數(shù)來確保保留子引用。
這確保了實(shí)際顯示的(先前的)索引小部件不會(huì)被刪除,因?yàn)橹挥兴娜萜鲿?huì)被刪除,從而允許我們?yōu)榱硪粋€(gè)索引設(shè)置該小部件。
幸運(yùn)的是,QTreeWidget 可以更輕松地訪問這些項(xiàng)目,因?yàn)檫@些項(xiàng)目是實(shí)際且持久的對象,即使在移動(dòng)后也可以對其進(jìn)行跟蹤(與 QTreeView 中的 QModelIndex 發(fā)生的情況不同)。
在下面的示例中(使用評論中提供的信息進(jìn)行更新),我正在創(chuàng)建頂級項(xiàng)目并僅允許在第一級上放置。這是一個(gè)基本示例,您可能想要添加一些功能:例如,如果項(xiàng)目組合未設(shè)置為“重復(fù)”,則防止掉落,甚至創(chuàng)建一個(gè)已設(shè)置為“重復(fù)”的新父項(xiàng)目并手動(dòng)添加子項(xiàng)目。
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 修復(fù))
似乎存在一個(gè)可能的錯(cuò)誤,該錯(cuò)誤發(fā)生在 Windows 中(至少在 Qt 5.13 和 Windows 10 中):單擊某個(gè)項(xiàng)目然后單擊組合框后,樹小部件會(huì)收到一堆mouseMoveEvent觸發(fā)拖動(dòng)的信息。不幸的是,我無法進(jìn)行進(jìn)一步的測試,但這是一個(gè)可能的解決方法:
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)
添加回答
舉報(bào)