Unity编辑器下拉菜单
大家好,我是阿赵。
在Unity引擎里面编写工具插件,有时候会用到一些特殊的菜单形式,比如下拉选项。
通过下拉菜单,给用户选择不同的选项。
如果只是一层的下拉列表,可以用EditorGUILayout.Popup来实现
简单的代码示例如下:
string[] tempList = new string[] { "菜单1", "菜单2", "菜单3" };private int tempIndex = 0;private void ShowPopMenu3(){tempIndex = EditorGUILayout.Popup(tempIndex, tempList, GUILayout.Width(100));
}
但如果是多级的下拉菜单,像这样:
EditorGUILayout.Popup是做不到的。
这里介绍2种方式在Unity的EditorWindow里面制作多层下拉选项。
一、 通过MenuItem实现
1、 MenuItem的介绍
在using UnityEditor;之后,就可以在代码里面给某个方法添加MenuItem标签,比如这样:
[MenuItem("Tools/测试下拉菜单")]
static void TestFun()
{}
这时候,在编辑器的菜单栏上面,就会出现菜单选项
注意,上面MenuItem里面只设置了一个参数,就是菜单的路径。这个路径是有特殊含义的,现在是”Tools/测试下拉菜单”,那么出现在菜单栏里面的将会是“Tools”入口,然后再下拉就会出现“测试下拉菜单”的选项。
如果再复杂一点,把路径改成
[MenuItem("Tools/测试下拉菜单/下拉菜单选项1")]static void TestFun1(){}[MenuItem("Tools/测试下拉菜单/下拉菜单选项2")]static void TestFun2(){PopMenuTestWin.Instance.Show();}
这时候,显示会变成:
所以我们可以知道,如果路径相同的情况下,MenuItem选项会合并显示。
然后路径的根目录的不同,也会出现不同的效果,比如改成
[MenuItem("Assets/测试下拉菜单/下拉菜单选项1")]static void TestFun1(){}[MenuItem("Assets/测试下拉菜单/下拉菜单选项2")]static void TestFun2(){}
把根目录改成“Assets”,那么菜单会出现在Project栏里面,鼠标右键点击某个文件夹,就会出现这组菜单。
如果把根目录改成GameObject,并且把最后的属性值设置1-49
[MenuItem("GameObject/测试下拉菜单",false,1)]
static void TestFun1()
{}
这时候MenuItem的菜单会显示在Hierarchy面板的鼠标右键菜单里面。
2、 多级MenuItem显示
假如我现在需要显示的是多层的下拉菜单,通过MenuItem的自动合并路径的特性,就可以这样:
[MenuItem("testMenu/测试菜单1/测试菜单1-1",false,101)]
static void TestMenu1_1()
{}
[MenuItem("testMenu/测试菜单1/测试菜单1-2",false,102)]
static void TestMenu1_2()
{}
[MenuItem("testMenu/测试菜单1/测试菜单1-3",false,103)]
static void TestMenu1_3()
{}
[MenuItem("testMenu/测试菜单2/测试菜单2-1",false,201)]static void TestMenu2_1()
{}
[MenuItem("testMenu/测试菜单2/测试菜单2-2",false,202)]static void TestMenu2_2()
{}
[MenuItem("testMenu/测试菜单3/测试菜单3-1",false,301)]static void TestMenu3_1()
{}
[MenuItem("testMenu/测试菜单4", false, 401)]static void TestMenu4_1()
{}
当这样定义了之后,那么在菜单上面就可以看到:
想继续添加子菜单,只需要继续扩展路径就行了。在MenuItem的最后有一个数字的参数,其实是一个排序显示的参数。数字越大,同级菜单的排序就会在越下面。
3、 把MenuItem显示到EditorWindow里面
刚才说了很久MenuItem的用法,接下来要把MenuItem显示在EditorWindow里面。
代码很简单,在需要显示的地方,加上:
if (GUI.Button(new Rect(0,0,100,30), "test"))
{EditorUtility.DisplayPopupMenu(new Rect(0, 30, 0, 0), "testMenu/", null);
}
这段代码的意思很简单,在按下按钮的时候,调用EditorUtility.DisplayPopupMenu,在指定的区域Rect(0, 30, 0, 0)内,显示菜单路径为”testMenu”作为根目录的所有菜单。
所以现在的效果会变成这样:
4、 优缺点
1.优点
这个方式制作编辑器下拉菜单,最大的好处是简单,添加得也很随意。当想添加或者删除菜单的时候,基本上可以做到互不影响。
2. 缺点
这个方式的缺点也很明显。
首先,这种方式制作的菜单,除了会显示在想要的编辑器面板里面,还会显示在顶部菜单栏或者其他的右键菜单里面,很难看。
然后,由于注册的代码是需要先写死的,所以很难通过配表之类的方式动态修改菜单选项。
二、 通过GenericMenu实现
1、GenericMenu的基本使用方法
GenericMenu的实现,需要先创建一个GenericMenu对象,然后给这个Menu对象添加Item。核心的代码大概就是这样:
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent(“测试菜单1/测试菜单1-1”), bool, OnSelectMenu,obj);
主要看看AddItem的三个参数:
第一个是路径,也就是显示菜单的层级
第二个是一个布尔值,如果想显示打钩,可以穿true。一般要配合别的选择逻辑去实现
第三OnSelectMenu是选项被触发时的回调参数,比如这样:
private void OnSelectMenu(object val)
{
}
第四个参数是一个object,对应前面的回调函数的参数,当选项被触发之后,会把指定的值传给回调函数。
最后,调用一下
menu.ShowAsContext();
把这个菜单显示出来。
2、 GenericMenu的扩展使用
如果需要添加很多菜单,当然也是可以一个一个手动的去AddItem,不过GenericMenu的自由度是比MenuItem高很多的,所以我们也可以根据实际情况来配表显示,或者用数组的方式来添加。
比如我现在指定一个简单的规则,我这里只有2级菜单,每个根菜单使用一个字符串数组来表示。
private string[] menu1 = new string[] { "测试菜单1", "测试菜单1-1", "测试菜单1-2", "测试菜单1-3", "测试菜单1-4" };private string[] menu2 = new string[] { "测试菜单2", "测试菜单2-1", "测试菜单2-2", "测试菜单2-3" };private string[] menu3 = new string[] { "测试菜单3", "测试菜单3-1", "测试菜单3-2"};private string[] menu4 = new string[] { "测试菜单4"};
是这样的规则,我就可以写个通用的添加菜单方法,把所有菜单生成出来:
比如这样:
private GenericMenu menu;
private string[] menu1 = new string[] { "测试菜单1", "测试菜单1-1", "测试菜单1-2", "测试菜单1-3", "测试菜单1-4" };
private string[] menu2 = new string[] { "测试菜单2", "测试菜单2-1", "测试菜单2-2", "测试菜单2-3" };
private string[] menu3 = new string[] { "测试菜单3", "测试菜单3-1", "测试菜单3-2"};
private string[] menu4 = new string[] { "测试菜单4"};
private string currentStr = "";
private void ShowPopMenu2()
{if (GUI.Button(new Rect(0, 50, 100, 30), "test2")){ShowGenericMenu();}}private void ShowGenericMenu()
{if(menu==null){CreateMenu();}menu.ShowAsContext();
}private void CreateMenu()
{menu = new GenericMenu();CreateSubMenu(menu1);menu.AddSeparator("");CreateSubMenu(menu2);menu.AddSeparator("");CreateSubMenu(menu3);menu.AddSeparator("");CreateSubMenu(menu4);
}private void CreateSubMenu(string[] strs)
{if(strs == null||strs.Length==0){return;}string key = strs[0];List<string> subStrList = new List<string>();if(strs.Length>1){for(int i = 1;i<strs.Length;i++){AddToMenu(key + "/" + strs[i], strs[i]);}}else{AddToMenu(key, key);}
}private void AddToMenu(string path,string content)
{menu.AddItem(new GUIContent(path), false, OnSelectMenu,content);
}private void OnSelectMenu(object val)
{currentStr = (string)val;//后续处理
}
这样,后续需要修改菜单的选项,就可以单纯的修改定义的变量就可以了。
当然,不同的规则会让生成的代码不一样。我这里只是2层菜单,如果是多层的,只要想好生成的逻辑,都可以统一生成。
如果想实现选中了就打钩的效果,可以在上面的基础上修改一下:
private GenericMenu menu;private string[] menu1 = new string[] { "测试菜单1", "测试菜单1-1", "测试菜单1-2", "测试菜单1-3", "测试菜单1-4" };private string[] menu2 = new string[] { "测试菜单2", "测试菜单2-1", "测试菜单2-2", "测试菜单2-3" };private string[] menu3 = new string[] { "测试菜单3", "测试菜单3-1", "测试菜单3-2"};private string[] menu4 = new string[] { "测试菜单4"};private string currentStr = "";private void ShowPopMenu2(){if (GUI.Button(new Rect(0, 50, 100, 30), "test2")){ShowGenericMenu();}}private void ShowGenericMenu(){if(menu==null){CreateMenu();}menu.ShowAsContext();}private void CreateMenu(){menu = new GenericMenu();CreateSubMenu(menu1);menu.AddSeparator("");CreateSubMenu(menu2);menu.AddSeparator("");CreateSubMenu(menu3);menu.AddSeparator("");CreateSubMenu(menu4);}private void CreateSubMenu(string[] strs){if(strs == null||strs.Length==0){return;}string key = strs[0];List<string> subStrList = new List<string>();if(strs.Length>1){for(int i = 1;i<strs.Length;i++){AddToMenu(key + "/" + strs[i], strs[i]);}}else{AddToMenu(key, key);}}private void AddToMenu(string path,string content){menu.AddItem(new GUIContent(path), currentStr == content, OnSelectMenu,content);}private void OnSelectMenu(object val){currentStr = (string)val;CreateMenu();}
主要是修改了在AddItem的时候,对比了传入的content是否和当前选择的currentStr相等,然后在OnSelectMenu回调时候,重新创建了一下菜单而已。
3、 优缺点
1. 优点
很明显,这个方式显示和逻辑方面都比较清晰,也可以通过规则统一生成和处理回调,感觉比较的规范。
2. 缺点
对比起MenuItem来说,GenericMenu的代码复杂程度会高一点,没有那么直观。不过我感觉也只是相对而已。