1. 简介:
IMGUI是代码驱动gui系统,由 OnGUI 函数驱动:
cs
void OnGUI() {
if (GUILayout.Button("Press Me")){
Debug.Log("Hello!");
}
}
IMGUI常用于:
创建 in-game debugging displays and tools;
为组件创建自定义inspector;
创建editor windows and tools;
2. IMGUI基础:
OnGUI()函数在脚本激活时每帧调用;
cs
using UnityEngine;
[ExecuteAlways]
public class TestGUI : MonoBehaviour
{
private void OnGUI()
{
GUI.Box(new Rect(10, 10, 100, 90), "Loader Menu");
if(GUI.Button(new Rect(20, 40, 80, 20), "Button 1"))
{
Debug.Log("Button 1");
}
if (GUI.Button(new Rect(20, 70, 80, 20), "Button 2"))
{
Debug.Log("Button 2");
}
}
}
- 调用函数来绘制控件:
(GUI/GUILayout).Type (Rect, Content);
定义 Rect 时,坐标系基于屏幕左上角,可使用 Screen.width 和 Screen.height ;
Content类型:
string;
Texture2D;
GUIContent;
cs
GUI.Box(new Rect(10, 10, 100, 50), icon);
GUI.Box(new Rect(10, 60, 100, 50), new GUIContent("text", icon));
GUI.Box(new Rect(10, 110, 100, 50), new GUIContent("text", icon, "This is the tooltip"));
GUI.Button(new Rect(10, 160, 100, 20), new GUIContent("Click me", "This is the tooltip"));
GUI.Label(new Rect(10, 180, 100, 20), GUI.tooltip); //显示当前的tooltip内容
3. IMGUI控件:
Label:
cs
GUI.Label(new Rect(100, 100, 100, 30), "show text");
Button:
cs
if(GUI.Button(new Rect(100, 100, 100, 30), "button"))
{
Debug.Log("Click button");
}
RepeatButton:
类似 Button,只是点击时一直返回 true;
TextField:
cs
str = GUI.TextField(new Rect(100, 100, 100, 30), str);
单行文本输入;
TextArea:
cs
str = GUI.TextArea(new Rect(100, 100, 100, 100), str);
多行文本输入;
Toggle:
cs
toggleVal = GUI.Toggle(new Rect(100, 100, 100, 30), toggleVal, "Toggle");
Toolbar:
一行单选按钮,toolbarInt 用来记录选择的按钮索引;
cs
private int toolbarInt = 0;
private string[] toolbarStrings = { "Toolbar1", "Toolbar2", "Toolbar3" };
void OnGUI()
{
toolbarInt = GUI.Toolbar(new Rect(100, 100, 300, 30), toolbarInt, toolbarStrings);
}
SelectionGrid:
多行Toolbar;
cs
private int selectionIndex = 0;
private string[] selectionStrings = { "Toolbar1", "Toolbar2", "Toolbar3", "Toolbar4" };
void OnGUI()
{
selectionIndex = GUI.SelectionGrid(new Rect(100, 100, 300, 60), selectionIndex, selectionStrings, 2);
}
HorizontalSlider:
水平滑动条
cs
private float sliderValue = 0.0f;
void OnGUI()
{
sliderValue = GUI.HorizontalSlider(new Rect(100, 100, 100, 30), sliderValue, 0.0f, 10.0f);
}
VerticalSlider:
垂直滑动条;
cs
private float sliderValue = 0.0f;
void OnGUI()
{
sliderValue = GUI.VerticalSlider(new Rect(100, 100, 100, 30), sliderValue, 0.0f, 10.0f);
}
HorizontalScrollbar:
类似 Slider,用于导航 ScrollView 控件,有一个额外参数控制旋钮宽度;
cs
private float scrollValue = 0.0f;
void OnGUI()
{
scrollValue = GUI.HorizontalScrollbar(new Rect(100, 100, 100, 30), scrollValue, 1, 0.0f, 10.0f);
}
VerticalScrollbar:
类似 HorizontalScrollbar;
ScrollView:
cs
private Vector2 scrollPosition = Vector2.zero;
void OnGUI()
{
//第一个参数指定可视区域,第三个参数指定在滚动列表内的大小
scrollPosition = GUI.BeginScrollView(new Rect(100, 100, 100, 100), scrollPosition, new Rect(0, 0, 400, 400));
GUI.Label(new Rect(0, 0, 400, 400), "text");
GUI.EndScrollView();
}
Window:
可拖动的容器;
cs
private Rect windowRect = new Rect(100, 100, 100, 100);
void OnGUI()
{
windowRect = GUI.Window(0, windowRect, WindowFunction, "My Window");
}
void WindowFunction(int id)
{
GUI.Label(new Rect(10, 20, 100, 30), "text");
GUI.DragWindow(); //让window可拖动
}
GUI.changed:
检测用户是否有操作;
cs
selectIndex = GUI.Toolbar(new Rect(100, 100, 200, 30), selectIndex, options);
if(GUI.changed)
{
Debug.Log("selectIndex = " + selectIndex);
}
4. 使用GUIStyles样式化控件:
GUIStyles 控制控件的外观显示;
GUISkins 包含一系列 GUIStyles;
所有控件可设置一个可选的 GUIStyle;
cs
//指定 GUIStyle 的名字 button
GUI.Label(new Rect(100, 100, 100, 30), "text", "button");
在脚本中声明一个序列化的GUIStyle ,查看属性:
cs
public GUIStyle style;
void OnGUI()
{
//指定 GUIStyle 的名字 button
GUI.Label(new Rect(100, 100, 100, 30), "text", style);
}
设置GUISkin:
cs
public GUISkin skin;
void OnGUI()
{
GUI.skin = skin;
//绘制
//...
//恢复默认skin
GUI.skin = null; //
}
改变GUIStyle:
cs
GUIStyle style = GUI.skin.GetStyle("label");
style.fontSize = 30;
GUI.Label(new Rect(100, 100, 400, 100), "text1");
style.fontSize = 50;
GUI.Label(new Rect(100, 150, 400, 100), "text2");
5. IMGUI Layout 模式:
固定布局和自动布局:
要使用自动布局,使用 GUILayout 代替GUI;
可以在 OnGUI() 函数中同时使用两种布局方式,但是两种布局相互独立;
GUILayout 不需要指定 Rect;
cs
// Fixed Layout
GUI.Button(new Rect(25, 25, 100, 30), "I am a Fixed Layout Button");
// Automatic Layout
GUILayout.Button("I am an Automatic Layout Button");
固定布局 Group:
Group内的控件将基于组的左上角定位;
cs
GUI.BeginGroup(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 50, 100, 100));
GUI.Box(new Rect(0, 0, 100, 100), "Group is here");
GUI.Button(new Rect(10, 40, 80, 30), "Click me");
GUI.EndGroup();
可以嵌套使用Group,可获取裁剪效果:
cs
// background image that is 256 x 32
public Texture2D bgImage;
// foreground image that is 256 x 32
public Texture2D fgImage;
// a float between 0.0 and 1.0
public float playerEnergy = 1.0f;
//使用默认style绘制box
public GUIStyle style = new GUIStyle();
void OnGUI()
{
if(bgImage == null || fgImage == null)
{
return;
}
GUI.BeginGroup(new Rect(300, 300, 256, 32));
// 绘制背景
GUI.Box(new Rect(0, 0, 256, 32), bgImage, style);
// 通过 playerEnergy 控制显示大小,达到裁剪效果
GUI.BeginGroup(new Rect(0, 0, playerEnergy * 256, 32));
GUI.Box(new Rect(0, 0, 256, 32), fgImage, style);
GUI.EndGroup();
GUI.EndGroup();
}
自动布局 Area:
类似固定布局 Group;
定义屏幕的一块区域来包含 GUILayout 控件,控件基于 Area 的左上角定位;
cs
GUILayout.Button("I am not inside an Area");
GUILayout.BeginArea(new Rect(Screen.width / 2, Screen.height / 2, 300, 300));
GUILayout.Button("I am completely inside an Area");
GUILayout.EndArea();
注意Area里控件的宽度将被拉伸到与Area一样;
自动布局 Horizontal and Vertical Group:
使用自动布局时,控件默认从上往下一个接一个;
为了修改这一行为,提供了Horizontal 和 Vertical Group;
Horizontal Group 里的控件被水平摆放;
GUILayout.BeginHorizontal() , GUILayout.EndHorizontal()
Vertical Group 里的控件被垂直摆放;
GUILayout.BeginVertical() , and GUILayout.EndVertical()
cs
GUILayout.BeginArea(new Rect(400, 400, 200, 100));
GUILayout.BeginHorizontal();
GUILayout.Button("Button\nfirst");
GUILayout.BeginVertical();
GUILayout.Button("Button2");
GUILayout.Label("Label");
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.EndArea();
GUILayoutOptions :
使用 GUILayoutOptions 覆盖一些自动布局参数;
cs
GUILayout.BeginArea(new Rect(100, 50, Screen.width - 200, Screen.height - 100));
GUILayout.Button("I am a regular Automatic Layout Button");
GUILayout.Button("My width has been overridden", GUILayout.Width(95));
GUILayout.EndArea();
6. 使用 IMGUI 扩展编辑器:
1. 创建自定义 Editor Window:
- 创建脚本继承 EditorWindow:
cs
using UnityEditor;
using UnityEngine;
public class MyWindow : EditorWindow
{
private void OnGUI()
{
}
}
- 显示window:
cs
using UnityEditor;
using UnityEngine;
public class MyWindow : EditorWindow
{
[MenuItem("Window/My Window")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(MyWindow));
}
private void OnGUI()
{
}
}
- 实现 gui:
EditorGUI 和 EditorGUILayout 提供了额外的控件;
cs
using UnityEditor;
using UnityEngine;
public class MyWindow : EditorWindow
{
string myString = "Hello World";
bool groupEnabled;
bool myBool = true;
float myFloat = 1.23f;
[MenuItem("Window/My Window")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(MyWindow));
}
private void OnGUI()
{
GUILayout.Label("Base Settings", EditorStyles.boldLabel);
myString = EditorGUILayout.TextField("Text Field", myString);
groupEnabled = EditorGUILayout.BeginToggleGroup("Optional Settings", groupEnabled);
myBool = EditorGUILayout.Toggle("Toggle", myBool);
myFloat = EditorGUILayout.Slider("Slider", myFloat, -3, 3);
EditorGUILayout.EndToggleGroup();
}
}
2. 自定义 Inspector:
1. 自定义 GUI of Serializable class:
cs
using System;
using UnityEngine;
public enum PersonLevel { A, B, C}
[Serializable]
public class Person
{
public string name;
public int age;
public PersonLevel personLevel;
}
public class PersonInfo : MonoBehaviour
{
public Person person;
public Person[] persons;
}
(默认显示)
使用 Property Drawer 来控制其在 Inspector 上的显示:
cs
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(Person))]
public class PersonDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Using BeginProperty / EndProperty on the parent property means that
// prefab override logic works on the entire property.
EditorGUI.BeginProperty(position, label, property);
// Draw label
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
// Don't make child fields be indented
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// Calculate rects
var nameRect = new Rect(position.x, position.y, 30, position.height);
var ageRect = new Rect(position.x + 35, position.y, 50, position.height);
var personLevelRect = new Rect(position.x + 90, position.y, position.width - 90, position.height);
// Draw fields - pass GUIContent.none to each so they are drawn without labels
EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);
EditorGUI.PropertyField(ageRect, property.FindPropertyRelative("age"), GUIContent.none);
EditorGUI.PropertyField(personLevelRect, property.FindPropertyRelative("personLevel"), GUIContent.none);
// Set indent back to what it was
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
2. 使用 Property Attribute 自定义脚本成员 GUI:
创建 Attribute:
cs
using UnityEngine;
public class MyRangeAttribute : PropertyAttribute
{
public float min;
public float max;
public MyRangeAttribute(float min, float max)
{
this.min = min;
this.max = max;
}
}
对 Attribute 进行自定义绘制:
cs
using UnityEditor;
using UnityEngine;
//对MyRangeAttribute
[CustomPropertyDrawer(typeof(MyRangeAttribute))]
public class RangeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// First get the attribute since it contains the range for the slider
MyRangeAttribute range = (MyRangeAttribute)attribute;
// Now draw the property as a Slider or an IntSlider based on whether it's a float or integer.
if (property.propertyType == SerializedPropertyType.Float)
EditorGUI.Slider(position, property, range.min, range.max, label);
else if (property.propertyType == SerializedPropertyType.Integer)
EditorGUI.IntSlider(position, property, (int)range.min, (int)range.max, label);
else
EditorGUI.LabelField(position, label.text, "Use MyRange with float or int.");
}
}
(注意,为了性能考虑,EditorGUILayout 不可在 PropertyDrawer 中使用)
使用 Attribute:
cs
using UnityEngine;
public class TestAttribute : MonoBehaviour
{
[MyRange(1.5f, 35f)]
public float fValue;
}
3. Component 自定义编辑器:
创建 component:
cs
using UnityEngine;
[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
public Vector3 lookAtPoint = Vector3.zero;
void Update()
{
transform.LookAt(lookAtPoint);
}
}
创建组件自定义编辑器:
cs
using UnityEditor;
[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
LookAtPoint targetComp;
SerializedProperty lookAtPoint;
void OnEnable()
{
lookAtPoint = serializedObject.FindProperty("lookAtPoint");
}
public override void OnInspectorGUI()
{
targetComp = target as LookAtPoint;
serializedObject.Update();
EditorGUILayout.PropertyField(lookAtPoint);
serializedObject.ApplyModifiedProperties();
}
}
通过 OnSceneGUI 往 Scene View 中添加 编辑器代码:
cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
private void OnSceneGUI()
{
LookAtPoint t = (LookAtPoint)target;
//使用 Handles 类来实现3D场景编辑控制
EditorGUI.BeginChangeCheck();
//给定位置,在场景中画中控制轴,拖动的话,返回变化的pos
Vector3 pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Move point");
t.lookAtPoint = pos;
t.Update();
}
}
}
如果要在场景中添加 GUI,使用 Handles.BeginGUI() and Handles.EndGUI();
4. TreeView :
是一个 IMGUI 控件,用于显示分层数据,可展开和折叠;
使用 TreeView 创建高度自定义的列表视图和多列表格;
按行渲染,每行代表一个 TreeViewItem, 包含父节点和子节点的信息;
TreeView 有一个隐藏的根 TreeViewItem,是所有其他 item的根;
重要的类和方法:
类 TreeViewState:
包含交互的状态信息,可序列化,以便在编辑器编译和进入Play模式时保存数据;
类 TreeViewItem:
包含每个 TreeView item 的数据,在 TreeView 中需要一个唯一 id;
depth 属性用来视觉缩进;
抽象方法 BuildRoot:
TreeView 类的抽象方法,每次调用 TreeView 的 Reload 方法就会触发;
用于创建 root item;
对于简单树,可创建所有的 TreeViewItem;
对于复杂树,通过覆写 BuildRows 来创建当前需要的行;
虚方法 BuildRows:
默认实现创建整个树;
可覆写只创建展开的行;
初始化 TreeView:
调用 Reload 方法来初始化;
两种方式设置 TreeView:
-
在 BuildRoot 中创建整个树;
-
在 BuildRoot 中只创建根,然后在 BuildRows 中只创建展开的 items;
Example 1: A simple TreeView:
cs
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;
public class SimpleTreeView : TreeView
{
public SimpleTreeView(TreeViewState state) : base(state)
{
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;
}
}
另一种方式添加 Item:
cs
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" };
//不指定depth,通过 TreeViewItem.AddChild 来添加节点
var animals = new TreeViewItem { id = 1, displayName = "Animals" };
root.AddChild(animals);
var mammals = new TreeViewItem { id = 2, displayName = "Mammals" };
animals.AddChild(mammals);
mammals.AddChild(new TreeViewItem { id = 3, displayName = "Tiger" });
mammals.AddChild(new TreeViewItem { id = 4, displayName = "Elephant" });
mammals.AddChild(new TreeViewItem { id = 5, displayName = "Okapi" });
mammals.AddChild(new TreeViewItem { id = 6, displayName = "Armadillo" });
var reptiles = new TreeViewItem { id = 7, displayName = "Reptiles" };
animals.AddChild(reptiles);
reptiles.AddChild(new TreeViewItem { id = 8, displayName = "Crocodile" });
reptiles.AddChild(new TreeViewItem { id = 9, displayName = "Lizard" });
SetupDepthsFromParentsAndChildren(root);
// Return root of the tree
return root;
}
使用 EditorWindow 显示 TreeView:
cs
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
public 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();
//创建 TreeView
m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
}
void OnGUI()
{
//绘制 TreeView
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("SimpleTreeView Window");
window.Show();
}
}
Example 2: A multi-column TreeView:
最终效果:
(列Header鼠标右键菜单)
EditorWindow:
cs
using System;
using System.Collections.Generic;
using UnityEditor.Callbacks;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace UnityEditor.TreeViewExamples
{
class MultiColumnWindow : EditorWindow
{
[NonSerialized] bool m_Initialized;
// Serialized in the window layout file so it survives assembly reloading
[SerializeField] TreeViewState m_TreeViewState;
[SerializeField] MultiColumnHeaderState m_MultiColumnHeaderState;
SearchField m_SearchField;
MultiColumnTreeView m_TreeView;
MyTreeAsset m_MyTreeAsset;
[MenuItem("TreeView Examples/Multi Columns")]
public static MultiColumnWindow GetWindow ()
{
var window = GetWindow<MultiColumnWindow>();
window.titleContent = new GUIContent("Multi Columns");
window.Focus();
window.Repaint();
return window;
}
//双击资源时触发
[OnOpenAsset]
public static bool OnOpenAsset (int instanceID, int line)
{
var myTreeAsset = EditorUtility.InstanceIDToObject (instanceID) as MyTreeAsset;
if (myTreeAsset != null)
{
var window = GetWindow ();
window.SetTreeAsset(myTreeAsset);
return true;
}
return false; // we did not handle the open
}
void SetTreeAsset (MyTreeAsset myTreeAsset)
{
m_MyTreeAsset = myTreeAsset;
m_Initialized = false;
}
// TreeView 显示区域
Rect multiColumnTreeViewRect
{
get { return new Rect(20, 30, position.width-40, position.height-60); }
}
// 搜索显示区域
Rect toolbarRect
{
get { return new Rect (20f, 10f, position.width-40f, 20f); }
}
//底部工具栏区域
Rect bottomToolbarRect
{
get { return new Rect(20f, position.height - 18f, position.width - 40f, 16f); }
}
public MultiColumnTreeView treeView
{
get { return m_TreeView; }
}
void InitIfNeeded ()
{
if (!m_Initialized)
{
// Check if it already exists (deserialized from window layout file or scriptable object)
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState();
//创建 HeaderState,包含各列头部ui相关显示
bool firstInit = m_MultiColumnHeaderState == null;
var headerState = MultiColumnTreeView.CreateDefaultMultiColumnHeaderState(multiColumnTreeViewRect.width);
//应用本地数据
if (MultiColumnHeaderState.CanOverwriteSerializedFields(m_MultiColumnHeaderState, headerState))
MultiColumnHeaderState.OverwriteSerializedFields(m_MultiColumnHeaderState, headerState);
m_MultiColumnHeaderState = headerState;
//根据 HeaderState 创建 MultiColumnHeader
var multiColumnHeader = new MyMultiColumnHeader(headerState);
if (firstInit)
multiColumnHeader.ResizeToFit ();
var treeModel = new TreeModel<MyTreeElement>(GetData());
//根据 TreeViewState,MultiColumnHeader, TreeModel 创建 TreeView
m_TreeView = new MultiColumnTreeView(m_TreeViewState, multiColumnHeader, treeModel);
//搜索输入框
m_SearchField = new SearchField();
m_SearchField.downOrUpArrowKeyPressed += m_TreeView.SetFocusAndEnsureSelectedItem;
m_Initialized = true;
}
}
// 获取列表数据
IList<MyTreeElement> GetData ()
{
if (m_MyTreeAsset != null && m_MyTreeAsset.treeElements != null && m_MyTreeAsset.treeElements.Count > 0)
return m_MyTreeAsset.treeElements;
// generate some test data
return MyTreeElementGenerator.GenerateRandomTree(130);
}
// 选择某个资源时触发
void OnSelectionChange ()
{
if (!m_Initialized)
return;
var myTreeAsset = Selection.activeObject as MyTreeAsset;
if (myTreeAsset != null && myTreeAsset != m_MyTreeAsset)
{
m_MyTreeAsset = myTreeAsset;
m_TreeView.treeModel.SetData (GetData ());
m_TreeView.Reload ();
}
}
void OnGUI ()
{
//初始化
InitIfNeeded();
SearchBar (toolbarRect);
DoTreeView (multiColumnTreeViewRect);
BottomToolBar (bottomToolbarRect);
}
void SearchBar (Rect rect)
{
//绘制搜索输入框
treeView.searchString = m_SearchField.OnGUI (rect, treeView.searchString);
}
void DoTreeView (Rect rect)
{
//绘制 TreeView
m_TreeView.OnGUI(rect);
}
//底部工具栏
void BottomToolBar (Rect rect)
{
GUILayout.BeginArea (rect);
using (new EditorGUILayout.HorizontalScope ())
{
var style = "miniButton";
if (GUILayout.Button("Expand All", style))
{
treeView.ExpandAll ();
}
if (GUILayout.Button("Collapse All", style))
{
treeView.CollapseAll ();
}
GUILayout.FlexibleSpace();
GUILayout.Label (m_MyTreeAsset != null ? AssetDatabase.GetAssetPath (m_MyTreeAsset) : string.Empty);
GUILayout.FlexibleSpace ();
if (GUILayout.Button("Set sorting", style))
{
var myColumnHeader = (MyMultiColumnHeader)treeView.multiColumnHeader;
myColumnHeader.SetSortingColumns (new int[] {4, 3, 2}, new[] {true, false, true});
myColumnHeader.mode = MyMultiColumnHeader.Mode.LargeHeader;
}
GUILayout.Label ("Header: ", "minilabel");
if (GUILayout.Button("Large", style))
{
var myColumnHeader = (MyMultiColumnHeader) treeView.multiColumnHeader;
myColumnHeader.mode = MyMultiColumnHeader.Mode.LargeHeader;
}
if (GUILayout.Button("Default", style))
{
var myColumnHeader = (MyMultiColumnHeader)treeView.multiColumnHeader;
myColumnHeader.mode = MyMultiColumnHeader.Mode.DefaultHeader;
}
if (GUILayout.Button("No sort", style))
{
var myColumnHeader = (MyMultiColumnHeader)treeView.multiColumnHeader;
myColumnHeader.mode = MyMultiColumnHeader.Mode.MinimumHeaderWithoutSorting;
}
GUILayout.Space (10);
if (GUILayout.Button("values <-> controls", style))
{
treeView.showControls = !treeView.showControls;
}
}
GUILayout.EndArea();
}
}
internal class MyMultiColumnHeader : MultiColumnHeader
{
Mode m_Mode;
public enum Mode
{
LargeHeader,
DefaultHeader,
MinimumHeaderWithoutSorting
}
public MyMultiColumnHeader(MultiColumnHeaderState state)
: base(state)
{
mode = Mode.DefaultHeader;
}
public Mode mode
{
get
{
return m_Mode;
}
set
{
m_Mode = value;
switch (m_Mode)
{
case Mode.LargeHeader:
canSort = true;
height = 37f;
break;
case Mode.DefaultHeader:
canSort = true;
height = DefaultGUI.defaultHeight;
break;
case Mode.MinimumHeaderWithoutSorting:
canSort = false;
height = DefaultGUI.minimumHeight;
break;
}
}
}
// 自定义 Head GUI 显示
protected override void ColumnHeaderGUI (MultiColumnHeaderState.Column column, Rect headerRect, int columnIndex)
{
// Default column header gui
base.ColumnHeaderGUI(column, headerRect, columnIndex);
// Add additional info for large header
if (mode == Mode.LargeHeader)
{
// Show example overlay stuff on some of the columns
if (columnIndex > 2)
{
headerRect.xMax -= 3f;
var oldAlignment = EditorStyles.largeLabel.alignment;
EditorStyles.largeLabel.alignment = TextAnchor.UpperRight;
GUI.Label(headerRect, 36 + columnIndex + "%", EditorStyles.largeLabel);
EditorStyles.largeLabel.alignment = oldAlignment;
}
}
}
}
}
MultiColumnTreeView:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
namespace UnityEditor.TreeViewExamples
{
internal class MultiColumnTreeView : TreeViewWithTreeModel<MyTreeElement>
{
const float kRowHeights = 20f;
const float kToggleWidth = 18f;
public bool showControls = true;
//编辑器图标资源
static GUIContent[] s_TestIcons =
{
EditorGUIUtility.IconContent ("Folder Icon"),
EditorGUIUtility.IconContent ("AudioSource Icon"),
EditorGUIUtility.IconContent ("Camera Icon"),
EditorGUIUtility.IconContent ("Windzone Icon"),
EditorGUIUtility.IconContent ("GameObject Icon")
};
// All columns
enum MyColumns
{
Icon1,
Icon2,
Name,
Value1,
Value2,
Value3,
}
public enum SortOption
{
Name,
Value1,
Value2,
Value3,
}
// Sort options per column
SortOption[] m_SortOptions =
{
SortOption.Value1,
SortOption.Value3,
SortOption.Name,
SortOption.Value1,
SortOption.Value2,
SortOption.Value3
};
//展开Tree,扁平处理,不包含Root
public static void TreeToList (TreeViewItem root, IList<TreeViewItem> result)
{
if (root == null)
throw new NullReferenceException("root");
if (result == null)
throw new NullReferenceException("result");
result.Clear();
if (root.children == null)
return;
//通过一个堆来展开
Stack<TreeViewItem> stack = new Stack<TreeViewItem>();
//后面的元素先入栈,后弹出
for (int i = root.children.Count - 1; i >= 0; i--)
stack.Push(root.children[i]);
while (stack.Count > 0)
{
TreeViewItem current = stack.Pop();
result.Add(current);
if (current.hasChildren && current.children[0] != null)
{
for (int i = current.children.Count - 1; i >= 0; i--)
{
stack.Push(current.children[i]);
}
}
}
}
public MultiColumnTreeView (TreeViewState state, MultiColumnHeader multicolumnHeader, TreeModel<MyTreeElement> model) : base (state, multicolumnHeader, model)
{
Assert.AreEqual(m_SortOptions.Length , Enum.GetValues(typeof(MyColumns)).Length, "Ensure number of sort options are in sync with number of MyColumns enum values");
// Custom setup
rowHeight = kRowHeights; //行高
columnIndexForTreeFoldouts = 2; //折叠图标在哪列显示
showAlternatingRowBackgrounds = true; //背景交替变化
showBorder = true; //显示边框
customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; // center foldout in the row since we also center content. See RowGUI
extraSpaceBeforeIconAndLabel = kToggleWidth;
multicolumnHeader.sortingChanged += OnSortingChanged; //排序发生变化
Reload();
}
//构建显示的行 TreeViewItem 数据
// Note we We only build the visible rows, only the backend has the full tree information.
// The treeview only creates info for the row list.
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
{
var rows = base.BuildRows (root);
SortIfNeeded (root, rows);
return rows;
}
void OnSortingChanged (MultiColumnHeader multiColumnHeader)
{
SortIfNeeded (rootItem, GetRows());
}
void SortIfNeeded (TreeViewItem root, IList<TreeViewItem> rows)
{
if (rows.Count <= 1)
return;
if (multiColumnHeader.sortedColumnIndex == -1)
{
return; // No column to sort for (just use the order the data are in)
}
// Sort the roots of the existing tree items
SortByMultipleColumns ();
TreeToList(root, rows);
Repaint();
}
void SortByMultipleColumns ()
{
//设置排序的列
var sortedColumns = multiColumnHeader.state.sortedColumns;
if (sortedColumns.Length == 0)
return;
//只排序根节点直属的子节点
var myTypes = rootItem.children.Cast<TreeViewItem<MyTreeElement> >();
//首选列优先排序
var orderedQuery = InitialOrder (myTypes, sortedColumns);
//其他列排序
for (int i=1; i<sortedColumns.Length; i++)
{
SortOption sortOption = m_SortOptions[sortedColumns[i]];
bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);
switch (sortOption)
{
case SortOption.Name:
// ThenBy 在现有排序的基础上,对于相等的元素,再深入排序
orderedQuery = orderedQuery.ThenBy(l => l.data.name, ascending);
break;
case SortOption.Value1:
orderedQuery = orderedQuery.ThenBy(l => l.data.floatValue1, ascending);
break;
case SortOption.Value2:
orderedQuery = orderedQuery.ThenBy(l => l.data.floatValue2, ascending);
break;
case SortOption.Value3:
orderedQuery = orderedQuery.ThenBy(l => l.data.floatValue3, ascending);
break;
}
}
rootItem.children = orderedQuery.Cast<TreeViewItem> ().ToList ();
}
IOrderedEnumerable<TreeViewItem<MyTreeElement>> InitialOrder(IEnumerable<TreeViewItem<MyTreeElement>> myTypes, int[] history)
{
SortOption sortOption = m_SortOptions[history[0]];
//是否升序
bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
switch (sortOption)
{
case SortOption.Name:
return myTypes.Order(l => l.data.name, ascending);
case SortOption.Value1:
return myTypes.Order(l => l.data.floatValue1, ascending);
case SortOption.Value2:
return myTypes.Order(l => l.data.floatValue2, ascending);
case SortOption.Value3:
return myTypes.Order(l => l.data.floatValue3, ascending);
default:
Assert.IsTrue(false, "Unhandled enum");
break;
}
// default
return myTypes.Order(l => l.data.name, ascending);
}
int GetIcon1Index(TreeViewItem<MyTreeElement> item)
{
return (int)(Mathf.Min(0.99f, item.data.floatValue1) * s_TestIcons.Length);
}
int GetIcon2Index (TreeViewItem<MyTreeElement> item)
{
return Mathf.Min(item.data.text.Length, s_TestIcons.Length-1);
}
//绘制一行的 GUI
protected override void RowGUI (RowGUIArgs args)
{
var item = (TreeViewItem<MyTreeElement>) args.item;
//列可通过Header菜单隐藏,故需要 args.GetNumVisibleColumns
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 cell rect vertically (makes it easier to place controls, icons etc in the cells)
CenterRectUsingSingleLineHeight(ref cellRect);
//根据类型绘制 GUI
switch (column)
{
case MyColumns.Icon1:
{
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)].image, ScaleMode.ScaleToFit);
}
break;
case MyColumns.Icon2:
{
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)].image, ScaleMode.ScaleToFit);
}
break;
case MyColumns.Name:
{
// Do toggle
Rect toggleRect = cellRect;
toggleRect.x += GetContentIndent(item);
toggleRect.width = kToggleWidth;
if (toggleRect.xMax < cellRect.xMax)
item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); // hide when outside cell rect
// Default icon and label
args.rowRect = cellRect;
base.RowGUI(args);
}
break;
case MyColumns.Value1:
case MyColumns.Value2:
case MyColumns.Value3:
{
if (showControls)
{
cellRect.xMin += 5f; // When showing controls make some extra spacing
if (column == MyColumns.Value1)
item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
if (column == MyColumns.Value2)
item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, typeof(Material), false);
if (column == MyColumns.Value3)
item.data.text = GUI.TextField(cellRect, item.data.text);
}
else
{
string value = "Missing";
if (column == MyColumns.Value1)
value = item.data.floatValue1.ToString("f5");
if (column == MyColumns.Value2)
value = item.data.floatValue2.ToString("f5");
if (column == MyColumns.Value3)
value = item.data.floatValue3.ToString("f5");
DefaultGUI.LabelRightAligned(cellRect, value, args.selected, args.focused);
}
}
break;
}
}
// Rename
//--------
protected override bool CanRename(TreeViewItem item)
{
// Only allow rename if we can show the rename overlay with a certain width (label might be clipped by other columns)
Rect renameRect = GetRenameRect (treeViewRect, 0, item);
return renameRect.width > 30;
}
protected override void RenameEnded(RenameEndedArgs args)
{
// Set the backend name and reload the tree to reflect the new model
if (args.acceptedRename)
{
var element = treeModel.Find(args.itemID);
element.name = args.newName;
Reload();
}
}
protected override Rect GetRenameRect (Rect rowRect, int row, TreeViewItem item)
{
Rect cellRect = GetCellRectForTreeFoldouts (rowRect);
CenterRectUsingSingleLineHeight(ref cellRect);
return base.GetRenameRect (cellRect, row, item);
}
// Misc
//--------
protected override bool CanMultiSelect (TreeViewItem item)
{
return true;
}
//创建 HeaderState
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState(float treeViewWidth)
{
var columns = new[]
{
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent(EditorGUIUtility.FindTexture("FilterByLabel"), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "),
contextMenuText = "Asset", //右键点击出现的菜单,可配合 allowToggleVisibility 来控制该列是否显示
headerTextAlignment = TextAlignment.Center,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 30,
minWidth = 30,
maxWidth = 60,
autoResize = false,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent(EditorGUIUtility.FindTexture("FilterByType"), "Sed hendrerit mi enim, eu iaculis leo tincidunt at."),
contextMenuText = "Type",
headerTextAlignment = TextAlignment.Center,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 30,
minWidth = 30,
maxWidth = 60,
autoResize = false,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Name"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 150,
minWidth = 60,
autoResize = false,
allowToggleVisibility = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Multiplier", "In sed porta ante. Nunc et nulla mi."),
headerTextAlignment = TextAlignment.Right,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Left,
width = 110,
minWidth = 60,
autoResize = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Material", "Maecenas congue non tortor eget vulputate."),
headerTextAlignment = TextAlignment.Right,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Left,
width = 95,
minWidth = 60,
autoResize = true,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Note", "Nam at tellus ultricies ligula vehicula ornare sit amet quis metus."),
headerTextAlignment = TextAlignment.Right,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Left,
width = 70,
minWidth = 60,
autoResize = true
}
};
Assert.AreEqual(columns.Length, Enum.GetValues(typeof(MyColumns)).Length, "Number of columns should match number of enum values: You probably forgot to update one of them.");
var state = new MultiColumnHeaderState(columns);
return state;
}
}
static class MyExtensionMethods
{
public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.OrderBy(selector);
}
else
{
return source.OrderByDescending(selector);
}
}
public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.ThenBy(selector);
}
else
{
return source.ThenByDescending(selector);
}
}
}
}
基类 TreeViewWithTreeModel:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace UnityEditor.TreeViewExamples
{
internal class TreeViewItem<T> : TreeViewItem where T : TreeElement
{
public T data { get; set; }
public TreeViewItem (int id, int depth, string displayName, T data) : base (id, depth, displayName)
{
this.data = data;
}
}
internal class TreeViewWithTreeModel<T> : TreeView where T : TreeElement
{
TreeModel<T> m_TreeModel;
readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);
public event Action treeChanged;
public TreeModel<T> treeModel { get { return m_TreeModel; } }
public event Action<IList<TreeViewItem>> beforeDroppingDraggedItems;
public TreeViewWithTreeModel (TreeViewState state, TreeModel<T> model) : base (state)
{
Init (model);
}
public TreeViewWithTreeModel (TreeViewState state, MultiColumnHeader multiColumnHeader, TreeModel<T> model)
: base(state, multiColumnHeader)
{
Init (model);
}
void Init (TreeModel<T> model)
{
m_TreeModel = model;
m_TreeModel.modelChanged += ModelChanged;
}
void ModelChanged ()
{
if (treeChanged != null)
treeChanged ();
Reload ();
}
//构建根节点
protected override TreeViewItem BuildRoot()
{
int depthForHiddenRoot = -1;
return new TreeViewItem<T>(m_TreeModel.root.id, depthForHiddenRoot, m_TreeModel.root.name, m_TreeModel.root);
}
//构建显示的行 TreeViewItem
protected override IList<TreeViewItem> BuildRows (TreeViewItem root)
{
if (m_TreeModel.root == null)
{
Debug.LogError ("tree model root is null. did you call SetData()?");
}
m_Rows.Clear ();
if (!string.IsNullOrEmpty(searchString))
{
Search (m_TreeModel.root, searchString, m_Rows);
}
else
{
if (m_TreeModel.root.hasChildren)
AddChildrenRecursive(m_TreeModel.root, 0, m_Rows);
}
// We still need to setup the child parent information for the rows since this
// information is used by the TreeView internal logic (navigation, dragging etc)
SetupParentsAndChildrenFromDepths (root, m_Rows);
return m_Rows;
}
void AddChildrenRecursive (T parent, int depth, IList<TreeViewItem> newRows)
{
foreach (T child in parent.children)
{
var item = new TreeViewItem<T>(child.id, depth, child.name, child);
newRows.Add(item);
if (child.hasChildren)
{
if (IsExpanded(child.id)) //展开显示的行
{
AddChildrenRecursive (child, depth + 1, newRows);
}
else
{
item.children = CreateChildListForCollapsedParent();
}
}
}
}
void Search(T searchFromThis, string search, List<TreeViewItem> result)
{
if (string.IsNullOrEmpty(search))
throw new ArgumentException("Invalid search: cannot be null or empty", "search");
const int kItemDepth = 0; // tree is flattened when searching
Stack<T> stack = new Stack<T>();
foreach (var element in searchFromThis.children)
stack.Push((T)element);
while (stack.Count > 0)
{
T current = stack.Pop();
// 名字匹配的行
if (current.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
result.Add(new TreeViewItem<T>(current.id, kItemDepth, current.name, current));
}
if (current.children != null && current.children.Count > 0)
{
foreach (var element in current.children)
{
stack.Push((T)element);
}
}
}
SortSearchResult(result);
}
protected virtual void SortSearchResult (List<TreeViewItem> rows)
{
rows.Sort ((x,y) => EditorUtility.NaturalCompare (x.displayName, y.displayName)); // sort by displayName by default, can be overriden for multicolumn solutions
}
protected override IList<int> GetAncestors (int id)
{
return m_TreeModel.GetAncestors(id);
}
protected override IList<int> GetDescendantsThatHaveChildren (int id)
{
return m_TreeModel.GetDescendantsThatHaveChildren(id);
}
// Dragging
//-----------
const string k_GenericDragID = "GenericDragColumnDragging";
protected override bool CanStartDrag (CanStartDragArgs args)
{
return true;
}
protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
{
if (hasSearch)
return;
DragAndDrop.PrepareStartDrag();
var draggedRows = GetRows().Where(item => args.draggedItemIDs.Contains(item.id)).ToList();
DragAndDrop.SetGenericData(k_GenericDragID, draggedRows);
DragAndDrop.objectReferences = new UnityEngine.Object[] { }; // this IS required for dragging to work
string title = draggedRows.Count == 1 ? draggedRows[0].displayName : "< Multiple >";
DragAndDrop.StartDrag (title);
}
protected override DragAndDropVisualMode HandleDragAndDrop (DragAndDropArgs args)
{
// Check if we can handle the current drag data (could be dragged in from other areas/windows in the editor)
var draggedRows = DragAndDrop.GetGenericData(k_GenericDragID) as List<TreeViewItem>;
if (draggedRows == null)
return DragAndDropVisualMode.None;
// Parent item is null when dragging outside any tree view items.
switch (args.dragAndDropPosition)
{
case DragAndDropPosition.UponItem:
case DragAndDropPosition.BetweenItems:
{
bool validDrag = ValidDrag(args.parentItem, draggedRows);
if (args.performDrop && validDrag)
{
T parentData = ((TreeViewItem<T>)args.parentItem).data;
OnDropDraggedElementsAtIndex(draggedRows, parentData, args.insertAtIndex == -1 ? 0 : args.insertAtIndex);
}
return validDrag ? DragAndDropVisualMode.Move : DragAndDropVisualMode.None;
}
case DragAndDropPosition.OutsideItems:
{
if (args.performDrop)
OnDropDraggedElementsAtIndex(draggedRows, m_TreeModel.root, m_TreeModel.root.children.Count);
return DragAndDropVisualMode.Move;
}
default:
Debug.LogError("Unhandled enum " + args.dragAndDropPosition);
return DragAndDropVisualMode.None;
}
}
public virtual void OnDropDraggedElementsAtIndex (List<TreeViewItem> draggedRows, T parent, int insertIndex)
{
if (beforeDroppingDraggedItems != null)
beforeDroppingDraggedItems (draggedRows);
var draggedElements = new List<TreeElement> ();
foreach (var x in draggedRows)
draggedElements.Add (((TreeViewItem<T>) x).data);
var selectedIDs = draggedElements.Select (x => x.id).ToArray();
m_TreeModel.MoveElements (parent, insertIndex, draggedElements);
SetSelection(selectedIDs, TreeViewSelectionOptions.RevealAndFrame);
}
bool ValidDrag(TreeViewItem parent, List<TreeViewItem> draggedItems)
{
TreeViewItem currentParent = parent;
while (currentParent != null)
{
if (draggedItems.Contains(currentParent))
return false;
currentParent = currentParent.parent;
}
return true;
}
}
}