pythonpyqtpyqt5qtreeviewqsortfilterproxymodel

Inserting rows in QTreeView that uses QSortFilterProxyModel


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:

  1. Click on any item
  2. Click on the "Add row" button
  3. Click on the newly created item
  4. 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"))

Solution

  • 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.