一、简介

  最近马三为公司开发了一款触发器编辑器,对于这个编辑器策划所要求的质量很高,是模仿暴雪的那个触发器编辑器来做的,而且之后这款编辑器要作为公司内部的一个通用工具链使用。其实,在这款触发器编辑器之前,已经有一款用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 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

转载于:https://www.cnblogs.com/msxh/p/9215015.html

【Unity编辑器】UnityEditor多重弹出窗体与编辑器窗口层级管理相关推荐

  1. Unity编辑器UnityEditor基础(二)

    Unity编辑器UnityEditor基础(二) 终极目标 利用学到的东西制作自己的工具(自定义的窗口.Inspector.菜单.插件等等). 准备工作 还是使用上一篇的 Unity 工程,然后在 S ...

  2. 《基于Qt的VR编辑器开发》(Yanlz+Unity+SteamVR+5G+AI+VR云游戏+Qt+编辑器+跨平台+人机交互+触发事件+立钻哥哥+==)

    <基于Qt的VR编辑器开发> <基于Qt的VR编辑器开发> 版本 作者 参与者 完成日期 备注 YanlzFramework_Qt_V01_1.0 严立钻 2019.09.04 ...

  3. winform 弹出窗体位置设定

    [转]https://www.cnblogs.com/liushenglin/p/5350641.html 一.C#中弹出窗口位置 加入命名空间using System.Drawing和using S ...

  4. 线程中使用SaveFileDialog不能弹出窗体

    在子线程中使用 SaveFileDialog 无法弹出窗体,主要是我们需要用主线程去处理 SaveFileDialog , 我们可以将子线程进行如下设置: public partial class F ...

  5. fancybox关闭弹出窗体parent.$.fancybox.close();

    fancybox弹出窗体右上角会自带一个关闭窗体,而且点击遮罩层也会关闭fancybox 有时我们不须要这样进行关闭,隐藏关闭窗体,而且遮罩层不可点击 在弹出窗体页面加一链接进行关闭 使用parent ...

  6. 记一次解决 quill(vue-quill-editor) 编辑器中莫名多出一行“pbr/p”的过程...

    问题描述: 在使用 vue-quill-editor 富文本编辑器过程中,加载已有的富文本数据到编辑器,经常会出现编辑器中莫名其妙多出一段换行内容 <p><br></p& ...

  7. Winfrom 弹出窗体位置设定

    Winfrom 窗体弹出位置设定,其实就是两种模式,第一种模式是通过Winform提供的属性来设定:第二种模式是自定义,可以相对于软件本身,也可以是相对于屏幕. 一.第一种模式 使用Winform提供 ...

  8. Bootstrap datepicker 在弹出窗体modal中不工作

    解决办法 在 show 方法后面 添加 下面一段代码 $('#modalCard').modal('show');-例子 打开 弹出窗体 //$('#modalCard').modal('hide') ...

  9. android 初学者实现 popupwindow的自动弹出,Android popupWindow弹出窗体实现方法分析

    本文实例讲述了Android popupWindow弹出窗体实现方法.分享给大家供大家参考,具体如下: 1. 建立popupwindow显示的布局页面(普通的view任意布局) android:lay ...

最新文章

  1. 1002: A+B for Input-Output Practice (II)
  2. Android Studio无法打开解决方法
  3. win32 API 遍历一个目录下的文件
  4. 【通知】有三AI GPU平台上线新功能,GPU/CPU可灵活选择
  5. 《SAP入门经典(第4版•修订版)》——2.5 4种视角相互结合
  6. 浏览器安全与MSAA
  7. 动态规划之多重部分和问题
  8. base64编码格式
  9. 到底啥是平台,到底啥是中台?李鬼太多,不得不说
  10. 【detectron】FPN网络中RPN构建与相应的损失函数
  11. Android自定义view之围棋动画,kotlin实现接口
  12. 开篇:为什么开始写博客
  13. java 压制警报_适用于Java开发人员的微服务:监视和警报
  14. 计算机音乐数字大全抖音,抖音歌曲大全100首,抖音最火的100首音乐
  15. 计算机系统内部存储器的存储单元,内部存储器练习
  16. 广汽埃安发布新LOGO,推出全新高端品牌Hyper昊铂
  17. 路由器中宽带密码查看
  18. kindle paperwhite 使用体验
  19. c++ overload 、override、overwrite
  20. 【1、雅思听力】何琼口语学习记录 第2节

热门文章

  1. 微软云架构服务器,微软云存储架构(Azure Cloud Storage)
  2. Centos7 网络配置 设置静态Ip
  3. zTree动态添加节点
  4. 南京师范大学与南京林业大学计算机,这8所高校“同宗同源”但不同命!有些是“985”,有些却是“双非”?...
  5. python爬虫源代码_【Python每日一问】Python爬虫能做什么?
  6. 电脑打字学习_新手如何学会电脑打字 走上盲打之路
  7. 系统学习NLP(十五)--seq2seq
  8. css不继承上级样式_这个笔记《CSS基本概念》,让菜鸟轻松学会给网页穿外衣
  9. linux 邮件服务器 并给外网发送邮件,Linux下判断公网IP是否改变,并发送邮件通知...
  10. scala List入门到熟悉