-1

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

Ferrus
  • 1
  • 2
  • That's not impossible, but also quite difficult if you don't have experience with tree models in Qt, their internal pointers and persistent model indexes. You would probably have better results by creating a *virtual model (maybe a QStandardItemModel) that just maps the tree items to the relative lists. – musicamante Aug 15 '23 at 21:36
  • See [ask]. It's unclear. This is an XY problem. You should not use the ```QAbstractProxyModel``` in this case. If you want a tree view, you normally define two tree models, one for pure Python and one for Qt(such as the ```QAbstractItemModel```). You didn't define both models. Start with the first one and then try the second one. – relent95 Aug 16 '23 at 01:13
  • For example, the ```AccountItem``` should not be coupled with the ```QModelIndex```. And the ```TreeProxyModel.index()``` should not create a new ```AccountItem``` instance, but refer to an already existing instance stored in the Python model. – relent95 Aug 16 '23 at 01:21
  • @relent95, I would like to have only one model with data. As I said, I use several different views for the same dataset. Only one of the views has a tree structure. I can try changing the source QAbstractListModel to QAbstractItemModel, but I don't need more than one column in the source model. For example, I use this source model to display a list of tokens in QComboBox. Therefore, I need the tree structure to be set in the proxy model. If I "should not use the QAbstractProxyModel in this case", then what proxy model should I use? Or do you suggest using QAbstractItemModel as a proxy model? – Ferrus Aug 16 '23 at 15:01
  • No, the ```QAbstractProxyModel``` is normally used for filtering or sorting items. You mean "one model with data" by a Qt Model with Python data? I mean they are two models. Your Python data should be a tree. Then you can define a ```QAbstractItemModel``` based Qt tree model with that Python data. I'm saying one Python data(model) and multiple Qt models for multiple views. – relent95 Aug 17 '23 at 00:53

1 Answers1

0

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

Ferrus
  • 1
  • 2