QTreeView QAbstractItemModel 父级在删除项目后崩溃,有时会崩溃

QTreeView QAbstractItemModel parent collapses after deleting item and sometimes crashes

我正在尝试在 QTreeView 中构建一个小条目列表,并且基于 on the example posted here,我通过添加的右键单击上下文菜单删除了所有子项。但是当我删除它时,我的父树崩溃了。在某些情况下,如果我按特定顺序删除特定项目,则会崩溃

我的理解是,这是因为删除后项目的索引发生了变化,为了防止QtCore.QPersistentModelIndex()可以根据这个线程使用:

尽管该示例使用了 QStandardItemModel(),但由于我使用的是 QAbstractItemModel(),我如何才能实现类似的概念,并防止其崩溃...?

import sys
from functools import partial
from PyQt4 import QtGui, QtCore

HORIZONTAL_HEADERS = ("Asset Name", "Date Added")


class AssetClass(object):
    '''
    a trivial custom data object
    '''

    def __init__(self, **kwargs):
        if not kwargs.get('name') or not kwargs.get('type'):
            return
        self.name = kwargs.get('name')
        self.date_added = kwargs.get('date_added')
        self.type = kwargs.get('type')

    def __repr__(self):
        return "%s - %s %s" % (self.type, self.name, self.date_added)


class TreeItem(object):
    '''
    a python object used to return row/column data, and keep note of
    it's parents and/or children
    '''

    def __init__(self, asset, header, parent_item):
        self.asset = asset
        self.parent_item = parent_item
        self.header = header
        self.child_items = []

    def appendChild(self, item):
        self.child_items.append(item)

    def removeChild(self, item):
        print 'removeChild: item is %s' % item
        print 'removeChild: self.child_items is %s' % self.child_items
        self.child_items.remove(item)

    def child(self, row):
        return self.child_items[row]

    def childCount(self):
        return len(self.child_items)

    def columnCount(self):
        return 2

    def data(self, column):
        if self.asset == None:
            if column == 0:
                return QtCore.QVariant(self.header)
            if column == 1:
                return QtCore.QVariant("")
        else:
            if column == 0:
                return QtCore.QVariant(self.asset.name)
            if column == 1:
                return QtCore.QVariant(self.asset.date_added)
        return QtCore.QVariant()

    def parent(self):
        return self.parent_item

    def row(self):
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0


class TreeModel(QtCore.QAbstractItemModel):
    '''
    a model to display a few names, ordered by sex
    '''

    def __init__(self, parent=None):
        super(TreeModel, self).__init__(parent)
        self.assets = []
        model_data = (("VEHICLE", "Truck", 'May 27th, 2020'),
                      ("VEHICLE", "Car", 'May 25th, 2020'),
                      ("CHARACTER", "Peter", 'May 27th, 2020'),
                      ("CHARACTER", "Rachel", 'May 29th, 2020'),
                      ("PROP", "Chair", 'May 27th, 2020'),
                      ("PROP", "Axe", 'May 17th, 2020'))
        for asset_type, name, date in model_data:
            asset = AssetClass(type=asset_type, name=name, date_added=date)
            self.assets.append(asset)

        self.rootItem = TreeItem(None, "ALL", None)
        self.parents = {0: self.rootItem}
        self.setupModelData()

    def columnCount(self, parent=None):
        if parent and parent.isValid():
            return parent.internalPointer().columnCount()
        else:
            return len(HORIZONTAL_HEADERS)

    def data(self, index, role):
        if not index.isValid():
            return QtCore.QVariant()

        item = index.internalPointer()
        if role == QtCore.Qt.DisplayRole:
            return item.data(index.column())
        if role == QtCore.Qt.UserRole:
            if item:
                return item.asset

        return QtCore.QVariant()

    def headerData(self, column, orientation, role):
        if (orientation == QtCore.Qt.Horizontal and
                role == QtCore.Qt.DisplayRole):
            try:
                return QtCore.QVariant(HORIZONTAL_HEADERS[column])
            except IndexError:
                pass

        return QtCore.QVariant()

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        if not parent.isValid():
            parent_item = self.rootItem
        else:
            parent_item = parent.internalPointer()

        childItem = parent_item.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()
        if not childItem:
            return QtCore.QModelIndex()

        parent_item = childItem.parent()

        if parent_item == self.rootItem:
            return QtCore.QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.column() > 0:
            return 0
        if not parent.isValid():
            p_Item = self.rootItem
        else:
            p_Item = parent.internalPointer()
        return p_Item.childCount()

    def setupModelData(self):
        for asset in self.assets:
            asset_type = asset.type

            if not self.parents.has_key(asset_type):
                new_parent = TreeItem(None, asset_type, self.rootItem)
                self.rootItem.appendChild(new_parent)

                self.parents[asset_type] = new_parent

            print 'self.parents: ', self.parents
            parent_item = self.parents[asset_type]
            new_item = TreeItem(asset, "", parent_item)
            parent_item.appendChild(new_item)

    def addSubRow(self, new_asset):
        asset_type, name, date = new_asset
        asset = AssetClass(type=asset_type, name=name, date_added=date)
        parent_item = self.parents[asset_type]
        already_exists = False
        for child in parent_item.child_items:
            if child.asset.name == name and child.asset.type == asset_type:
                already_exists = True
        if already_exists:
            print 'this asset already exists'
            return
        new_item = TreeItem(asset, "", parent_item)
        parent_item.appendChild(new_item)

    def removeRow(self, rowIndexes):
        child_tree_item = rowIndexes[0].internalPointer()
        asset_type = rowIndexes[0].parent().data().toString()
        parent_item = self.parents[str(asset_type)]
        # hint to keep the tree open after deleting: 
        self.beginRemoveRows(QtCore.QModelIndex(), rowIndexes[0].row(), rowIndexes[0].row() + 1)
        parent_item.removeChild(child_tree_item)
        self.endRemoveRows()

    def searchModel(self, asset):
        '''
        get the modelIndex for a given appointment
        '''

        def searchNode(node):
            '''
            a function called recursively, looking at all nodes beneath node
            '''
            for child in node.child_items:
                print child.childCount()
                if asset == child.asset:
                    index = self.createIndex(child.row(), 0, child)
                    return index

                if child.childCount() > 0:
                    result = searchNode(child)
                    if result:
                        return result

        retarg = searchNode(self.parents[0])
        print retarg
        return retarg

    def findAssetName(self, name):
        app = None
        for asset in self.assets:
            print asset.name
            if asset.name == name:
                app = asset
                break
        if app != None:
            index = self.searchModel(app)
            return (True, index)
        return (False, None)


class TreeView(QtGui.QTreeView):
    right_button_clicked = QtCore.pyqtSignal(list, int)

    def __init__(self, parent=None):
        super(TreeView, self).__init__(parent)
        # self.clicked.connect(self.on_treeView_clicked)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.openMenu)

    def selectedRows(self):
        rows = []
        for index in self.selectedIndexes():
            if index.column() == 0:
                rows.append(index.row())
        print type(rows)
        return rows

    def openMenu(self, position):
        indexes = self.selectedIndexes()
        if len(indexes) > 0:

            level = 0
            index = indexes[0]
            while index.parent().isValid():
                index = index.parent()
                level += 1

        menu = QtGui.QMenu()
        editMenu = None
        if level == 0:
            editMenu = QtGui.QAction("Edit person", self)
            menu.addAction(editMenu)
        elif level == 1:
            editMenu = QtGui.QAction("Delete", self)
            menu.addAction(editMenu)
        elif level == 2:
            editMenu = QtGui.QAction("Edit object", self)
            menu.addAction(editMenu)

        if editMenu:
            editMenu.triggered.connect(partial(self._on_right_click, indexes, level))

        menu.exec_(self.viewport().mapToGlobal(position))

    def _on_right_click(self, indexes, level):
        self.right_button_clicked.emit(indexes, level)

    def delete_test(self):
        print 'addButton clicked'
        new_asset = ("CHARACTER", "Smith", 'May 28th, 2020')
        asset_type, name, date = new_asset
        asset = AssetClass(type=asset_type, name=name, date_added=date)
        parent_item = self.tree_view.model().parents[asset_type]
        already_exists = False
        for child in parent_item.child_items:
            if child.asset.name == name and child.asset.type == asset_type:
                already_exists = True
        if already_exists:
            print 'this asset already exists'
            return
        new_item = TreeItem(asset, "", parent_item)
        parent_item.appendChild(new_item)
        self.tree_view.model().layoutChanged.emit()

class Dialog(QtGui.QDialog):
    add_signal = QtCore.pyqtSignal(int)

    def __init__(self, parent=None):
        super(Dialog, self).__init__(parent)
        self.setMinimumSize(300, 150)

        self.model = TreeModel()
        layout = QtGui.QVBoxLayout(self)

        self.tree_view = TreeView(self)
        self.tree_view.setModel(self.model)
        self.tree_view.setAlternatingRowColors(True)
        self.tree_view.right_button_clicked.connect(self.deleteButtonClicked)
        layout.addWidget(self.tree_view)

        label = QtGui.QLabel("Search for the following person")
        layout.addWidget(label)

        buts = []
        frame = QtGui.QFrame(self)
        layout2 = QtGui.QHBoxLayout(frame)

        for asset in self.model.assets:
            but = QtGui.QPushButton(asset.name, frame)
            buts.append(but)
            layout2.addWidget(but)
            QtCore.QObject.connect(but, QtCore.SIGNAL("clicked()"), self.but_clicked)

        layout.addWidget(frame)

        self.add_button = QtGui.QPushButton("Add \"Character - Smith\"")
        layout.addWidget(self.add_button)
        QtCore.QObject.connect(self.add_button, QtCore.SIGNAL("clicked()"), self.addButtonClicked)

        self.delete_button = QtGui.QPushButton("Delete Selected")
        layout.addWidget(self.delete_button)
        QtCore.QObject.connect(self.delete_button, QtCore.SIGNAL("clicked()"), self.tree_view.clearSelection)

        self.but = QtGui.QPushButton("Clear Selection")
        layout.addWidget(self.but)
        QtCore.QObject.connect(self.but, QtCore.SIGNAL("clicked()"), self.tree_view.clearSelection)

        QtCore.QObject.connect(self.tree_view, QtCore.SIGNAL("clicked (QModelIndex)"), self.row_clicked)

    def row_clicked(self, index):
        '''
        when a row is clicked... show the name
        '''
        print 'row_clicked index type: %s' % index
        print self.tree_view.model().data(index, QtCore.Qt.UserRole)


    def but_clicked(self):
        '''
        when a name button is clicked, I iterate over the model,
        find the person with this name, and set the treeviews current item
        '''
        name = self.sender().text()
        print "BUTTON CLICKED:", name
        result, index = self.model.findAssetName(name)
        if result:
            if index:
                self.tree_view.setCurrentIndex(index)
                return
        self.tree_view.clearSelection()

    def addButtonClicked(self):
        print 'addButton clicked'
        new_asset = ("CHARACTER", "Smith", 'May 28th, 2020')
        self.tree_view.model().addSubRow(new_asset)
        self.tree_view.model().layoutChanged.emit()

    @QtCore.pyqtSlot(list, int)
    def deleteButtonClicked(self, indexes, level):
        print 'deleteButton clicked'
        self.tree_view.model().removeRow(indexes)
        self.tree_view.model().layoutChanged.emit()

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    dialog = Dialog()
    dialog.show()
    sys.exit(app.exec_())

beginRemoveRows() 期望 QModelIndex 作为第一个参数,它是要删除的 QModelIndex 的父级。关于您在 table 类型模型的代码注释中指出的示例,索引没有父级,因此传递了一个无效的 QModelIndex。

def removeRow(self, rowIndexes):
    child_tree_item = rowIndexes[0].internalPointer()
    parent_item = child_tree_item.parent()
    self.beginRemoveRows(
        rowIndexes[0].parent(), rowIndexes[0].row(), rowIndexes[0].row() + 1
    )
    parent_item.removeChild(child_tree_item)
    self.endRemoveRows()