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:
- Click on any item
- Click on the “Add row” button
- Click on the newly created item
- Click “Add row” button again Expected result: a child item is added Actual result: nothing happens.
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"))
Advertisement
Answer
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.