How to use a proxy-model to make tree-model from source model in qt?

211 Views Asked by At

I have a source model source_model = TokenModel(QAbstractListModel), which I use with several views at once. It contains a list of tokens _tokens: list[TokenClass]. I also want to use it in QTreeView. To do this, I create a TreeProxyModel(QAbstractProxyModel), but I can not achieve the display of the child elements of the tree structure. Is it possible to convert QAbstractListModel to QTreeView using a proxy model? How to do it?

    import typing
    from PyQt6 import QtWidgets
    from PyQt6.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant, QAbstractProxyModel

    class TokenClass:
        def __init__(self, token: str, accounts: list[str]):
            self.token: str = token
            self.accounts: list[str] = accounts  # Список счетов.

    class TokenModel(QAbstractListModel):
        def __init__(self, token_class_list: list[TokenClass]):
            super().__init__()  # __init__() QAbstractListModel.
            self._tokens: list[TokenClass] = token_class_list

        def rowCount(self, parent: QModelIndex = ...) -> int:
            return len(self._tokens)

        def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
            if role == Qt.ItemDataRole.DisplayRole:
                token_class: TokenClass = self._tokens[index.row()]
                return QVariant(token_class.token)
            else:
                return QVariant()

        def getTokenClass(self, row: int) -> TokenClass:
            if 0 <= row < self.rowCount():
                return self._tokens[row]
            else:
                raise ValueError("Invalid row value in getTokenClass() ({0})!".format(row))

    class AccountItem:
        def __init__(self, parent: QModelIndex, account: str):
            self._account: str = account
            self._parent: QModelIndex = parent

        def parent(self) -> QModelIndex:
            return self._parent

        def data(self) -> str:
            return self._account

    class TreeProxyModel(QAbstractProxyModel):
        def rowCount(self, parent: QModelIndex = ...) -> int:
            if parent.isValid():
                token: TokenClass = parent.internalPointer()
                return len(token.accounts)
            else:
                return self.sourceModel().rowCount()

        def columnCount(self, parent: QModelIndex = ...) -> int:
            return 1

        def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
            index_item: TokenClass | AccountItem = index.internalPointer()
            if role == Qt.ItemDataRole.DisplayRole:
                if type(index_item) == TokenClass:
                    return index_item.token
                elif type(index_item) == AccountItem:
                    return index_item.data()

        def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
            if parent.isValid():
                token: TokenClass = parent.internalPointer()
                account: str = token.accounts[row]
                return self.createIndex(row, column, AccountItem(parent, account))
            else:
                token: TokenClass = self.sourceModel().getTokenClass(row)
                return self.createIndex(row, column, token)

        def parent(self, child: QModelIndex) -> QModelIndex:
            if child.isValid():
                data: TokenClass | AccountItem = child.internalPointer()
                if type(data) == TokenClass:
                    return QModelIndex()
                elif type(data) == AccountItem:
                    return data.parent()
                else:
                    raise TypeError('Invalid element type: Type: {0}, Value: {1}!'.format(type(data), data))
            else:  # Если индекс child недействителен, то child - это счёт.
                return QModelIndex()

        def mapFromSource(self, sourceIndex: QModelIndex) -> QModelIndex:
            return self.index(sourceIndex.row(), 0, QModelIndex())

        def mapToSource(self, proxyIndex: QModelIndex) -> QModelIndex:
            parent: QModelIndex = proxyIndex.parent()
            if parent.isValid():
                return QModelIndex()
            else:
                return self.sourceModel().index(proxyIndex.row(), 0, QModelIndex())

    class Form(QtWidgets.QMainWindow):
        def __init__(self, tokens: list[TokenClass]):
            super().__init__()  # __init__() QMainWindow.
            self.centralwidget = QtWidgets.QWidget(self)
            self.main_verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
            self.treeView_tokens = QtWidgets.QTreeView(self.centralwidget)
            self.main_verticalLayout.addWidget(self.treeView_tokens)
            self.setCentralWidget(self.centralwidget)

            source_model: TokenModel = TokenModel(tokens)
            proxy_model: TreeProxyModel = TreeProxyModel()
            proxy_model.setSourceModel(source_model)
            self.treeView_tokens.setModel(proxy_model)

    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)

        token1: TokenClass = TokenClass('token1', ['account1', 'account2', 'account3'])
        token2: TokenClass = TokenClass('token2', [])
        token3: TokenClass = TokenClass('token3', ['account1'])
        tokens: list[TokenClass] = [token1, token2, token3]

        window = Form(tokens)
        window.show()
        sys.exit(app.exec())

window

1

There are 1 best solutions below

0
Ferrus On

I managed to achieve the desired data display. To do this, I replaced QAbstractListModel with QAbstractItemModel and QAbstractProxyModel with QAbstractItemModel. Replacing QAbstractListModel with QAbstractItemModel allowed me to display the disclosure icons to the left of the tokens, but the disclosure itself terminated the program with an error. Replacing QAbstractProxyModel with QAbstractItemModel removed this error. I had to use QAbstractItemModel as a proxy-model for TokenModel. This is not exactly what I wanted, I had to compromise to get the result. In order for the TreeProxyModel to react to changes in the source data, I used the dataChanged signal.

from __future__ import annotations
import typing
from PyQt6 import QtWidgets
from PyQt6.QtCore import QModelIndex, Qt, QVariant, QAbstractItemModel

class TokenClass:
    def __init__(self, token: str, accounts: list[str]):
        self.token: str = token
        self.accounts: list[str] = accounts

class TokenModel(QAbstractItemModel):
    def __init__(self, token_class_list: list[TokenClass]):
        super().__init__()  # __init__() QAbstractListModel.
        self._tokens: list[TokenClass] = token_class_list

    def rowCount(self, parent: QModelIndex = ...) -> int:
        return len(self._tokens)

    def columnCount(self, parent: QModelIndex = ...) -> int:
        return 1

    def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
        return self.createIndex(row, column)

    def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
        if role == Qt.ItemDataRole.DisplayRole:
            token_class: TokenClass = self._tokens[index.row()]
            return QVariant(token_class.token)
        else:
            return QVariant()

    def getTokens(self) -> list[TokenClass]:
        return self._tokens

    def getTokenClass(self, row: int) -> TokenClass:
        if 0 <= row < self.rowCount():
            return self._tokens[row]
        else:
            raise ValueError('Invalid row value in getTokenClass() ({0})!'.format(row))

class TreeItem:
    def __init__(self, parent: TreeItem | None, data, children: list[TreeItem], row: int):
        self._parent: TreeItem | None = parent
        self.data = data
        self._children: list[TreeItem] = children
        self._row: int = row

    def parent(self) -> TreeItem | None:
        return self._parent

    def setChildren(self, children: list[TreeItem]):
        self._children = children

    def childrenCount(self) -> int:
        return len(self._children)

    def child(self, row: int) -> TreeItem | None:
        if 0 <= row < self.childrenCount():
            return self._children[row]
        else:
            return None

    def row(self) -> int:
        return self._row

class TreeProxyModel(QAbstractItemModel):
    def __init__(self, sourceModel: TokenModel):
        super().__init__()  # __init__() QAbstractProxyModel.
        self._root_item: TreeItem = TreeItem(None, None, [], 0)
        self._source_model: TokenModel = sourceModel
        self._setTokens()
        self._source_model.dataChanged.connect(self._setTokens)

    def _setTokens(self):
        self.beginResetModel()
        token_list: list[TreeItem] = []
        for row, token in enumerate(self._source_model.getTokens()):
            token_item: TreeItem = TreeItem(self._root_item, token.token, [], row)
            token_item.setChildren([TreeItem(token_item, account, [], j) for j, account in enumerate(token.accounts)])
            token_list.append(token_item)
        self._root_item.setChildren(token_list)
        self.endResetModel()

    def rowCount(self, parent: QModelIndex = ...) -> int:
        if parent.column() > 0: return 0
        if parent.isValid():
            tree_item: TreeItem = parent.internalPointer()
            assert type(tree_item) == TreeItem
        else:
            tree_item: TreeItem = self._root_item
        return tree_item.childrenCount()

    def columnCount(self, parent: QModelIndex = ...) -> int:
        return 1

    def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
        tree_item: TreeItem = index.internalPointer()
        assert type(tree_item) == TreeItem
        if role == Qt.ItemDataRole.DisplayRole:
            return tree_item.data

    def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
        if parent.isValid():
            token_item: TreeItem = parent.internalPointer()
            assert type(token_item) == TreeItem and token_item.parent() == self._root_item
            account_item: TreeItem | None = token_item.child(row)
            if account_item is None:
                return QModelIndex()
            else:
                return self.createIndex(row, column, account_item)
        else:
            token_item: TreeItem | None = self._root_item.child(row)
            if token_item is None:
                return QModelIndex()
            else:
                return self.createIndex(row, column, token_item)

    def parent(self, child: QModelIndex) -> QModelIndex:
        if child.isValid():
            tree_item: TreeItem = child.internalPointer()
            assert type(tree_item) == TreeItem
            parent_item: TreeItem | None = tree_item.parent()
            if tree_item.parent() is None:
                return QModelIndex()
            elif parent_item == self._root_item:
                return QModelIndex()
            else:
                return self.createIndex(parent_item.row(), 0, parent_item)
        else:
            return QModelIndex()

class Form(QtWidgets.QMainWindow):
    def __init__(self, token_list: list[TokenClass]):
        super().__init__()  # __init__() QMainWindow.
        self.centralwidget = QtWidgets.QWidget(self)
        self.main_verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.treeView_tokens = QtWidgets.QTreeView(self.centralwidget)
        self.main_verticalLayout.addWidget(self.treeView_tokens)
        self.setCentralWidget(self.centralwidget)

        source_model: TokenModel = TokenModel(token_list)
        proxy_model: TreeProxyModel = TreeProxyModel(source_model)
        self.treeView_tokens.setModel(proxy_model)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)

    token1: TokenClass = TokenClass('token1', ['account1', 'account2', 'account3'])
    token2: TokenClass = TokenClass('token2', [])
    token3: TokenClass = TokenClass('token3', ['account1'])
    tokens: list[TokenClass] = [token1, token2, token3]

    window = Form(tokens)
    window.show()
    sys.exit(app.exec())

window