一、简介

  最近马三为公司开发了一款触发器编辑器,对于这个编辑器策划所要求的质量很高,是模仿暴雪的那个触发器编辑器来做的,而且之后这款编辑器要作为公司内部的一个通用工具链使用。其实,在这款触发器编辑器之前,已经有一款用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编辑器窗口基类代码如下:

 using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine; /// <summary>
/// 编辑器窗口基类
/// </summary>
public class EditorWindowBase : EditorWindow
{
/// <summary>
/// 界面层级管理,根据界面优先级访问界面焦点
/// </summary>
public int Priority { get; set; } private void OnFocus()
{
//重写OnFocus方法,让EditorWindowMgr去自动排序汇聚焦点
EditorWindowMgr.FoucusWindow();
}
}

  再来看看EditorWindowMgr编辑器窗口管理类是如何实现的:

 using System.Collections;
using System.Collections.Generic;
using UnityEngine; /// <summary>
/// 编辑器窗口管理类
/// </summary>
public class EditorWindowMgr
{
/// <summary>
/// 所有打开的编辑器窗口的缓存列表
/// </summary>
private static List<EditorWindowBase> windowList = new List<EditorWindowBase>(); /// <summary>
/// 重复弹出的窗口的优先级
/// </summary>
private static int repeateWindowPriroty = ; /// <summary>
/// 添加一个重复弹出的编辑器窗口到缓存中
/// </summary>
/// <param name="window"></param>
public static void AddRepeateWindow(EditorWindowBase window)
{
repeateWindowPriroty++;
window.Priority = repeateWindowPriroty;
AddEditorWindow(window);
} /// <summary>
/// 从缓存中移除一个重复弹出的编辑器窗口
/// </summary>
/// <param name="window"></param>
public static void RemoveRepeateWindow(EditorWindowBase window)
{
repeateWindowPriroty--;
window.Priority = repeateWindowPriroty;
RemoveEditorWindow(window);
} /// <summary>
/// 添加一个编辑器窗口到缓存中
/// </summary>
/// <param name="window"></param>
public static void AddEditorWindow(EditorWindowBase window)
{
if (!windowList.Contains(window))
{
windowList.Add(window);
SortWinList();
}
} /// <summary>
/// 从缓存中移除一个编辑器窗口
/// </summary>
/// <param name="window"></param>
public static void RemoveEditorWindow(EditorWindowBase window)
{
if (windowList.Contains(window))
{
windowList.Remove(window);
SortWinList();
}
} /// <summary>
/// 管理器强制刷新Window焦点
/// </summary>
public static void FoucusWindow()
{
if (windowList.Count > )
{
windowList[windowList.Count - ].Focus();
}
} /// <summary>
/// 关闭所有界面,并清理WindowList缓存
/// </summary>
public static void DestoryAllWindow()
{
foreach (EditorWindowBase window in windowList)
{
if (window != null)
{
window.Close();
}
}
windowList.Clear();
} /// <summary>
/// 对当前缓存窗口列表中的窗口按优先级升序排序
/// </summary>
private static void SortWinList()
{
windowList.Sort((x, y) =>
{
return x.Priority.CompareTo(y.Priority);
});
}
}

  对每个打开的窗体我们都通过AddEditorWindow操作将其加入到windowList缓存列表中,每个关闭的窗体我们会执行RemoveEditorWindow方法,将其从缓存列表中移除,每当增加或者删除窗体的时候,都会执行SortWinList方法,对缓存列表中的窗体按照Priority进行升序排列。而对于可重复弹出的窗口,我们提供了AddRepeateWindow 和 RemoveRepeateWindow这两个特殊接口,主要是对可重复弹出的窗口的优先级进行自动管理。DestoryAllWindow方法提供了在主界面关闭的时候,强制关闭所有的子界面的功能。最后还有一个比较重要的FoucusWindow方法,它是管理器强制刷新Window焦点,每次会把焦点强制聚焦到缓存列表中的最后一个元素,即优先级最大的界面上面,其实也就是最后创建的界面上面。通过重写每个界面的OnFocus函数为如下形式,手动调用EditorWindowMgr.FoucusWindow()让管理器去自动管理界面层级:

private void OnFocus()
{
EditorWindowMgr.FoucusWindow();
}

  接下来让我们看一下我们的编辑器主界面部分的代码,就是绘制了一些Label和按钮,没有什么太需要注意的地方,只要记得设置一下Priority的值即可:

 using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine; /// <summary>
/// 编辑器主界面
/// </summary>
public class MainWindow : EditorWindowBase
{
private static MainWindow window;
private static Vector2 minResolution = new Vector2(, );
private static Rect middleCenterRect = new Rect(, , , );
private GUIStyle labelStyle; /// <summary>
/// 对外的访问接口
/// </summary>
[MenuItem("Tools/RepeateWindow")]
public static void Popup()
{
window = EditorWindow.GetWindow(typeof(MainWindow), true, "多重窗口编辑器") as MainWindow;
window.minSize = minResolution;
window.Init();
EditorWindowMgr.AddEditorWindow(window);
window.Show();
} /// <summary>
/// 在这里可以做一些初始化工作
/// </summary>
private void Init()
{
Priority = ; labelStyle = new GUIStyle();
labelStyle.normal.textColor = Color.red;
labelStyle.alignment = TextAnchor.MiddleCenter;
labelStyle.fontSize = ;
labelStyle.border = new RectOffset(, , , );
} private void OnGUI()
{
ShowEditorGUI();
} /// <summary>
/// 绘制编辑器界面
/// </summary>
private void ShowEditorGUI()
{
GUILayout.BeginArea(middleCenterRect);
GUILayout.BeginVertical();
EditorGUILayout.LabelField("点击下面的按钮创建重复弹出窗口", labelStyle, GUILayout.Width());
if (GUILayout.Button("创建窗口", GUILayout.Width()))
{
RepeateWindow.Popup(window.position.position);
}
GUILayout.EndVertical();
GUILayout.EndArea();
} private void OnDestroy()
{
//主界面销毁的时候,附带销毁创建出来的子界面
EditorWindowMgr.RemoveEditorWindow(window);
EditorWindowMgr.DestoryAllWindow();
} private void OnFocus()
{
//重写OnFocus方法,让EditorWindowMgr去自动排序汇聚焦点
EditorWindowMgr.FoucusWindow();
}
}

  最后让我们看一下可重复弹出窗口是如何实现的,代码如下,有了前面的铺垫和代码中的注释相信大家一看就会明白,这里就不再逐条进行解释了:

 using System;
using UnityEditor;
using UnityEngine; /// <summary>
/// 重复弹出的编辑器窗口
/// </summary>
public class RepeateWindow : EditorWindowBase
{ private static Vector2 minResolution = new Vector2(, );
private static Rect leftUpRect = new Rect(new Vector2(, ), minResolution); public static void Popup(Vector3 position)
{
// RepeateWindow window = new RepeateWindow();
RepeateWindow window = GetWindowWithRectPrivate(typeof(RepeateWindow), leftUpRect, true, "重复弹出窗口") as RepeateWindow;
window.minSize = minResolution;
//要在设置位置之前,先把窗体注册到管理器中,以便更新窗体的优先级
EditorWindowMgr.AddRepeateWindow(window);
//刷新界面偏移量
int offset = (window.Priority - ) * ;
window.position = new Rect(new Vector2(position.x + offset, position.y + offset), new Vector2(, ));
window.Show();
//手动聚焦
window.Focus();
} /// <summary>
/// 重写EditorWindow父类的创建窗口函数
/// </summary>
/// <param name="t"></param>
/// <param name="rect"></param>
/// <param name="utility"></param>
/// <param name="title"></param>
/// <returns></returns>
private static EditorWindow GetWindowWithRectPrivate(Type t, Rect rect, bool utility, string title)
{
//UnityEngine.Object[] array = Resources.FindObjectsOfTypeAll(t);
EditorWindow editorWindow = null;/*= (array.Length <= 0) ? null : ((EditorWindow)array[0]);*/
if (!(bool)editorWindow)
{
editorWindow = (ScriptableObject.CreateInstance(t) as EditorWindow);
editorWindow.minSize = new Vector2(rect.width, rect.height);
editorWindow.maxSize = new Vector2(rect.width, rect.height);
editorWindow.position = rect;
if (title != null)
{
editorWindow.titleContent = new GUIContent(title);
}
if (utility)
{
editorWindow.ShowUtility();
}
else
{
editorWindow.Show();
}
}
else
{
editorWindow.Focus();
}
return editorWindow;
} private void OnGUI()
{
OnEditorGUI();
} private void OnEditorGUI()
{
GUILayout.Space();
GUILayout.BeginVertical();
EditorGUILayout.LabelField("我是重复弹出的窗体", GUILayout.Width());
if (GUILayout.Button("创建窗体", GUILayout.Width()))
{
//重复创建自己
Popup(this.position.position);
}
GUILayout.Space();
if (GUILayout.Button("关闭窗体", GUILayout.Width()))
{
this.Close();
}
GUILayout.EndVertical();
} private void OnDestroy()
{
//销毁窗体的时候,从管理器中移除该窗体的缓存,并且重新刷新焦点
EditorWindowMgr.RemoveRepeateWindow(this);
EditorWindowMgr.FoucusWindow();
} private void OnFocus()
{
EditorWindowMgr.FoucusWindow();
}
}

四、总结

  通过本篇博客,我们一起学习了如何在Unity编辑器中创建可重复的弹出界面与编辑器界面的层级如何管理。由于时间匆忙,本篇博客中的DEMO在所难免会有一些纰漏,欢迎大家共同完善。希望本文能够为大家的工作中带来一些启发与提示。

  本篇博客中的所有代码已经托管到Github,开源地址:https://github.com/XINCGer/Unity3DTraining/tree/master/UnityEditorExtension/MultiEditorWindow

作者:马三小伙儿
出处:https://www.cnblogs.com/msxh/p/9215015.html 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

【Unity编辑器】UnityEditor多重弹出窗体与编辑器窗口层级管理的更多相关文章

  1. MVVM模式下弹出窗体

    原地址:http://www.cnblogs.com/yk250/p/5773425.html 在mvvm模式下弹出窗体,有使用接口模式传入参数new一个对象的,还有的是继承于一个window,然后在 ...

  2. 解决在 MVC  局部视图中加载 ueditor 编辑器时, 编辑器加载不出的 bug

    在 MVC 局部视图中,有时我们需要 加载 ueditor 编辑器,或进行局部刷新, 但是在加载局部视图后,ueditor 编辑器加载不出,这是由于 ueditor 使用的缓存,只要清空缓存,重新实例 ...

  3. bootstrap中弹出窗体dialog的自定义

    感谢nakupanda的https://github.com/nakupanda/bootstrap3-dialog 根据需要弹出窗体,但是可以移动,不遮挡下面的内容,所以就修改了源代码,添加了一个属 ...

  4. CSS3/jQuery自己定义弹出窗体

    简单演示一下,精简了演示效果和css样式文件,更利于在项目中的实际应用 引入style.css   index.js <!DOCTYPE HTML PUBLIC "-//W3C//DT ...

  5. gridView AspNetPager 翻页时 弹出窗体关闭报错

    gridView AspNetPager 翻页后,你右击刷新或F5会发现弹出一个刷新页面. 这是因为默认翻页都是用dopostback方式回发的.因为这时的页面已经不是原来的页面.所以会弹出提示. 这 ...

  6. Ext入门学习系列(二)弹出窗体

    第二章 弹出窗体 上节学习了Ext的环境搭建和最基本的一个操作——弹出对话框,作为一个引子,本节讲述如何弹出一个新窗体,从实例讲解Ext的基本运行原理. 一.Ext的窗体长什么样? 先来看看几个效果, ...

  7. C#利用API制作类似QQ一样的右下角弹出窗体

    C#利用API制作类似QQ一样的右下角弹出窗体 (2009-03-21 15:02:49) 转载▼ 标签: 杂谈 分类: .NET using System;using System.Collecti ...

  8. EBS OAF开发中实现參数式弹出窗体

    EBS OAF开发中实现參数式弹出窗体 (版权声明,本人原创或者翻译的文章如需转载,如转载用于个人学习,请注明出处:否则请与本人联系,违者必究) 概览 參数式弹出窗体和嵌入式弹出窗体不一样,它拥有独立 ...

  9. Android初级教程以动画的形式弹出窗体

    这一篇集合动画知识和弹出窗体知识,综合起来以动画的形式弹出窗体. 动画的知识前几篇已经做过详细的介绍,可翻阅前面写的有关动画博文.先简单介绍一下弹出窗体效果的方法: 首先,需要窗体的实例:PopupW ...

随机推荐

  1. vue(3)—— vue的全局组件、局部组件

    组件 vue有局部组件和全局组件,这个组件后期用的会比较多,也是非常重要的 局部组件 template与components属性结合使用挂载 其中 Vmain.Vheader.Vleft.Vconte ...

  2. TiDB 架构及设计实现

    一. TiDB的核心特性 高度兼容 MySQL 大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移. 水平弹性扩展 ...

  3. golang 使用匿名结构体的问题

    golang允许使用匿名结构体,形如 type Test struct { param1 struct { param2 string } } 一般在使用的时候可以直接这样初始化 a := Test{ ...

  4. Python之python的一些理解

    应用领域: 1. 网络爬虫 2. 大数据分析与挖掘 3. 机器学习 4. web应用 5. 游戏开发 6. 自动化运维 入门学习网站: imooc,廖雪峰,黑马 环境变量 --- 就是告诉电脑,你的程 ...

  5. 设置MYSQL数据库编码为UTF-8

    设置MYSQL数据库编码为UTF-8   1.  编辑MySql的配置文件 MySql的配置文件Windows下一般在系统目录下或者在MySql的安装目录下名字叫my.ini,可以搜索,Linux下一 ...

  6. python3 员工信息表

    这是最后一条NLP了......来吧 十二,动机和情绪总不会错,只是行为没有效果而已 动机在潜意识里,总是正面的.潜意识从来不会伤害自己,只会误会的以为某行为可以满足该动机,而又不知道有其他做法的可能 ...

  7. 【P2577】 午餐

    题目简述 THU ACM小组一行N个人去食堂吃饭,计划是这样的:先把所有的人分成两队,并安排好每队中各人的排列顺序,然后一号队伍到一号窗口去排队打饭,二号队伍到二号窗口去排队打饭.每个人打完饭后立刻开 ...

  8. Swagger 报错 no mapping found for http request with uri [/***/swagger-ui.html] in dispatcherservlet with name '***'

    swagger报错: no mapping found for http request with uri [/***/swagger-ui.html] in dispatcherservlet wi ...

  9. 使用python对py文件程序代码复用度检查

    #!/user/bin/env python # @Time :2018/6/5 14:58 # @Author :PGIDYSQ #@File :PyCheck.py from os.path im ...

  10. Hbuilder工具使用

    现在用的版本是:HBuilder 9.1.19.201808300739 前段时间自动更新了下,也忘记了是更新后js代码不能正常代码提示,还是又发生了什么事情,导致了不能正常提示,也没时间去排查,卸载 ...