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)
添加回答
舉報