1. 程式人生 > >在Unity中實現TreeView

在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); });
        
    }
}