Version: 2020.2
언어: 한국어
커스텀 에디터
에디터 문제 해결

트리 뷰

이 페이지의 정보는 독자가 IMGUI(Immediate Mode GUI) 개념에 관한 기본 지식이 있음을 전제로 합니다. IMGUI와 에디터 창의 커스텀화에 대한 내용은 에디터 확장IMGUI Unity 블로그를 참조하십시오.

TreeView는 펼치고 접을 수 있는 계층 구조적 데이터를 나타낼 때 사용하는 IMGUI 컨트롤입니다. 다른 IMGUI 컨트롤 및 컴포넌트와 함께 사용할 수 있는 고도로 커스텀화 가능한 리스트 뷰를 생성할 때와 에디터 창을 위한 다중 열로 구성된 표를 생성할 때 TreeView를 사용합니다.

사용 가능한 TreeView API 함수에 대한 내용은 TreeView Unity 스크립팅 API 문서를 참조하십시오.

MultiColumnHeader와 SearchField를 포함한 TreeView의 예
MultiColumnHeader와 SearchField를 포함한 TreeView의 예

TreeView는 트리 데이터 모델이 아님에 유의하십시오. 선호하는 모든 트리 데이터 구조를 사용하여 TreeView를 구성할 수 있습니다. C# 트리 모델 또는 Transform 계층 구조와 같은 Unity 기반 트리 구조가 될 수 있습니다.

TreeView의 렌더링은 행이라고 하는 펼쳐지는 아이템의 리스트을 결정하여 처리됩니다. 각 행은 하나의 TreeViewItem을 나타냅니다. 각 TreeViewItem은 부모와 자식 정보를 포함하며 TreeView에서 내비게이션(키와 마우스 입력)을 처리하는 데 사용됩니다.

TreeView는 숨겨져 있고 에디터에는 보이지 않는 단일 루트 TreeViewItem을 가지고 있습니다. 이 아이템은 다른 모든 아이템의 루트입니다.

중요 클래스와 메서드

TreeView 자체를 제외한 가장 중요한 클래스는 TreeViewItemTreeViewState입니다.

TreeViewState (TreeViewState)에는 에디터에서 TreeView 필드와 연결할 때 변경되는 상태 정보(예: 선택 상태, 확장 상태, 내비게이션 상태, 스크롤 상태)가 들어 있습니다. TreeViewState직렬화가 가능한 유일한 상태입니다. TreeView 자체는 직렬화되지 않습니다. 대신에 구성되거나 다시 로드될 때 표시하는 데이터에서 재구성됩니다. EditorWindow 파생 클래스에 TreeViewState를 필드로 추가하면 스크립트를 다시 로드하거나 재생 모드를 사용할 때에도 사용자가 변경한 상태가 손실되지 않습니다(자세한 내용은 에디터 확장에 관한 문서 참조). TreeViewState 필드가 포함된 클래스 예제는 아래의 예제 1: 간단한 TreeView를 참조하십시오.

TreeViewItem (TreeViewItem)은 개별 TreeView 아이템에 관한 데이터를 포함하며 에디터의 트리 구조 모양을 만들 때 사용됩니다. 각각의 TreeViewItem은 고유 정수 ID(TreeView의 모든 아이템 사이에서 고유함)로 구성되어야 합니다. ID는 선택 상태, 확장 상태, 내비게이션 상태를 위한 아이템을 트리에서 찾는 데 사용됩니다. 트리가 Unity 오브젝트를 나타내는 경우, GetInstanceID를 각 오브젝트를 위한 TreeViewItem의 ID로 사용합니다. ID는 에디터에서 스크립트 재로딩 또는 플레이 모드 시작 시 TreeViewState에서 사용자 변경 상태(펼쳐진 아이템과 같은)를 유지시키기 위해 사용됩니다.

모든 TreeViewItemsdepth 프로퍼티를 가지며, 이 프로퍼티는 그려지는 순서를 나타냅니다. 더 많은 정보를 보려면 아래 Initializing a TreeView 예제를 참조하십시오.

BuildRoot (BuildRoot)는 TreeView를 생성하기 위해 실행해야 하는 TreeView 클래스의 단일 추상 메서드입니다. 이 메서드를 사용하여 트리의 루트 아이템 생성을 처리합니다. 이 메서드는 트리 Reload가 호출될 때마다 호출됩니다. 작은 데이터 세트를 사용하는 단순한 트리를 위해서는 BuildRoot의 루트 아이템에 TreeViewItems 트리 전체를 생성합니다. 아주 큰 트리의 경우, 트리 전체를 재로드할 때마다 생성하는 것이 최상의 방법은 아닙니다. 이 경우에는 루트를 생성한 다음에 BuildRows 메서드를 오버라이드하여 현재 행을 위한 아이템만 생성하도록 합니다. BuildRoot가 사용되는 예제를 보려면 아래의 예제 1: 간단한 TreeView를 참조하십시오.

BuildRows(BuildRows)는 기본 구현이 BuildRoot에 생성된 트리 전체를 기반으로 행 리스트 구축을 처리하는 가상 메서드입니다. BuildRoot에 루트만 생성되었다면, 이 메서드는 확장된 행을 처리하도록 오버라이드 되어야 합니다. 더 많은 내용은 아래 TreeView 초기화를 참조하십시오.

이 다이어그램은 TreeView의 수명 주기 동안 BuildRootBuildRows 이벤트 메서드의 순서와 반복을 요약합니다. BuildRoot 메서드는 Reload가 호출될 때마다 한 번 호출되는 점에 유의하십시오. BuildRowsReload(BuildRoot 직후) 시에 한 번 그리고 TreeViewItem이 확장되거나 접힐 때마다 호출되기 때문에 더 자주 호출됩니다.

TreeView 초기화

TreeView는 Reload 메서드가 TreeView 오브젝트로부터 호출되었을 때 초기화됩니다.

TreeView를 설정하는 방법은 두 가지가 있습니다.

  1. 트리 전체 생성 - 모든 아이템을 위한 TreeViewItem을 트리 모델 데이터에 생성합니다. 이 방법이 기본값이며 설정에 더 적은 코드를 요구합니다. 트리 전체는 BuildRoot가 TreeView 오브젝트로부터 호출되었을 때 만들어집니다.

  2. 확장된 아이템만 생성 - 이 방법은 사용자가 BuildRows를 오버라이드하여 수동으로 보여지는 행을 컨트롤해야 하고 BuildRoot는 루트 TreeViewItem 생성을 위해서만 사용해야 합니다. 이 방법은 큰 데이터 세트 또는 자주 바뀌는 데이터에 가장 적합합니다.

첫 번째 방법은 작은 데이터 세트 또는 자주 바뀌지 않는 데이터에 사용합니다. 두 번째 방법은 큰 데이터 세트 또는 자주 바뀌는 데이터에 사용합니다. 트리 전체를 생성하는 것보다 확장된 아이템만 생성하는 것이 더 빠르기 때문입니다.

TreeViewItem을 설정하는 방법은 세 가지가 있습니다.

  • 시작할 때 TreeViewItem을 자식, 부모, 뎁스를 초기화하여 생성합니다.

  • TreeViewItem을 부모와 자식과 함께 생성한 다음 SetupDepthsFromParentsAndChildren을 사용하여 뎁스를 설정합니다.

  • TreeViewItem을 뎁스 정보만 가지고 생성한 다음 SetupDepthsFromParentsAndChildren을 사용하여 부모와 자식에 대한 참조를 설정합니다.

예제

아래 예제에서 프로젝트와 소스 코드를 보려면 TreeViewExamples.zip을 다운로드합니다.

예제 1: 간단한 TreeView

TreeView를 생성하려면 TreeView 클래스를 확장하는 클래스를 생성하고 추상 메서드 BuildRoot를 실행합니다. 다음 예제는 단순한 TreeView를 생성합니다.

class SimpleTreeView : TreeView
{
    public SimpleTreeView(TreeViewState treeViewState)
        : base(treeViewState)
    {
        Reload();
    }
        
    protected override TreeViewItem BuildRoot ()
    {
        // BuildRoot is called every time Reload is called to ensure that TreeViewItems 
        // are created from data. Here we create a fixed set of items. In a real world example,
        // a data model should be passed into the TreeView and the items created from the model.

        // This section illustrates that IDs should be unique. The root item is required to 
        // have a depth of -1, and the rest of the items increment from that.
        var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
        var allItems = new List<TreeViewItem> 
        {
            new TreeViewItem {id = 1, depth = 0, displayName = "Animals"},
            new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"},
            new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"},
            new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"},
            new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"},
            new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"},
            new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"},
            new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"},
            new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"},
        };
            
        // Utility method that initializes the TreeViewItem.children and .parent for all items.
        SetupParentsAndChildrenFromDepths (root, allItems);
            
        // Return root of the tree
        return root;
    }
}

이 예제에서, 뎁스 정보는 TreeView를 만들기 위해 사용됩니다. 마지막으로 SetupDepthsFromParentsAndChildren을 호출하면 TreeViewItem의 부모와 자식 데이터가 설정됩니다.

TreeViewItem을 설정하는 방법에는 부모와 자식을 직접 설정하거나 다음 예제에서 보여주는 바와 같이 AddChild 메서드를 사용하는 방법이 있습니다.

protected override TreeViewItem BuildRoot()
{
    var root = new TreeViewItem      { id = 0, depth = -1, displayName = "Root" };
    var animals = new TreeViewItem   { id = 1, displayName = "Animals" };
    var mammals = new TreeViewItem   { id = 2, displayName = "Mammals" };
    var tiger = new TreeViewItem     { id = 3, displayName = "Tiger" };
    var elephant = new TreeViewItem  { id = 4, displayName = "Elephant" };
    var okapi = new TreeViewItem     { id = 5, displayName = "Okapi" };
    var armadillo = new TreeViewItem { id = 6, displayName = "Armadillo" };
    var reptiles = new TreeViewItem  { id = 7, displayName = "Reptiles" };
    var croco = new TreeViewItem     { id = 8, displayName = "Crocodile" };
    var lizard = new TreeViewItem    { id = 9, displayName = "Lizard" };

    root.AddChild(animals);
    animals.AddChild(mammals);
    animals.AddChild(reptiles);
    mammals.AddChild(tiger);
    mammals.AddChild(elephant);
    mammals.AddChild(okapi);
    mammals.AddChild(armadillo);
    reptiles.AddChild(croco);
    reptiles.AddChild(lizard);

    SetupDepthsFromParentsAndChildren(root);

    return root;
}

위의 SimpleTreeView 클래스를 위한 얼터너티브 BuildRoot 메서드

다음 예제는 SimpleTreeView를 포함하는 EditorWindow를 보여줍니다. TreeView는 TreeViewState 인스턴스와 함께 구축됩니다. TreeView를 구현하는 사람은 뷰 상태가 어떻게 처리되어야 하는지를 정해야 합니다. Unity의 다음 세션까지 상태를 유지할지 아니면 스크립트가 재로드(플레이 모드를 시작하거나 스크립트를 재컴파일할 때)되었을 때만 상태를 유지할지를 정해야 합니다. 이 예제에서는 TreeViewStateEditorWindow에서 직렬화되어 TreeView가 에디터를 닫았다가 다시 열었을 때 상태가 유지되도록 보장합니다.

using System.Collections.Generic;
using UnityEngine;
using UnityEditor.IMGUI.Controls;

class SimpleTreeViewWindow : EditorWindow
{
    // SerializeField is used to ensure the view state is written to the window 
    // layout file. This means that the state survives restarting Unity as long as the window
    // is not closed. If the attribute is omitted then the state is still serialized/deserialized.
    [SerializeField] TreeViewState m_TreeViewState;

    //The TreeView is not serializable, so it should be reconstructed from the tree data.
    SimpleTreeView m_SimpleTreeView;

    void OnEnable ()
    {
        // Check whether there is already a serialized view state (state 
        // that survived assembly reloading)
        if (m_TreeViewState == null)
            m_TreeViewState = new TreeViewState ();

        m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
    }

    void OnGUI ()
    {
        m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
    }

    // Add menu named "My Window" to the Window menu
    [MenuItem ("TreeView Examples/Simple Tree Window")]
    static void ShowWindow ()
    {
        // Get existing open window or if none, make a new one:
        var window = GetWindow<SimpleTreeViewWindow> ();
        window.titleContent = new GUIContent ("My Window");
        window.Show ();
    }
}

예제 2: 다중 열 TreeView

이 예제는 MultiColumnHeader 클래스를 사용하는 다중 열의 TreeView를 보여줍니다.

MultiColumnHeader는 아이템 이름 변경, 다중 선택, (슬라이더 및 오브젝트 필드와 같은)일반 IMGUI 컨트롤을 사용한 아이템 및 커스텀 행 콘텐츠 순서 재구성, 열 정렬, 행 필터링 및 검색과 같은 기능을 지원합니다.

이 예제는 TreeElementTreeModel 클래스를 사용하여 데이터 모델을 생성합니다. TreeView는 이 “TreeModel”로부터 데이터를 가져옵니다. 이 예제에서 TreeElementTreeModel 클래스는 TreeView 클래스의 기능을 보여주기 위해 만들어졌습니다. 이들 클래스는 TreeView 예제 프로젝트(TreeViewExamples.zip)에 포함되었습니다. 예제에서는 또한 트리 모델 구조가 어떻게 ScriptableObject로 직렬화되는지와 에셋으로 저장되는지를 보여줍니다.

[Serializable]
//The TreeElement data class is extended to hold extra data, which you can show and edit in the front-end TreeView.
internal class MyTreeElement : TreeElement
{
    public float floatValue1, floatValue2, floatValue3;
    public Material material;
    public string text = "";
    public bool enabled = true;

    public MyTreeElement (string name, int depth, int id) : base (name, depth, id)
    {
        floatValue1 = Random.value;
        floatValue2 = Random.value;
        floatValue3 = Random.value;
    }
}

다음 ScriptableObject 클래스는 트리가 직렬화되었을 때 에셋의 데이터가 보존되도록 보장합니다.

[CreateAssetMenu (fileName = "TreeDataAsset", menuName = "Tree Asset", order = 1)]
public class MyTreeAsset : ScriptableObject
{
    [SerializeField] List<MyTreeElement> m_TreeElements = new List<MyTreeElement> ();

    internal List<MyTreeElement> treeElements
    {
        get { return m_TreeElements; }
        set { m_TreeElements = value; }
    }
}

MultiColumnTreeView 클래스 구축

다음 예제는 다중 열 GUI를 어떻게 얻는지 보여주는 MultiColumnTreeView 클래스의 일부분을 보여줍니다. 소스 코드 전체는 TreeView 예제 프로젝트(TreeViewExamples.zip)에 있습니다.

public MultiColumnTreeView (TreeViewState state, 
                            MultiColumnHeader multicolumnHeader, 
                            TreeModel<MyTreeElement> model) 
                            : base (state, multicolumnHeader, model)
{
    // Custom setup
    rowHeight = 20;
    columnIndexForTreeFoldouts = 2;
    showAlternatingRowBackgrounds = true;
    showBorder = true;
    customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; 
    extraSpaceBeforeIconAndLabel = kToggleWidth;
    multicolumnHeader.sortingChanged += OnSortingChanged;
            
    Reload();
}

위 코드 샘플의 커스텀 변경사항은 다음 사항을 조정합니다.

  • rowHeight = 20: 기본 높이(EditorGUIUtility.singleLineHeight의 16포인트를 기반으로 함)를 20으로 바꿔 GUI 컨트롤을 위해 더 많은 공간을 추가합니다.

  • columnIndexForTreeFoldouts = 2: 예제에서 이 값이 2로 설정되어 있기 때문에 폴드아웃 화살표는 세 번째 열에 보입니다. (위 그림 참조)이 값이 바뀌지 않으면, “columnIndexForTreeFoldouts”가 기본적으로 0이기 때문에, 폴드아웃은 첫 번째 열에 렌더링됩니다.

  • showAlternatingRowBackgrounds = true: 각 행이 구별될 수 있도록 교대로 다른 행 배경 컬러를 활성화합니다.

  • showBorder = true: TreeView를 마진으로 둘러 렌더링하여 얇은 테두리로 나머지 콘텐츠와 구분할 수 있게 합니다.

  • customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f: 센터가 행에 수직으로 폴드아웃 - 아래 GUI 커스터마이징를 참조하십시오.

  • extraSpaceBeforeIconAndLabel = 20: 토글 버튼이 보이도록 트리가 레이블하기 전에 공간을 만듭니다.

  • multicolumnHeader.sortingChanged += OnSortingChanged: 헤더 컴포넌트의 정렬이 언제 바뀌는지(헤더 열이 클릭된 경우)를 감지하도록 이벤트에 메서드를 지정하여 TreeView의 행이 정렬 상태를 반영하여 바뀌게 합니다.

GUI 커스텀화

디폴트 RowGUI 처리가 사용되면 TreeView는 폴드아웃과 레이블만 있는 위의 SimpleTreeView 예제처럼 보입니다. 각 아이템을 위한 다수의 데이터 값을 사용할 때는 이러한 값을 시각화하기 위해 RowGUI 메서드를 오버라이드해야 합니다.

보호된 override void RowGUI(RowGUIArgs args)

다음 코드 샘플은 RowGUIArgs 구조의 인수 구조입니다.

protected struct RowGUIArgs
{
    public TreeViewItem item;
    public string label;
    public Rect rowRect;
    public int row;
    public bool selected;
    public bool focused;
    public bool isRenaming;

    public int GetNumVisibleColumns ()
    public int GetColumn (int visibleColumnIndex)
    public Rect GetCellRect (int visibleColumnIndex)
}

TreeViewItem을 확장할 수 있으며 추가 사용자 데이터(TreeViewItem으로부터 파생되는 클래스 생성)를 추가할 수 있습니다. 그런 다음 RowGUI 콜백 안에서 사용자 데이터를 사용할 수 있습니다. 예제는 아래에 있습니다. override void RowGUI를 참조하십시오. 이 예제는 입력 아이템을 TreeViewItem<MyTreeElement>에 캐스트합니다.

열 처리와 관련된 메서드는 다음의 세 가지가 있습니다. GetNumVisibleColumns, GetColumn, and GetCellRect. TreeView가 MultiColumnHeader로 구축되었을 때만 위 메서드를 호출할 수 있으며, 그렇지 않은 경우 예외가 발생합니다.

protected override void RowGUI (RowGUIArgs args)
{
    var item = (TreeViewItem<MyTreeElement>) args.item;

    for (int i = 0; i < args.GetNumVisibleColumns (); ++i)
    {
        CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
    }
}
void CellGUI (Rect cellRect, TreeViewItem<MyTreeElement> item, MyColumns column, ref RowGUIArgs args)
{
    // Center the cell rect vertically using EditorGUIUtility.singleLineHeight.
// This makes it easier to place controls and icons in the cells.
    CenterRectUsingSingleLineHeight(ref cellRect);

    switch (column)
    {

        case MyColumns.Icon1:
            
            // Draw custom texture
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Icon2:

//Draw custom texture 
            GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Name:

            // Make a toggle button to the left of the label text
            Rect toggleRect = cellRect;
            toggleRect.x += GetContentIndent(item);
            toggleRect.width = kToggleWidth;
            if (toggleRect.xMax < cellRect.xMax)
                item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); 

            // Default icon and label
            args.rowRect = cellRect;
            base.RowGUI(args);
            break;

        case MyColumns.Value1:

// Show a Slider control for value 1
            item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
            break;

        case MyColumns.Value2:

// Show an ObjectField for materials
            item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, 
                                          typeof(Material), false);
            break;

        case MyColumns.Value3:

// Show a TextField for the data text string
            item.data.text = GUI.TextField(cellRect, item.data.text);
            break;
    }
}

TreeView 자주 묻는 질문

Q: TreeView 서브클래스에 함수 BuildRootRowGUI가 있습니다. RowGUI가 빌드 함수에 추가된 모든 TreeViewItem을 위해 호출됩니까? 아니면 스크롤 뷰에서 화면에 보이는 아이템을 위해서만 호출됩니까?

A: RowGUI는 화면에 보이는 아이템을 위해서만 호출됩니다. 예를 들어, 10,000개의 아이템이 있다면, 화면에 보이는 20개 아이템만 RowGUI를 호출합니다.

Q: 화면에 보이는 행의 인덱스를 얻을 수 있습니까?

A: 네, GetFirstAndLastVisibleRows 메서드를 사용하면 됩니다.

Q: BuildRows에 만들어진 행의 리스트를 얻을 수 있습니까?

A: 네, GetRows 메서드를 이용합니다.

Q: 모든 오버라이드된 함수가 base.Method를 호출해야 됩니까?

A: 메서드가 사용자가 확장하고자 하는 기본 동작이 있는 경우에만 그렇습니다.

Q: 트리가 아닌 아이템 리스트만 만들고 싶습니다. 루트를 생성해야 합니까?

A: 네, 언제나 루트가 있어야 합니다. 루트 아이템을 생성한 다음 root.children = rows로 설정하여 빠르게 설정할 수 있습니다.

Q: 행에 토글을 추가했습니다. 왜 클릭했을 때 메시지가 해당 행으로 가지 않습니까?

A: 기본적으로, 마우스로 누를 때 행의 콘텐츠가 사용되지 않는 경우에만 행을 선택할 수 있습니다. 이 경우에는 토글이 이벤트를 사용합니다. 이 문제를 해결하려면 SelectionClick 메서드를 사용한 후 토글 버튼을 호출하십시오.

Q: 모든 RowGUI 메서드가 호출되기 전 또는 후에 사용할 수 있는 메서드가 있습니까?

A: 네, API 관련 문서 BeforeRowsGUIAfterRowsGUI를 참조하십시오.

Q: 키 포커스를 API에서 TreeView로 돌리는 간단한 방법이 있습니까? 행에서 FloatField를 선택하면, 선택된 행은 회색으로 변합니다. 어떻게 다시 파란색으로 바꿀 수 있습니까?

A: 파란색은 어느 행에 키 포커스가 있는지를 나타냅니다. FloatField에 포커스가 있기 때문에, TreeView는 포커스를 잃게 되므로, 이것은 의도된 동작입니다. 필요한 경우에는 GUIUtility.keyboardControl = treeViewControlID로 설정합니다.

Q: 어떻게 id에서 TreeViewItem으로 전환할 수 있습니까?

A: FindItem 또는 FindRows를 사용합니다.

Q: 사용자가 TreeView에서 선택한 것을 바꿨을 때, 어떻게 콜백을 받습니까?

A: SelectionChanged 메서드를 오버라이드합니다(기타 유용한 콜백: DoubleClickedItemContextClickedItem).

커스텀 에디터
에디터 문제 해결