Unity UGUI自定义树形菜单(TreeView)

27 篇文章 10 订阅

先上几张效果图:

        

 

如果你需要的也是这种效果,那你就来对地方了!

 

目前,我们这个树形菜单展现出来的功能如下:

1、可以动态配置数据源;

2、点击每个元素的上下文菜单按钮(也就是图中的三角形按钮),可以收缩或展开它的子元素;

3、可以单独判断某一元素的复选框是否被勾选,或者直接获取当前树形菜单中所有被勾选的元素;

4、树形菜单统一控制其下所有子元素按钮的事件分发;

5、可自动调节的滚动视野边缘,根据当前可见的子元素数量进行横向以及纵向的伸缩;

 

一、首先,我们先制作子元素的模板(Template),也就是图中菜单的单个元素,用它来根据数据源动态克隆出多个子元素,这里的话,很显然我们的模板是由两个Button加一个Toggle和一个Text组成的,如下:

 

ContextButton    TreeViewToggle     TreeViewButton(TreeViewText)

 

 

图中的text是一个文本框,用于描述此元素的名称或内容,它们对应的结构就是这样:

 

 

二、我们的每个子元素都会携带一个TreeViewItem脚本,用于描述自身在整个树形菜单中与其他元素的父子关系,而整个树形菜单的控制由TreeViewControl来实现,首先,TreeViewControl会根据提供的数据源来生成所有的子元素,当然,改变数据源之后进行重新生成的时候也是这个方法,干的事情很简单,就是用模板不停的创建元素,并给他们建立父子关系:

 

/// <summary>
    /// 生成树形菜单
    /// </summary>
    public void GenerateTreeView()
    {
        //删除可能已经存在的树形菜单元素
        if (_treeViewItems != null)
        {
            for (int i = 0; i < _treeViewItems.Count; i++)
            {
                Destroy(_treeViewItems[i]);
            }
            _treeViewItems.Clear();
        }
        //重新创建树形菜单元素
        _treeViewItems = new List<GameObject>();
        for (int i = 0; i < Data.Count; i++)
        {
            GameObject item = Instantiate(Template);

            if (Data[i].ParentID == -1)
            {
                item.GetComponent<TreeViewItem>().SetHierarchy(0);
                item.GetComponent<TreeViewItem>().SetParent(null);
            }
            else
            {
                TreeViewItem tvi = _treeViewItems[Data[i].ParentID].GetComponent<TreeViewItem>();
                item.GetComponent<TreeViewItem>().SetHierarchy(tvi.GetHierarchy() + 1);
                item.GetComponent<TreeViewItem>().SetParent(tvi);
                tvi.AddChildren(item.GetComponent<TreeViewItem>());
            }

            item.transform.name = "TreeViewItem";
            item.transform.FindChild("TreeViewText").GetComponent<Text>().text = Data[i].Name;
            item.transform.SetParent(TreeItems);
            item.transform.localPosition = Vector3.zero;
            item.transform.localScale = Vector3.one;
            item.transform.localRotation = Quaternion.Euler(Vector3.zero);
            item.SetActive(true);

            _treeViewItems.Add(item);
        }
    }

 

 

 

 

 

三、树形菜单生成完毕之后此时所有元素虽然都记录了自身与其他元素的父子关系,但他们的位置都是在Vector3.zero的,毕竟我们的菜单元素在创建的时候都是一股脑儿的丢到原点位置的,创建君可不管这么多元素挤在一堆会不会憋死,好吧,之后规整列队的事情就交给刷新君来完成了,刷新君玩的一手好递归,它会遍历所有元素并剔除不可见的元素(也就是点击三角按钮隐藏了),并将它们一个一个的重新排列整齐,子排在父之后,孙排在子之后,以此类推......它会遍历每个元素的子元素列表,发现子元素可见便进入子元素列表,发现孙元素可见便进入孙元素列表:

 

/// <summary>
    /// 刷新树形菜单
    /// </summary>
    public void RefreshTreeView()
    {
        _yIndex = 0;
        _hierarchy = 0;

        //复制一份菜单
        _treeViewItemsClone = new List<GameObject>(_treeViewItems);

        //用复制的菜单进行刷新计算
        for (int i = 0; i < _treeViewItemsClone.Count; i++)
        {
            //已经计算过或者不需要计算位置的元素
            if (_treeViewItemsClone[i] == null || !_treeViewItemsClone[i].activeSelf)
            {
                continue;
            }

            TreeViewItem tvi = _treeViewItemsClone[i].GetComponent<TreeViewItem>();

            _treeViewItemsClone[i].GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetHierarchy() * HorizontalItemSpace, _yIndex,0);
            _yIndex += (-(ItemHeight + VerticalItemSpace));
            if (tvi.GetHierarchy() > _hierarchy)
            {
                _hierarchy = tvi.GetHierarchy();
            }

            //如果子元素是展开的,继续向下刷新
            if (tvi.IsExpanding)
            {
                RefreshTreeViewChild(tvi);
            }

            _treeViewItemsClone[i] = null;
        }

        //重新计算滚动视野的区域
        float x = _hierarchy * HorizontalItemSpace + ItemWidth;
        float y = Mathf.Abs(_yIndex);
        transform.GetComponent<ScrollRect>().content.sizeDelta = new Vector2(x, y);

        //清空复制的菜单
        _treeViewItemsClone.Clear();
    }
    /// <summary>
    /// 刷新元素的所有子元素
    /// </summary>
    void RefreshTreeViewChild(TreeViewItem tvi)
    {
        for (int i = 0; i < tvi.GetChildrenNumber(); i++)
        {
            tvi.GetChildrenByIndex(i).gameObject.GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetChildrenByIndex(i).GetHierarchy() * HorizontalItemSpace, _yIndex, 0);
            _yIndex += (-(ItemHeight + VerticalItemSpace));
            if (tvi.GetChildrenByIndex(i).GetHierarchy() > _hierarchy)
            {
                _hierarchy = tvi.GetChildrenByIndex(i).GetHierarchy();
            }

            //如果子元素是展开的,继续向下刷新
            if (tvi.GetChildrenByIndex(i).IsExpanding)
            {
                RefreshTreeViewChild(tvi.GetChildrenByIndex(i));
            }

            int index = _treeViewItemsClone.IndexOf(tvi.GetChildrenByIndex(i).gameObject);
            if (index >= 0)
            {
                _treeViewItemsClone[index] = null;
            }
        }
    }


我这里将所有的元素复制了一份用于计算位置,主要就是为了防止在进行一轮刷新时某个元素被访问两次或以上,因为刷新的时候会遍历所有可见元素,如果第一次访问了元素A(元素A的位置被刷新),根据元素A的子元素列表访问到了元素B(元素B的位置被刷新),一直到达子元素的底部后,当不存在更深层次的子元素时,那么返回到元素A之后的元素继续访问,这时在所有元素列表中元素B可能在元素A之后,也就是说元素B已经通过父元素访问过了,不需要做再次访问,他的位置已经是最新的了,而之后根据列表索引很可能再次访问到元素B,如果是这样的话元素B的位置又要被刷新一次,甚至多次,性能影响不说,第二次计算的位置已经不是正确的位置了(总之也就是一个计算逻辑的问题,没看明白可以直接忽略)。

 

 

四、菜单已经创建完毕并且经过了一轮刷新,此时它展示出来的就是这样一个所有子元素都展开的形状(我在demo中指定了数据源,关于数据源怎么设置在后面):

 

 

我们要在每个元素都携带的脚本TreeViewItem中对自身的那个三角形的上下文按钮监听,当鼠标点击它时它的子元素就会被折叠或者展开:

 

/// <summary>
    /// 点击上下文菜单按钮,元素的子元素改变显示状态
    /// </summary>
    void ContextButtonClick()
    {
        if (IsExpanding)
        {
            transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 90);
            IsExpanding = false;
            ChangeChildren(this, false);
        }
        else
        {
            transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 0);
            IsExpanding = true;
            ChangeChildren(this, true);
        }

        //刷新树形菜单
        Controler.RefreshTreeView();
    }
    /// <summary>
    /// 改变某一元素所有子元素的显示状态
    /// </summary>
    void ChangeChildren(TreeViewItem tvi, bool value)
    {
        for (int i = 0; i < tvi.GetChildrenNumber(); i++)
        {
            tvi.GetChildrenByIndex(i).gameObject.SetActive(value);
            ChangeChildren(tvi.GetChildrenByIndex(i), value);
        }
    }


IsExpanding做为每个元素的字段用于设置或读取自身子元素的显示状态,这里根据改变的状态会递归循环此元素的所有子元素及孙元素,让他们可见或隐藏。

 

 

 

 

五、对所有的子元素进行统一的事件分发,这里主要就有鼠标点击这一个事件:

 

每个元素都会注册这个事件:(TreeViewItem.cs)

 

void Awake()
    {
        //上下文按钮点击回调
        transform.FindChild("ContextButton").GetComponent<Button>().onClick.AddListener(ContextButtonClick);
        transform.FindChild("TreeViewButton").GetComponent<Button>().onClick.AddListener(delegate () {
            Controler.ClickItem(gameObject);
        });
    }

树形菜单控制器统一分发:(TreeViewControl.cs)

 

 

public delegate void ClickItemdelegate(GameObject item);
    public event ClickItemdelegate ClickItemEvent;

/// <summary>
    /// 鼠标点击子元素事件
    /// </summary>
    public void ClickItem(GameObject item)
    {
        ClickItemEvent(item);
    }

 


六、获取元素的复选框状态判断是否被勾选:

 

 

 

根据元素名称进行筛选,获取此元素的选中状态,如果存在同名元素的话这个可能不好使:

 

/// <summary>
    /// 返回指定名称的子元素是否被勾选
    /// </summary>
    public bool ItemIsCheck(string itemName)
    {
        for (int i = 0; i < _treeViewItems.Count; i++)
        {
            if (_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text == itemName)
            {
                return _treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn;
            }
        }
        return false;
    }


返回树形菜单中所有被勾选的子元素名称集合:

 

 

/// <summary>
    /// 返回树形菜单中被勾选的所有子元素名称
    /// </summary>
    public List<string> ItemsIsCheck()
    {
        List<string> items = new List<string>();

        for (int i = 0; i < _treeViewItems.Count; i++)
        {
            if (_treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn)
            {
                items.Add(_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text);
            }
        }

        return items;
    }

 

 


七、接下来是我们的数据格式TreeViewData,树形菜单的数据源是由这个格式组成的集合:

 

 

/// <summary>
    /// 当前树形菜单的数据源
    /// </summary>
    [HideInInspector]
    public List<TreeViewData> Data = null;


每一个TreeViewData代表一个元素,Name为显示的文本内容,ParentID为它指向的父元素在整个数据集合中的索引,从0开始,-1代表不存在父元素的根元素,当然有时候数据源并不是这个样子的,可能是XML,可能是json,不过都可以通过解析数据源之后再变换成这种方式:

 

 

/// <summary>
/// 树形菜单数据
/// </summary>
public class TreeViewData
{
    /// <summary>
    /// 数据内容
    /// </summary>
    public string Name;
    /// <summary>
    /// 数据所属的父ID
    /// </summary>
    public int ParentID;
}

 

 

八、属性面板的参数:

Template:当前树形菜单的元素模板;

TreeItems:当前树形菜单的元素根物体,自动指定的,这个别去动;

VerticalItemSpace:相邻元素之间的纵向间距;

HorizontalItemSpace:不同层级元素之间的横向间距;

ItemWidth:元素的宽度,若自行修改过Template,这里的值需要自己去计算Template的大概宽度;

ItemHeight:元素的高度,若自行修改过Template,这里的值需要自己去计算Template的大概高度;

 


九、我已经将TreeView打包成了一个插件,在Unity中导入他,便可以直接使用TreeView:

 

导入TreeView.unitypackage以后,先在场景中创建一个Canvas(画布),然后右键直接创建TreeView:

 

 

之后在其他脚本中拿到这个TreeView,直接为他指定数据源(我这里是手动生成,篇幅有点长):

 

//生成数据
        List<TreeViewData> datas = new List<TreeViewData>();

        TreeViewData data = new TreeViewData();
        data.Name = "第一章";
        data.ParentID = -1;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.第一节";
        data.ParentID = 0;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.第二节";
        data.ParentID = 0;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.第一课";
        data.ParentID = 1;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.2.第一课";
        data.ParentID = 2;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.第二课";
        data.ParentID = 1;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.1.第一篇";
        data.ParentID = 3;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.1.第二篇";
        data.ParentID = 3;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.1.2.第一段";
        data.ParentID = 7;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.1.2.第二段";
        data.ParentID = 7;
        datas.Add(data);

        data = new TreeViewData();
        data.Name = "1.1.1.2.1.第一题";
        data.ParentID = 8;
        datas.Add(data);

        //指定数据源
        TreeView.Data = datas;


然后生成树形菜单,连带刷新一次:

 

 

//重新生成树形菜单
        TreeView.GenerateTreeView();
        //刷新树形菜单
        TreeView.RefreshTreeView();


然后注册子元素的鼠标点击事件(委托类型为返回值void,带一个Gameobject类型参数,参数item为被鼠标点中的那个元素的gameobject):

 

 

//注册子元素的鼠标点击事件
        TreeView.ClickItemEvent += CallBack;

void CallBack(GameObject item)
    {
        Debug.Log("点击了 " + item.transform.FindChild("TreeViewText").GetComponent<Text>().text);
    }


以及要获取某一元素的勾选状态:

 

 

bool isCheck = TreeView.ItemIsCheck("第一章");
            Debug.Log("当前树形菜单中的元素 第一章 " + (isCheck?"已被选中!":"未被选中!"));


和获取所有被勾选的元素:

 

 

List<string> items = TreeView.ItemsIsCheck();
            for (int i = 0; i < items.Count; i++)
            {
                Debug.Log("当前树形菜单中被选中的元素有:" + items[i]);
            }

 

 


效果图如下:

 

源码链接:http://download.csdn.net/detail/qq992817263/9750031

 

 

 

  • 12
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
### 回答1: Unity自带的菜单选项可以为开发人员提供快捷的功能,但是当我们需要一个更加专业化的菜单时,Unity的默认菜单选项可能无法满足需求。此时,自定义树形菜单就应运而生。 Unity自定义树形菜单是基于EditorGUILayout的GUI实现的。首先,我们需要扩展Editor类并使用MenuCommand属性,以及EditorGUI类,来为自定义菜单添加菜单项和功能选项。然后,我们可以使用GUILayout实现垂直排列的树形菜单,在每个菜单项的后面添加子菜单。 接下来,我们应该为自定义菜单添加相应的功能。在每个菜单项中,我们可以通过EditorGUILayout实现按钮、文本框、下拉菜单等等,为每个功能选项设计对应的功能。比如,我们可以在菜单项中添加新的对象、展开/折叠所有对象、删除特定的对象等功能。 最后,我们需要重写OnGUI方法,在其中调用自己的菜单绘制函数。这样,在编辑器中打开自定义窗口时,我们就可以看到新的树形菜单和功能选项了。另外,我们要记得在Menu选项中添加自定义的扩展菜单,这样才能在编辑器菜单中添加自己的菜单项。 总而言之,Unity自定义树形菜单功能可以让开发人员更加简便地实现自己的的菜单和功能选项。如果你想为Unity添加一些自定义的、专业化的菜单和功能选项,自定义树形菜单将是一个十分有效的解决方案。 ### 回答2: Unity是一款非常强大的游戏引擎,在其中可以进行大量的开发工作。其中,Unity提供了很多的功能和工具,其中就包括了自定义树形菜单。 在Unity中,树形菜单是一种非常方便的工具,可以让用户轻松地查看所有的功能和对象,并且方便进行管理。如果用户想要自定义树形菜单,可以按照以下的步骤进行操作: 1. 创建一个新的编辑器窗口,负责显示自定义树形菜单。 2. 在这个编辑器窗口中,可以使用GUILayout或者IMGUI等工具来自定义绘制。 3. 使用EditorApplication.hierarchyWindowChanged事件去监听当场景objectId的集合发生了改变时触发。 4. 使用UnityEditor.GUILayout.Popup方法样式,用户可以为每个节点添加下拉箭头。 5. 使用自定义GUILayout.Button方法样式,用户可以为每个节点添加列表中的按钮。 上述步骤是在Unity自定义树形菜单的基本过程,用户可以根据自己的需求进行更改和调整。自定义树形菜单可以大大提高工作效率,让开发者更加方便地管理和操作游戏中的场景、对象和功能。 ### 回答3: Unity是一款十分强大的游戏引擎,是许多游戏制作人员所钟爱的工具。Unity自带的菜单栏虽然已经很强大,但是我们还是有时候需要自定义树形菜单,来更好的实现游戏中的各种功能。这里我们将介绍如何使用Unity自定义树形菜单。 首先,打开Unity的编辑器并打开项目。然后在项目中新建一个C#脚本,并将其命名为“CustomMenu.cs”。我们将在这个脚本中编写我们的树形菜单。 接下来,我们需要为我们的树形菜单设置一个名称。在“CustomMenu.cs”中,使用“[MenuItem(“Custom/MyMenu”)]”,产生一个名为“MyMenu”的菜单。 接下来,我们可以在Unity的编辑器中创建一个新的文件夹,将其命名为“Custom”,这个文件夹将成为我们创建的菜单的父级。 接下来,在“CustomMenu.cs”中,我们可以使用“[MenuItem(“Custom/MyMenu/Do Something”)]”创建菜单的子项,并添加相应的函数来实现这个子项的功能。 最后,在Unity中运行我们的项目,并点击菜单栏“Custom”选项,我们就可以看到我们刚刚创建的树形菜单了。当我们点击“MyMenu”时,会显示“Do Something”子项。当我们点击“Do Something”时,它将调用相应的函数并执行相应的操作。 综上所述,Unity自定义树形菜单的实现方法是很简单的。我们只需要编写一个包含菜单名称和相应函数的脚本即可。可以根据需要创建任意多的子项,并为它们添加相应的功能。自定义菜单能够帮助我们更好地实现游戏中的各种功能,提高我们的工作效率,让我们更加专注于游戏的创作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神码编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值