I am using PySide6 to build a UI with a QTreeView that allows users to select a single item from a hierarchy. I have implemented the QStandardItemModel with user-checkable items, and I am updating the check state of the items in response to user input.
However, after the first time an item is checked, the program crashes with the error "RuntimeError: Internal C++ object (PySide6.QtGui.QStandardItem) already deleted."
I have reduced the code to a minimal example, which follows:
from PySide6.QtWidgets import QDialog, QPushButton, QWidget, QLineEdit
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy, QTreeView
from PySide6.QtGui import QIcon, QStandardItemModel, QStandardItem
from PySide6.QtCore import Qt
class MinimalModel(QStandardItemModel):
def __init__(self, parent=None):
super(MinimalModel, self).__init__(parent)
self.root_item = self.invisibleRootItem()
for i in range(2):
item = QStandardItem("parent " + str(i))
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Unchecked)
self.root_item.appendRow(item)
parentItem = item
for j in range(4):
item = QStandardItem("item "+ str(j))
item.setFlags(item.flags() | Qt.ItemIsUserCheckable | Qt.ItemNeverHasChildren)
item.setCheckState(Qt.Unchecked)
parentItem.appendRow(item)
class TreeWidgetSingleSelector(QTreeView):
executing = False
def __init__(self, model):
super().__init__()
self.setModel(model)
self.setHeaderHidden(True)
self.model().dataChanged.connect(self.on_check_state_changed)
def on_check_state_changed(self, index, column):
if self.executing:
return
self.executing = True
# rest of the method code
item = self.model().itemFromIndex(index)
root = self.model().root_item # invisibleRootItem()
for i in range(root.rowCount()):
root.child(i).setCheckState(Qt.Unchecked)
if root.child(i).hasChildren():
for j in range(root.child(i).rowCount()):
root.child(i).child(j).setCheckState(Qt.Unchecked)
item.setCheckState(Qt.Checked)
self.executing = False
class TreeWidgetSingleSelectorWindow(QDialog):
def __init__(self, model, line_widget):
super().__init__()
self.model = model
self.tree = TreeWidgetSingleSelector(self.model)
self.line_widget = line_widget
self.setWindowTitle("Select Single Item")
layout = QVBoxLayout()
layout.addWidget(self.tree)
save_button = QPushButton("Select Item")
save_button.clicked.connect(self.save_item)
h_lout = QHBoxLayout()
h_lout.addStretch(1)
h_lout.addWidget(save_button)
layout.addLayout(h_lout)
self.setLayout(layout)
self.finished.connect(self.deleteLater)
def save_item(self):
if self.tree.model().rowCount() > 0:
items = [self.tree.model().root_item.child(i) for i in range(self.tree.model().root_item.rowCount())]
for item in items:
for i in range(item.rowCount()):
if item.child(i).checkState() == Qt.Checked:
self.line_widget.clear()
self.line_widget.setText(item.text() + "." + item.child(i).text())
self.accept()
def closeEvent(self, event):
# Clean up the tree views when the main window is closed
self.tree.deleteLater()
event.accept()
class LineEditTreeSelector(QWidget):
def __init__(self, model):
super().__init__()
self.model = model
self.line_edit = QLineEdit()
button = QPushButton()
button.setFixedWidth(20)
button.setFixedHeight(20)
button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
button.setIcon(QIcon("art/add_24.png"))
layout = QHBoxLayout(self)
layout.addWidget(button)
layout.addWidget(self.line_edit)
layout.setSpacing(0)
button.clicked.connect(self.on_button_clicked)
def on_button_clicked(self):
tree_selector = TreeWidgetSingleSelectorWindow(self.model, self.line_edit)
tree_selector.exec()
def get_text(self):
return self.line_edit.text()
class scatterplot_widget(QWidget):
def __init__(self, model):
QWidget.__init__(self)
vertical_box = QVBoxLayout(self)
self.tree_selector_x = LineEditTreeSelector(model)
self.tree_selector_y = LineEditTreeSelector(model)
vertical_box.addWidget(self.tree_selector_x)
vertical_box.addWidget(self.tree_selector_y)
if __name__ == '__main__':
from PySide6.QtWidgets import QApplication
import sys
# creating application
app = QApplication(sys.argv)
model = MinimalModel()
main = scatterplot_widget(model)
# showing the window
main.show()
# loop
sys.exit(app.exec_())
Any ideas? Thanks in advance.
I cannot reproduce the issue with PyQt6, but can with PySide (both in Qt 5 and 6). It's probably related to the different way PyQt and PySide wrap the Qt objects and keep their references and how it attempts to destroy them, so it could be a bug.
The issue is mainly caused by this line:
self.root_item = self.invisibleRootItem()
As a rule of thumb, you should never use static references to dynamic objects, and while the root item of a QStandardItemModel should theoretically never change during the lifespan of the model, it still is a QStandardItem that has a reference that could be destroyed, and that's why it should always be accessed dynamically.
If you are doing it for shortness or your own readability, then use a property:
class MinimalModel(QStandardItemModel):
# ...
@property
def root_item(self):
return self.invisibleRootItem()
Note that the issue doesn't happen also if you avoid those explicit deleteLater()
calls, which, by the way, are completely useless: a modal dialog will always automatically destroy itself even if it has a parent, as long as no other reference to the dialog exists, and calling deleteLater()
on the tree is consequentially pointless as well, since objects will recursively delete all their children when they are destroyed.