在Unity中实现TreeView

在Unity中要实现如下的树形状结构显示,是比较复杂的,相比于专门做二维的软件,效果也不咋样;但想想毕竟Unity主要是开发三维场景的工具,用来做二维界面确实有点可笑,但是也不是说不能实现,只要Unity有Image,那什么都是可以实现的...【Demo下载地址

如何实现这种效果呢,主要的难点在哪里?加载数据并保存到对象中不难,利用得到的数据进行UI动态生成才是关键。

程序设计思路:

1、创建一个通用的预制体,加载各级的条目

2、以TreeView对象为父级,加载第一层选项,以每一个条目下的一个组件为父级加载下一级

3、解析xml得到的数据对象转化为条目对象

4、在条目对象自身的脚本下进行初始化


能想到以上一几点基本上也就有了大概思路了;本程序的实现过程使用了pureMvc架构(如果不想用,那解析xml数据的方法直接写入到静态工具类中也未尝不可,只是程序的耦合度增加),UI界面相对比较独立

那么具体实现需要进行以下几点的深入:

1、制作数据模型

2、解析xml数据到数据模型

3、正确使用pureMVC架构

4、制作UI预制体

5、将数据对象转换为UI对象

6、动态显示功能

7、事件注册

这些问题 是在制作程序的过程中遇到的,也算是一点小经验吧,不一定都能适用,实现TreeView的效果也可能只有这一种。


下面是具体实现:

一、数据模型:

树形图数据的一个特点就是父节点有一堆子节点,和xml数据差不多(解析起来也比较容易),定义了一个XMLDataProxy类,有一个父结点和子结点列表

public class XMLDataProxy : Proxy {
    public XMLDataProxy(string name):base(name){
    }
    public XMLDataProxy ParentNode { get; set; }
    public List<XMLDataProxy> ChildNodes { get; set; }
}

二、解析XML数据到XMLDataProxy


由于程序使用了PureMVC架构,所以这里直接用Commond类来实现以上数据的解析和注册

public class XMLLoadCommond : SimpleCommand
{
    public override void Execute(INotification notification)
    {
        string xmlPath = Application.streamingAssetsPath + "/Projects.xml";
        XMLDataProxy m_XmlDataProxy = XMLParse(xmlPath);                                   //解析数据的核心
        AppFacade.Instance.RegisterProxy(m_XmlDataProxy);                                    //将解析得到的数据注册了Model
        AppFacade.Instance.SendNotification(NotiConst.ThreeView);                        //通知View层进行解析
    }
    /// <summary>
    /// 从XML源加载数据
    /// </summary>
    /// <param name="fileName"></param>
    /// <returns></returns>
    public static XMLDataProxy XMLParse(string fileName)
    {
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(fileName);
        XmlNode rootNode = xmlDoc.SelectSingleNode("Projects");

        XMLDataProxy rootProxyNode = new XMLDataProxy("Project");

        XMLDataProxyAppendChild(rootNode, rootProxyNode);                                //解析第一层数据

        return rootProxyNode;
    }
    /// <summary>
    /// 利用XMLNode创建xmlDataProxy
    /// </summary>
    /// <param name="xNode"></param>
    /// <returns></returns>
    public static void XMLDataProxyAppendChild(XmlNode xNode, XMLDataProxy parent)                         //递归遍历所有子结点数据
    {
        if (!xNode.HasChildNodes)
        {
            return;
        }
        else
        {
            foreach (XmlElement item in xNode)
            {
                XMLDataProxy cNode = new XMLDataProxy(item.GetAttribute("name"));
                cNode.ParentNode = parent;
                if (parent.ChildNodes == null)
                {
                    parent.ChildNodes = new List<XMLDataProxy>();
                }
                parent.ChildNodes.Add(cNode);
                XMLDataProxyAppendChild(item, cNode);
            }
        }
    }
}
三、关于PureMvc的使用

pureMVC中的mvc的对象要经过注册也能使用,实现INotifier接口的对象一般有三种发送信息的方式,一是只发送通知的内容,二是发送通知的内容和数据包(object)三是还要发送类型。其中最常用的是第二种,对指定的观察者发送一个数据包。但由于加载 的数据常常不是死的,而是动态加载出来的而且常常不是一个简单的int,float ,string 和bool等类型。更为高级的方式是将proxy 数据注册到model中,在Media需要的时候查找出来就可以了。而加载数据这个过程,交给commond最适合不过了,相对于静态工具类也更为合理。

1.commond类的注册与执行

void Start () {
        AppFacade.Instance.RegisterCommand(NotiConst.LoadProject, typeof(XMLLoadCommond));
    }
    // Update is called once per frame
    void OnGUI()
    {
        if (GUILayout.Button("加载xml文档"))
        {
            AppFacade.Instance.SendNotification(NotiConst.LoadProject);
        }
    }

2、meida类的注册

public class TreeView : Mediator
{
    private XMLDataProxy m_XmlDataProxy;
    private GameObject itemPfb;//一级节点
    public override IList<NotiConst> ListNotificationInterests()
    {
        return new List<NotiConst>() { NotiConst.ThreeView};
    }
    public override void HandleNotification(INotification notification)
    {
        //将信息加载到View
        m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
        LoadAllNodes();
    }
    void Awake()
    {
        itemPfb = transform.Find("Item").gameObject;
        AppFacade.Instance.RegisterMediator(this);
    }

}

四、制作预制体

1、根目录上添加两个组件,一个是动态调整,一个是垂直列表

2、通用预制体对象

将Item作为根目录的子物体,这样就可以实现列表效果了

这个过程最最关键的是锚点问题,可以从上图看到,TreeView对象的锚点在左上角的同时,其中心点也必须是左上角,因为动态加载Item时,希望是最高点不发出动,整个向下扩张。

在第一级制作成功后,想第二级当然也要实现如第一级一样的扩展性,于是在item下创建了一个Panel,也增加了如TreeView的两个组件。但为什么要放置在Contant下,见下图:



如果想偷懒不在代码中来控制Panel的座标,这个方法实现是不错,在Panel下增加一个item后的效果变成:


很显然后Panel的对齐方式是右上角对齐...也就是说只需要一个item对象,就可以创建无限多个层级了。其实有人已经发现了这个过程中每创建出来一个末端就会多出一个Content和一个Panel,这位下来影响程序的性能,遇到这个问题,其实将panel和item拆开加载也是可以的。,这个程序还是很有优化的空间的,在说吧...

五,将数据对象转换为UI对象

这个过程是在TreeView得到了数据后进行的,由于创建对象过程中需要进行多个调整,最好的办法还是创建一个TreeItem脚本,来操作创建出来 的对象:

public class TreeItem : MonoBehaviour

{

public string NodeName {
        get
        {
            return GetComponentInChildren<Text>().text;
        }
        set
        {
            GetComponentInChildren<Text>().text = value;
        }
    }//中文名
    public Transform Parent {
        set { transform.SetParent(value); }
        get { return transform.parent; }
    }
    public Transform ClildPanel {
        get { return transform.Find("Contant/Panel"); }
    }
    public ToggleGroup ToggleGroup{
        set { GetComponent<Toggle>().group = value; }
    }//设置group
    private bool isLastOne;//最后一层,取消自动选中
    private bool Selected
    {
        get { return ClildPanel.gameObject.activeSelf; }
        set { ClildPanel.gameObject.SetActive(value); }
    }//是否选中(同级之中最多只有一个可以选中)
    private Toggle m_toggle;
    void Awake () {
        ClildPanel.gameObject.SetActive(false);
        m_toggle = GetComponent<Toggle>();
        m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
    }
    void Start()
    {
        transform.localScale = Vector3.one;
    }

}

将对象上的一些组件性质与属性进行绑定,这样,只要对这些属性进行赋值和取值就可以了

在TreeView获得数据后进行创建的过程写在自身脚本中:

public class TreeView : Mediator
{
    private XMLDataProxy m_XmlDataProxy;
    private GameObject itemPfb;//一级节点
    public override IList<NotiConst> ListNotificationInterests()
    {
        return new List<NotiConst>() { NotiConst.ThreeView};
    }
    public override void HandleNotification(INotification notification)
    {
        //将信息加载到View
        m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
        LoadAllNodes();
    }
    void Awake()
    {
        itemPfb = transform.Find("Item").gameObject;
        AppFacade.Instance.RegisterMediator(this);
    }
    private void LoadAllNodes()
    {
        ToggleGroup tgg = gameObject.AddComponent<ToggleGroup>();
        foreach (var item in m_XmlDataProxy.ChildNodes)
        {
            CreateQuaders(item, transform, itemPfb,tgg);
        }
        itemPfb.SetActive(false);
        AppFacade.Instance.RemoveCommand(NotiConst.LoadProject);
    }
    public void CreateQuaders(XMLDataProxy data, Transform parent, GameObject btnpfb,ToggleGroup tgg)                                 //递归全部创建
    {
        TreeItem treeItem = Instantiate(btnpfb).GetComponent<TreeItem>();
        treeItem.NodeName = data.ProxyName;
        treeItem.Parent = parent;
        treeItem.ToggleGroup = tgg;

        if (data.ChildNodes != null && data.ChildNodes.Count > 0)
        {
            ToggleGroup Newtgg = treeItem.gameObject.AddComponent<ToggleGroup>();

            foreach (var item in data.ChildNodes)
            {
                CreateQuaders(item, treeItem.ClildPanel, btnpfb, Newtgg);
            }
        }
        else//给最后一级toggle添加事件
        {
            treeItem.ToggleSelected(OnLastItemSelected);
        }
    }
    private void OnLastItemSelected(string itemName)
    {
        Debug.LogWarning("you need notify"+itemName);
    }
}

六、动态显示功能

动态显示鼠标移动到的对象上,这些条目本身是考虑用Button的,但想到这个问题的时候,实现过程比较困难,还需要考虑哪些条目刚刚打开了,哪些条目需要进行关闭。最后想到Toggle有一个属性是可以实现多个Toggle同时只有一个是isOn。这样一想和treeView的效果简直就是一模一样嘛。于是在创建对象的过程上中只需要在父级上添加 一 个ToggleGroup,并将Toggle的这个属性赋上这个ToggleGroup。这样就实现了点击打开对应的项目。

说好的鼠标移动到对象上可以动态显示呢,不急,点击都实现了还差一个OnMouseEnter类似的功能么,当然,在UGUI上鼠标移入的事件是继承了IPointerEnterHandler,完善条目脚本TreeItem如下,其中点击事件的注册也写入了,值得注意的最后一级常常需要触发不同的事件(不单单是展开),于是在创建对象的时候判断并将OnLastItemSelected这个方法注册到上上点中事件。


public class TreeItem : MonoBehaviour,IPointerEnterHandler {
    public string NodeName {
        get
        {
            return GetComponentInChildren<Text>().text;
        }
        set
        {
            GetComponentInChildren<Text>().text = value;
        }
    }//中文名
    public Transform Parent {
        set { transform.SetParent(value); }
        get { return transform.parent; }
    }
    public Transform ClildPanel {
        get { return transform.Find("Contant/Panel"); }
    }
    public ToggleGroup ToggleGroup{
        set { GetComponent<Toggle>().group = value; }
    }//设置group
    private bool isLastOne;//最后一层,取消自动选中
    private bool Selected
    {
        get { return ClildPanel.gameObject.activeSelf; }
        set { ClildPanel.gameObject.SetActive(value); }
    }//是否选中(同级之中最多只有一个可以选中)
    private Toggle m_toggle;
    void Awake () {
        ClildPanel.gameObject.SetActive(false);
        m_toggle = GetComponent<Toggle>();
        m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
    }
    void Start()
    {
        transform.localScale = Vector3.one;
    }
    public void OnPointerEnter(PointerEventData eventData)
    {

        m_toggle.isOn = !isLastOne;
    }

    /// <summary>
    /// 点击回调
    /// </summary>
    /// <param name="action"></param>
    public void ToggleSelected(UnityAction<string> action)
    {
        isLastOne = true;
        m_toggle.onValueChanged.RemoveAllListeners();
        m_toggle.onValueChanged.AddListener((x) => { if (x) action(NodeName); });
        
    }
}


发布了32 篇原创文章 · 获赞 12 · 访问量 5万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章