QTreeView QAbstractItemModel parent collapses after deleting item and sometimes crashes

南笙酒味 提交于 2021-02-11 15:26:23

问题


Im trying to build a little list of entries within a QTreeView, and based on the example posted here, I got it to delete any child items via the right click context menu i added. but when i delete it the parent tree i have collapses. And in some cases if i delete a certain item in a certain order this crashes

My understanding is that this is because upon deletion the indices of the items changes, and to prevent that QtCore.QPersistentModelIndex() can be utilized according to this thread: How to delete multiple rows in a QTableView widget?

Although that example uses a QStandardItemModel(), since mine uses a QAbstractItemModel() how can I implement a similar concept, and also prevent this from crashing...?

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: https://stackoverflow.com/questions/48121393/how-to-delete-multiple-rows-in-a-qtableview-widget
        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_())

回答1:


beginRemoveRows() expects the QModelIndex, which is the parent of the QModelIndex to be removed, as the first parameter. Regarding the example that you indicate in the comments of your code in the table type models, the indexes do not have a parent, so it is passed an invalid 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()


来源:https://stackoverflow.com/questions/63367912/qtreeview-qabstractitemmodel-parent-collapses-after-deleting-item-and-sometimes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!