참고: Unity 에디터를 확장하려면 UI 툴킷을 사용할 것을 강력히 권장하며, 이는 IMGUI보다 더 최신의 유연하고 확장 가능한 솔루션을 제공하기 때문입니다.
이 페이지에서는 독자가 IMGUI(즉시 모드 GUI) 개념에 대한 기본 지식을 가지고 있다고 가정합니다. IMGUI 및 에디터 창 커스터마이징에 대한 자세한 내용은 에디터 확장 및 IMGUI Unity 블로그를 참조하십시오.
TreeView는 펼치고 접을 수 있는 계층 구조적 데이터를 나타낼 때 사용하는 IMGUI 컨트롤입니다. 다른 IMGUI 컨트롤 및 컴포넌트와 함께 사용할 수 있는 고도로 커스터마이징 가능한 리스트 뷰를 생성할 때와 에디터 창을 위한 다중 열로 구성된 표를 생성할 때 TreeView를 사용합니다.
사용 가능한 TreeView API 함수에 대한 내용은 TreeView Unity 스크립팅 API 기술 자료를 참조하십시오.
TreeView는 트리 데이터 모델이 아닙니다. 원하는 트리 데이터 구조를 사용하여 TreeView를 구성할 수 있습니다. 이는 C# 트리 모델 또는 트랜스폼 계층 구조와 같은 Unity 기반 트리 구조일 수 있습니다.
TreeView의 렌더링은 행이라는 확장된 항목 목록을 결정하여 처리됩니다. 각 행은 하나의 TreeViewItem을 나타냅니다. 각 TreeViewItem에는 TreeView가 내비게이션(키 및 마우스 입력)을 처리하는 데 사용하는 부모 및 자식 정보가 포함되어 있습니다.
TreeView에는 숨겨져 에디터에 표시되지 않는 단일 루트 TreeViewItem이 있습니다. 이 항목은 다른 모든 항목의 루트입니다.
TreeView 자체를 제외하고 가장 중요한 클래스는 TreeViewItem과 TreeViewState입니다.
TreeViewState(TreeViewState)에는 에디터에서 TreeView 필드와 상호 작용할 때 변경되는 상태 정보(예: 선택 상태, 확장 상태, 내비게이션 상태, 스크롤 상태)가 포함되어 있습니다. TreeViewState는 직렬화 가능한 유일한 상태입니다. TreeView 자체는 직렬화할 수 없으며, 구성되거나 다시 로드될 때 나타내는 데이터를 통해 다시 구성됩니다. 스크립트를 리로드하거나 플레이 모드를 시작할 때 사용자가 변경한 상태가 손실되지 않도록 EditorWindow 파생 클래스의 필드로 TreeViewState를 추가합니다. 이 작업을 수행하는 방법에 대한 자세한 내용은 에디터 확장 기술 자료를 참조하십시오. TreeViewState 필드가 포함된 클래스의 예시는 아래의 예제 1: 간단한 TreeView를 참조하십시오.
TreeViewItem(TreeViewItem)은 개별 TreeView 항목에 대한 데이터를 포함하며, 에디터에서 트리 구조의 표현을 작성하는 데 사용됩니다. 각 TreeViewItem은 고유한 정수 ID(TreeView의 모든 항목에서 고유한 ID)로 구성되어야 합니다. 이 ID는 선택 상태, 확장 상태, 내비게이션에 대한 트리의 항목을 찾는 데 사용됩니다. 트리가 Unity 오브젝트를 나타내는 경우 각 오브젝트에 대해 TreeViewItem의 ID로 GetInstanceID를 사용합니다. 이 ID는 스크립트를 다시 로드하거나 에디터에서 플레이 모드로 전환할 때 사용자가 변경한 상태(예: 확장 항목)를 유지하기 위해 TreeViewState에서 사용됩니다.
모든 TreeViewItems에는 시각적 들여쓰기를 나타내는 depth 프로퍼티가 있습니다. 자세한 내용은 아래의 TreeView 초기화 예시를 참조하십시오.
BuildRoot(BuildRoot)는 TreeView를 생성하기 위해 구현해야 하는 TreeView 클래스의 단일 추상 메서드입니다. 이 메서드를 사용하여 트리의 루트 항목 생성을 처리합니다. 이 옵션은 트리에서 Reload가 호출될 때마다 호출됩니다. 작은 데이터 세트를 사용하는 단순한 트리의 경우 TreeViewItems의 전체 트리를 BuildRoot의 루트 항목 아래에 생성합니다. 매우 큰 트리의 경우 다시 로드할 때마다 전체 트리를 생성하는 것은 최적의 방법이 아닙니다. 이 상황에서 루트를 생성한 다음 BuildRows 메서드를 오버라이드하여 현재 행에 대한 항목만 생성합니다. 사용 중인 BuildRoot의 예제는 아래의 예제 1: 간단한 TreeView를 참조하십시오.
BuildRows(BuildRows)는 기본 구현이 BuildRoot에서 만든 전체 트리를 기반으로 행 목록을 작성하여 처리하는 가상 메서드입니다. 루트만 BuildRoot에서 생성된 경우 이 메서드를 오버라이드하여 확장된 행을 처리해야 합니다. 자세한 내용은 아래의 TreeView 초기화를 참조하십시오.
이 다이어그램은 TreeView의 수명 동안 BuildRoot 및 BuildRows 이벤트 메서드의 순서와 반복을 요약합니다. BuildRoot 메서드는 Reload가 호출될 때마다 한 번 호출됩니다. BuildRows는 Reload(BuildRoot 직후)에서 한 번 호출되며, TreeViewItem이 확장되거나 축소될 때마다 호출되기 때문에 더 자주 호출됩니다.
TreeView는 Reload 메서드가 TreeView 오브젝트로부터 호출되었을 때 초기화됩니다.
TreeView를 설정하는 방법은 두 가지가 있습니다.
전체 트리 생성 - 트리 모델 데이터의 모든 항목에 대해 TreeViewItem을 생성합니다. 이는 기본값이며 설정하는 데 코드가 더 적습니다. 전체 트리는 BuildRoot가 TreeView 오브젝트에서 호출될 때 구축됩니다.
확장된 항목만 생성 - 이 방법을 사용할 경우 표시되는 행을 수동으로 제어하려면 BuildRows를 오버라이드해야 하며, BuildRoot는 루트 TreeViewItem을 만드는 데만 사용됩니다. 이 접근 방식은 대규모 데이터 세트나 자주 변경되는 데이터에 가장 적합합니다.
첫 번째 방법은 작은 데이터 세트 또는 자주 바뀌지 않는 데이터에 사용합니다. 두 번째 방법은 큰 데이터 세트 또는 자주 바뀌는 데이터에 사용합니다. 트리 전체를 생성하는 것보다 확장된 항목만 생성하는 것이 더 빠르기 때문입니다.
TreeViewItem을 설정하는 방법은 세 가지가 있습니다.
시작할 때 TreeViewItem을 자식, 부모, 뎁스를 초기화하여 생성합니다.
부모와 자식으로 TreeViewItem을 생성한 다음 SetupDepthsFromParentsAndChildren을 사용하여 뎁스를 설정합니다.
뎁스 정보만 사용하여 TreeViewItem을 생성한 다음 SetupDepthsFromParentsAndChildren을 사용하여 부모 및 자식 레퍼런스를 설정합니다.
아래 예제에서 프로젝트와 소스 코드를 보려면 TreeViewExamples.zip을 다운로드합니다.
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를 보여 줍니다. TreeViews는 TreeViewState 인스턴스로 구성됩니다. TreeView를 구현할 경우 이 뷰 상태가 다음 Unity 세션까지 유지되어야 하는지, 또는 스크립트가 다시 로드된 후(플레이 모드로 진입하거나 스크립트를 다시 컴파일할 때)에만 상태를 유지해야 하는지 여부를 결정해야 합니다. 이 예시에서는 EditorWindow에서 TreeViewState가 직렬화되어 에디터가 닫히고 다시 열릴 때 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 ();
}
}
이 예제는 MultiColumnHeader 클래스를 사용하는 다중 열 TreeView를 보여 줍니다.
MultiColumnHeader는 아이템 이름 변경, 다중 선택, (슬라이더 및 오브젝트 필드와 같은)일반 IMGUI 컨트롤을 사용한 항목 및 커스텀 행 콘텐츠 순서 재구성, 열 정렬, 행 필터링 및 검색과 같은 기능을 지원합니다.
이 예제에서는 클래스 TreeElement 및 TreeModel을 사용하여 데이터 모델을 만듭니다. TreeView는 이 ‘TreeModel’의 데이터를 가져옵니다. 이 예시에서는 TreeView 클래스의 기능을 보여 주기 위해 TreeElement 및 TreeModel 클래스가 구축되었습니다. 이러한 클래스는 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; }
}
}
다음 예시는 다중 열 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의 행이 정렬 상태를 반영하여 바뀌게 합니다.
기본 RowGUI 처리를 사용하는 경우 TreeView는 폴드아웃과 레이블만 있는 위의 SimpleTreeView 예와 같습니다. 각 항목에 여러 데이터 값을 사용하는 경우 RowGUI 메서드를 오버라이드하여 이러한 값을 시각화해야 합니다.
protected 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 및 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;
}
}
Q: TreeView 서브 클래스에는 BuildRoot 및 RowGUI 함수가 있습니다. 빌드 함수에 추가된 모든 TreeViewItem에 대해 RowGUI가 호출됩니까? 아니면 스크롤 뷰에서 화면에 표시되는 항목에 대해서만 호출됩니까?
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: 네. BeforeRowsGUI 및 AfterRowsGUI에 대한 API 기술 자료를 참조하십시오.
Q: API에서 TreeView에 키 포커스를 반환하는 간단한 방법이 있습니까? 행에서 FloatField를 선택하면 행 선택이 회색으로 변합니다. 어떻게 다시 파란색으로 만들 수 있습니까?
A: 파란색은 현재 키 포커스가 있는 행을 나타냅니다. FloatField에 포커스가 있기 때문에 TreeView가 포커스를 잃으므로 이는 의도된 동작입니다. 필요한 경우 GUIUtility.keyboardControl = treeViewControlID를 설정합니다.
Q: 어떻게 id에서 TreeViewItem으로 전환합니까?
A: FindItem 또는 FindRows를 사용합니다.
Q: 사용자가 TreeView에서 선택 항목을 바꿨을 때, 어떻게 콜백을 받습니까?
A: SelectionChanged 메서드를 오버라이드합니다(기타 유용한 콜백: DoubleClickedItem 및 ContextClickedItem).