I have a very basic app that displays a tree view and a button that adds items to that tree view, using current selection as a parent. Inserting a first level child works well while inserting the 3d level child fails for some reason (it just is not displayed after inserting is done. I've prepared fully verifiable code that you can test yourself, the test case is the following:
Here is the code
main.py
from PyQt5 import QtWidgets
import application
import sys
def main():
app = QtWidgets.QApplication(sys.argv)
window = application.Application()
window.show()
app.exec_()
main()
application.py
from PyQt5 import QtWidgets
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
import tree
from TreeModel import TreeModel
class Application(QtWidgets.QMainWindow, tree.Ui_MainWindow):
data = [
"test1",
"test2",
"test3"
]
def __init__(self):
super().__init__()
self.setupUi(self)
self.proxy_model = QSortFilterProxyModel(self.treeView)
self.model = TreeModel(self.treeView)
for data in self.data:
index = QModelIndex()
self.model.insertRows(self.model.rowCount(index), 1, index)
self.model.setData(self.model.index(self.model.rowCount(index) - 1, 0, index), data)
self.proxy_model.setSourceModel(self.model)
self.treeView.setModel(self.proxy_model)
self.pushButton.clicked.connect(lambda: self.add_row_click())
def add_row_click(self):
index = self.treeView.selectionModel().selectedIndexes()[0]
self.proxy_model.insertRows(self.proxy_model.rowCount(index), 1, index)
self.proxy_model.setData(self.proxy_model.index(self.proxy_model.rowCount(index) - 1, 0, index), "new_test")
TreeItem.py
class TreeItem(object):
ind_column_name = 0
ind_column_id = 1
ind_column_parent_id = 2
key_name = "name"
key_id = "id"
key_parent_id = "parent"
key_new_id = "new_item_id"
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def child(self, row):
try:
return self.childItems[row]
except IndexError:
return ""
def childCount(self):
return len(self.childItems)
def childNumber(self):
if self.parentItem is None:
return self.parentItem.childItems.index(self)
return 0
def columnCount(self):
return len(self.itemData)
def data(self, column):
if column != self.ind_column_id and column != self.ind_column_parent_id:
return self.itemData[column]
return None
def id_data(self, column):
if column == self.ind_column_id or column == self.ind_column_parent_id:
return self.itemData[column]
def insertChildren(self, position, count, columns):
if position < 0 or position > len(self.childItems):
return False
for row in range(count):
data = [None for v in range(columns)]
item = TreeItem(data, self)
self.childItems.insert(position, item)
return True
def insertColumns(self, position, columns):
if position < 0 or position > len(self.itemData):
return False
for column in range(columns):
self.itemData.insert(position, None)
for child in self.childItems:
child.insertColumns(position, columns)
return True
def parent(self):
return self.parentItem
def removeChildren(self, position, count):
if position < 0 or position + count > len(self.childItems):
return False
for row in range(count):
self.childItems.pop(position)
return True
def removeColumns(self, position, columns):
if position < 0 or position + columns > len(self.itemData):
return False
for column in range(columns):
self.itemData.pop(position)
for child in self.childItems:
child.removeColumns(position, columns)
return True
def setData(self, column, value):
if column < 0 or column >= len(self.itemData):
return False
self.itemData[column] = value
return True
def to_json(self):
parent_id = self.itemData[self.ind_column_parent_id]
item_id = self.itemData[self.ind_column_id]
json_data = dict()
json_data[self.key_name] = self.itemData[self.ind_column_name]
if parent_id is not None:
json_data[self.key_parent_id] = parent_id
if item_id is not None:
json_data[self.key_id] = item_id
return json_data
TreeModel.py
from PyQt5.QtCore import (QAbstractItemModel, QModelIndex, Qt)
from TreeItem import TreeItem
class TreeModel(QAbstractItemModel):
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem(["Категории", None, None])
def columnCount(self, parent=QModelIndex()):
# subtract hidden columns
return self.rootItem.columnCount() - 2
def all_rows_count(self, root_item, row_count=0):
if root_item is None:
root_item = self.rootItem
for x in range(root_item.childCount()):
row_count += 1
row_count = self.all_rows_count(root_item.child(x), row_count)
return row_count
def data(self, index, role):
if not index.isValid():
return None
if role != Qt.DisplayRole and role != Qt.EditRole:
return None
item = self.getItem(index)
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return 0
return Qt.ItemIsEditable | super(TreeModel, self).flags(index)
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item
return self.rootItem
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent=QModelIndex()):
if parent.isValid() and parent.column() != 0:
return QModelIndex()
parentItem = self.getItem(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QModelIndex()
def insertColumns(self, position, columns, parent=QModelIndex()):
self.beginInsertColumns(parent, position, position + columns - 1)
success = self.rootItem.insertColumns(position, columns)
self.endInsertColumns()
return success
def insertRows(self, position, rows, parent=QModelIndex(), *args, **kwargs):
parentItem = self.getItem(parent)
self.beginInsertRows(parent, position, position + rows - 1)
success = parentItem.insertChildren(position, rows,
self.rootItem.columnCount())
self.endInsertRows()
return success
def parent(self, index):
if not index.isValid():
return QModelIndex()
childItem = self.getItem(index)
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QModelIndex()
return self.createIndex(parentItem.childNumber(), 0, parentItem)
def removeColumns(self, position, columns, parent=QModelIndex()):
self.beginRemoveColumns(parent, position, position + columns - 1)
success = self.rootItem.removeColumns(position, columns)
self.endRemoveColumns()
if self.rootItem.columnCount() == 0:
self.removeRows(0, self.rowCount())
return success
def removeRows(self, position, rows, parent=QModelIndex()):
parentItem = self.getItem(parent)
self.beginRemoveRows(parent, position, position + rows - 1)
success = parentItem.removeChildren(position, rows)
self.endRemoveRows()
return success
def rowCount(self, parent=QModelIndex()):
parentItem = self.getItem(parent)
return parentItem.childCount()
def setData(self, index, value, role=Qt.EditRole):
if role != Qt.EditRole:
return False
item = self.getItem(index)
result = item.setData(index.column(), value)
if result:
print("setData(), item name = %s, index row = %d" % (str(item.data(TreeItem.ind_column_name)), index.row()))
self.dataChanged.emit(index, index)
else:
print("Failed to set value: " + str(value))
return result
def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
if role != Qt.EditRole or orientation != Qt.Horizontal:
return False
result = self.rootItem.setData(section, value)
if result:
self.headerDataChanged.emit(orientation, section, section)
return result
tree.py (the view file)
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'forso.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 523)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.treeView = QtWidgets.QTreeView(self.centralwidget)
self.treeView.setGeometry(QtCore.QRect(10, 10, 771, 411))
self.treeView.setObjectName("treeView")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(10, 430, 75, 23))
self.pushButton.setObjectName("pushButton")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "Add row"))
The problem only occurs when the element editor is activated, which means that in some way the view is not notified of the changes in the model. At that time, dataChanged
of the model is not sent to the proxy. An alternative solution is to call the proxy dataChanged
and change the state of the expansion or contraction to refresh the view, at the end restore the state of expansion and contraction.
def add_row_click(self):
index = self.treeView.selectionModel().selectedIndexes()[0]
self.treeView.setCurrentIndex(index)
self.proxy_model.insertRows(self.proxy_model.rowCount(index), 1, index)
self.proxy_model.setData(self.proxy_model.index(self.proxy_model.rowCount(index) - 1, 0, index), "new_test")
self.proxy_model.dataChanged.emit(index, index)
v = self.treeView.isExpanded(index)
self.treeView.setExpanded(index, not v)
self.treeView.setExpanded(index, v)
In conclusion if a child is inserted but the view is not updated correctly, this can be verified using another QTreeView
and setting self.model
as a model.