如何在不丢失PyQt5中基本的拖放功能的情况下在treeview中更改dropEvent操作?

问题描述 投票:0回答:1

我正在将我的自定义项目模型(从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()
python pyqt drag-and-drop pyqt5 qtreeview
1个回答
1
投票

setDragDropMode(QAbstractItemView.InternalMove)仅允许移动操作(顾名思义,尽管the docs确实在声明方式上留下了一些不确定性)。您可能需要将其设置为QAbstractItemView.DragDrop模式。您可以使用setDefaultDropAction()设置默认操作。除此之外,模型还需要返回正确的项目标志和supportedDropActions() / canDropMimeData(),就像您的一样。还有一个dragDropOverwriteMode属性可能很有趣。

[令我惊讶的一件事是,在模型的dragDropOverwriteMode方法中,如果您从dropMimeData()返回True,则Qt.MoveAction将自动从模型中删除被拖动的项(使用QAbstractItemView / removeRows()调用您的模型)。如果您的模型实际上已经移动了该行(并删除了旧的那一行),则可能导致令人困惑的结果。我从不完全了解这种行为。 OTOH如果返回removeColumns(),则对项目视图没有关系,只要实际正确地移动/更新了数据即可。

© www.soinside.com 2019 - 2024. All rights reserved.