UI crashes when deleting the last child of an item in my QTreeView

61 Views Asked by At

my UI crashes without returning any error message when I try to delete the last child of an Item in my QTreeView. I saw this post that gave a solution but it dosn't work for me, even if I add those lines in the index method:

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

So I tried to use the QAbstractItemModelTester(self.model, QAbstractItemModelTester.FailureReportingMode.Fatal) to see if my model has any problem. It returns this error message : FAIL! flags == Qt::ItemIsDropEnabled || flags == 0 () returned FALSE (C:\Users\qt\work\qt\qtbase\src\testlib\qabstractitemmodeltester.cpp:373) Firstly, I don't know how to fix this error, so if you have any ideas to do so you are welcome. Secondly, I have no idea if it is the reason why the UI crashes when deleting the last child of an item. I tried to make an MRE, but the error doesn't show up in it. The Ui doesn't crash at all, so I'll just leave it bellow to let you check if you see any error that I didn't see and that explains this error in the model.

import sys

from PySide6.QtGui import *
from PySide6.QtCore import *
from PySide6.QtWidgets import *
from PySide6.QtTest import QAbstractItemModelTester




class MainWindow(QMainWindow):
    def __init__(self, parent: QWidget = None):
        super().__init__(parent)
        self.resize(573, 468)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.contextMenuEvent)
        self.menu : QMenu = QMenu(self)
        self.delete_action = self.menu.addAction("Delete")
        self.delete_action.triggered.connect(self.delitem)

        self.frame = QFrame()
        self.setCentralWidget(self.frame)
        self.hlayout = QHBoxLayout()
        self.frame.setLayout(self.hlayout)


        self.view = QTreeView()
        self.view.setAlternatingRowColors(True)
        self.view.setSelectionBehavior(QAbstractItemView.SelectItems)
        self.view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.view.setAnimated(False)
        self.view.setAllColumnsShowFocus(True)
        self.hlayout.addWidget(self.view)
        headers = ["value",'Type','Progress Bar','Test','Test','Test','Test']


        self.model = TreeModel(headers, self)
        self.view.setModel(self.model)

        # QAbstractItemModelTester(self.model, QAbstractItemModelTester.FailureReportingMode.Fatal)

        self.view.expandAll()
        for column in range(self.model.columnCount()):
            self.view.resizeColumnToContents(column)

    def delitem(self) :
        index: QModelIndex = self.view.currentIndex()
        model: QAbstractItemModel = self.model
        model.removeRow(index.row(), index.parent())
        print('deleted')

    def contextMenuEvent(self, event: QContextMenuEvent, point : QPoint = QPoint()) -> None:
        selection_model = self.view.selectionModel()
        has_selection: bool = not selection_model.selection().isEmpty()
        if has_selection :
            self.menu.exec(QCursor.pos())




class TreeItem:

    def __init__(self, data: list, parent: 'TreeItem' = None):
        self.item_data = data
        self.parent_item = parent
        self.child_items = []

    def child(self, number: int) -> 'TreeItem':
        if number < 0 or number >= len(self.child_items):
            return None
        return self.child_items[number]

    def last_child(self) -> 'TreeItem':
        return self.child_items[-1] if self.child_items else None

    def child_count(self) -> int:
        return len(self.child_items)

    def child_number(self) -> int:
        if self.parent_item:
            return self.parent_item.child_items.index(self)
        return 0

    def column_count(self) -> int:
        return len(self.item_data)

    def data(self, column: int):
        if column < 0 or column >= len(self.item_data):
            return None
        return self.item_data[column]

    def insert_children(self, position: int, count: int, columns: int) -> bool:
        if position < 0 or position > len(self.child_items):
            return False

        for row in range(count):
            data = [None] * columns
            item = TreeItem(data.copy(), self)
            self.child_items.insert(position, item)

        return True

    def insert_columns(self, position: int, columns: int) -> bool:
        if position < 0 or position > len(self.item_data):
            return False

        for column in range(columns):
            self.item_data.insert(position, None)

        for child in self.child_items:
            child.insert_columns(position, columns)

        return True

    def parent(self):
        return self.parent_item
    
    def has_parent(self) -> bool:
        if self.parent_item :
            return(True)
        else : 
            return(False)

    def remove_children(self, position: int, count: int) -> bool:
        if position < 0 or position + count > len(self.child_items):
            return False

        for row in range(count):
            self.child_items.pop(position)

        return True

    def remove_columns(self, position: int, columns: int) -> bool:
        if position < 0 or position + columns > len(self.item_data):
            return False

        for column in range(columns):
            self.item_data.pop(position)

        for child in self.child_items:
            child.remove_columns(position, columns)

        return True

    def set_data(self, column: int, value):
        if column < 0 or column >= len(self.item_data):
            return False

        self.item_data[column] = value
        return True

    def __repr__(self) -> str:
        result = f"<treeitem.TreeItem at 0x{id(self):x}"
        for d in self.item_data:
            result += f' "{d}"' if d else " <None>"
        result += f", {len(self.child_items)} children>"
        return result
    
    def __str__(self) -> str:
        result : str = str(self.parent())
        for data in self.item_data :
            result += ',' + str(data)
        return(result)




class TreeModel(QAbstractItemModel):
    
    def __init__(self, headers: list, parent=None):
        super().__init__(parent)

        self.root_data = headers
        self.root_item = TreeItem(self.root_data.copy())
        self.setup_model_data(self.root_item)


    def columnCount(self, parent: QModelIndex = None) -> int:
        return self.root_item.column_count()

    def data(self, index: QModelIndex, role: int = None):
        if not index.isValid():
            return None

        if role not in (0,1,2) :
            return None
        
        item: TreeItem = self.get_item(index)

        return item.data(index.column())

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        if not index.isValid():
            return Qt.ItemIsEditable

        return Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | QAbstractItemModel.flags(self, index)
    
    def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem:
        if index.isValid():
            item: TreeItem = index.internalPointer()
            if item:
                return item

        return self.root_item
      
    def headerData(self, section: int, orientation: Qt.Orientation,
                   role: int = Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.root_item.data(section)

        return None

    def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:

        # if not self.hasIndex(row, column, parent) :
        #     return QModelIndex()

        if parent.isValid() and parent.column() != 0:
            return QModelIndex()

        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return QModelIndex()
        

        child_item: TreeItem = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    def insertColumns(self, position: int, columns: int,
                      parent: QModelIndex = QModelIndex()) -> bool:
        self.beginInsertColumns(parent, position, position + columns - 1)
        success: bool = self.root_item.insert_columns(position, columns)
        self.endInsertColumns()

        return success

    def insertRows(self, position: int, rows: int,
                   parent: QModelIndex = QModelIndex()) -> bool:
        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return False

        self.beginInsertRows(parent, position, position + rows - 1)
        column_count = self.root_item.column_count()
        success: bool = parent_item.insert_children(position, rows, column_count)
        self.endInsertRows()

        return success

    def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex:
        if not index.isValid():
            return QModelIndex()

        child_item: TreeItem = self.get_item(index)
        if child_item:
            parent_item: TreeItem = child_item.parent()
        else:
            parent_item = None

        if parent_item == self.root_item or not parent_item:
            return QModelIndex()

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

    def removeColumns(self, position: int, columns: int,
                      parent: QModelIndex = QModelIndex()) -> bool:
        self.beginRemoveColumns(parent, position, position + columns - 1)
        success: bool = self.root_item.remove_columns(position, columns)
        self.endRemoveColumns()

        if self.root_item.column_count() == 0:
            self.removeRows(0, self.rowCount())

        return success

    def removeRows(self, position: int, rows: int,
                   parent: QModelIndex = QModelIndex()) -> bool:
        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return False

        self.beginRemoveRows(parent, position, position + rows - 1)
        success: bool = parent_item.remove_children(position, rows)
        self.endRemoveRows()

        return success

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        if parent.isValid() and parent.column() > 0:
            return 0

        parent_item: TreeItem = self.get_item(parent)
        if not parent_item:
            return 0
        return parent_item.child_count()

    def setData(self, index: QModelIndex, value, role: int) -> bool:
        if role != Qt.EditRole:
            return False

        item: TreeItem = self.get_item(index)
        result: bool = item.set_data(index.column(), value)

        if result:
            self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole])

        return result
    
    def setData2(self, index: QModelIndex, value, role: int) -> bool:
        if role != Qt.EditRole:
            return False

        item: TreeItem = self.get_item(index)
        result: bool = item.set_data(index.column(), value)

        return result

    def setHeaderData(self, section: int, orientation: Qt.Orientation, value,
                      role: int = None) -> bool:
        if role != Qt.EditRole or orientation != Qt.Horizontal:
            return False

        result: bool = self.root_item.set_data(section, value)

        if result:
            self.headerDataChanged.emit(orientation, section, section)

        return result

    def setup_model_data(self, parent: TreeItem):
    

        parents = [parent] 
        parent : TreeItem = parents[0] 
        col_count = self.root_item.column_count()
        parent.insert_children(parent.child_count(), 1, col_count) 
        column_data = ['test 1','test 2','test 3','test 4','test 5'] 

        for column in range(len(column_data)):
            child = parent.last_child()
            child.set_data(column, column_data[column])

        for i in range(4) :
            child : TreeItem = parent.last_child()
            child.insert_children(child.child_count(), 1, col_count)
            column_data = ['test 4','test 9','test 4','test 0','test 6'] 

            for column in range(len(column_data)):
                child2 = child.last_child()
                child2.set_data(column, column_data[column])


    def _repr_recursion(self, item: TreeItem, indent: int = 0) -> str:
        result = " " * indent + repr(item) + "\n"
        for child in item.child_items:
            result += self._repr_recursion(child, indent + 2)
        return result

    def __repr__(self) -> str:
        return self._repr_recursion(self.root_item)
    

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Now the following piece of code shows what setup_model_data method I have :

def setup_model_data(self, parent: TreeItem, id_projet):

        parents = [parent]
        con : Connexion = Connexion()
        with con.pool.acquire() as connexion :
            with connexion.cursor() as cursor :
                for ensemble in cursor.execute("SELECT nom,id_sous_ensemble,id FROM Sous_Ensemble WHERE id_sous_ensemble = 0 AND id_projet = :1 ORDER BY id_sous_ensemble,nom",[id_projet]) : 
                    parent : TreeItem = parents[0] 
                    col_count = self.root_item.column_count() 
                    parent.insert_children(parent.child_count(), 1, col_count)
                    column_data = [ensemble[0],None,None,-2,ensemble[2]]

                    for column in range(len(column_data)):
                        child = parent.last_child()
                        child.set_data(column, column_data[column])

                    
                    self.recursiviter(ensemble[2],child)


            with connexion.cursor() as cursor1 :
                for piece in cursor1.execute("SELECT designation, id_sous_ensemble, id FROM piece WHERE id_sous_ensemble = 0") :
                    
                    with connexion.cursor() as cursor2 :
                        for rev_info in cursor2.execute("SELECT id_rev,id_mat FROM link_piece_rev WHERE id_piece = :1 ORDER BY id_rev DESC FETCH NEXT 1 ROWS ONLY",[piece[2]]) :
                            id_rev = rev_info[0]
                            id_mat = rev_info[1]
                            parent.insert_children(parent.child_count(), 1, col_count)
                            column_data =[piece[0],id_rev,id_mat,-1,piece[2]]

                            for column in range(len(column_data)):
                                child = parent.last_child()
                                child.set_data(column, column_data[column])
    
    def recursiviter(self,id, parent : TreeItem, id_projet = 1) :
        
        con1 : Connexion = Connexion()
        with con1.pool.acquire() as connexion1 :
            with connexion1.cursor() as cursor2 :
                for ensemble in (cursor2.execute("SELECT nom,id_sous_ensemble,id FROM SOUS_ENSEMBLE WHERE id_sous_ensemble = :1 AND id_projet = :2",[id,id_projet])) :
                    if ensemble :
                        col_count = self.root_item.column_count()
                        parent.insert_children(parent.child_count(),1,col_count)
                        column_data = [ensemble[0],None,None,-2,ensemble[2]]

                        for column in range(len(column_data)):
                            child = parent.last_child()
                            child.set_data(column, column_data[column])
                        
                        self.recursiviter(ensemble[2],parent.last_child())
                    else : 
                        return
                    
                with connexion1.cursor() as cursor3 :
                    for piece in cursor3.execute("SELECT designation, id_sous_ensemble, id FROM piece WHERE id_sous_ensemble = :1",[id]) :
                        if piece :
                            with connexion1.cursor() as cursor4 :
                                for rev_info in cursor4.execute("SELECT id_rev,id_mat FROM link_piece_rev WHERE id_piece = :1 ORDER BY id_rev DESC FETCH NEXT 1 ROWS ONLY",[piece[2]]) :
                                    id_rev = rev_info[0]
                                    id_mat = rev_info[1]
                                    col_count = self.root_item.column_count()
                                    parent.insert_children(parent.child_count(), 1, col_count)
                                    column_data =[piece[0],id_rev,id_mat,-1,piece[2]]
                                    for column in range(len(column_data)):
                                        child = parent.last_child()
                                        child.set_data(column, column_data[column])
                        else :
                            return

I have no idea if this will help you, especially the setup_model_data and recusititer methods as I am creating the model from my database. I followed the exemple that Qt's official website gives to create my QTreeView. I have no clue what I can give you to solve my problem. Feel free to ask for any information in the comments, I will be pleased to answer it quickly.

1

There are 1 best solutions below

1
AudioBubble On

I was able to solve the issue by clearing the model selection after reading out and saving the currentIndex value:

self.TreeView.selectionModel().clear()