I created a custom model for QTreeView. The complete minimal code to show the problem is below. If I add a new node to the root node, by clicking "Add Level 1", it shows up. But if I add a new node to the second level by clicking "Add Level 2", it does not show up. That node only shows up if I collapse the parent node and then expand it again. What part of my MyTreeModel
is wrong?
I am adding the QT tag, even though my code is PySide6, because the fault is probably in my understanding of the methods of QAbstractItemModel, which is not something specific to Python or to PySide.
Full Code
from __future__ import annotations
from typing import Optional
from PySide6.QtCore import QAbstractItemModel, QModelIndex
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QHBoxLayout, QTreeView
class TreeNode:
def __init__(self, name: str, parent_node):
self.name = name
self.parent_node = parent_node
self.children: list[TreeNode] = []
def get_child_by_name(self, name) -> Optional[TreeNode]:
for child in self.children:
if child.name == name:
return child
return None
class MyTreeModel(QAbstractItemModel):
def __init__(self):
super().__init__()
self.root_node = TreeNode("root", None)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return "Name"
def rowCount(self, parentIndex):
if not parentIndex.isValid():
parentNode = self.root_node
else:
parentNode = parentIndex.internalPointer()
return len(parentNode.children)
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
if role == Qt.DisplayRole:
node: TreeNode = index.internalPointer()
column = index.column()
match column:
case 0:
return node.name
case _:
return None
else:
return None
def parent(self, index):
if not index.isValid():
return QModelIndex()
childNode: TreeNode = index.internalPointer()
parentNode = childNode.parent_node
if parentNode == self.root_node:
return QModelIndex()
row_within_parent = parentNode.children.index(childNode)
return self.createIndex(row_within_parent, 0, parentNode);
def index(self, row, column, parentIndex):
if not self.hasIndex(row, column, parentIndex):
return QModelIndex()
if not parentIndex.isValid():
parentNode = self.root_node
else:
parentNode = parentIndex.internalPointer()
child_node = parentNode.children[row]
if child_node:
return self.createIndex(row, column, child_node)
else:
return QModelIndex()
def set_data(self, data: []):
self.beginResetModel()
self.apply_data(data)
self.endResetModel()
def update_data(self, data: []):
self.apply_data(data, True)
def apply_data(self, data, notify=False):
for item in data:
parent_node = self.root_node;
for part in item.split("/"):
existing = parent_node.get_child_by_name(part)
if existing:
parent_node = existing
else:
if notify:
parent_index = self.get_index(parent_node)
count = len(parent_node.children)
self.beginInsertRows(parent_index, count, count)
new_node = TreeNode(part, parent_node)
parent_node.children.append(new_node)
parent_node = new_node
if notify:
self.endInsertRows()
def get_index(self, node: TreeNode):
if not node.parent_node:
return QModelIndex()
row = node.parent_node.children.index(node)
return self.createIndex(row, 0, node.parent_node)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(600, 400)
button1 = QPushButton("Add Level 1")
button1.clicked.connect(self.add1)
button2 = QPushButton("Add Level 2")
button2.clicked.connect(self.add2)
row1 = QHBoxLayout()
row1.setAlignment(Qt.AlignLeft)
row1.addWidget(button1)
row1.addWidget(button2)
self.tree_view = QTreeView()
layout = QVBoxLayout()
layout.addLayout(row1)
layout.addWidget(self.tree_view, stretch=1)
self.central_widget = QWidget()
self.central_widget.setLayout(layout)
self.setCentralWidget(self.central_widget)
def showEvent(self, event):
data = ["mammals", "birds", "mammals/dog", "birds/eagle", "mammals/cat"]
my_model = MyTreeModel()
my_model.set_data(data)
self.tree_view.setModel(my_model)
self.tree_view.expandAll()
def add1(self):
data = ["reptiles"]
m = self.tree_view.model()
m.update_data(data)
def add2(self):
data = ["mammals/rat"]
m = self.tree_view.model()
m.update_data(data)
app = QApplication([])
win = MainWindow()
win.show()
app.exec()
The problem is that you're creating QModelIndexes inconsistently with createIndex()
.
In the index()
override, you use the node itself as pointer for createIndex()
:
def index(self, row, column, parentIndex):
...
if child_node:
return self.createIndex(row, column, child_node)
In get_index()
, instead, you use the parent:
def get_index(self, node: TreeNode):
...
return self.createIndex(row, 0, node.parent_node)
This results in an inconsistent behavior, which you can verify by printing the data of the parent_index = self.get_index(parent_node)
created within the if notify
block:
if notify:
parent_index = self.get_index(parent_node)
print('parent:', parent_index.data()) # < add this line
Which will output:
parent: root
The reason for which it "works" (meaning that the child is properly created) is that the child item is actually added to the correct parent node. The reason for which you don't see it immediately is because you're providing the wrong parent QModelIndex() in beginInsertRow()
, so the view doesn't know what to do with it, as the given parent is not the one that should show further child items, therefore it simply ignores it.
The solution is then simple: change the last argument of createIndex()
to reflect the same behavior:
def get_index(self, node: TreeNode):
...
return self.createIndex(row, 0, node)
Note that your parent()
implementation is also inconsistent, as you're using the wrong pointer and reference. Let's analyze its code:
def parent(self, index):
# correct
if not index.isValid():
return QModelIndex()
childNode = index.internalPointer()
parentNode = childNode.parent_node
# again, correct
if parentNode == self.root_node:
return QModelIndex()
# this only finds the row within the parent!
row_within_parent = parentNode.children.index(childNode)
# this returns an inconsistent index!
return self.createIndex(row_within_parent, 0, parentNode)
parent()
should return the QModelIndex of a given item, but row_within_parent
actually represents the row of the item relative to the parent. createIndex()
is then inconsistent, because the above row does not represent the correct pointer used for it. Instead, you should find the row of the parent through the grand parent.
This is also reflected by the inconsistent display of the branch lines (which may not be shown on some OS/QStyles):
As you can see, the "cat" item doesn't show the branch line of the parent (unlike "dog" does), while the top level "birds" item shows a branch that shouldn't exist, since there are no other siblings after it.
Here is the fixed version of the parent()
function:
def parent(self, index):
if not index.isValid():
return QModelIndex()
childNode = index.internalPointer()
parentNode = childNode.parent_node
if parentNode == self.root_node:
return QModelIndex()
grandParent = parentNode.parent_node
row_within_parent = grandParent.children.index(parentNode)
return self.createIndex(row_within_parent, 0, parentNode)
And the branches properly drawn:
Finally, you probably did this for testing purposes, but you should be very careful in what you do within a showEvent()
, because it can be triggered in many ways and also completely out of control of the user.
If you want to do something when a widget is shown the first time, then either use a QTimer.singleShot(0, someFunction)
, or use an internal bool flag.
class MainWindow(QMainWindow):
def __init__(self):
...
QTimer.singleShot(0, self.initModel)
def initModel(self):
data = ["mammals", "birds", "mammals/dog", "birds/eagle", "mammals/cat"]
my_model = MyTreeModel()
my_model.set_data(data)
self.tree_view.setModel(my_model)
self.tree_view.expandAll()
# alternatively:
class MainWindow(QMainWindow):
_firstShow = False
def showEvent(self, event):
if not self._firstShow:
self._firstShow = True
my_model = MyTreeModel()
...
In reality, the above is probably unnecessary, since the view/model is normally unaffected by delayed show events, and they have internal ways to deal with updates and geometry requirements delayed by later displaying.