【Unity编辑器】UnityEditor多重弹出窗体与编辑器窗口层级管理
一、简介
最近马三为公司开发了一款触发器编辑器,对于这个编辑器策划所要求的质量很高,是模仿暴雪的那个触发器编辑器来做的,而且之后这款编辑器要作为公司内部的一个通用工具链使用。其实,在这款触发器编辑器之前,已经有一款用WinForm开发的1.0版触发器编辑器了,不过由于界面不太友好、操作繁琐以及学习使用成本较高,所以也饱受策划们的吐槽。而新研发的这款编辑器是直接嵌入在Unity中,作为Unity的拓展编辑器来使用的。当然在开发中,马三也遇到了种种的问题,不过还好,在同事的帮助下都一一解决了。本篇博客,马三就来和大家分享一下其中一个比较有趣的需求,RT,“UnityEditor多重弹出窗体与编辑器窗口层级管理”。
针对一些逻辑和数据部分的代码,由于是公司机密而且与本文的内容联系不大,马三就不和大家探讨了,本文中我们只关注UI的表现部分。(本文中所有的样例代码均经过重写,只用了原来的思想,代码结构已经和公司的编辑器完全不一样了,因此不涉及保密协议,完全开源,大家可以放心使用)先来说下今天我们要探讨的这个需求吧:
- 针对表达式进行解析,然后弹出可编辑的嵌套窗体。表达式有可能是嵌套的结构,因此弹出的窗体也要是多重弹出且嵌套的。
- 对于多重弹出的窗体,均为模态窗口,要有UI排序,新弹出的窗体要在原来的窗体的上面,且要有一定的自动偏移。上层窗体打开的状态下不能对下面的窗体进行操作(拖拽窗体是允许的,只是不能点击界面上的按钮,输入文字等等行为)。
- 界面自动聚焦,新创建窗体的时候,焦点会自动转移到新的窗体上,焦点一直保持在最上层的UI上面。
- 主界面关闭的时候,自动关闭其他打开的子界面。
所以策划要求的其实就是类似下面的这个样子的一个效果:
图1:最终效果图
这其中有两个比较值得注意的点:1.如何在Unity编辑器中创建可重复的弹出界面;2.界面的层级如何管理。下面我们将围绕这两个点逐一讨论。
二、如何在Unity编辑器中创建可重复的弹出窗体
众所周知,如果想要在Unity中创建出一个窗体,一般需要新建一个窗体类并继承自EditorWindow,然后调用EditorWindow.GetWindow()方法返回一个本类型的窗体,然后再对这个窗体进行show操作,这个窗体就显示出来了,总共算起来也就是下面两行代码:
window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重窗口编辑器") as MainWindow; window.Show();
我们可以把上面的操作封装到一个名叫Popup的静态方法中,这样在外部每次一调用Popup方法,我们的窗体就创建出来了。但是无论如何我们调用多少次Popup,在界面上始终只会有一个窗体出现,并不能出现多个同样的窗体存在。其原因我们可以在API文档中得到:
图2:官网API解释
如果界面上没有该窗体的实例,会创建、显示并返回该窗体的实例。否则,每次会返回第一个该窗体实例。这就不难解释为什么不能创建多个相同窗体的原因了,我们可以把他类比为一个单例模式的存在,如果没有就创建,如果有就返回当前的实例。再进一步我们可以通过反编译UnityEditor.dll来查看一下,他在底层是怎样实现的。UnityEditor.dll一般位于: X:\Program Files\Unity\Editor\Data\Managed\UnityEditor.dll 路径下面。
图3:反编译结果1
重载的几个 GetWindow 方法在最后都调用了 GetWindowPrivate 这个方法,我们再看一下对于 GetWindowPrivate 这个方法,Unity是如何实现它的:
图4:反编译结果2
结果一目了然,首先会调用Resources.FindObjectsOfTypeAll(t) 返回Unity中所有已经加载了的类型为 t 的实例并存储到array数组中,然后对editorWindow进行赋值,如果array数据没有数据则赋值为null,否则取数组中的第一个元素。接着,如果发现内存中没有该类型的实例, 通过editorWindow = (ScriptableObject.CreateInstance(t) as EditorWindow);创建一个类型为EditorWindow的实例,也就是一个新的窗体,对他进行了一系列的初始化以后,将其显示出来,并返回该类型的实例。如果内存中有该类型的实例,则调用show方法,并且把焦点聚焦到该窗体上,然后返回该类型的实例。
我们从源码的层面了解到了不能创建多个重复窗体的原因,并且搞清了他的创建原理,这样创建多个相同重复窗体的功能就不难写出来了,我们只要将 GetWindowPrivate 方法中的前两行代码替换为EditorWindow editorWindow = null 改造为我们自己的方法;用我们自己的 GetWindowPrivate 方法去创建,就可以得到无限多的重复窗体了。尽管通过 RepeateWindow window = new RepeateWindow() 的方法,我们也可以很轻松地得到无限多的重复窗体,但是这样操作会在Unity中报出警告信息,因为我们的EditorWindow都是继承自 ScriptableObject,自然要通过ScriptableObject.CreateInstance来创建实例,而不是直接通过构造器来创建。
三、编辑器UI的具体实现与层级管理
为了管理我们的编辑器窗口,马三引入了一个Priority的属性,它代表了界面的优先级。因为我们的所有的编辑器窗口都要参与管理,因此我们不妨直接先定义一个EditorWindowBase编辑器窗口基类,然后我们的后续的编辑器窗口类都继承自它,并且EditorWindowMgr编辑器窗口管理类也直接对该类型及其派生类型的窗体进行管理与操作。EditorWindowBase编辑器窗口基类代码如下:
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEditor; 4 using UnityEngine; 5 6 /// <summary> 7 /// 编辑器窗口基类 8 /// </summary> 9 public class EditorWindowBase : EditorWindow 10 { 11 /// <summary> 12 /// 界面层级管理,根据界面优先级访问界面焦点 13 /// </summary> 14 public int Priority { get; set; } 15 16 private void OnFocus() 17 { 18 //重写OnFocus方法,让EditorWindowMgr去自动排序汇聚焦点 19 EditorWindowMgr.FoucusWindow(); 20 } 21 }
再来看看EditorWindowMgr编辑器窗口管理类是如何实现的:
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 5 /// <summary> 6 /// 编辑器窗口管理类 7 /// </summary> 8 public class EditorWindowMgr 9 { 10 /// <summary> 11 /// 所有打开的编辑器窗口的缓存列表 12 /// </summary> 13 private static List<EditorWindowBase> windowList = new List<EditorWindowBase>(); 14 15 /// <summary> 16 /// 重复弹出的窗口的优先级 17 /// </summary> 18 private static int repeateWindowPriroty = 10; 19 20 /// <summary> 21 /// 添加一个重复弹出的编辑器窗口到缓存中 22 /// </summary> 23 /// <param name="window"></param> 24 public static void AddRepeateWindow(EditorWindowBase window) 25 { 26 repeateWindowPriroty++; 27 window.Priority = repeateWindowPriroty; 28 AddEditorWindow(window); 29 } 30 31 /// <summary> 32 /// 从缓存中移除一个重复弹出的编辑器窗口 33 /// </summary> 34 /// <param name="window"></param> 35 public static void RemoveRepeateWindow(EditorWindowBase window) 36 { 37 repeateWindowPriroty--; 38 window.Priority = repeateWindowPriroty; 39 RemoveEditorWindow(window); 40 } 41 42 /// <summary> 43 /// 添加一个编辑器窗口到缓存中 44 /// </summary> 45 /// <param name="window"></param> 46 public static void AddEditorWindow(EditorWindowBase window) 47 { 48 if (!windowList.Contains(window)) 49 { 50 windowList.Add(window); 51 SortWinList(); 52 } 53 } 54 55 /// <summary> 56 /// 从缓存中移除一个编辑器窗口 57 /// </summary> 58 /// <param name="window"></param> 59 public static void RemoveEditorWindow(EditorWindowBase window) 60 { 61 if (windowList.Contains(window)) 62 { 63 windowList.Remove(window); 64 SortWinList(); 65 } 66 } 67 68 /// <summary> 69 /// 管理器强制刷新Window焦点 70 /// </summary> 71 public static void FoucusWindow() 72 { 73 if (windowList.Count > 0) 74 { 75 windowList[windowList.Count - 1].Focus(); 76 } 77 } 78 79 /// <summary> 80 /// 关闭所有界面,并清理WindowList缓存 81 /// </summary> 82 public static void DestoryAllWindow() 83 { 84 foreach (EditorWindowBase window in windowList) 85 { 86 if (window != null) 87 { 88 window.Close(); 89 } 90 } 91 windowList.Clear(); 92 } 93 94 /// <summary> 95 /// 对当前缓存窗口列表中的窗口按优先级升序排序 96 /// </summary> 97 private static void SortWinList() 98 { 99 windowList.Sort((x, y) => 100 { 101 return x.Priority.CompareTo(y.Priority); 102 }); 103 } 104 }
对每个打开的窗体我们都通过AddEditorWindow操作将其加入到windowList缓存列表中,每个关闭的窗体我们会执行RemoveEditorWindow方法,将其从缓存列表中移除,每当增加或者删除窗体的时候,都会执行SortWinList方法,对缓存列表中的窗体按照Priority进行升序排列。而对于可重复弹出的窗口,我们提供了AddRepeateWindow 和 RemoveRepeateWindow这两个特殊接口,主要是对可重复弹出的窗口的优先级进行自动管理。DestoryAllWindow方法提供了在主界面关闭的时候,强制关闭所有的子界面的功能。最后还有一个比较重要的FoucusWindow方法,它是管理器强制刷新Window焦点,每次会把焦点强制聚焦到缓存列表中的最后一个元素,即优先级最大的界面上面,其实也就是最后创建的界面上面。通过重写每个界面的OnFocus函数为如下形式,手动调用EditorWindowMgr.FoucusWindow()让管理器去自动管理界面层级:
private void OnFocus() { EditorWindowMgr.FoucusWindow(); }
接下来让我们看一下我们的编辑器主界面部分的代码,就是绘制了一些Label和按钮,没有什么太需要注意的地方,只要记得设置一下Priority的值即可:
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEditor; 4 using UnityEngine; 5 6 /// <summary> 7 /// 编辑器主界面 8 /// </summary> 9 public class MainWindow : EditorWindowBase 10 { 11 private static MainWindow window; 12 private static Vector2 minResolution = new Vector2(800, 600); 13 private static Rect middleCenterRect = new Rect(200, 100, 400, 400); 14 private GUIStyle labelStyle; 15 16 /// <summary> 17 /// 对外的访问接口 18 /// </summary> 19 [MenuItem("Tools/RepeateWindow")] 20 public static void Popup() 21 { 22 window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重窗口编辑器") as MainWindow; 23 window.minSize = minResolution; 24 window.Init(); 25 EditorWindowMgr.AddEditorWindow(window); 26 window.Show(); 27 } 28 29 /// <summary> 30 /// 在这里可以做一些初始化工作 31 /// </summary> 32 private void Init() 33 { 34 Priority = 1; 35 36 labelStyle = new GUIStyle(); 37 labelStyle.normal.textColor = Color.red; 38 labelStyle.alignment = TextAnchor.MiddleCenter; 39 labelStyle.fontSize = 14; 40 labelStyle.border = new RectOffset(1, 1, 2, 2); 41 } 42 43 private void OnGUI() 44 { 45 ShowEditorGUI(); 46 } 47 48 /// <summary> 49 /// 绘制编辑器界面 50 /// </summary> 51 private void ShowEditorGUI() 52 { 53 GUILayout.BeginArea(middleCenterRect); 54 GUILayout.BeginVertical(); 55 EditorGUILayout.LabelField("点击下面的按钮创建重复弹出窗口", labelStyle, GUILayout.Width(220)); 56 if (GUILayout.Button("创建窗口", GUILayout.Width(80))) 57 { 58 RepeateWindow.Popup(window.position.position); 59 } 60 GUILayout.EndVertical(); 61 GUILayout.EndArea(); 62 } 63 64 private void OnDestroy() 65 { 66 //主界面销毁的时候,附带销毁创建出来的子界面 67 EditorWindowMgr.RemoveEditorWindow(window); 68 EditorWindowMgr.DestoryAllWindow(); 69 } 70 71 private void OnFocus() 72 { 73 //重写OnFocus方法,让EditorWindowMgr去自动排序汇聚焦点 74 EditorWindowMgr.FoucusWindow(); 75 } 76 }
最后让我们看一下可重复弹出窗口是如何实现的,代码如下,有了前面的铺垫和代码中的注释相信大家一看就会明白,这里就不再逐条进行解释了:
1 using System; 2 using UnityEditor; 3 using UnityEngine; 4 5 /// <summary> 6 /// 重复弹出的编辑器窗口 7 /// </summary> 8 public class RepeateWindow : EditorWindowBase 9 { 10 11 private static Vector2 minResolution = new Vector2(300, 200); 12 private static Rect leftUpRect = new Rect(new Vector2(0, 0), minResolution); 13 14 public static void Popup(Vector3 position) 15 { 16 // RepeateWindow window = new RepeateWindow(); 17 RepeateWindow window = GetWindowWithRectPrivate(typeof(RepeateWindow), leftUpRect, true, "重复弹出窗口") as RepeateWindow; 18 window.minSize = minResolution; 19 //要在设置位置之前,先把窗体注册到管理器中,以便更新窗体的优先级 20 EditorWindowMgr.AddRepeateWindow(window); 21 //刷新界面偏移量 22 int offset = (window.Priority - 10) * 30; 23 window.position = new Rect(new Vector2(position.x + offset, position.y + offset), new Vector2(800, 400)); 24 window.Show(); 25 //手动聚焦 26 window.Focus(); 27 } 28 29 /// <summary> 30 /// 重写EditorWindow父类的创建窗口函数 31 /// </summary> 32 /// <param name="t"></param> 33 /// <param name="rect"></param> 34 /// <param name="utility"></param> 35 /// <param name="title"></param> 36 /// <returns></returns> 37 private static EditorWindow GetWindowWithRectPrivate(Type t, Rect rect, bool utility, string title) 38 { 39 //UnityEngine.Object[] array = Resources.FindObjectsOfTypeAll(t); 40 EditorWindow editorWindow = null;/*= (array.Length <= 0) ? null : ((EditorWindow)array[0]);*/ 41 if (!(bool)editorWindow) 42 { 43 editorWindow = (ScriptableObject.CreateInstance(t) as EditorWindow); 44 editorWindow.minSize = new Vector2(rect.width, rect.height); 45 editorWindow.maxSize = new Vector2(rect.width, rect.height); 46 editorWindow.position = rect; 47 if (title != null) 48 { 49 editorWindow.titleContent = new GUIContent(title); 50 } 51 if (utility) 52 { 53 editorWindow.ShowUtility(); 54 } 55 else 56 { 57 editorWindow.Show(); 58 } 59 } 60 else 61 { 62 editorWindow.Focus(); 63 } 64 return editorWindow; 65 } 66 67 68 private void OnGUI() 69 { 70 OnEditorGUI(); 71 } 72 73 private void OnEditorGUI() 74 { 75 GUILayout.Space(12); 76 GUILayout.BeginVertical(); 77 EditorGUILayout.LabelField("我是重复弹出的窗体", GUILayout.Width(200)); 78 if (GUILayout.Button("创建窗体", GUILayout.Width(100))) 79 { 80 //重复创建自己 81 Popup(this.position.position); 82 } 83 GUILayout.Space(12); 84 if (GUILayout.Button("关闭窗体", GUILayout.Width(100))) 85 { 86 this.Close(); 87 } 88 GUILayout.EndVertical(); 89 } 90 91 private void OnDestroy() 92 { 93 //销毁窗体的时候,从管理器中移除该窗体的缓存,并且重新刷新焦点 94 EditorWindowMgr.RemoveRepeateWindow(this); 95 EditorWindowMgr.FoucusWindow(); 96 } 97 98 private void OnFocus() 99 { 100 EditorWindowMgr.FoucusWindow(); 101 } 102 }
四、总结
通过本篇博客,我们一起学习了如何在Unity编辑器中创建可重复的弹出界面与编辑器界面的层级如何管理。由于时间匆忙,本篇博客中的DEMO在所难免会有一些纰漏,欢迎大家共同完善。希望本文能够为大家的工作中带来一些启发与提示。
本篇博客中的所有代码已经托管到Github,开源地址:https://github.com/XINCGer/Unity3DTraining/tree/master/UnityEditorExtension/MultiEditorWindow
作者:马三小伙儿
出处:https://www.cnblogs.com/msxh/p/9215015.html
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!