You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Qt中通过代理模型将列表源模型转为树形模型?

问题描述

我有一个基于QAbstractListModel的源模型TokenModel,该模型同时被多个视图使用,内部包含令牌列表_tokens: list[TokenClass]。我希望将其用于QTreeView,为此创建了TreeProxyModel(QAbstractProxyModel),但无法正常显示树形结构的子元素。请问是否可以通过代理模型将QAbstractListModel转换为适配QTreeView的形式?具体该如何实现?

现有代码
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__()
        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(f"getTokenClass()中传入无效行值: {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(f"无效元素类型: 类型: {type(data)}, 值: {data}!")
        else:
            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__()
        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())
问题分析与修复方案

可以通过代理模型将列表模型转换为树形结构,原代码的核心问题在于:

  1. rowCount方法未区分父节点类型:当父节点是账户项(AccountItem)时,尝试访问其accounts属性会引发错误,导致子元素无法正确渲染。
  2. data方法未处理默认情况:未指定角色或其他角色时,未返回QVariant(),不符合Qt模型的规范。
  3. 类型判断使用type()而非isinstance(),兼容性较差。

修复后的TreeProxyModel代码如下:

class TreeProxyModel(QAbstractProxyModel):
    def rowCount(self, parent: QModelIndex = ...) -> int:
        if parent.isValid():
            parent_item = parent.internalPointer()
            # 只有TokenClass类型的节点才有子项(账户)
            if isinstance(parent_item, TokenClass):
                return len(parent_item.accounts)
            # AccountItem类型节点没有子项
            return 0
        else:
            return self.sourceModel().rowCount()

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

    def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
        if not index.isValid():
            return QVariant()
        index_item = index.internalPointer()
        if role == Qt.ItemDataRole.DisplayRole:
            if isinstance(index_item, TokenClass):
                return QVariant(index_item.token)
            elif isinstance(index_item, AccountItem):
                return QVariant(index_item.data())
        # 其他角色返回空QVariant
        return QVariant()

    def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
        if not self.hasIndex(row, column, parent):
            return QModelIndex()
        
        if parent.isValid():
            parent_item = parent.internalPointer()
            # 只有TokenClass的父节点才能创建子账户索引
            if isinstance(parent_item, TokenClass):
                account = parent_item.accounts[row]
                return self.createIndex(row, column, AccountItem(parent, account))
            return QModelIndex()
        else:
            # 顶层节点对应源模型的TokenClass
            token = self.sourceModel().getTokenClass(row)
            return self.createIndex(row, column, token)

    def parent(self, child: QModelIndex) -> QModelIndex:
        if not child.isValid():
            return QModelIndex()
        
        child_item = child.internalPointer()
        if isinstance(child_item, TokenClass):
            # 顶层节点无父节点
            return QModelIndex()
        elif isinstance(child_item, AccountItem):
            # 账户节点的父节点是对应的Token节点
            return child_item.parent()
        else:
            raise TypeError(f"无效元素类型: 类型: {type(child_item)}, 值: {child_item}!")

    def mapFromSource(self, sourceIndex: QModelIndex) -> QModelIndex:
        if not sourceIndex.isValid():
            return QModelIndex()
        # 源模型的索引对应代理模型的顶层节点
        return self.index(sourceIndex.row(), 0, QModelIndex())

    def mapToSource(self, proxyIndex: QModelIndex) -> QModelIndex:
        if not proxyIndex.isValid():
            return QModelIndex()
        parent = proxyIndex.parent()
        if parent.isValid():
            # 账户节点没有对应的源模型索引,返回空
            return QModelIndex()
        else:
            # 顶层节点对应源模型的索引
            return self.sourceModel().index(proxyIndex.row(), 0, QModelIndex())
关键改进点
  • 在rowCount中区分父节点类型,避免访问不存在的属性
  • 完善data方法的默认返回值,符合Qt模型规范
  • 使用isinstance进行类型判断,支持子类扩展
  • 添加hasIndex检查,确保索引创建的合法性

替换修复后的代理模型后,QTreeView就能正确显示树形结构:顶层显示令牌,展开后显示对应的账户列表。

内容的提问来源于stack exchange,提问作者Ferrus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 00:08:10