1

I am learning how to use proxy models recently, And I want to create a custom proxy model that can flat nodes in a source tree model to a list model, I have found a good solution to this: How to create a proxy model that would flatten nodes of a QAbstractItemModel into a list in PySide?

However, when I try to removeRows() (remove tree node) from the source tree model, the proxy model crashes, I guess it's because the source model didn't emit layoutChanged signal to the proxy model to refresh the self.m_rowMap and self.m_indexMap?

【question1】: How to fix the crash?

【question2】:For QSortFilterProxyModel, removeRows() from the source model won't crash the proxy model, so I also want to know the underlying mechanism of QSortFilterProxyModel, especially the implementation of the following methods:

setSourceModel(),
mapFromSource(),
mapToSource(),
mapSelectionFromSource(),
mapSelectionToSource()

Especially how it emits signals between the sourceModel and the QSortFilterProxyModel?

Reproduce example:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import (QAbstractProxyModel, QModelIndex, pyqtSlot)


class FlatProxyModel(QAbstractProxyModel):
    def __init__(self, parent=None):
        # super(FlatProxyModel, self).__init__(parent)
        super().__init__(parent)

    def buildMap(self, model, parent=QModelIndex(), row=0):
        """
        Usage:
            * to build the rowMap and indexMap of the treeModel

        """
        if row == 0:
            self.m_rowMap = {}
            self.m_indexMap = {}
        rows = model.rowCount(parent)
        for r in range(rows):
            index = model.index(r, 0, parent)
            print('row', row, 'item', model.data(index))
            self.m_rowMap[index] = row
            self.m_indexMap[row] = index
            row = row + 1
            if model.hasChildren(index):
                row = self.buildMap(model, index, row)
        return row

    def setSourceModel(self, model):
        QAbstractProxyModel.setSourceModel(self, model)
        self.buildMap(model)
        print(flush=True)
        model.dataChanged.connect(self.sourceDataChanged)

    def mapFromSource(self, index):
        if index not in self.m_rowMap:
            return QModelIndex()
        # print('mapping to row', self.m_rowMap[index], flush = True)
        return self.createIndex(self.m_rowMap[index], index.column())

    def mapToSource(self, index):
        if not index.isValid() or (index.row() not in self.m_indexMap):
            return QModelIndex()
        # print('mapping from row', index.row(), flush = True)
        return self.m_indexMap[index.row()]

    def columnCount(self, parent):
        return QAbstractProxyModel.sourceModel(self).columnCount(self.mapToSource(parent))

    def rowCount(self, parent):
        # print('rows:', len(self.m_rowMap), flush=True)
        return len(self.m_rowMap) if not parent.isValid() else 0

    def index(self, row, column, parent):
        # print('index for:', row, column, flush=True)
        if parent.isValid():
            return QModelIndex()
        return self.createIndex(row, column)

    def parent(self, index):
        return QModelIndex()

    @pyqtSlot(QModelIndex, QModelIndex)
    def sourceDataChanged(self, topLeft, bottomRight):
        self.dataChanged.emit(self.mapFromSource(topLeft),
                              self.mapFromSource(bottomRight))


class myWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.model = QtGui.QStandardItemModel()
        names = ['Foo', 'Bar', 'Baz']
        for first in names:
            row = QtGui.QStandardItem(first)
            for second in names:
                row.appendRow(QtGui.QStandardItem(first + second))
            self.model.appendRow(row)

        self.proxy = FlatProxyModel()
        self.proxy.setSourceModel(self.model)

        self.nestedProxy = FlatProxyModel()
        self.nestedProxy.setSourceModel(self.proxy)

        vLayout = QtWidgets.QVBoxLayout(self)
        hLayout = QtWidgets.QHBoxLayout()
        self.treeView = QtWidgets.QTreeView()
        self.treeView.setModel(self.model)
        self.treeView.expandAll()
        self.treeView.header().hide()
        hLayout.addWidget(self.treeView)

        self.listView1 = QtWidgets.QListView()
        self.listView1.setModel(self.proxy)
        hLayout.addWidget(self.listView1)

        self.listView2 = QtWidgets.QListView()
        self.listView2.setModel(self.nestedProxy)
        hLayout.addWidget(self.listView2)

        vLayout.addLayout(hLayout)

        removeButton = QtWidgets.QPushButton('Remove')
        removeButton.clicked.connect(self.removeItems)
        vLayout.addWidget(removeButton)

    def removeItems(self):
        index = self.treeView.currentIndex()
        model = index.model()
        model.removeRows(index.row(), 1, index.parent())


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = myWidget()
    w.show()
    sys.exit(app.exec_())

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
bactone
  • 107
  • 1
  • 8

1 Answers1

2

The problem is that when removing an item from the source model the proxy is not notified and the "map" is not updated. One possible solution is to connect the rowsRemoved signal to the buildMap.

class FlatProxyModel(QAbstractProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.connections = []

    def buildMap(self, model, parent=QModelIndex(), row=0):
        # ...

    def setSourceModel(self, model):
        if self.sourceModel() is not None:
            for connection in self.connections:
                self.sourceModel().disconnect(connection)
        QAbstractProxyModel.setSourceModel(self, model)
        if self.sourceModel() is None:
            self.connections = []
            return
        self.connections = [
            self.sourceModel().dataChanged.connect(self.sourceDataChanged),
            self.sourceModel().rowsRemoved.connect(self.reload_model),
            self.sourceModel().modelReset.connect(self.reload_model),
            self.sourceModel().rowsInserted.connect(self.reload_model)
        ]
        self.reload_model()

    def reload_model(self):
        self.beginResetModel()
        self.buildMap(self.sourceModel())
        self.endResetModel()

    # ...

    def removeItems(self):
        index = self.treeView.currentIndex()
        if not index.isValid():
            return
        model = index.model()
        model.removeRows(index.row(), 1, index.parent())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • thanks @ eyllanesc, I followed your suggestion and the proxy model doesn't crash, but when I remove rows from the source model, only blank rows is displayed in the listview? – bactone Aug 06 '21 at 05:46