Unity杂文——Editor的Tree

  1. 简介
  2. TreeElementGUI
  3. TreeViewItemGUI
  4. TreeGUIUtility
  5. TreeGUIModel
  6. TreeViewWithGUIModel

原文地址

简介

本文介绍了一种用于编辑器开发过程中树形结构数据进行渲染的辅助脚本。

TreeElementGUI

TreeElementGUI是树形结构元素渲染的基类,包含了节点深度、父节点、子节点、节点名字、节点ID等属性,并且提供了无参构造函数和有参构造函数。

TreeElementGUI
/// <summary>
/// 树结构的元素
/// </summary>
public abstract class TreeElementGUI
{
    private int m_ID;
    private string m_Name;
    private int m_Depth;
    [NonSerialized] private TreeElementGUI m_Parent;
    [NonSerialized] private List<TreeElementGUI> m_Children;

    /// <summary>
    /// 深度
    /// </summary>
    public int Depth
    {
        get => m_Depth;
        set => m_Depth = value;
    }

    /// <summary>
    /// 父节点
    /// </summary>
    public TreeElementGUI Parent
    {
        get => m_Parent;
        set => m_Parent = value;
    }

    /// <summary>
    /// 子节点
    /// </summary>
    public List<TreeElementGUI> Children
    {
        get => m_Children;
        set => m_Children = value;
    }
    
    /// <summary>
    /// 是否有子节点
    /// </summary>
    public bool HasChildren => m_Children != null && m_Children.Count > 0;

    /// <summary>
    /// 节点名字
    /// </summary>
    public string Name
    {
        get => m_Name;
        set => m_Name = value;
    }

    /// <summary>
    /// 节点ID
    /// </summary>
    public int Id
    {
        get => m_ID;
        set => m_ID = value;
    }

    /// <summary>
    /// 无参构造函数
    /// </summary>
    protected TreeElementGUI() :this(-1, -1, "")
    {
        
    }

    /// <summary>
    /// 有参构造函数
    /// </summary>
    /// <param name="id"></param>
    /// <param name="depth"></param>
    /// <param name="name"></param>
    protected TreeElementGUI(int id, int depth, string name)
    {
        m_Name = name;
        m_ID = id;
        m_Depth = depth;
    }

    public abstract void OnGUI(Rect rect, int columnIndex);
    public abstract bool IsMatchSearch(string search);
}

TreeViewItemGUI

TreeViewItemGUI是节点元素显示的类,继承自TreeViewItem,并且包含了一个泛型参数T,其中T必须是TreeElementGUI的子类。TreeViewItemGUI包含了一个Data属性,用于获取节点元素的数据,同时提供了OnGUI和IsMatchSearch方法,用于在UI上绘制节点元素和进行搜索匹配。

TreeViewItemGUI
/// <summary>
/// 树结构编辑器显示
/// </summary>
/// <typeparam name="T"></typeparam>
public class TreeViewItemGUI<T> : TreeViewItem where T : TreeElementGUI
{
    private readonly T m_Data;

    public T Data => m_Data;

    public TreeViewItemGUI(int id, int depth, string displayName, T data) : base(id, depth, displayName)
    {
        m_Data = data;
    }

    public void OnGUI(Rect rect, int columnIndex)
    {
        m_Data.OnGUI(rect, columnIndex);
    }

    public bool IsMatchSearch(string search)
    {
        return m_Data.IsMatchSearch(search);
    }
}

TreeGUIUtility

TreeGUIUtility是一个用于编辑器开发过程中树形结构数据进行渲染的辅助脚本。该脚本包含了一些常用的方法,可以帮助开发者处理树形结构数据。

其中,TreeToList方法可以将树形结构数据转换为列表形式,Find方法可以在树形结构数据中查找符合条件的节点元素,ListToTree方法可以将列表形式的数据转换为树形结构数据,ValidateDepthValues方法可以检查列表中的深度值是否合法,UpdateDepthValues方法可以更新树形结构数据中的深度值,FindCommonAncestorsWithinList方法可以查找列表中共同的祖先节点。

TreeGUIUtility
public static class TreeGUIUtility
{
    public static void TreeToList<T>(T root, IList<T> result) where T : TreeElementGUI
    {
        if (result == null)
            throw new NullReferenceException("The input 'IList<T> result' list is null");
        result.Clear();

        var stack = new Stack<T>();
        stack.Push(root);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            result.Add(current);

            if (current.Children != null && current.Children.Count > 0)
            {
                for (var i = current.Children.Count - 1; i >= 0; i--)
                {
                    stack.Push((T)current.Children[i]);
                }
            }
        }
    }
    
    public static T Find<T>(T root, Func<T, bool> comparer) where T : TreeElementGUI
    {
        var stack = new Stack<T>();
        stack.Push(root);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if(root != current && comparer(current))
            {
                return current;
            }
            if (current.Children != null && current.Children.Count > 0)
            {
                for (var i = current.Children.Count - 1; i >= 0; i--)
                {
                    stack.Push((T)current.Children[i]);
                }
            }
        }

        return null;
    }

    /// <summary>
    /// List转成树结构
    /// </summary>
    /// <param name="list"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T ListToTree<T>(IList<T> list) where T : TreeElementGUI
    {
        // 验证深度的值
        ValidateDepthValues(list);

        // 清理状态
        foreach (var element in list)
        {
            element.Parent = null;
            element.Children = null;
        }

        // 设置子节点和父节点
        for (var parentIndex = 0; parentIndex < list.Count; parentIndex++)
        {
            var parent = list[parentIndex];
            var alreadyHasValidChildren = parent.Children != null;
            if (alreadyHasValidChildren)
                continue;

            var parentDepth = parent.Depth;
            var childCount = 0;

            // Count children based depth value, we are looking at children until it's the same depth as this object
            for (var i = parentIndex + 1; i < list.Count; i++)
            {
                if (list[i].Depth == parentDepth + 1)
                    childCount++;
                if (list[i].Depth <= parentDepth)
                    break;
            }

            // Fill child array
            List<TreeElementGUI> childList = null;
            if (childCount != 0)
            {
                childList = new List<TreeElementGUI>(childCount); // Allocate once
                childCount = 0;
                for (var i = parentIndex + 1; i < list.Count; i++)
                {
                    if (list[i].Depth == parentDepth + 1)
                    {
                        list[i].Parent = parent;
                        childList.Add(list[i]);
                        childCount++;
                    }

                    if (list[i].Depth <= parentDepth)
                        break;
                }
            }

            parent.Children = childList;
        }

        return list[0];
    }

    /// <summary>
    /// 检查List的深度值
    /// </summary>
    /// <param name="list"></param>
    /// <typeparam name="T"></typeparam>
    /// <exception cref="ArgumentException"></exception>
    public static void ValidateDepthValues<T>(IList<T> list) where T : TreeElementGUI
    {
        if (list.Count == 0)
            throw new ArgumentException("list should have items, count is 0, check before calling ValidateDepthValues", nameof(list));

        if (list[0].Depth != -1)
            throw new ArgumentException("list item at index 0 should have a depth of -1 (since this should be the hidden root of the tree). Depth is: " + list[0].Depth, nameof(list));

        for (var i = 0; i < list.Count - 1; i++)
        {
            var depth = list[i].Depth;
            var nextDepth = list[i + 1].Depth;
            if (nextDepth > depth && nextDepth - depth > 1)
                throw new ArgumentException(string.Format("Invalid depth info in input list. Depth cannot increase more than 1 per row. Index {0} has depth {1} while index {2} has depth {3}", i, depth, i + 1, nextDepth));
        }

        for (var i = 1; i < list.Count; ++i)
            if (list[i].Depth < 0)
                throw new ArgumentException("Invalid depth value for item at index " + i + ". Only the first item (the root) should have depth below 0.");

        if (list.Count > 1 && list[1].Depth != 0)
            throw new ArgumentException("Input list item at index 1 is assumed to have a depth of 0", nameof(list));
    }


    /// <summary>
    /// 更新深度值
    /// </summary>
    /// <param name="root"></param>
    /// <typeparam name="T"></typeparam>
    /// <exception cref="ArgumentNullException"></exception>
    public static void UpdateDepthValues<T>(T root) where T : TreeElementGUI
    {
        if (root == null)
            throw new ArgumentNullException(nameof(root), "The root is null");

        if (!root.HasChildren)
            return;

        var stack = new Stack<TreeElementGUI>();
        stack.Push(root);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if (current.Children != null)
            {
                foreach (var child in current.Children)
                {
                    child.Depth = current.Depth + 1;
                    stack.Push(child);
                }
            }
        }
    }

    /// <summary>
    /// 判断是否是子节点
    /// </summary>
    /// <param name="child"></param>
    /// <param name="elements"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    static bool IsChildOf<T>(T child, IList<T> elements) where T : TreeElementGUI
    {
        while (child != null)
        {
            child = (T)child.Parent;
            if (elements.Contains(child))
                return true;
        }
        return false;
    }

    /// <summary>
    /// 查找共同的祖先节点
    /// </summary>
    /// <param name="elements"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static IList<T> FindCommonAncestorsWithinList<T>(IList<T> elements) where T : TreeElementGUI
    {
        if (elements.Count == 1)
            return new List<T>(elements);

        var result = new List<T>(elements);
        result.RemoveAll(g => IsChildOf(g, elements));
        return result;
    }
}

TreeGUIModel

TreeGUIModel是一个用于管理树形结构数据的类,包含了增加、移除、移动、清空、更新等方法,可以帮助开发者更方便地处理树形结构数据。

TreeGUIModel
public class TreeGUIModel<T> where T : TreeElementGUI, new()
{
    private T m_Root;
    private int m_MaxID;
    private bool m_IsDirty;

    public T Root => m_Root;

    public event Action<T> added;
    public event Action<T> removed;

    public int Count => m_Root.Children.Count;
    public bool IsDirty => m_IsDirty;

    public TreeGUIModel()
    {
        m_Root = new T
        {
            Id = GenerateUniqueID(), Depth = -1, Name = $"{typeof(T).Name} - Root",
            Children = new List<TreeElementGUI>()
        };
        m_IsDirty = true;
    }

    /// <summary>
    /// 根据id查找元素
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public T Find(int id)
    {
        return (T)m_Root.Children.FirstOrDefault(element => element.Id == id);
    }

    /// <summary>
    /// 自动生成唯一ID
    /// </summary>
    /// <returns></returns>
    public int GenerateUniqueID()
    {
        return ++m_MaxID;
    }

    /// <summary>
    /// 获得所有的子节点
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public IList<int> GetAncestors(int id)
    {
        var parents = new List<int>();
        var item = Find(id);
        if (item != null)
        {
            while (item.Parent != null)
            {
                parents.Add(item.Parent.Id);
                item = (T)item.Parent;
            }
        }
        return parents;
    }

    /// <summary>
    /// 获得有子节点的所有子节点
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public IList<int> GetDescendantsThatHaveChildren(int id)
    {
        var searchFromThis = Find(id);
        return searchFromThis != null ? GetParentsBelowStackBased(searchFromThis) : new List<int>();
    }

    /// <summary>
    /// 获得基于栈的所有子节点
    /// </summary>
    /// <param name="searchFromThis"></param>
    /// <returns></returns>
    private IList<int> GetParentsBelowStackBased(TreeElementGUI searchFromThis)
    {
        var stack = new Stack<TreeElementGUI>();
        stack.Push(searchFromThis);

        var parentsBelow = new List<int>();
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if (current.HasChildren)
            {
                parentsBelow.Add(current.Id);
                foreach (var T in current.Children)
                {
                    stack.Push(T);
                }
            }
        }

        return parentsBelow;
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elementID"></param>
    public void RemoveElements(int elementID)
    {
        var elements = m_Root.Children.Where(element => element.Id == elementID).Cast<T>().ToArray();
        RemoveElements(elements);
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elementIDs"></param>
    public void RemoveElements(IList<int> elementIDs)
    {
        var elements = m_Root.Children.Where(element => elementIDs.Contains(element.Id)).Cast<T>().ToArray();
        RemoveElements(elements);
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elements"></param>
    public void RemoveElements(IList<T> elements)
    {
        var commonAncestors = TreeGUIUtility.FindCommonAncestorsWithinList(elements);

        foreach (var element in commonAncestors)
        {
            element.Parent.Children.Remove(element);
            element.Parent = null;
            removed?.Invoke(element);
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="elements"></param>
    /// <param name="parent"></param>
    /// <param name="insertPosition"></param>
    /// <param name="isNew"></param>
    /// <exception cref="ArgumentNullException"></exception>
    public void AddElements(IList<T> elements, TreeElementGUI parent, int insertPosition, bool isNew = false)
    {
        if (elements == null)
            throw new ArgumentNullException(nameof(elements), "elements is null");
        if (elements.Count == 0)
            throw new ArgumentNullException(nameof(elements), "elements Count is 0: nothing to add");
        if (parent == null)
            throw new ArgumentNullException(nameof(parent), "parent is null");

        parent.Children ??= new List<TreeElementGUI>();

        parent.Children.InsertRange(insertPosition, elements);
        foreach (var element in elements)
        {
            element.Parent = parent;
            element.Depth = parent.Depth + 1;
            TreeGUIUtility.UpdateDepthValues(element);
            if(isNew)
            {
                added?.Invoke(element);
            }
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="root"></param>
    /// <param name="isNew"></param>
    public void AddElement(T root, bool isNew = false)
    {
        root.Id = GenerateUniqueID();
        root.Depth = -1;
        root.Parent = m_Root;
        m_Root.Children.Add(root);
        if(isNew)
        {
            added?.Invoke(root);
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="element"></param>
    /// <param name="parent"></param>
    /// <param name="insertPosition"></param>
    /// <param name="isNew"></param>
    public void AddElement(T element, T parent, int insertPosition, bool isNew = false)
    {
        parent.Children ??= new List<TreeElementGUI>();
        parent.Children.Insert(insertPosition, element);
        element.Parent = parent;

        TreeGUIUtility.UpdateDepthValues(parent);

        if (isNew)
        {
            added?.Invoke(element);
        }

        SetDirty();
    }

    /// <summary>
    /// 移动元素
    /// </summary>
    /// <param name="parentElement"></param>
    /// <param name="insertionIndex"></param>
    /// <param name="elements"></param>
    /// <exception cref="ArgumentException"></exception>
    public void MoveElements(TreeElementGUI parentElement, int insertionIndex, List<TreeElementGUI> elements)
    {
        if (insertionIndex < 0)
            throw new ArgumentException("Invalid input: insertionIndex is -1, client needs to decide what index elements should be reparented at");

        // Invalid reparenting input
        if (parentElement == null)
            return;

        // We are moving items so we adjust the insertion index to accomodate that any items above the insertion index is removed before inserting
        if (insertionIndex > 0)
            insertionIndex -= parentElement.Children.GetRange(0, insertionIndex).Count(elements.Contains);

        // Remove draggedItems from their parents
        foreach (var draggedItem in elements)
        {
            draggedItem.Parent.Children.Remove(draggedItem);    // remove from old parent
            draggedItem.Parent = parentElement;                 // set new parent
        }

        parentElement.Children ??= new List<TreeElementGUI>();

        // Insert dragged items under new parent
        parentElement.Children.InsertRange(insertionIndex, elements);

        TreeGUIUtility.UpdateDepthValues(Root);

        SetDirty();
    }

    /// <summary>
    /// 标记为脏
    /// </summary>
    private void SetDirty()
    {
        m_IsDirty = true;
    }

    /// <summary>
    /// 清理
    /// </summary>
    public void Clear()
    {
        m_Root.Children.Clear();
        SetDirty();
    }

    /// <summary>
    /// 更新
    /// </summary>
    internal void Update()
    {
        m_IsDirty = true;
    }
}

TreeViewWithGUIModel

TreeViewWithGUIModel是一个抽象类,继承自TreeView,用于在编辑器中渲染树形结构数据。该类包含了一些常用的方法,可以帮助开发者处理树形结构数据。

该类还包含了一些属性,用于控制树形结构数据的外观和行高等。

TreeViewWithGUIModel类的子类可以通过实现抽象方法来自定义树形结构数据的渲染和行为。

TreeViewWithGUIModel
public abstract class TreeViewWithGUIModel<T> : TreeView where  T : TreeElementGUI, new()
{
    protected TreeGUIModel<T> m_TreeModel;
    private readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);
    public bool ShowAlternatingRowBackgrounds
    {
        get => showAlternatingRowBackgrounds;
        set => showAlternatingRowBackgrounds = value;
    }

    public bool ShowBorder
    {
        get => showBorder;
        set => showBorder = value;
    }

    public float RowHeight
    {
        get => rowHeight;
        set => rowHeight = value;
    }

    protected TreeViewWithGUIModel(TreeViewState state, TreeGUIModel<T> model) : base(state)
    {
        Init(model);
    }

    protected TreeViewWithGUIModel(TreeViewState state, MultiColumnHeader multiColumnHeader, TreeGUIModel<T> model) : base(state, multiColumnHeader)
    {
        Init(model);
        multiColumnHeader.sortingChanged += OnSortingChanged;

    }
    private void Init(TreeGUIModel<T> model)
    {
        m_TreeModel = model;
    }

    private void OnSortingChanged(MultiColumnHeader _multiColumnHeader)
    {
        SortIfNeeded(rootItem, m_Rows);
    }

    private void SortIfNeeded(TreeViewItem root, List<TreeViewItem> rows)
    {
        if( null == multiColumnHeader) return;

        if ( rows.Count <= 1)
            return;

        if (multiColumnHeader.sortedColumnIndex == -1)
        {
            return; // No column to sort for (just use the order the data are in)
        }

        // Sort the roots of the existing tree items
        rootItem.children = SortByMultipleColumns(rows);
        TreeToList(root, rows);
        Repaint();
    }

    public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
    {
        if (root == null)
            throw new NullReferenceException("root");
        if (result == null)
            throw new NullReferenceException("result");

        result.Clear();

        if (root.children == null)
            return;

        var stack = new Stack<TreeViewItem>();
        for (var i = root.children.Count - 1; i >= 0; i--)
            stack.Push(root.children[i]);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            result.Add(current);

            if (current.hasChildren && current.children[0] != null)
            {
                for (var i = current.children.Count - 1; i >= 0; i--)
                {
                    stack.Push(current.children[i]);
                }
            }
        }
    }
    protected override TreeViewItem BuildRoot()
    {
        return null == m_TreeModel.Root
            ? new TreeViewItemGUI<T>(0, -1, "Root", null)
            : new TreeViewItemGUI<T>(m_TreeModel.Root.Id, -1, m_TreeModel.Root.Name, m_TreeModel.Root);
    }

    protected override bool DoesItemMatchSearch(TreeViewItem item, string search)
    {
        var target = (TreeViewItemGUI<T>)item;
        return target.IsMatchSearch(search);
    }

    protected override void RowGUI(RowGUIArgs args)
    {
        var item = (TreeViewItemGUI<T>) args.item;
        if (null == multiColumnHeader)
        {
            item.OnGUI(args.rowRect, 0);
        }
        else
        {
            var columns = args.GetNumVisibleColumns();
            for (var i = 0; i < columns; i++)
            {
                var rt = args.GetCellRect(i);
                CenterRectUsingSingleLineHeight(ref rt);
                item.OnGUI(rt, args.GetColumn(i));
            }
        }
    }

    protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        m_Rows.Clear();
        if (m_TreeModel.Root == null)
        {
            return m_Rows;
        }

        if (hasSearch)
        {
            Search(m_TreeModel.Root, searchString, m_Rows);
        }
        else if (m_TreeModel.Root.HasChildren)
        {
            AddChildrenRecursive(root, m_TreeModel.Root, 0, m_Rows);
        }
        SortIfNeeded(root, m_Rows);
        return m_Rows;
    }

    private void AddChildrenRecursive(TreeViewItem root, T parent, int depth, IList<TreeViewItem> newRows)
    {
        foreach (var treeElement in parent.Children)
        {
            var child = (T) treeElement;
            var item = new TreeViewItemGUI<T>(child.Id, depth, child.Name, child);
            newRows.Add(item);
            root.AddChild(item);

            if (child.HasChildren)
            {
                if (IsExpanded(child.Id))
                {
                    AddChildrenRecursive(item, child, depth + 1, newRows);
                }
                else
                {
                    item.children = CreateChildListForCollapsedParent();
                }
            }
        }
    }

    private void Search(T searchFromThis, string search, List<TreeViewItem> result)
    {
        if (string.IsNullOrEmpty(search))
            throw new ArgumentException("Invalid search: cannot be null or empty", nameof(search));

        var stack = new Stack<T>();
        foreach (var element in searchFromThis.Children)
            stack.Push((T)element);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            // Matches search?
            if (current.IsMatchSearch(search))
            {
                result.Add(new TreeViewItemGUI<T>(current.Id, 0, current.Name, current));
            }

            if (current.Children != null && current.Children.Count > 0)
            {
                foreach (var element in current.Children)
                {
                    stack.Push((T)element);
                }
            }
        }
        SortSearchResult(result);
    }

    public override IList<TreeViewItem> GetRows()
    {
        return m_Rows;
    }

    protected virtual void SortSearchResult(List<TreeViewItem> rows)
    {
        // sort by displayName by default, can be overriden for multColumn solutions
        rows.Sort((x, y) => EditorUtility.NaturalCompare(x.displayName, y.displayName));
    }

    protected virtual List<TreeViewItem> SortByMultipleColumns(List<TreeViewItem> children)
    {
        return children;
    }


    public Rect DoLayout(params GUILayoutOption[] options)
    {
        if(m_TreeModel.IsDirty)
        {
            m_TreeModel.Update();
            Reload();
        }

        GUILayout.BeginVertical();
        var rect = GUILayoutUtility.GetRect(GUIContent.none,
                                           GUIStyle.none,
                                           options);
        OnGUI(rect);
        GUILayout.EndVertical();

        return rect;
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 841774407@qq.com

×

喜欢就点赞,疼爱就打赏