我正在将我的自定义项目模型(从QAbstractItemModel子类化)与自定义QTreeView一起使用。我想允许内部拖放动作(MoveAction)和,当按下修饰键或鼠标右键时,将CopyAction传递到我的模型(到dropMimeData)以复制项目。但是,QTreeView中dropEvent()的默认实现似乎(从C代码开始)似乎只能传递MoveAction,但是当我尝试在QTreeView子类中重新实现dropEvent()时,如下所示:
def dropEvent(self, e):
index = self.indexAt(e.pos())
parent = index.parent()
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
...它可以工作,但是在用户交互方面却非常糟糕,因为有大量复杂的代码确定在默认实现中放置项目的正确索引。当我尝试修改操作并调用超类时:super(Tree, self).dropEvent(e)
dropAction()数据也丢失。
为了修改dropAction而又不失去default dropEvent为我所做的所有奇特的事情,我该怎么办?
我当前的WIP代码太糟了(我希望它接近最小的示例)
from copy import deepcopy
import pickle
import config_editor
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt
from PyQt5.QtGui import QCursor, QStandardItemModel
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu
class ConfigModelItem:
def __init__(self, label, value="", is_section=False, state='default', parent=None):
self.itemData = [label, value]
self.is_section = is_section
self.state = state
self.childItems = []
self.parentItem = parent
if self.parentItem is not None:
self.parentItem.appendChild(self)
def appendChild(self, item):
self.childItems.append(item)
item.parentItem = self
def addChildren(self, items, row):
if row == -1:
row = 0
self.childItems[row:row] = items
for item in items:
item.parentItem = self
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return 2
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def set_data(self, data, column):
try:
self.itemData[column] = data
except IndexError:
return False
return True
def parent(self):
return self.parentItem
def row(self):
if self.parentItem is not None:
return self.parentItem.childItems.index(self)
return 0
def removeChild(self, position):
if position < 0 or position > len(self.childItems):
return False
child = self.childItems.pop(position)
child.parentItem = None
return True
def __repr__(self):
return str(self.itemData)
class ConfigModel(QtCore.QAbstractItemModel):
def __init__(self, data, parent=None):
super(ConfigModel, self).__init__(parent)
self.rootItem = ConfigModelItem("Option", "Value")
self.setup(data)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.rootItem.data(section)
def columnCount(self, parent):
return 2
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
parentItem = self.nodeFromIndex(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem or parentItem is None:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def nodeFromIndex(self, index):
if index.isValid():
return index.internalPointer()
return self.rootItem
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole or role == Qt.EditRole:
return item.data(index.column())
return None
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
item = index.internalPointer()
if role == Qt.EditRole:
item.set_data(value, index.column())
self.dataChanged.emit(index, index, (role,))
return True
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # Qt.NoItemFlags
item = index.internalPointer()
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 0:
flags |= int(QtCore.Qt.ItemIsDragEnabled)
if item.is_section:
flags |= int(QtCore.Qt.ItemIsDropEnabled)
if index.column() == 1 and not item.is_section:
flags |= Qt.ItemIsEditable
return flags
def supportedDropActions(self):
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
def mimeTypes(self):
return ['app/configitem', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
index = indexes[0]
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
print('action', action)
if action == Qt.IgnoreAction:
return True
droppedNode = deepcopy(pickle.loads(mimedata.data('app/configitem')))
print('copy', action & Qt.CopyAction)
print(droppedNode.itemData, 'node')
self.insertItems(row, [droppedNode], parentIndex)
self.dataChanged.emit(parentIndex, parentIndex)
if action & Qt.CopyAction:
return False # to not delete original item
return True
def removeRows(self, row, count, parent):
print('rem', row, count)
self.beginRemoveRows(parent, row, row+count-1)
parentItem = self.nodeFromIndex(parent)
for x in range(count):
parentItem.removeChild(row)
self.endRemoveRows()
print('removed')
return True
@QtCore.pyqtSlot()
def removeRow(self, index):
parent = index.parent()
self.beginRemoveRows(parent, index.row(), index.row())
parentItem = self.nodeFromIndex(parent)
parentItem.removeChild(index.row())
self.endRemoveRows()
return True
def insertItems(self, row, items, parentIndex):
print('ins', row)
parent = self.nodeFromIndex(parentIndex)
self.beginInsertRows(parentIndex, row, row+len(items)-1)
parent.addChildren(items, row)
print(parent.childItems)
self.endInsertRows()
self.dataChanged.emit(parentIndex, parentIndex)
return True
def setup(self, data: dict, parent=None):
if parent is None:
parent = self.rootItem
for key, value in data.items():
if isinstance(value, dict):
item = ConfigModelItem(key, parent=parent, is_section=True)
self.setup(value, parent=item)
else:
parent.appendChild(ConfigModelItem(key, value))
def to_dict(self, parent=None) -> dict:
if parent is None:
parent = self.rootItem
data = {}
for item in parent.childItems:
item_name, item_data = item.itemData
if item.childItems:
data[item_name] = self.to_dict(item)
else:
data[item_name] = item_data
return data
@property
def dict(self):
return self.to_dict()
class ConfigDialog(config_editor.Ui_config_dialog):
def __init__(self, data):
super(ConfigDialog, self).__init__()
self.model = ConfigModel(data)
def setupUi(self, config_dialog):
super(ConfigDialog, self).setupUi(config_dialog)
self.config_view = Tree()
self.config_view.setObjectName("config_view")
self.config_view.setModel(self.model)
self.gridLayout.addWidget(self.config_view, 0, 0, 1, 1)
self.config_view.expandAll()
#self.config_view.setDragDropMode(True)
#self.setDragDropMode(QAbstractItemView.InternalMove)
#self.setDragEnabled(True)
#self.setAcceptDrops(True)
#self.setDropIndicatorShown(True)
self.delete_button.pressed.connect(self.remove_selected)
def remove_selected(self):
index = self.config_view.selectedIndexes()[0]
self.model.removeRow(index)\
class Tree(QTreeView):
def __init__(self):
QTreeView.__init__(self)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.open_menu)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setAnimated(True)
def dropEvent(self, e):
print(e.dropAction(), 'baseact', QtCore.Qt.CopyAction)
# if e.keyboardModifiers() & QtCore.Qt.AltModifier:
# #e.setDropAction(QtCore.Qt.CopyAction)
# print('copy')
# else:
# #e.setDropAction(QtCore.Qt.MoveAction)
# print("drop")
print(e.dropAction())
#super(Tree, self).dropEvent(e)
index = self.indexAt(e.pos())
parent = index.parent()
print('in', index.row())
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
def open_menu(self):
menu = QMenu()
menu.addAction("Create new item")
menu.exec_(QCursor.pos())
if __name__ == '__main__':
import sys
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
sys.excepthook = except_hook
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
data = {"section 1": {"opt1": "str", "opt2": 123, "opt3": 1.23, "opt4": False, "...": {'subopt': 'bal'}},
"section 2": {"opt1": "str", "opt2": [1.1, 2.3, 34], "opt3": 1.23, "opt4": False, "...": ""}}
ui = ConfigDialog(data)
ui.setupUi(Dialog)
print(Qt.DisplayRole)
Dialog.show()
print(app.exec_())
print(Dialog.result())
print(ui.model.to_dict())
sys.exit()
setDragDropMode(QAbstractItemView.InternalMove)
仅允许移动操作(顾名思义,尽管the docs确实在声明方式上留下了一些不确定性)。您可能需要将其设置为QAbstractItemView.DragDrop
模式。您可以使用setDefaultDropAction()
设置默认操作。除此之外,模型还需要返回正确的项目标志和supportedDropActions()
/ canDropMimeData()
,就像您的一样。还有一个dragDropOverwriteMode
属性可能很有趣。
[令我惊讶的一件事是,在模型的dragDropOverwriteMode
方法中,如果您从dropMimeData()
返回True
,则Qt.MoveAction
将自动从模型中删除被拖动的项(使用QAbstractItemView
/ removeRows()
调用您的模型)。如果您的模型实际上已经移动了该行(并删除了旧的那一行),则可能导致令人困惑的结果。我从不完全了解这种行为。 OTOH如果返回removeColumns()
,则对项目视图没有关系,只要实际正确地移动/更新了数据即可。