首页 > 解决方案 > 当源模型中的“removeRows()”时自定义代理模型崩溃

问题描述

我最近正在学习如何使用代理模型,并且我想创建一个可以将源树模型中的节点扁平化为列表模型的自定义代理模型,我找到了一个很好的解决方案: How to create a proxy model that would将 QAbstractItemModel 的节点展平为 PySide 中的列表?

但是,当我尝试removeRows()从源树模型中(删除树节点)时,代理模型崩溃,我猜这是因为源模型没有layoutChanged向代理模型发出信号以刷新self.m_rowMapself.m_indexMap

【问题1】:如何修复崩溃?

【问题2】:对于QSortFilterProxyModelremoveRows()从源模型不会使代理模型崩溃,所以我也想知道它的底层机制QSortFilterProxyModel,特别是以下方法的实现:

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

sourceModel尤其是它如何在和之间发出信号QSortFilterProxyModel

重现示例

#!/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_())

标签: pythonpyqt5

解决方案


问题是当从源模型中删除一个项目时,不会通知代理并且“地图”没有更新。一种可能的解决方案是将 rowsRemoved 信号连接到 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())

推荐阅读