版本:Unity 6 (6000.0)
语言:英语
使用 IMGUI 创建自定义编辑器
UI 和 UI 详细信息分析器

使用 IMGUI 创建 TreeView

注意:强烈建议使用 UI 工具包 来扩展 Unity 编辑器,因为它提供了比 IMGUI 更现代、灵活和可扩展的解决方案。

本页面的信息假设读者对 IMGUI(立即模式 GUI)概念有基本了解。有关 IMGUI 和自定义编辑器窗口的信息,请参阅 扩展编辑器IMGUI Unity 博客

TreeView 是一种 IMGUI 控件,用于显示可以展开和折叠的分层数据。使用 TreeView 为编辑器窗口创建高度可定制的列表视图和多列表格,您可以将这些视图和表格与其他 IMGUI 控件和组件一起使用。

有关可用的 TreeView API 函数的信息,请参阅 Unity 脚本 API 文档中的 TreeView

Example of a TreeView with a MultiColumnHeader and a SearchField.
带有多列标题和搜索栏的 TreeView 示例。

请注意,TreeView 不是 树形数据模型。您可以使用任何您喜欢的树形数据结构来构建 TreeView。这可以是 C# 树形模型,也可以是基于 Unity 的树形结构,例如 Transform 层级结构。

TreeView 的渲染通过确定称为行的展开项列表来处理。每行代表一个 TreeViewItem。每个 TreeViewItem 包含父项和子项信息,TreeView 使用这些信息来处理导航(键盘和鼠标输入)。

TreeView 只有一个根 TreeViewItem,它被隐藏,不会出现在编辑器中。该项是所有其他项的根。

重要类和方法

除了 TreeView 本身之外,最重要的类是 TreeViewItemTreeViewState

TreeViewState (TreeViewState) 包含在与编辑器中的 TreeView 字段交互时发生更改的状态信息,例如选择状态、展开状态、导航状态和滚动状态。TreeViewState 是唯一 可序列化 的状态。TreeView 本身不可序列化 - 它是在构建或重新加载时从它所代表的数据中重建的。在您的 EditorWindow 派生类中添加 TreeViewState 作为字段,以确保在重新加载 脚本一段代码,允许您创建自己的组件,触发游戏事件,随时间推移修改组件属性并以您喜欢的任何方式响应用户输入。 更多信息
参见 词汇表
或进入播放模式时,不会丢失用户更改的状态(请参阅有关 扩展编辑器 的文档,了解如何执行此操作)。有关包含 TreeViewState 字段的类的示例,请参阅下面的 示例 1:一个简单的 TreeView

TreeViewItem (TreeViewItem) 包含有关单个 TreeView 项的数据,并用于构建编辑器中树结构的表示形式。每个 TreeViewItem 必须使用唯一的整数 ID(在 TreeView 中的所有项中唯一)进行构建。ID 用于在树中查找项,以进行选择状态、展开状态和导航。如果树表示 Unity 对象,请对每个对象使用 GetInstanceID 作为 TreeViewItem 的 ID。这些 ID 用于 TreeViewState 中,以在重新加载脚本或在编辑器中进入播放模式时持久化用户更改的状态(例如展开的项)。

所有 TreeViewItems 都有一个 depth 属性,它指示视觉缩进。有关更多信息,请参阅下面的 初始化 TreeView 示例。

BuildRoot (BuildRoot) 是 TreeView 类必须实现的单个抽象方法,用于创建 TreeView。使用此方法来处理创建树的根项。这在每次对树调用 Reload 时都会被调用。对于使用小型数据集的简单树,请在 BuildRoot 中在根项下创建整个树的 TreeViewItems。对于非常大的树,在每次重新加载时创建整棵树并不理想。在这种情况下,请创建根,然后覆盖 BuildRows 方法以仅为当前行创建项。有关 BuildRoot 使用的示例,请参阅下面的 示例 1:一个简单的 TreeView

BuildRows (BuildRows) 是一种虚拟方法,其中默认实现处理根据在 BuildRoot 中创建的完整树构建行列表。如果在 BuildRoot 中只创建了根,则应覆盖此方法以处理展开的行。有关更多信息,请参阅下面的 初始化 TreeView

该图总结了在 TreeView 的生命周期中 BuildRootBuildRows 事件方法的顺序和重复。请注意,BuildRoot 方法在每次调用 Reload 时都会被调用一次。BuildRows 被调用的次数更多,因为它在 Reload 时被调用一次(紧随 BuildRoot 之后),以及在每次展开或折叠 TreeViewItem 时被调用一次。

初始化 TreeView

TreeView 在从 TreeView 对象调用 Reload 方法时被初始化。

有两种方法可以设置 TreeView

  1. 创建完整的树 - 为树模型数据中的所有项创建 TreeViewItem。这是默认方法,需要较少的代码来设置。完整的树是在从 TreeView 对象调用 BuildRoot 时构建的。

  2. 只创建展开的项 - 此方法要求您覆盖 BuildRows 以手动控制要显示的行,而 BuildRoot 只用于创建根 TreeViewItem。此方法在大型数据集或经常更改的数据中具有最佳的可扩展性。

对于小型数据集或不经常更改的数据,请使用第一种方法。对于大型数据集或经常更改的数据,请使用第二种方法,因为它比创建完整的树更快,只需要创建展开的项。

有三种方法可以设置 TreeViewItems

示例

要查看下面显示的示例的项目和源代码,请下载 TreeViewExamples.zip

示例 1:一个简单的 TreeView

要创建 TreeView,请创建一个扩展 TreeView 类的类,并实现抽象方法 BuildRoot。以下示例创建一个简单的 TreeView。

class SimpleTreeView : TreeView
{
    public SimpleTreeView(TreeViewState treeViewState)
        : base(treeViewState)
    {
        Reload();
    }
        
    protected override TreeViewItem BuildRoot ()
    {
        // BuildRoot is called every time Reload is called to ensure that TreeViewItems 
        // are created from data. Here we create a fixed set of items. In a real world example,
        // a data model should be passed into the TreeView and the items created from the model.

        // This section illustrates that IDs should be unique. The root item is required to 
        // have a depth of -1, and the rest of the items increment from that.
        var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
        var allItems = new List<TreeViewItem> 
        {
            new TreeViewItem {id = 1, depth = 0, displayName = "Animals"},
            new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"},
            new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"},
            new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"},
            new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"},
            new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"},
            new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"},
            new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"},
            new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"},
        };
            
        // Utility method that initializes the TreeViewItem.children and .parent for all items.
        SetupParentsAndChildrenFromDepths (root, allItems);
            
        // Return root of the tree
        return root;
    }
}

在此示例中,深度信息用于构建 TreeView。最后,对 SetupDepthsFromParentsAndChildren 的调用设置了 TreeViewItem 的父项和子项数据。

请注意,有两种方法可以设置 TreeViewItem:直接设置父项和子项,或者使用 AddChild 方法,如下面的示例所示

protected override TreeViewItem BuildRoot()
{
    var root = new TreeViewItem      { id = 0, depth = -1, displayName = "Root" };
    var animals = new TreeViewItem   { id = 1, displayName = "Animals" };
    var mammals = new TreeViewItem   { id = 2, displayName = "Mammals" };
    var tiger = new TreeViewItem     { id = 3, displayName = "Tiger" };
    var elephant = new TreeViewItem  { id = 4, displayName = "Elephant" };
    var okapi = new TreeViewItem     { id = 5, displayName = "Okapi" };
    var armadillo = new TreeViewItem { id = 6, displayName = "Armadillo" };
    var reptiles = new TreeViewItem  { id = 7, displayName = "Reptiles" };
    var croco = new TreeViewItem     { id = 8, displayName = "Crocodile" };
    var lizard = new TreeViewItem    { id = 9, displayName = "Lizard" };

    root.AddChild(animals);
    animals.AddChild(mammals);
    animals.AddChild(reptiles);
    mammals.AddChild(tiger);
    mammals.AddChild(elephant);
    mammals.AddChild(okapi);
    mammals.AddChild(armadillo);
    reptiles.AddChild(croco);
    reptiles.AddChild(lizard);

    SetupDepthsFromParentsAndChildren(root);

    return root;
}

上面 SimpleTreeView 类的替代 BuildRoot 方法

以下示例显示了包含 SimpleTreeViewEditorWindow。TreeView 是使用 TreeViewState 实例构建的。TreeView 的实现者应确定如何处理此视图状态:其状态是否应持续到 Unity 的下一次会话,或者它是否应该只在脚本重新加载后保留其状态(在进入播放模式或重新编译脚本时)。在此示例中,TreeViewStateEditorWindow 中被序列化,确保在关闭并重新打开编辑器时,TreeView 保留其状态。

using System.Collections.Generic;
using UnityEngine;
using UnityEditor.IMGUI.Controls;

class SimpleTreeViewWindow : EditorWindow
{
    // SerializeField is used to ensure the view state is written to the window 
    // layout file. This means that the state survives restarting Unity as long as the window
    // is not closed. If the attribute is omitted then the state is still serialized/deserialized.
    [SerializeField] TreeViewState m_TreeViewState;

    //The TreeView is not serializable, so it should be reconstructed from the tree data.
    SimpleTreeView m_SimpleTreeView;

    void OnEnable ()
    {
        // Check whether there is already a serialized view state (state 
        // that survived assembly reloading)
        if (m_TreeViewState == null)
            m_TreeViewState = new TreeViewState ();

        m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
    }

    void OnGUI ()
    {
        m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
    }

    // Add menu named "My Window" to the Window menu
    [MenuItem ("TreeView Examples/Simple Tree Window")]
    static void ShowWindow ()
    {
        // Get existing open window or if none, make a new one:
        var window = GetWindow<SimpleTreeViewWindow> ();
        window.titleContent = new GUIContent ("My Window");
        window.Show ();
    }
}

示例 2:一个多列 TreeView

此示例说明了一个使用 MultiColumnHeader 类的多列 TreeView。

MultiColumnHeader 支持以下功能:重命名项、多选、重新排序项以及使用普通 IMGUI 控件(例如滑块和对象字段)自定义行内容、对列进行排序,以及对行进行过滤和搜索。

此示例使用 TreeElementTreeModel 类创建了一个数据模型。TreeView 从这个“TreeModel”中获取数据。在此示例中,TreeElementTreeModel 类已内置,以演示 TreeView 类的功能。这些类已包含在 TreeView 示例项目 (TreeViewExamples.zip) 中。该示例还展示了如何将树模型结构序列化为 ScriptableObject 并保存在资源中。

[Serializable]
//The TreeElement data class is extended to hold extra data, which you can show and edit in the front-end TreeView.
internal class MyTreeElement : TreeElement
{
    public float floatValue1, floatValue2, floatValue3;
    public Material material;
    public string text = "";
    public bool enabled = true;

    public MyTreeElement (string name, int depth, int id) : base (name, depth, id)
    {
        floatValue1 = Random.value;
        floatValue2 = Random.value;
        floatValue3 = Random.value;
    }
}

以下 ScriptableObject 类确保在序列化树时数据会持久化到资源中。

[CreateAssetMenu (fileName = "TreeDataAsset", menuName = "Tree Asset", order = 1)]
public class MyTreeAsset : ScriptableObject
{
    [SerializeField] List<MyTreeElement> m_TreeElements = new List<MyTreeElement> ();

    internal List<MyTreeElement> treeElements
    {
        get { return m_TreeElements; }
        set { m_TreeElements = value; }
    }
}

MultiColumnTreeView 类的构建

以下示例显示了 MultiColumnTreeView 类的代码片段,它说明了如何实现多列 GUI。在 TreeView 示例项目 (TreeViewExamples.zip) 中可以找到完整的源代码。

public MultiColumnTreeView (TreeViewState state, 
                            MultiColumnHeader multicolumnHeader, 
                            TreeModel<MyTreeElement> model) 
                            : base (state, multicolumnHeader, model)
{
    // Custom setup
    rowHeight = 20;
    columnIndexForTreeFoldouts = 2;
    showAlternatingRowBackgrounds = true;
    showBorder = true;
    customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; 
    extraSpaceBeforeIconAndLabel = kToggleWidth;
    multicolumnHeader.sortingChanged += OnSortingChanged;
            
    Reload();
}

上面代码示例中的自定义更改执行以下调整

  • rowHeight = 20:将默认高度(基于 EditorGUIUtility.singleLineHeight 的 16 个点)更改为 20,以便为 GUI 控件留出更多空间。

  • columnIndexForTreeFoldouts = 2:在此示例中,折叠箭头显示在第三列,因为此值设置为 2(参见上面的图像)。如果没有更改此值,折叠箭头将渲染在第一列,因为默认情况下“columnIndexForTreeFoldouts”为 0。

  • showAlternatingRowBackgrounds = true:启用交替行背景颜色,以便每行都独一无二。

  • showBorder = true:使用边距渲染 TreeView,以便显示细边框将其与其他内容分隔开来

  • customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f:在行中垂直居中折叠箭头 - 请参阅下面的 自定义 GUI

  • extraSpaceBeforeIconAndLabel = 20:在树标签之前留出空间,以便显示切换按钮。

  • multicolumnHeader.sortingChanged += OnSortingChanged:将一个方法分配给该事件,以检测标题组件中的排序何时发生变化(当点击标题列时),以便 TreeView 的行更改以反映排序状态。

自定义 GUI

如果使用默认的 RowGUI 处理,则 TreeView 看起来像上面的 SimpleTreeView 示例,只有展开和标签。当使用多个数据值来表示每个项目时,您必须覆盖 RowGUI 方法以可视化这些值。

protected override void RowGUI (RowGUIArgs args)

以下代码示例是 RowGUIArgs 结构体的参数结构。

protected struct RowGUIArgs
{
    public TreeViewItem item;
    public string label;
    public Rect rowRect;
    public int row;
    public bool selected;
    public bool focused;
    public bool isRenaming;

    public int GetNumVisibleColumns ()
    public int GetColumn (int visibleColumnIndex)
    public Rect GetCellRect (int visibleColumnIndex)
}

您可以扩展 TreeViewItem 并添加额外的用户数据(这会创建一个从 TreeViewItem 派生的类)。然后,您可以在 RowGUI 回调中使用此用户数据。下面提供了一个示例。请参阅 override void RowGUI - 此示例将输入项目转换为 TreeViewItem<MyTreeElement>

有三个与列处理相关的方法:GetNumVisibleColumnsGetColumnGetCellRect。只有在使用 MultiColumnHeader 构造 TreeView 时才能调用这些方法,否则会抛出异常。

protected override void RowGUI (RowGUIArgs args)
{
    var item = (TreeViewItem<MyTreeElement>) args.item;

    for (int i = 0; i < args.GetNumVisibleColumns (); ++i)
    {
        CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
    }
}
void CellGUI (Rect cellRect, TreeViewItem<MyTreeElement> item, MyColumns column, ref RowGUIArgs args)
{
    // Center the cell rect vertically using EditorGUIUtility.singleLineHeight.
// This makes it easier to place controls and icons in the cells.
    CenterRectUsingSingleLineHeight(ref cellRect);

    switch (column)
    {

        case MyColumns.Icon1:
            
            // Draw custom texture
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Icon2:

//Draw custom texture 
            GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Name:

            // Make a toggle button to the left of the label text
            Rect toggleRect = cellRect;
            toggleRect.x += GetContentIndent(item);
            toggleRect.width = kToggleWidth;
            if (toggleRect.xMax < cellRect.xMax)
                item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); 

            // Default icon and label
            args.rowRect = cellRect;
            base.RowGUI(args);
            break;

        case MyColumns.Value1:

// Show a Slider control for value 1
            item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
            break;

        case MyColumns.Value2:

// Show an ObjectField for materials
            item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, 
                                          typeof(Material), false);
            break;

        case MyColumns.Value3:

// Show a TextField for the data text string
            item.data.text = GUI.TextField(cellRect, item.data.text);
            break;
    }
}

TreeView 常见问题解答

问:在我的 TreeView 子类中,我有函数 BuildRootRowGUIRowGUI 是针对构建函数中添加的每个 TreeViewItem 调用,还是只针对滚动视图中在屏幕上可见的项目调用?

答:RowGUI 仅针对屏幕上可见的项目调用。例如,如果您有 10,000 个项目,则只有屏幕上可见的 20 个项目的 RowGUI 会被调用。

问:我可以获取屏幕上可见行的索引吗?

答:可以。使用方法 GetFirstAndLastVisibleRows

问:我可以获取在 BuildRows 中构建的行的列表吗?

答:可以。使用方法 GetRows

问:任何被重写函数都必须调用 base.Method 吗?

答:只有当该方法具有您想要扩展的默认行为时才需要。

问:我只想制作项目列表(而不是树)。我必须创建根节点吗?

答:是的,您始终应该有一个根节点。您可以创建根节点并设置 root.children = rows 以快速设置。

问:我在行中添加了一个 Toggle - 为什么当我点击它时选择不会跳到该行?

答:默认情况下,只有当鼠标按下没有被行内容消耗时才会选择该行。在这里,您的 Toggle 消耗了事件。要解决此问题,请在调用 Toggle 按钮之前使用方法 SelectionClick

问:在所有 RowGUI 方法调用之前或之后,我可以使用哪些方法?

答:可以。请参阅 API 文档中的 BeforeRowsGUIAfterRowsGUI

问:有没有一种简单的方法可以从 API 返回焦点到 TreeView?如果我在行中选择了一个 FloatField,则行选择会变为灰色。如何使它再次变为蓝色?

答:蓝色表示当前哪个行具有键盘焦点。因为 FloatField 具有焦点,所以 TreeView 失去了焦点,因此这是预期的行为。在需要时设置 GUIUtility.keyboardControl = treeViewControlID

问:如何从 id 转换为 TreeViewItem

答:使用 FindItem 或 FindRows。

问:用户在 TreeView 中更改选择时如何接收回调?

答:重写 SelectionChanged 方法(其他有用的回调:DoubleClickedItemContextClickedItem)。

使用 IMGUI 创建自定义编辑器
UI 和 UI 详细信息分析器