在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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章