如何实现Qt MVC代理模型以重新排列树视图中的项/索引?
方案可行性与问题解决
方案原理可行性
完全可行。QAbstractProxyModel(包括QIdentityProxyModel的子类)的核心作用就是基于源模型重构数据的展示结构,无需维护独立的副本模型。你提到的选择模型同步、自动响应源模型数据/结构变化的优势,正是代理模型的原生特性,完全匹配你的需求——通过代理模型将源模型(家庭层级)的叶子节点重新归类到新的父节点(场景层级),完全符合代理模型的设计场景。
代码异常的核心原因与解决方法
你遇到的异常,本质是没有正确处理代理模型自身的层级结构创建:代理模型的场景父节点是源模型中不存在的虚拟节点,不能直接通过源模型的索引映射生成,必须手动在代理模型中创建这些虚拟父索引,并正确实现index()、parent()这两个树模型的核心方法,同时配合mapFromSource()/mapToSource()完成双向映射。
以下是可运行的PySide6示例代码,基于你的需求实现场景层级代理模型:
from PySide6.QtCore import QModelIndex, QAbstractProxyModel, Qt from PySide6.QtGui import QStandardItemModel, QStandardItem from PySide6.QtWidgets import QApplication, QTreeView, QSplitter import sys class SceneProxyModel(QAbstractProxyModel): def __init__(self, scene_mapping, parent=None): super().__init__(parent) # 映射:源叶子索引的唯一标识 -> 场景名称 # 示例:(家庭行, 子行) -> 场景名 self.scene_mapping = scene_mapping # 反向映射:场景名 -> 对应的源叶子索引列表 self.reverse_scene_map = {} for key, scene_name in self.scene_mapping.items(): if scene_name not in self.reverse_scene_map: self.reverse_scene_map[scene_name] = [] self.reverse_scene_map[scene_name].append(key) # 场景列表,作为代理模型的根节点子项 self.scenes = list(self.reverse_scene_map.keys()) def setSourceModel(self, source_model): super().setSourceModel(source_model) # 监听源模型变化,自动同步代理模型 source_model.dataChanged.connect(self.dataChanged) source_model.rowsInserted.connect(self.layoutChanged) source_model.rowsRemoved.connect(self.layoutChanged) # --- 树模型核心方法 --- def index(self, row, column, parent=QModelIndex()): if not self.hasIndex(row, column, parent): return QModelIndex() # 根节点的子项是场景节点 if not parent.isValid(): return self.createIndex(row, column, self.scenes[row]) # 场景节点的子项是对应的源叶子节点 else: scene_name = parent.internalPointer() source_keys = self.reverse_scene_map[scene_name] family_row, child_row = source_keys[row] source_index = self.sourceModel().index(child_row, column, self.sourceModel().index(family_row, 0)) # 用源索引作为代理索引的内部数据,便于后续映射 return self.createIndex(row, column, source_index) def parent(self, index): if not index.isValid(): return QModelIndex() internal_data = index.internalPointer() # 如果内部数据是源索引,说明是叶子节点,父节点是对应的场景 if isinstance(internal_data, QModelIndex): # 找到该源索引对应的场景名 source_key = (internal_data.parent().row(), internal_data.row()) scene_name = self.scene_mapping[source_key] scene_row = self.scenes.index(scene_name) return self.createIndex(scene_row, index.column(), scene_name) # 如果内部数据是场景名,父节点是根 else: return QModelIndex() def rowCount(self, parent=QModelIndex()): if not parent.isValid(): # 根节点的行数是场景数量 return len(self.scenes) else: internal_data = parent.internalPointer() if isinstance(internal_data, str): # 场景节点的行数是对应源叶子的数量 return len(self.reverse_scene_map.get(internal_data, [])) # 叶子节点没有子项 return 0 def columnCount(self, parent=QModelIndex()): # 和源模型列数一致 return self.sourceModel().columnCount() if self.sourceModel() else 0 # --- 索引映射方法 --- def mapFromSource(self, source_index): if not source_index.isValid() or source_index.parent().isValid() is False: # 源模型的家庭节点(非叶子),代理模型不展示,返回无效索引 return QModelIndex() # 源索引是叶子节点,找到对应的场景和行 source_key = (source_index.parent().row(), source_index.row()) scene_name = self.scene_mapping.get(source_key) if not scene_name: return QModelIndex() scene_row = self.scenes.index(scene_name) source_keys = self.reverse_scene_map[scene_name] child_row = source_keys.index(source_key) return self.createIndex(child_row, source_index.column(), source_index) def mapToSource(self, proxy_index): if not proxy_index.isValid(): return QModelIndex() internal_data = proxy_index.internalPointer() if isinstance(internal_data, QModelIndex): # 代理叶子节点对应源索引 return internal_data # 场景节点没有对应的源索引 return QModelIndex() # --- 数据展示 --- def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None internal_data = index.internalPointer() if isinstance(internal_data, str): # 场景节点展示场景名 if role == Qt.DisplayRole: return internal_data else: # 叶子节点直接用源模型的数据 return self.sourceModel().data(self.mapToSource(index), role) if __name__ == "__main__": app = QApplication(sys.argv) splitter = QSplitter() # 构建源模型:家庭层级 source_model = QStandardItemModel() family1 = QStandardItem("家庭1") family1.appendRow(QStandardItem("子A")) family1.appendRow(QStandardItem("子B")) family2 = QStandardItem("家庭2") family2.appendRow(QStandardItem("子C")) source_model.appendRow(family1) source_model.appendRow(family2) # 定义映射:(家庭行, 子行) -> 场景名 scene_mapping = { (0, 0): "场景X", (0, 1): "场景Y", (1, 0): "场景X" } # 创建代理模型 proxy_model = SceneProxyModel(scene_mapping) proxy_model.setSourceModel(source_model) # 两个视图分别展示源模型和代理模型 source_view = QTreeView() source_view.setModel(source_model) proxy_view = QTreeView() proxy_view.setModel(proxy_model) splitter.addWidget(source_view) splitter.addWidget(proxy_view) splitter.show() sys.exit(app.exec())
关键修复点说明
- 虚拟场景节点的创建:代理模型的场景父节点是自定义的虚拟节点,通过
createIndex()创建时,用场景名作为内部数据,而非依赖源模型的索引。 index()/parent()的双向关联:明确区分代理模型中不同层级节点的内部数据类型(场景名/源索引),以此正确返回父索引或子索引,这是解决树结构异常的核心。- 索引映射的边界处理:源模型的非叶子节点(家庭节点)在代理模型中不展示,
mapFromSource()直接返回无效索引;代理模型的场景节点没有对应源索引,mapToSource()返回无效索引。 - 源模型变化同步:通过监听源模型的
dataChanged、rowsInserted等信号,触发代理模型的布局更新,保证数据同步。
内容的提问来源于stack exchange,提问作者sogrady
相关产品推荐
相关产品推荐

