如何在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())
问题分析与修复方案
可以通过代理模型将列表模型转换为树形结构,原代码的核心问题在于:
rowCount方法未区分父节点类型:当父节点是账户项(AccountItem)时,尝试访问其accounts属性会引发错误,导致子元素无法正确渲染。data方法未处理默认情况:未指定角色或其他角色时,未返回QVariant(),不符合Qt模型的规范。- 类型判断使用
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
相关产品推荐
相关产品推荐

