【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编辑器窗口基类代码如下:
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多重弹出窗体与编辑器窗口层级管理的更多相关文章
- MVVM模式下弹出窗体
原地址:http://www.cnblogs.com/yk250/p/5773425.html 在mvvm模式下弹出窗体,有使用接口模式传入参数new一个对象的,还有的是继承于一个window,然后在 ...
- 解决在 MVC 局部视图中加载 ueditor 编辑器时, 编辑器加载不出的 bug
在 MVC 局部视图中,有时我们需要 加载 ueditor 编辑器,或进行局部刷新, 但是在加载局部视图后,ueditor 编辑器加载不出,这是由于 ueditor 使用的缓存,只要清空缓存,重新实例 ...
- bootstrap中弹出窗体dialog的自定义
感谢nakupanda的https://github.com/nakupanda/bootstrap3-dialog 根据需要弹出窗体,但是可以移动,不遮挡下面的内容,所以就修改了源代码,添加了一个属 ...
- CSS3/jQuery自己定义弹出窗体
简单演示一下,精简了演示效果和css样式文件,更利于在项目中的实际应用 引入style.css index.js <!DOCTYPE HTML PUBLIC "-//W3C//DT ...
- gridView AspNetPager 翻页时 弹出窗体关闭报错
gridView AspNetPager 翻页后,你右击刷新或F5会发现弹出一个刷新页面. 这是因为默认翻页都是用dopostback方式回发的.因为这时的页面已经不是原来的页面.所以会弹出提示. 这 ...
- Ext入门学习系列(二)弹出窗体
第二章 弹出窗体 上节学习了Ext的环境搭建和最基本的一个操作——弹出对话框,作为一个引子,本节讲述如何弹出一个新窗体,从实例讲解Ext的基本运行原理. 一.Ext的窗体长什么样? 先来看看几个效果, ...
- C#利用API制作类似QQ一样的右下角弹出窗体
C#利用API制作类似QQ一样的右下角弹出窗体 (2009-03-21 15:02:49) 转载▼ 标签: 杂谈 分类: .NET using System;using System.Collecti ...
- EBS OAF开发中实现參数式弹出窗体
EBS OAF开发中实现參数式弹出窗体 (版权声明,本人原创或者翻译的文章如需转载,如转载用于个人学习,请注明出处:否则请与本人联系,违者必究) 概览 參数式弹出窗体和嵌入式弹出窗体不一样,它拥有独立 ...
- Android初级教程以动画的形式弹出窗体
这一篇集合动画知识和弹出窗体知识,综合起来以动画的形式弹出窗体. 动画的知识前几篇已经做过详细的介绍,可翻阅前面写的有关动画博文.先简单介绍一下弹出窗体效果的方法: 首先,需要窗体的实例:PopupW ...
随机推荐
- git错误--ssh: Could not resolve hostname ssh.github.com: Name or service not known--解决方式
错误如下: git push origin ssh: Could not resolve hostname ssh.github.com: Name or service not known fata ...
- 经度和纬度在SQL中的数据类型
冬天太冷,等坐公司班车也很冷,就萌生了给班车做一个到站查询功能. 在某宝上买了汽车在线的GPS设备, 终生免费的服务的. 这里不得不提下这个设备的优点, 它提供API接口,还是免费的. 所以在班车上装 ...
- activemq读取剩余消息队列中消息的数量
先上原文链接: http://blog.csdn.net/bodybo/article/details/5647968 ActiveMQ在C#中的应用 ActiveMQ是个好东东,不必多说.Acti ...
- Redis中5种数据结构的使用场景
一.redis 数据结构使用场景 原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码.目前目标是吃透 redis 的数据结构.我们都知道,在 ...
- JSX有感
开发一个网页,我们要写视图部分HTML,也要写交互逻辑JS. 写JS时,不断翻看HTML,确保querySelector能取到期望的元素. 改HTML时,一个个排查JS文件,确保其没受影响. -- 类 ...
- Java基础系列--03_Java中的方法描述
方法 (1)方法的定义:就是完成特定功能的代码块. 注意:在很多语言里面有函数的定义,而在Java中,函数被称为方法. (2)格式: 修饰符 返回值类型 方法名(参数类型 参数名1,参数类型 参数名2 ...
- Web后台快速开发框架(.NET Core)
Web后台快速开发框架(.NET Core) Coldairarrow 目录 目录 第1章 目录 1 第2章 简介 3 第3章 基础准备 4 3.1 开发环境 ...
- Python--day05(数字、字符串、列表)
1.数字类型 1. 整型 int long(py2) 2. 小数 float 3. 布尔 bool 4. 复数 complex 2. 字符串类型 只能存一个值,是有序的不可变类型 2. ...
- 详解WTL应用向导
之前向 VS2019 中添加了 WTL 应用向导,今天来分析下该应用向导安装的相关文件,最终达到拷贝相关文件到 VS2019 的相应目录中即可直接使用 WTL 应用向导的目的. 在 VS2017 之前 ...
- 【刷题】Git工作流-相关知识点
参考资料:[学习总结]Git学习-GIT工作流-千峰教育(来自B站) 1-Git工作流 GitFlow流五大分支: 主干分支 热修复分支 预发布分支 开发分支 功能分支 GitFlow 工作流定义了一 ...