Files
XCEngine/docs/api/XCEngine/Editor/UI/TreeView/TreeView.md
2026-03-29 01:36:53 +08:00

5.6 KiB
Raw Blame History

TreeView

命名空间: XCEngine::Editor::UI

类型: header-helper + class + struct

源文件: editor/src/UI/TreeView.h

描述: 提供编辑器树形视图的统一节点状态、前缀绘制槽、交互回调和展开状态持久化机制。

概述

TreeView.h 是当前 Editor UI 重构里最重要的公共基础设施之一。它把原本很容易散落在 HierarchyPanelProjectPanel 中的树节点绘制逻辑抽成一层统一接口,让不同面板能够共享:

  • 相同的树节点视觉风格。
  • 相同的展开 / 收起行为。
  • 相同的前缀图标插槽模型。
  • 相同的点击、双击、右键、拖拽扩展接入点。

如果从使用体验上理解,它很接近商业编辑器中“导航树控件”的内建基础层。HierarchyPanelProjectPanel 的左侧目录树并没有各写一套独立的 tree widget而是都基于这层构建。

公开类型与函数

成员 说明
class TreeViewState 保存节点展开状态的内存态容器。
TreeNodeOptions 控制选中、叶子节点、默认展开、点击展开策略等基础选项。
TreeNodeResult 返回节点本帧是否打开、是否点击、是否双击、是否右键等交互结果。
TreeNodePrefixContext 给前缀绘制回调提供绘制区域、选中状态和 hover 状态。
TreeNodePrefixSlot 定义一个可选的前缀绘制槽。
TreeNodeCallbacks 定义交互回调和额外渲染回调。
TreeNodeDefinition 组合节点选项、持久化 key、样式、前缀槽与回调。
ResetTreeLayout() 清空当前树布局的缩进栈,开始一棵新的树。
DrawTreeNode(...) 绘制节点并返回本帧结果。
EndTreeNode() 与打开状态的节点配对,弹出一层树缩进。

TreeViewState 的角色

TreeViewState 当前只做一件事:根据 std::string_view key 保存节点展开状态。

它不是场景数据,也不是项目数据,只是某个 UI 实例自己的展开状态缓存。因此:

  • HierarchyPanel 用它记住实体树哪些节点已经展开。
  • ProjectPanel 用它记住目录树哪些文件夹已经展开。

当前实现采用 std::unordered_map<std::string, bool>,所以最好的 persistenceKey 是稳定且能跨帧复现的标识,例如:

  • 场景对象的 UUID
  • 资源目录的完整路径

当前实现行为

editor/src/UI/TreeView.h 的实现,当前版本有几个关键事实:

  • 树缩进不是保存在某个对象里,而是保存在一个静态 TreeIndentStack() 中。
  • 因此每次绘制一棵新的根树之前都应该先调用 ResetTreeLayout()
  • DrawTreeNode(...) 若返回 open = true,调用方在绘制完所有子节点后必须调用 EndTreeNode() 与之配对。
  • 节点背景当前直接使用 ImGuiCol_Header / HeaderHovered / HeaderActive 这一组颜色绘制选中与 hover 态。
  • 展开箭头由 Core.h 里的 DrawDisclosureArrow(...) 手工绘制。
  • prefix slot 允许调用方在标签前插入小图标、状态灯或其他自定义绘制内容。
  • callbacks.onInteraction 适合接选择、右键菜单、双击重命名等语义。
  • callbacks.onRenderExtras 适合接拖拽源 / 拖拽目标之类必须紧跟节点绘制的额外逻辑。

这说明它不是对 ImGui::TreeNodeEx(...) 的轻封装,而是自己接管了树节点的大部分视觉和交互行为。

设计说明

为什么要单独造这一层,而不是每个面板直接用 ImGui 原生 tree API

  • HierarchyProject 都需要统一的左侧导航树风格,这种复用非常适合抽成共享 helper。
  • 商业编辑器中的树视图往往不仅是“文本 + 箭头”,还需要图标前缀、整行高亮、拖放热点、重命名切换等语义。
  • 一旦把节点定义、展开状态和回调插槽标准化,上层面板代码就能更聚焦于业务对象,而不是 UI 细节。

如果和 Unity 对照可以把它理解成“Hierarchy / Project 导航树的共用控件层”,而不是某个特定面板的局部技巧。

典型用法

UI::ResetTreeLayout();

UI::TreeNodeDefinition node;
node.options.selected = isSelected;
node.options.leaf = !hasChildren;
node.persistenceKey = stablePath;
node.style = UI::ProjectFolderTreeStyle();
node.prefix.width = UI::NavigationTreePrefixWidth();
node.prefix.draw = DrawFolderPrefix;
node.callbacks.onInteraction = [&](const UI::TreeNodeResult& result) {
    if (result.clicked) {
        NavigateToFolder();
    }
};

const UI::TreeNodeResult result =
    UI::DrawTreeNode(&m_treeState, folder.get(), folder->name.c_str(), node);

if (result.open) {
    DrawChildren();
    UI::EndTreeNode();
}

生命周期与线程语义

  • TreeViewState 由具体面板持有,通常作为成员变量存在于面板实例生命周期内。
  • 整套 API 都依赖当前 ImGui 帧状态,只能在 UI 线程使用。
  • 它不负责把展开状态写入磁盘或布局文件;当前只是内存级持久化。

当前限制

  • 静态 TreeIndentStack() 让它更像“约定式 helper”而不是完全可重入的小部件对象。
  • 没有键盘导航、滚动到可见、虚拟化大树等更高级的树控件能力。
  • 展开状态目前只保存在面板实例中,重启编辑器后不会自动恢复。
  • 当前回调模型偏即时模式,适合编辑器内部使用,但不是声明式 UI 框架。

相关文档