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

如何实现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())

关键修复点说明

  1. 虚拟场景节点的创建:代理模型的场景父节点是自定义的虚拟节点,通过createIndex()创建时,用场景名作为内部数据,而非依赖源模型的索引。
  2. index()/parent()的双向关联:明确区分代理模型中不同层级节点的内部数据类型(场景名/源索引),以此正确返回父索引或子索引,这是解决树结构异常的核心。
  3. 索引映射的边界处理:源模型的非叶子节点(家庭节点)在代理模型中不展示,mapFromSource()直接返回无效索引;代理模型的场景节点没有对应源索引,mapToSource()返回无效索引。
  4. 源模型变化同步:通过监听源模型的dataChanged、rowsInserted等信号,触发代理模型的布局更新,保证数据同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 15:04:52