Skip to content

Latest commit

 

History

History
1950 lines (1472 loc) · 59.9 KB

File metadata and controls

1950 lines (1472 loc) · 59.9 KB

PyMeshGen GUI 模型树组件详细设计报告

文档版本: v2.0 (整合版) 文档位置: gui/模型树组件详细设计报告.md 适用范围: PyMeshGen GUI 中"模型树"控件(gui/model_tree.py)的设计、实现与扩展 当前日期: 2026-01-18 整合来源:

  • gui/模型树组件设计报告.md
  • gui/ModelTree_DesignReport.md
  • gui/temp_docs/model_tree_design.md
  • gui/temp_docs/模型树设计报告.md

目录

  1. 概述
  2. 背景与目标
  3. 总体架构
  4. 数据模型与状态管理
  5. UI设计
  6. 核心功能实现
  7. 交互功能设计
  8. 性能优化策略
  9. 数据管理
  10. 与其他模块的集成
  11. 使用示例
  12. 设计模式
  13. 扩展性设计
  14. 测试建议
  15. 风险、边界与改进建议
  16. 关键实现清单
  17. 后续扩展建议
  18. 术语表
  19. 总结

1. 概述

1.1 组件简介

模型树组件(ModelTreeWidget)是PyMeshGen GUI系统的核心组件之一,负责以树形结构统一展示和管理几何模型、网格数据和部件信息。该组件采用三层架构设计,分别为几何层(Geometry)、网格层(Mesh)和部件层(Parts),为用户提供直观的层次化数据管理界面。

1.2 设计目标

模型树组件的设计目标包括:

  • 统一展示:在一个树形控件中统一管理几何、网格和部件三层结构
  • 高效性能:支持大规模数据的加载和显示,避免界面卡顿
  • 交互友好:提供直观的勾选、展开/折叠、右键菜单等交互方式
  • 数据关联:与视图显示、属性面板等组件紧密联动
  • 扩展性强:易于添加新的元素类型和功能
  • 可扩展:后续增加"边界条件""材料""求解设置"等节点类型时成本低
  • 性能可控:面对大模型(>1e4 / 1e5 拓扑元素)避免 UI 冻结
  • 交互闭环:树上的点击/勾选/右键菜单能驱动 3D 视图显示、对象选择、部件管理等

1.3 技术选型

  • GUI框架: PyQt5
  • 树形控件: QTreeWidget(项目当前选择的是 item-based Tree 控件,而非 model/view 的 QTreeView + QAbstractItemModel
  • 异步处理: QTimer
  • 几何引擎: OpenCASCADE (pythonocc-core)
  • 可视化: VTK
  • 数据结构: QTreeWidget, QTreeWidgetItem

2. 背景与目标

2.1 业务需求

PyMeshGen GUI 需要一个统一的"模型树"来承载三类核心对象的可视化与交互:

  1. 几何(Geometry):来自 OpenCASCADE(OCC)的 TopoDS_Shape 及其子拓扑元素
  2. 网格(Mesh):网格节点/边/面/体等实体(可能来自内部数据结构或导入格式)
  3. 部件(Parts):对几何/网格进行工程化组织(分组、命名、参数、可见性等)

2.2 技术挑战

  • 数据规模:几何模型可能包含数万甚至数十万个拓扑元素
  • 实时性:用户操作需要即时响应,不能阻塞UI线程
  • 内存管理:大量树项的创建和销毁需要合理控制内存占用
  • 状态同步:树项状态需要与3D视图、属性面板等保持同步

2.3 解决方案概述

  • 采用分批加载策略,避免一次性创建大量树项
  • 使用延迟加载机制,超过阈值的元素不完全加载到树中
  • 实现虚拟化支持,超过最大限制时显示摘要信息
  • 通过回调机制实现与其他组件的松耦合

3. 总体架构

3.1 文件与入口

  • 核心实现: gui/model_tree.py
  • 可视组件: PyQt5 QTreeWidget

ModelTreeWidget 并非 QWidget 子类,而是一个"组件封装类",对外提供:

  • self.widget:可直接嵌入布局的容器 QWidget
  • self.tree:内部的 QTreeWidget
  • 一组 load_* / update_* 方法:用于按业务数据重建/刷新树

这种封装结构的优点:

  • 调用者只需把 model_tree.widget 加到布局即可
  • 组件内部可以维护额外状态(geometry_data / mesh_data / parts_data 等)

3.2 整体架构

模型树采用分层设计,主要包含以下层次:

ModelTreeWidget (主控制器)
├── UI层 (QTreeWidget + QTreeWidgetItem)
├── 数据层 (几何/网格/部件数据)
├── 交互层 (事件处理、右键菜单)
└── 接口层 (对外API)

3.3 三层顶层结构(Top Level)

模型树固定建立三个顶层节点(TopLevelItem):

  1. 几何Qt.UserRole = "geometry"
  2. 网格Qt.UserRole = "mesh"
  3. 部件Qt.UserRole = "parts"

其中几何与网格下均预置四个二级分类节点:

  • 点(vertices)
  • 线(edges)
  • 面(faces)
  • 体(bodies)

对应 Qt.UserRole 存储为元组以表达节点语义,例如:

  • ("geometry", "vertices")
  • ("mesh", "faces")

部件节点下则由 parts_data 动态创建。

3.4 树形结构详细设计

根节点
├── 几何 (Geometry)
│   ├── 点 (Vertices)
│   │   ├── 点_0
│   │   ├── 点_1
│   │   └── ...
│   ├── 线 (Edges)
│   ├── 面 (Faces)
│   └── 体 (Bodies)
├── 网格 (Mesh)
│   ├── 点 (Vertices)
│   ├── 线 (Edges)
│   ├── 面 (Faces)
│   └── 体 (Bodies)
└── 部件 (Parts)
    ├── 部件1
    ├── 部件2
    └── ...

3.5 组件关系

  • ModelTreeWidget 作为主控制器,负责整体逻辑管理
  • PartManager 组件紧密协作,处理部件相关操作
  • MeshDisplayArea 组件协同,控制可视化显示
  • 通过回调机制与主窗口通信

4. 数据模型与状态管理

4.1 组件内部状态

ModelTreeWidget 维护三类数据引用及名称:

  • geometry_data, geometry_name
  • mesh_data, mesh_name
  • parts_data, parts_name

并通过 _updating_items(或 blockSignals(True/False))规避"程序性修改 item 导致的递归事件触发"。

4.2 节点语义(UserRole 数据约定)

模型树大量依赖 QTreeWidgetItem.setData(0, Qt.UserRole, payload) 来携带业务信息。

建议在代码层面保持一致的数据结构约定:

  • 顶层类目: "geometry" | "mesh" | "parts"
  • 二级类目: (domain, category) 例如 ( "geometry", "vertices" )
  • 叶子对象: (domain, category, obj_ref, index)

其中:

  • domain 用于区分几何/网格/部件
  • category 用于区分 vertices/edges/faces/bodies
  • obj_ref 是 OCC 拓扑对象引用(或网格/部件对象引用)
  • index 用于稳定命名与定位(例如 点_123

说明:由于 QTreeWidgetItem 本身并不适合存储大型对象图,更理想的做法是只存储轻量 ID,再通过仓库(dict / manager)映射到对象;当前实现直接存储 TopoDS_* 引用属于"可用但需谨慎"的方案(见第15章风险)。

4.3 数据存储格式

4.3.1 几何元素数据

几何元素使用OpenCASCADE的TopoDS对象:

  • 顶点TopoDS_Vertex - 存储坐标信息
  • TopoDS_Edge - 存储曲线信息和长度
  • TopoDS_Face - 存储曲面信息和面积
  • TopoDS_Solid - 存储实体信息和体积

4.3.2 网格元素数据

网格元素使用自定义数据结构:

  • 节点node_coords - 节点坐标数组
  • 单元cell_containercells - 单元容器

4.3.3 部件数据

部件数据支持多种格式:

# 字典格式
{
    'parts_info': [
        {
            'part_name': '部件名称',
            'bc_type': '边界条件类型',
            'geometry_elements': {...},
            'mesh_elements': {...}
        }
    ]
}

# 对象属性格式
parts_data.parts_info = [...]
parts_data.boundary_info = {...}

4.4 性能相关常量

MAX_TREE_ITEMS = 100000      # 最大树项数量,超过则使用虚拟化
LAZY_LOAD_THRESHOLD = 10000  # 延迟加载阈值
BATCH_SIZE = 1000            # 每批处理的元素数量

5. UI设计

5.1 树表头与列设计

  • 两列名称数量
  • 列宽策略
    • 第 0 列 Stretch
    • 第 1 列 ResizeToContents

"数量"列承担两类含义:

  1. 类目节点(如 点/线/面/体)的数量统计
  2. 异步/分批加载时的状态提示(例如 "加载中...")

5.2 可见性/选择的统一交互(CheckState)

所有关键节点默认带复选框(Qt.Checked):

  • 顶层(几何/网格/部件)
  • 子类目(点/线/面/体)
  • 叶子对象(具体点/线/面/体)

勾选动作通常语义为"显示/隐藏"或"启用/禁用"。

实现上通常需要:

  • 向下传播:父节点取消勾选应影响子节点
  • 向上回填:子节点部分选中时父节点应为 PartiallyChecked

当前代码通过 itemChanged + _updating_items/blockSignals 机制避免递归。

5.3 右键菜单(Context Menu)

模型树在 Qt.CustomContextMenu 模式下工作:

  • customContextMenuRequested -> _show_context_menu(pos)

常见菜单操作(取决于 parent/part_manager 是否提供 handler):

  • 部件:创建/删除/重命名/设置参数/导出
  • 几何:定位、显示/隐藏、(可能)隔离显示
  • 网格:显示网格质量、导出某一部分

实现策略:模型树不直接执行业务逻辑,而是通过 _get_parent_handler() 反射查找:

  1. parent.part_manager.<handler_name>
  2. parent.<handler_name>

这使得模型树与业务管理器解耦,便于复用。

5.4 UI样式设计

  • 交替行颜色:启用 setAlternatingRowColors(True) 提高可读性
  • 统一行高:启用 setUniformRowHeights(True) 优化渲染性能
  • 工具提示:为元素添加详细信息提示(坐标、长度、面积、体积等)

6. 核心功能实现

6.1 类设计

class ModelTreeWidget:
    """统一模型树组件 - 三层结构:几何、网格、部件"""
    
    # 性能相关常量
    MAX_TREE_ITEMS = 100000      # 最大树项数量
    LAZY_LOAD_THRESHOLD = 10000  # 延迟加载阈值
    
    def __init__(self, parent=None)
    def _create_tree_widget(self)
    def _setup_ui(self)
    def _init_tree_structure(self)
    
    # 数据加载方法
    def load_geometry(self, shape, geometry_name="几何")
    def load_mesh(self, mesh_data, mesh_name="网格")
    def load_parts(self, parts_data=None)
    
    # 元素提取方法
    def _extract_geometry_elements(self, shape)
    def _extract_mesh_elements(self, mesh_data)
    def _extract_parts_elements(self, parts_data)
    
    # 事件处理方法
    def _on_item_changed(self, item, column)
    def _on_item_clicked(self, item, column)
    def _handle_visibility_change(self, item, element_data)
    def _handle_selection_change(self, item, element_data)
    
    # 右键菜单方法
    def _show_context_menu(self, position)
    def _show_vertex_properties(self, vertex, item)
    def _show_edge_properties(self, edge, item)
    def _show_face_properties(self, face, item)
    def _show_solid_properties(self, solid, item)
    
    # 可见性控制方法
    def _update_child_items(self, item, check_state)
    def _update_parent_item(self, item)
    def set_element_visibility(self, category, element_type, element_index, visible)
    def set_category_visibility(self, category, element_type, visible)
    
    # 查询方法
    def get_visible_elements(self, category=None, element_type=None)
    def get_geometry_type_states(self)
    def get_visible_parts(self)
    
    # 工具方法
    def clear(self)
    def _get_parent_handler(self, handler_name)

6.2 树形控件初始化

6.2.1 控件创建

def _create_tree_widget(self):
    """创建树形控件"""
    self.tree = QTreeWidget()
    self.tree.setHeaderLabels(["名称", "数量"])
    self.tree.setColumnWidth(0, 200)
    self.tree.setColumnWidth(1, 60)
    self.tree.setAlternatingRowColors(True)
    self.tree.setUniformRowHeights(True)
    
    header = self.tree.header()
    header.setStretchLastSection(False)
    header.setSectionResizeMode(0, QHeaderView.Stretch)
    header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
    
    self.tree.itemChanged.connect(self._on_item_changed)
    self.tree.itemClicked.connect(self._on_item_clicked)
    self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
    self.tree.customContextMenuRequested.connect(self._show_context_menu)

设计要点

  • 双列显示:名称列(可伸缩)和数量列(自适应内容)
  • 启用交替行颜色,提高可读性
  • 统一行高,优化渲染性能
  • 连接信号槽,处理交互事件

6.2.2 结构初始化

def _init_tree_structure(self):
    """初始化三层模型树结构"""
    self.tree.clear()
    self.tree.blockSignals(True)
    
    # 创建几何节点
    geometry_item = QTreeWidgetItem(self.tree)
    geometry_item.setText(0, self.geometry_name)
    geometry_item.setText(1, "")
    geometry_item.setExpanded(True)
    geometry_item.setCheckState(0, Qt.Checked)
    geometry_item.setData(0, Qt.UserRole, "geometry")
    
    # 创建几何子节点(点、线、面、体)
    geometry_vertices_item = QTreeWidgetItem(geometry_item)
    geometry_vertices_item.setText(0, "点")
    geometry_vertices_item.setText(1, "0")
    geometry_vertices_item.setCheckState(0, Qt.Checked)
    geometry_vertices_item.setExpanded(False)
    geometry_vertices_item.setData(0, Qt.UserRole, ("geometry", "vertices"))
    
    geometry_edges_item = QTreeWidgetItem(geometry_item)
    geometry_edges_item.setText(0, "线")
    geometry_edges_item.setText(1, "0")
    geometry_edges_item.setCheckState(0, Qt.Checked)
    geometry_edges_item.setExpanded(False)
    geometry_edges_item.setData(0, Qt.UserRole, ("geometry", "edges"))
    
    geometry_faces_item = QTreeWidgetItem(geometry_item)
    geometry_faces_item.setText(0, "面")
    geometry_faces_item.setText(1, "0")
    geometry_faces_item.setCheckState(0, Qt.Checked)
    geometry_faces_item.setExpanded(False)
    geometry_faces_item.setData(0, Qt.UserRole, ("geometry", "faces"))
    
    geometry_bodies_item = QTreeWidgetItem(geometry_item)
    geometry_bodies_item.setText(0, "体")
    geometry_bodies_item.setText(1, "0")
    geometry_bodies_item.setCheckState(0, Qt.Checked)
    geometry_bodies_item.setExpanded(False)
    geometry_bodies_item.setData(0, Qt.UserRole, ("geometry", "bodies"))
    
    # 创建网格节点
    mesh_item = QTreeWidgetItem(self.tree)
    mesh_item.setText(0, self.mesh_name)
    mesh_item.setText(1, "")
    mesh_item.setExpanded(True)
    mesh_item.setCheckState(0, Qt.Checked)
    mesh_item.setData(0, Qt.UserRole, "mesh")
    
    # 创建网格子节点
    mesh_vertices_item = QTreeWidgetItem(mesh_item)
    mesh_vertices_item.setText(0, "点")
    mesh_vertices_item.setText(1, "0")
    mesh_vertices_item.setCheckState(0, Qt.Checked)
    mesh_vertices_item.setData(0, Qt.UserRole, ("mesh", "vertices"))
    
    mesh_edges_item = QTreeWidgetItem(mesh_item)
    mesh_edges_item.setText(0, "线")
    mesh_edges_item.setText(1, "0")
    mesh_edges_item.setCheckState(0, Qt.Checked)
    mesh_edges_item.setData(0, Qt.UserRole, ("mesh", "edges"))
    
    mesh_faces_item = QTreeWidgetItem(mesh_item)
    mesh_faces_item.setText(0, "面")
    mesh_faces_item.setText(1, "0")
    mesh_faces_item.setCheckState(0, Qt.Checked)
    mesh_faces_item.setData(0, Qt.UserRole, ("mesh", "faces"))
    
    mesh_bodies_item = QTreeWidgetItem(mesh_item)
    mesh_bodies_item.setText(0, "体")
    mesh_bodies_item.setText(1, "0")
    mesh_bodies_item.setCheckState(0, Qt.Checked)
    mesh_bodies_item.setData(0, Qt.UserRole, ("mesh", "bodies"))
    
    # 创建部件节点
    parts_item = QTreeWidgetItem(self.tree)
    parts_item.setText(0, self.parts_name)
    parts_item.setText(1, "0")
    parts_item.setExpanded(True)
    parts_item.setCheckState(0, Qt.Checked)
    parts_item.setData(0, Qt.UserRole, "parts")
    
    self.tree.blockSignals(False)

设计要点

  • 使用blockSignals(True)防止初始化时触发事件
  • 使用Qt.UserRole存储元素数据,便于后续访问
  • 所有节点默认展开和选中状态
  • 为每个节点设置正确的数据结构

6.3 几何数据加载

6.3.1 加载流程

def load_geometry(self, shape, geometry_name="几何"):
    """
    加载几何模型到树中(使用分批加载避免阻塞)
    
    Args:
        shape: OpenCASCADE TopoDS_Shape对象
        geometry_name: 几何模型名称
    """
    self.geometry_data = shape
    self.geometry_name = geometry_name
    
    # 更新几何节点名称
    geometry_item = self.tree.topLevelItem(0)
    if geometry_item:
        geometry_item.setText(0, geometry_name)
    
    self._clear_geometry_elements()
    
    if shape is None:
        return
    
    geometry_item = self.tree.topLevelItem(0)
    if geometry_item is None:
        return
    
    vertices_item = geometry_item.child(0)
    edges_item = geometry_item.child(1)
    faces_item = geometry_item.child(2)
    bodies_item = geometry_item.child(3)
    
    from OCC.Core.TopExp import TopExp_Explorer
    from OCC.Core.TopAbs import TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE, TopAbs_SOLID
    
    self.tree.blockSignals(True)
    
    vertex_count = 0
    edge_count = 0
    face_count = 0
    body_count = 0
    
    self.tree.blockSignals(False)
    
    vertices_item.setText(1, "加载中...")
    edges_item.setText(1, "加载中...")
    faces_item.setText(1, "加载中...")
    bodies_item.setText(1, "加载中...")
    
    self._batch_load_geometry_elements(shape, vertices_item, edges_item, faces_item, bodies_item)

6.3.2 分批加载实现

def _batch_load_geometry_elements(self, shape, vertices_item, edges_item, faces_item, bodies_item):
    """
    分批加载几何元素到树中
    
    Args:
        shape: OpenCASCADE TopoDS_Shape对象
        vertices_item: 顶点树项
        edges_item: 边树项
        faces_item: 面树项
        bodies_item: 体树项
    """
    from OCC.Core.TopExp import TopExp_Explorer
    from OCC.Core.TopAbs import TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE, TopAbs_SOLID
    
    BATCH_SIZE = 1000  # 每批处理的元素数量
    
    vertex_explorer = TopExp_Explorer(shape, TopAbs_VERTEX)
    edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
    face_explorer = TopExp_Explorer(shape, TopAbs_FACE)
    body_explorer = TopExp_Explorer(shape, TopAbs_SOLID)
    
    vertex_count = 0
    edge_count = 0
    face_count = 0
    body_count = 0
    
    def process_batch():
        nonlocal vertex_count, edge_count, face_count, body_count
        
        self.tree.blockSignals(True)
        
        # 处理顶点
        for _ in range(BATCH_SIZE):
            if vertex_explorer.More():
                vertex = vertex_explorer.Current()
                
                if vertex_count < self.LAZY_LOAD_THRESHOLD:
                    vertex_item = QTreeWidgetItem(vertices_item)
                    vertex_item.setText(0, f"点_{vertex_count}")
                    vertex_item.setText(1, "")
                    vertex_item.setCheckState(0, Qt.Checked)
                    vertex_item.setData(0, Qt.UserRole, ("geometry", "vertices", vertex, vertex_count))
                
                vertex_count += 1
                vertex_explorer.Next()
        
        # 处理边、面、体(类似)
        # ...
        
        # 更新计数
        vertices_item.setText(1, str(vertex_count))
        edges_item.setText(1, str(edge_count))
        faces_item.setText(1, str(face_count))
        bodies_item.setText(1, str(body_count))
        
        self.tree.blockSignals(False)
        
        # 如果还有元素,继续下一批
        if vertex_explorer.More() or edge_explorer.More() or face_explorer.More() or body_explorer.More():
            QTimer.singleShot(0, process_batch)
        else:
            # 加载完成,更新显示
            handler = self._get_parent_handler('_update_geometry_element_display')
            if handler:
                handler()
    
    QTimer.singleShot(0, process_batch)

设计要点

  • 使用QTimer.singleShot(0, ...)实现异步加载,避免阻塞UI
  • 每批处理1000个元素,平衡性能和响应速度
  • 超过LAZY_LOAD_THRESHOLD(10000)的元素不创建树项,仅更新计数
  • 使用blockSignals防止加载过程中触发事件
  • 加载完成后通知父级更新显示

6.4 网格数据加载

def load_mesh(self, mesh_data, mesh_name="网格"):
    """
    加载网格模型到树中
    
    Args:
        mesh_data: 网格数据对象
        mesh_name: 网格模型名称
    """
    self.mesh_data = mesh_data
    self.mesh_name = mesh_name
    
    mesh_item = self.tree.topLevelItem(1)
    if mesh_item:
        mesh_item.setText(0, mesh_name)
    
    self._clear_mesh_elements()
    self._extract_mesh_elements(mesh_data)

def _extract_mesh_elements(self, mesh_data):
    """
    从网格数据中提取网格元素并添加到树中
    
    Args:
        mesh_data: 网格数据对象
    """
    if mesh_data is None:
        return
    
    mesh_item = self.tree.topLevelItem(1)
    if mesh_item is None:
        return
    
    vertices_item = mesh_item.child(0)
    edges_item = mesh_item.child(1)
    faces_item = mesh_item.child(2)
    bodies_item = mesh_item.child(3)
    
    vertex_count = 0
    edge_count = 0
    face_count = 0
    body_count = 0
    
    self.tree.blockSignals(True)
    
    # 提取顶点
    if hasattr(mesh_data, 'node_coords'):
        node_coords = mesh_data.node_coords
        vertex_count = len(node_coords)
        
        if vertex_count <= self.LAZY_LOAD_THRESHOLD:
            for i, coord in enumerate(node_coords):
                vertex_item = QTreeWidgetItem(vertices_item)
                vertex_item.setText(0, f"点_{i}")
                vertex_item.setText(1, "")
                vertex_item.setCheckState(0, Qt.Checked)
                vertex_item.setData(0, Qt.UserRole, ("mesh", "vertex", i, coord))
                
                # 添加工具提示
                coord_str = f"({coord[0]:.3f}, {coord[1]:.3f}"
                if len(coord) > 2:
                    coord_str += f", {coord[2]:.3f})"
                else:
                    coord_str += ", 0.000)"
                vertex_item.setToolTip(0, f"坐标: {coord_str}")
        else:
            # 超过阈值,只显示前MAX_TREE_ITEMS个
            for i in range(0, min(vertex_count, self.MAX_TREE_ITEMS)):
                coord = node_coords[i]
                vertex_item = QTreeWidgetItem(vertices_item)
                vertex_item.setText(0, f"点_{i}")
                vertex_item.setText(1, "")
                vertex_item.setCheckState(0, Qt.Checked)
                vertex_item.setData(0, Qt.UserRole, ("mesh", "vertex", i, coord))
                
                coord_str = f"({coord[0]:.3f}, {coord[1]:.3f}"
                if len(coord) > 2:
                    coord_str += f", {coord[2]:.3f})"
                else:
                    coord_str += ", 0.000)"
                vertex_item.setToolTip(0, f"坐标: {coord_str}")
            
            if vertex_count > self.MAX_TREE_ITEMS:
                summary_item = QTreeWidgetItem(vertices_item)
                summary_item.setText(0, f"... (还有 {vertex_count - self.MAX_TREE_ITEMS} 个节点)")
                summary_item.setText(1, "")
                summary_item.setCheckState(0, Qt.Checked)
                summary_item.setData(0, Qt.UserRole, ("mesh", "vertex_summary", self.MAX_TREE_ITEMS, vertex_count))
    
    # 提取边、面、体(类似)
    # ...
    
    # 更新计数
    vertices_item.setText(1, str(vertex_count))
    edges_item.setText(1, str(edge_count))
    faces_item.setText(1, str(face_count))
    bodies_item.setText(1, str(body_count))
    
    self.tree.blockSignals(False)

设计要点

  • 支持多种网格数据格式(通过hasattr检查)
  • 为每个节点添加工具提示,显示坐标信息
  • 超过MAX_TREE_ITEMS时显示摘要信息
  • 支持节点、边、面、体四种网格元素

6.5 部件数据加载

def load_parts(self, parts_data=None):
    """
    加载部件信息到树中
    
    Args:
        parts_data: 部件数据对象(可选,如果不提供则从mesh_data中提取)
    """
    if parts_data is not None:
        self.parts_data = parts_data
    elif self.mesh_data is not None:
        self.parts_data = self.mesh_data
    
    self._clear_parts_elements()
    self._extract_parts_elements(self.parts_data)

def _extract_parts_elements(self, parts_data):
    """
    从部件数据中提取部件信息并添加到树中
    
    Args:
        parts_data: 部件数据对象
    """
    if parts_data is None:
        return
    
    parts_item = self.tree.topLevelItem(2)
    if parts_item is None:
        return
    
    self.tree.blockSignals(True)
    
    part_count = 0
    
    # 处理字典格式的部件数据
    if isinstance(parts_data, dict):
        for part_name, part_info in parts_data.items():
            if part_name in PARTS_INFO_RESERVED_KEYS:
                continue
            
            part_item = QTreeWidgetItem(parts_item)
            part_item.setText(0, part_name)
            
            # 计算部件元素数量
            node_count = part_info.get('node_count', 0)
            face_count = len(part_info.get('faces', []))
            total_count = node_count + face_count
            
            part_item.setText(1, str(total_count))
            part_item.setCheckState(0, Qt.Checked)
            part_item.setData(0, Qt.UserRole, ("parts", part_info, part_count))
            
            # 添加工具提示
            bc_type = part_info.get('bc_type', 'unknown')
            part_item.setToolTip(0, f"边界类型: {bc_type}, 节点: {node_count}, 面: {face_count}")
            
            part_count += 1
    
    # 处理列表格式的部件数据
    elif isinstance(parts_data, list):
        for i, part_info in enumerate(parts_data):
            part_name = part_info.get('part_name', f'部件{i}')
            
            part_item = QTreeWidgetItem(parts_item)
            part_item.setText(0, part_name)
            
            node_count = part_info.get('node_count', 0)
            face_count = len(part_info.get('faces', []))
            total_count = node_count + face_count
            
            part_item.setText(1, str(total_count))
            part_item.setCheckState(0, Qt.Checked)
            part_item.setData(0, Qt.UserRole, ("parts", part_info, part_count))
            
            bc_type = part_info.get('bc_type', 'unknown')
            part_item.setToolTip(0, f"边界类型: {bc_type}, 节点: {node_count}, 面: {face_count}")
            
            part_count += 1
    
    parts_item.setText(1, str(part_count))
    self.tree.blockSignals(False)

设计要点

  • 支持字典和列表两种格式的部件数据
  • 跳过保留键(PARTS_INFO_RESERVED_KEYS
  • 显示部件的边界类型、节点数和面数
  • 自动计算部件元素总数

7. 交互功能设计

7.1 可见性控制

7.1.1 复选框状态变化处理

def _on_item_changed(self, item, column):
    """
    树项改变时的回调
    
    Args:
        item: 改变的树项
        column: 改变的列
    """
    if column == 0:
        if self._updating_items:
            return
        
        self._updating_items = True
        self._update_child_items(item, item.checkState(0))
        self._update_parent_item(item)
        self._updating_items = False
        
        element_data = item.data(0, Qt.UserRole)
        self._handle_visibility_change(item, element_data)

设计要点

  • 使用_updating_items标志防止递归调用
  • 同步更新子项和父项状态
  • 触发可见性变化事件

7.1.2 子项状态更新

def _update_child_items(self, item, check_state):
    """
    更新子项的选中状态
    
    Args:
        item: 父项
        check_state: 选中状态
    """
    self.tree.blockSignals(True)
    
    def update_descendants(parent_item):
        for i in range(parent_item.childCount()):
            child = parent_item.child(i)
            child.setCheckState(0, check_state)
            update_descendants(child)
    
    update_descendants(item)
    self.tree.blockSignals(False)

设计要点

  • 递归更新所有后代节点
  • 使用blockSignals防止触发事件

7.1.3 父项状态更新

def _update_parent_item(self, item):
    """
    更新父项的选中状态
    
    Args:
        item: 子项
    """
    parent = item.parent()
    if parent is None:
        return
    
    all_checked = True
    all_unchecked = True
    
    for i in range(parent.childCount()):
        child = parent.child(i)
        if child.checkState(0) == Qt.Checked:
            all_unchecked = False
        elif child.checkState(0) == Qt.Unchecked:
            all_checked = False
        else:
            all_checked = False
            all_unchecked = False
    
    self.tree.blockSignals(True)
    if all_checked:
        parent.setCheckState(0, Qt.Checked)
    elif all_unchecked:
        parent.setCheckState(0, Qt.Unchecked)
    else:
        parent.setCheckState(0, Qt.PartiallyChecked)
    self.tree.blockSignals(False)
    
    # 递归更新上级父项
    self._update_parent_item(parent)

设计要点

  • 根据子项状态确定父项状态
  • 支持三种状态:全选、全不选、部分选中
  • 递归更新所有祖先节点

7.2 点击事件处理

def _on_item_clicked(self, item, column):
    """
    树项点击时的回调
    
    Args:
        item: 点击的树项
        column: 点击的列
    """
    element_data = item.data(0, Qt.UserRole)
    self._handle_selection_change(item, element_data)

def _handle_selection_change(self, item, element_data):
    """
    处理选择改变
    
    Args:
        item: 树项
        element_data: 元素数据
    """
    handler = self._get_parent_handler('on_model_tree_selected')
    if not handler:
        return
    
    if isinstance(element_data, tuple) and len(element_data) >= 3:
        category = element_data[0]
        
        # 对于部件,element_data 格式为 ("parts", part_data, part_count)
        if category == 'parts' and len(element_data) >= 3:
            part_data = element_data[1]
            element_index = element_data[2]
            # 对于部件,element_type 可以是部件名称
            element_type = item.text(0)
            handler(category, element_type, element_index, part_data)
        elif len(element_data) >= 4:
            element_type = element_data[1]
            element_index = element_data[3]
            element_obj = element_data[2] if len(element_data) >= 3 else None
            handler(category, element_type, element_index, element_obj)

设计要点

  • 区分部件和其他元素的处理逻辑
  • 通过_get_parent_handler获取父级处理函数
  • 传递完整的元素信息给父级

7.3 右键菜单

def _show_context_menu(self, position):
    """
    显示右键菜单
    
    Args:
        position: 鼠标位置
    """
    item = self.tree.itemAt(position)
    if item is None:
        return
    
    element_data = item.data(0, Qt.UserRole)
    
    menu = QMenu()
    
    # 基础菜单项
    show_action = QAction("显示", self.tree)
    show_action.triggered.connect(lambda: self._set_item_visibility(item, True))
    menu.addAction(show_action)
    
    hide_action = QAction("隐藏", self.tree)
    hide_action.triggered.connect(lambda: self._set_item_visibility(item, False))
    menu.addAction(hide_action)
    
    expand_action = QAction("展开", self.tree)
    expand_action.triggered.connect(lambda: item.setExpanded(True))
    menu.addAction(expand_action)
    
    collapse_action = QAction("折叠", self.tree)
    collapse_action.triggered.connect(lambda: item.setExpanded(False))
    menu.addAction(collapse_action)
    
    # 类别特定菜单项
    if element_data in ("geometry", "mesh", "parts"):
        menu.addSeparator()
        full_action = QAction("整体显示", self.tree)
        full_action.triggered.connect(lambda: self._set_display_mode("full"))
        menu.addAction(full_action)
        
        element_action = QAction("元素显示", self.tree)
        element_action.triggered.connect(lambda: self._set_display_mode("elements"))
        menu.addAction(element_action)
    
    # 几何元素特定菜单项
    if isinstance(element_data, tuple) and len(element_data) >= 3:
        category = element_data[0]
        element_type = element_data[1]
        element_obj = element_data[2]
        
        if category == 'geometry':
            menu.addSeparator()
            
            if element_type == 'vertices':
                view_coords_action = QAction("查看坐标", self.tree)
                view_coords_action.triggered.connect(lambda: self._show_vertex_properties(element_obj, item))
                menu.addAction(view_coords_action)
            elif element_type == 'edges':
                view_length_action = QAction("查看长度", self.tree)
                view_length_action.triggered.connect(lambda: self._show_edge_properties(element_obj, item))
                menu.addAction(view_length_action)
            elif element_type == 'faces':
                view_area_action = QAction("查看面积", self.tree)
                view_area_action.triggered.connect(lambda: self._show_face_properties(element_obj, item))
                menu.addAction(view_area_action)
            elif element_type == 'bodies':
                view_volume_action = QAction("查看体积", self.tree)
                view_volume_action.triggered.connect(lambda: self._show_solid_properties(element_obj, item))
                menu.addAction(view_volume_action)
    
    # 部件特定菜单项
    if element_data == "parts":
        menu.addSeparator()
        create_part_action = QAction("创建部件", self.tree)
        create_part_action.triggered.connect(lambda: self._create_part_dialog())
        menu.addAction(create_part_action)
    
    if not menu.isEmpty():
        menu.exec_(self.tree.mapToGlobal(position))

设计要点

  • 根据元素类型动态生成菜单项
  • 支持显示/隐藏、展开/折叠等基础操作
  • 提供元素属性查看功能
  • 支持显示模式切换(整体/元素)

7.4 属性查看

7.4.1 顶点属性

def _show_vertex_properties(self, vertex, item):
    """
    显示顶点属性(按需计算)
    
    Args:
        vertex: OpenCASCADE TopoDS_Vertex对象
        item: 树项
    """
    from OCC.Core.BRep import BRep_Tool
    
    try:
        pnt = BRep_Tool.Pnt(vertex)
        coords = f"({pnt.X():.6f}, {pnt.Y():.6f}, {pnt.Z():.6f})"
        
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"顶点坐标: {coords}")
        
        self.tree.blockSignals(True)
        item.setToolTip(0, f"坐标: ({pnt.X():.3f}, {pnt.Y():.3f}, {pnt.Z():.3f})")
        self.tree.blockSignals(False)
    except Exception as e:
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"获取顶点坐标失败: {str(e)}")

7.4.2 边属性

def _show_edge_properties(self, edge, item):
    """
    显示边属性(按需计算)
    
    Args:
        edge: OpenCASCADE TopoDS_Edge对象
        item: 树项
    """
    from OCC.Core.GCPnts import GCPnts_AbscissaPoint
    from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
    
    try:
        adaptor = BRepAdaptor_Curve(edge)
        length = GCPnts_AbscissaPoint.Length(adaptor)
        
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"边长度: {length:.6f}")
        
        self.tree.blockSignals(True)
        item.setToolTip(0, f"长度: {length:.3f}")
        self.tree.blockSignals(False)
    except Exception as e:
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"获取边长度失败: {str(e)}")

7.4.3 面属性

def _show_face_properties(self, face, item):
    """
    显示面属性(按需计算)
    
    Args:
        face: OpenCASCADE TopoDS_Face对象
        item: 树项
    """
    from OCC.Core.BRepGProp import brepgprop
    from OCC.Core.GProp import GProp_GProps
    
    try:
        props = GProp_GProps()
        brepgprop.SurfaceProperties(face, props)
        area = props.Mass()
        
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"面面积: {area:.6f}")
        
        self.tree.blockSignals(True)
        item.setToolTip(0, f"面积: {area:.3f}")
        self.tree.blockSignals(False)
    except Exception as e:
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"获取面面积失败: {str(e)}")

7.4.4 体属性

def _show_solid_properties(self, solid, item):
    """
    显示体属性(按需计算)
    
    Args:
        solid: OpenCASCADE TopoDS_Solid对象
        item: 树项
    """
    from OCC.Core.BRepGProp import brepgprop
    from OCC.Core.GProp import GProp_GProps
    
    try:
        props = GProp_GProps()
        brepgprop.VolumeProperties(solid, props)
        volume = props.Mass()
        
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"体体积: {volume:.6f}")
        
        self.tree.blockSignals(True)
        item.setToolTip(0, f"体积: {volume:.3f}")
        self.tree.blockSignals(False)
    except Exception as e:
        if hasattr(self.parent, 'log_info'):
            self.parent.log_info(f"获取体体积失败: {str(e)}")

设计要点

  • 使用OpenCASCADE的几何计算功能
  • 按需计算,避免不必要的性能开销
  • 计算结果同时显示在日志和工具提示中
  • 异常处理确保稳定性

8. 性能优化策略

8.1 分批加载

BATCH_SIZE = 1000  # 每批处理的元素数量

def process_batch():
    nonlocal vertex_count, edge_count, face_count, body_count
    
    self.tree.blockSignals(True)
    
    # 处理一批元素
    for _ in range(BATCH_SIZE):
        if vertex_explorer.More():
            # ... 处理顶点
            vertex_count += 1
            vertex_explorer.Next()
    
    # ... 处理其他元素
    
    self.tree.blockSignals(False)
    
    # 继续下一批
    if vertex_explorer.More() or edge_explorer.More() or face_explorer.More() or body_explorer.More():
        QTimer.singleShot(0, process_batch)

优化效果

  • 避免一次性加载大量元素导致UI冻结
  • 每批处理1000个元素,平衡性能和响应速度
  • 使用QTimer实现异步加载

8.2 延迟加载

LAZY_LOAD_THRESHOLD = 10000  # 延迟加载阈值

if vertex_count < self.LAZY_LOAD_THRESHOLD:
    # 创建树项
    vertex_item = QTreeWidgetItem(vertices_item)
    vertex_item.setText(0, f"点_{vertex_count}")
    vertex_item.setData(0, Qt.UserRole, ("geometry", "vertices", vertex, vertex_count))
else:
    # 仅更新计数,不创建树项
    pass

优化效果

  • 超过阈值的元素不创建树项,减少内存占用
  • 仍然显示正确的元素数量
  • 避免创建过多树项导致的性能问题

8.3 信号阻塞

self.tree.blockSignals(True)
# 批量操作
self.tree.blockSignals(False)

优化效果

  • 批量操作时阻塞信号,避免频繁触发事件
  • 减少不必要的UI更新
  • 提高批量操作的效率

8.4 统一行高

self.tree.setUniformRowHeights(True)

优化效果

  • 启用统一行高,优化渲染性能
  • 减少布局计算开销
  • 提高滚动流畅度

8.5 虚拟化支持

MAX_TREE_ITEMS = 100000  # 最大树项数量

if vertex_count > self.MAX_TREE_ITEMS:
    # 使用虚拟化或摘要显示
    summary_item = QTreeWidgetItem(vertices_item)
    summary_item.setText(0, f"... (还有 {vertex_count - self.MAX_TREE_ITEMS} 个节点)")

优化效果

  • 超过最大限制时显示摘要信息
  • 避免创建过多树项
  • 保持UI响应性

9. 数据管理

9.1 数据存储

使用Qt.UserRole存储元素数据:

# 类别节点
item.setData(0, Qt.UserRole, "geometry")

# 元素类型节点
item.setData(0, Qt.UserRole, ("geometry", "vertices"))

# 具体元素节点
item.setData(0, Qt.UserRole, ("geometry", "vertices", vertex, vertex_count))

数据格式

  • 类别节点:字符串(如"geometry""mesh""parts"
  • 元素类型节点:元组(如("geometry", "vertices")
  • 具体元素节点:元组(如("geometry", "vertices", vertex, vertex_count)

9.2 数据查询

9.2.1 获取可见元素

def get_visible_elements(self, category=None, element_type=None):
    """
    获取可见的元素
    
    Args:
        category: 类别("geometry", "mesh", "parts"),None表示所有类别
        element_type: 元素类型("vertices", "edges", "faces", "bodies"),None表示所有类型
    
    Returns:
        可见元素字典
    """
    visible_elements = {}
    
    for i in range(self.tree.topLevelItemCount()):
        category_item = self.tree.topLevelItem(i)
        category_data = category_item.data(0, Qt.UserRole)
        
        if category is not None and category_data != category:
            continue
        
        if category_data is None:
            continue
        
        if category_data not in visible_elements:
            visible_elements[category_data] = {}
        
        for j in range(category_item.childCount()):
            element_type_item = category_item.child(j)
            element_type_data = element_type_item.data(0, Qt.UserRole)
            
            if isinstance(element_type_data, tuple) and len(element_type_data) >= 2:
                elem_category, elem_type = element_type_data[0], element_type_data[1]
                
                if element_type is not None and elem_type != element_type:
                    continue
                
                if elem_type is None:
                    continue
                
                if elem_type not in visible_elements[category_data]:
                    visible_elements[category_data][elem_type] = []
                
                for k in range(element_type_item.childCount()):
                    element_item = element_type_item.child(k)
                    
                    if element_item.checkState(0) == Qt.Checked:
                        element_data = element_item.data(0, Qt.UserRole)
                        if isinstance(element_data, tuple) and len(element_data) >= 4:
                            elem_category_data, elem_type_data, elem_obj, elem_index = element_data
                            if elem_type_data == elem_type:
                                visible_elements[category_data][elem_type].append((elem_index, elem_obj))
    
    return visible_elements

9.2.2 获取几何类型状态

def get_geometry_type_states(self):
    """获取几何类型勾选状态"""
    geometry_item = self.tree.topLevelItem(0)
    if geometry_item is None:
        return {}
    
    type_order = [
        ('vertices', 0),
        ('edges', 1),
        ('faces', 2),
        ('bodies', 3)
    ]
    
    states = {}
    for type_name, index in type_order:
        child = geometry_item.child(index)
        if child is not None:
            states[type_name] = child.checkState(0) != Qt.Unchecked
    
    return states

9.2.3 获取可见部件

def get_visible_parts(self):
    """
    获取可见的部件
    
    Returns:
        可见部件名称列表
    """
    visible_parts = []
    
    parts_item = self.tree.topLevelItem(2)
    if parts_item is None:
        return visible_parts
    
    for i in range(parts_item.childCount()):
        part_item = parts_item.child(i)
        if part_item.checkState(0) == Qt.Checked:
            part_name = part_item.text(0)
            visible_parts.append(part_name)
    
    return visible_parts

9.3 数据更新

9.3.1 设置元素可见性

def set_element_visibility(self, category, element_type, element_index, visible):
    """
    设置特定元素的可见性
    
    Args:
        category: 类别("geometry", "mesh", "parts")
        element_type: 元素类型("vertices", "edges", "faces", "bodies")
        element_index: 元素索引
        visible: 是否可见
    """
    for i in range(self.tree.topLevelItemCount()):
        category_item = self.tree.topLevelItem(i)
        category_data = category_item.data(0, Qt.UserRole)
        
        if category_data != category:
            continue
        
        for j in range(category_item.childCount()):
            element_type_item = category_item.child(j)
            element_type_data = element_type_item.data(0, Qt.UserRole)
            
            if isinstance(element_type_data, tuple) and len(element_type_data) >= 2:
                elem_category, elem_type = element_type_data[0], element_type_data[1]
                
                if elem_type != element_type:
                    continue
                
                for k in range(element_type_item.childCount()):
                    element_item = element_type_item.child(k)
                    element_data = element_item.data(0, Qt.UserRole)
                    
                    if isinstance(element_data, tuple) and len(element_data) >= 4:
                        elem_category, elem_type, elem_obj, elem_index_data = element_data
                        if elem_index_data == element_index:
                            element_item.setCheckState(0, Qt.Checked if visible else Qt.Unchecked)
                            return

9.3.2 设置类别可见性

def set_category_visibility(self, category, element_type, visible):
    """
    设置类别的可见性
    
    Args:
        category: 类别("geometry", "mesh", "parts")
        element_type: 元素类型("vertices", "edges", "faces", "bodies")
        visible: 是否可见
    """
    for i in range(self.tree.topLevelItemCount()):
        category_item = self.tree.topLevelItem(i)
        category_data = category_item.data(0, Qt.UserRole)
        
        if category_data != category:
            continue
        
        for j in range(category_item.childCount()):
            element_type_item = category_item.child(j)
            element_type_data = element_type_item.data(0, Qt.UserRole)
            
            if isinstance(element_type_data, tuple) and len(element_type_data) >= 2:
                elem_category, elem_type = element_type_data[0], element_type_data[1]
                
                if elem_type != element_type:
                    continue
                
                element_type_item.setCheckState(0, Qt.Checked if visible else Qt.Unchecked)
                return

10. 与其他模块的集成

10.1 与主窗口的集成

在gui_main.py中,模型树组件被集成到主窗口的左侧面板:

def _create_model_tree_widget(self):
    """创建模型树组件"""
    self.model_tree_widget = ModelTreeWidget(parent=self)
    
    model_tree_frame_container = QGroupBox("模型树")
    model_tree_layout = QVBoxLayout(model_tree_frame_container)
    model_tree_layout.setSpacing(2)
    
    model_tree_layout.addWidget(self.model_tree_widget.widget)
    
    self.model_tree_widget.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    
    if hasattr(self, 'left_splitter'):
        self.left_splitter.addWidget(model_tree_frame_container)

10.2 与部件管理器的集成

模型树通过_get_parent_handler方法与部件管理器交互:

def _get_parent_handler(self, handler_name):
    """获取父级处理函数(优先使用part_manager)"""
    manager = getattr(self.parent, 'part_manager', None)
    if manager and hasattr(manager, handler_name):
        return getattr(manager, handler_name)
    if hasattr(self.parent, handler_name):
        return getattr(self.parent, handler_name)
    return None

10.3 事件处理

模型树通过以下事件与父窗口通信:

  1. 可见性变化事件on_model_tree_visibility_changed
  2. 选择变化事件on_model_tree_selected
  3. 显示模式变化事件on_display_mode_changed
  4. 部件创建事件on_part_created

10.4 PartManager中的回调处理

def on_model_tree_visibility_changed(self, *args):
    """模型树可见性改变的回调"""
    if len(args) == 2:
        category, visible = args
        
        if category == 'geometry':
            self._update_geometry_element_display()
        elif category == 'mesh':
            self._update_mesh_part_display()
        elif category == 'parts':
            self.refresh_display_all_parts()

11. 使用示例

11.1 加载几何模型

# 加载OpenCASCADE几何模型
shape = ...  # TopoDS_Shape对象
model_tree_widget.load_geometry(shape, "立方体")

11.2 加载网格数据

# 加载网格数据
mesh_data = ...  # 网格数据对象
model_tree_widget.load_mesh(mesh_data, "网格1")

11.3 加载部件信息

# 加载部件信息
parts_data = {
    "wall1": {
        "part_name": "wall1",
        "bc_type": "wall",
        "node_count": 100,
        "faces": [...]
    },
    "inlet": {
        "part_name": "inlet",
        "bc_type": "inlet",
        "node_count": 50,
        "faces": [...]
    }
}
model_tree_widget.load_parts(parts_data)

11.4 查询可见元素

# 获取所有可见的几何元素
visible_elements = model_tree_widget.get_visible_elements(category="geometry")

# 获取可见的网格顶点
visible_vertices = model_tree_widget.get_visible_elements(category="mesh", element_type="vertices")

# 获取可见的部件
visible_parts = model_tree_widget.get_visible_parts()

11.5 设置元素可见性

# 设置特定元素的可见性
model_tree_widget.set_element_visibility("geometry", "vertices", 0, False)

# 设置整个类别的可见性
model_tree_widget.set_category_visibility("mesh", "faces", True)

12. 设计模式

12.1 观察者模式

模型树组件通过信号机制实现观察者模式:

  • 主题(Subject):模型树组件
  • 观察者(Observer):父窗口(PyMeshGenGUI)
  • 事件:可见性变化、选择变化、显示模式变化
# 模型树发出事件
handler = self._get_parent_handler('on_model_tree_visibility_changed')
if handler:
    handler(category, element_type, element_index, visible)

# 父窗口处理事件
def on_model_tree_visibility_changed(self, *args):
    # 更新3D视图
    self._update_3d_display()

12.2 策略模式

不同类型的数据加载使用不同的策略:

  • 几何数据加载策略:使用OpenCASCADE探索器
  • 网格数据加载策略:直接访问网格数据属性
  • 部件数据加载策略:解析字典或列表格式
def load_geometry(self, shape, geometry_name="几何"):
    # 几何加载策略
    self._batch_load_geometry_elements(...)

def load_mesh(self, mesh_data, mesh_name="网格"):
    # 网格加载策略
    self._extract_mesh_elements(mesh_data)

def load_parts(self, parts_data=None):
    # 部件加载策略
    self._extract_parts_elements(parts_data)

12.3 模板方法模式

元素提取使用模板方法模式:

def _extract_geometry_elements(self, shape):
    # 模板方法
    vertex_count = self._extract_vertices(shape, vertices_item)
    edge_count = self._extract_edges(shape, edges_item)
    face_count = self._extract_faces(shape, faces_item)
    body_count = self._extract_bodies(shape, bodies_item)
    
    # 更新计数
    vertices_item.setText(1, str(vertex_count))
    edges_item.setText(1, str(edge_count))
    faces_item.setText(1, str(face_count))
    bodies_item.setText(1, str(body_count))

13. 扩展性设计

13.1 支持新的数据类型

要支持新的数据类型,需要:

  1. _init_tree_structure中添加新的顶级节点
  2. 实现相应的load_*方法
  3. 实现_extract_*_elements方法
  4. 更新事件处理逻辑

13.2 支持新的元素类型

要支持新的元素类型,需要:

  1. 在相应的类别节点下添加新的子节点
  2. _extract_*_elements中添加提取逻辑
  3. 在右键菜单中添加相应的操作
  4. 更新属性查看方法

13.3 自定义渲染

可以通过重绘代理实现自定义渲染:

class CustomTreeItemDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # 自定义绘制逻辑
        pass

# 应用自定义代理
self.tree.setItemDelegate(CustomTreeItemDelegate())

13.4 插件化架构

通过回调机制支持功能扩展:

def _get_parent_handler(self, handler_name):
    """获取父级处理函数(优先使用part_manager)"""
    manager = getattr(self.parent, 'part_manager', None)
    if manager and hasattr(manager, handler_name):
        return getattr(manager, handler_name)
    if hasattr(self.parent, handler_name):
        return getattr(self.parent, handler_name)
    return None

14. 测试建议

14.1 单元测试

  • 测试树结构初始化
  • 测试数据加载功能
  • 测试可见性控制
  • 测试事件处理
  • 测试查询功能

14.2 性能测试

  • 测试大规模数据加载性能
  • 测试分批加载效果
  • 测试内存占用
  • 测试UI响应速度

14.3 集成测试

  • 测试与主窗口的集成
  • 测试与部件管理器的集成
  • 测试与3D视图的集成
  • 测试完整工作流程

14.4 测试要点

  1. 小模型:几十个元素,确保叶子节点可正常创建、点击高亮可用
  2. 中模型:1e4 量级,验证分批加载无冻结、数量正确
  3. 大模型:>1e5,验证阈值策略生效、UI 不崩溃
  4. 勾选传播:父子勾选联动正确,不出现递归卡死
  5. 右键菜单:不同节点类型显示不同菜单项;handler 缺失时安全降级

15. 风险、边界与改进建议

15.1 在 UserRole 中存储 OCC 对象引用

风险

  • OCC 对象生命周期受 Python 包装层/底层句柄影响
  • 若 shape 被释放或替换,树项中的引用可能失效

建议

  • 更稳妥:只存储可序列化的 element_id(例如 hash / index),对象由"几何仓库"统一管理。

15.2 QTreeWidget 的可扩展性

QTreeWidget 对轻量数据简单好用,但超大数据量(>1e5)时性能瓶颈明显。

当需求上升到"百万级元素可视化"时,建议迁移到:

  • QTreeView + QAbstractItemModel(真正的虚拟化/按需取数)

15.3 懒加载交互一致性

当超过 LAZY_LOAD_THRESHOLD 不创建叶子节点时:

  • 勾选类目节点仍可控制全体元素显示/隐藏
  • 但用户无法点选某个具体元素

这需要在 UI 上给出明确提示,例如:

  • 展开节点时显示一个占位子项:"元素数量过多,已启用懒加载/不显示明细"
  • 或提供搜索/过滤/分页

15.4 内存管理

问题

  • 大量树项的创建会占用较多内存
  • OCC对象引用可能导致内存泄漏

建议

  • 及时清理不再使用的树项
  • 使用弱引用存储对象
  • 实现对象池管理

15.5 线程安全

问题

  • 分批加载使用QTimer在主线程执行
  • 大量计算可能阻塞UI

建议

  • 将计算密集型任务移到后台线程
  • 使用QThread或QThreadPool
  • 通过信号槽机制更新UI

16. 关键实现清单(与源码映射)

  • ModelTreeWidget.__init__():初始化、创建控件、建立默认三层结构
  • _create_tree_widget():QTreeWidget 基础属性、信号连接、菜单模式
  • _setup_ui():设置UI布局
  • _init_tree_structure():构建固定顶层与二级节点,并设置 UserRole 标签
  • load_geometry():几何加载入口,清空旧数据并触发分批加载
  • _batch_load_geometry_elements():分批遍历 OCC 拓扑,按阈值创建叶子或只计数
  • load_mesh():网格加载入口
  • _extract_mesh_elements():提取网格元素信息
  • load_parts():部件加载入口
  • _extract_parts_elements():提取部件信息
  • _on_item_changed():处理复选框状态变化
  • _on_item_clicked():处理项点击事件
  • _handle_visibility_change():处理可见性改变
  • _handle_selection_change():处理选择改变
  • _show_context_menu():显示右键菜单
  • _show_vertex_properties():显示顶点属性
  • _show_edge_properties():显示边属性
  • _show_face_properties():显示面属性
  • _show_solid_properties():显示体属性
  • _update_child_items():更新子项的选中状态
  • _update_parent_item():更新父项的选中状态
  • get_visible_elements():获取可见的元素
  • get_geometry_type_states():获取几何类型勾选状态
  • get_visible_parts():获取可见的部件
  • set_element_visibility():设置特定元素的可见性
  • set_category_visibility():设置类别的可见性
  • set_all_parts_visible():设置所有部件的可见性
  • clear():清空树
  • _get_parent_handler():通过 parent/part_manager 解耦调用实际业务处理逻辑

17. 后续扩展建议(Roadmap)

17.1 短期目标

  • 增加"搜索框 + 过滤条件":按名称/ID/类型快速定位
  • 为大数据启用"分页/虚拟节点":扩展懒加载策略
  • 统一定义 UserRole payload 的数据类(dataclass)并提供序列化/调试输出

17.2 中期目标

  • 引入"选择集(Selection Set)"节点:保存用户的一组选择
  • 结合 3D 视图:双向同步(3D 选中 -> 树定位;树选中 -> 3D 高亮)
  • 支持拖放操作:在树中拖动元素进行重新组织

17.3 长期目标

  • 迁移到 QTreeView + QAbstractItemModel:支持百万级元素
  • 实现真正的虚拟化:按需加载和渲染
  • 支持多选和批量操作:提高工作效率
  • 添加撤销/重做功能:支持操作历史

18. 术语表

  • OCC:OpenCASCADE,几何内核
  • TopoDS_Shape:OCC 的形体对象
  • TopExp_Explorer:OCC 拓扑遍历器
  • 批处理/分批加载(Batch Load):将大量 UI 创建工作拆分到多个事件循环周期
  • 懒加载(Lazy Load):仅在需要显示时创建明细节点
  • 虚拟化(Virtualization):只渲染可见项,按需创建树项
  • UserRole:Qt 中用于存储自定义数据的角色
  • CheckState:Qt 中复选框的状态(Checked/Unchecked/PartiallyChecked)
  • Callback/Handler:回调函数/处理函数
  • Signal/Slot:Qt 的信号槽机制
  • QTreeWidget:Qt 的树形控件(基于项)
  • QTreeView:Qt 的树形视图(基于模型)
  • QAbstractItemModel:Qt 的抽象项模型

19. 总结

模型树组件是PyMeshGen GUI系统的核心组件之一,提供了统一的数据展示和管理界面。该组件采用三层架构设计,支持几何、网格、部件三种数据类型,实现了高效的元素可见性控制和丰富的交互功能。

19.1 核心优势

  1. 统一架构:三种数据类型(几何、网格、部件)统一管理
  2. 高性能:分批加载、延迟加载、虚拟化等优化策略
  3. 易扩展:插件化架构、回调机制、设计模式应用
  4. 用户友好:直观的交互方式、丰富的功能、良好的反馈

19.2 技术亮点

  • 异步加载:使用QTimer实现非阻塞的分批加载
  • 智能阈值:根据数据规模自动选择加载策略
  • 事件驱动:通过信号槽机制实现松耦合
  • 设计模式:观察者模式、策略模式、模板方法模式的综合应用

19.3 应用价值

模型树组件为PyMeshGen提供了:

  • 直观的数据管理界面
  • 高效的元素可见性控制
  • 灵活的扩展机制
  • 良好的用户体验

该组件的设计和实现充分考虑了性能优化和用户体验,为PyMeshGen的整体架构提供了坚实的基础。未来可以通过虚拟化、懒加载、缓存等技术进一步提升性能,通过支持更多数据类型和元素类型来扩展功能。


文档结束