首页/新闻资讯/正文详情

Unity编辑器深度定制:UI Toolkit、CustomPropertyDrawer与性能优化全解析

发布时间:2026/9/24 18:43:50 来源:云帆数科 栏目:资讯中心
Unity编辑器深度定制:UI Toolkit、CustomPropertyDrawer与性能优化全解析
第3章做完时留言区问得最多的一句话是能不能把工具栏也做成自己想要的写第3章的时候我分享过怎么用MenuItem把自定义功能塞进菜单栏怎么用EditorWindow创建独立工具面板也提过CustomEditor重写 Inspector 的基本套路。当时评论区里问得最多的一句话是这些我照着敲也能出来但做出来的东西一眼看上去还是Unity默认那个味儿怎么把编辑器工具做成像是自己产品的一部分这一章我想正面回答这个问题。标题里写了续是因为上一章末尾已经搭好了一个基础工具窗口但那个窗口的观感、状态保持、属性展示方式和对性能的影响都没来得及深入。本篇会从四个方向继续往下挖把工具窗口从 IMGUI 迁移到 UI Toolkit用 UXML 和 USS 做真正的界面布局与皮肤用CustomPropertyDrawer把 Inspector 里的字段展示成好用的控件而不是一排默认文本框让编辑器记住用户上次的操作状态打开工具时不用从头再来最后聊一个大家在定制界面前很容易忽略的问题——改完界面之后编辑器变卡了问题出在哪儿、怎么定位。适合的读者范围我直接说清楚如果你已经会写最基本的 EditorWindow 和 MenuItem这一章的代码你可以直接抄如果你连 EditorWindow 都没建过建议先回头翻这个系列的前两章把基础操作过一遍再回来看阅读体验会顺很多。1. 先定位问题工具能跑和工具好用差的到底是什么在贴代码之前我想先把为什么很多自定义工具做出来很丑这个问题聊透。很多教程会告诉你 Layout 怎么用、控件怎么摆但很少分析一套内部分发使用的编辑器工具到底在哪些地方和普通游戏 UI 不一样。1.1 从熟练调用API到把界面当产品做之间的距离我见过不少团队的内部工具功能一点不少但界面停留在一个空窗口里竖着排一堆按钮的水平。这不是开发者能力问题而是大多数人默认了编辑器工具只是给自己用的能跑就行。但现实是工具一旦做到第二种规模你就会发现三个特别现实的问题布局缺乏层次。所有控件挤在一起眼睛根本不知道该先看哪儿。样式不统一。有的按钮是默认灰有的用了GUIStyle硬调颜色有的干脆贴了一张图整个界面看起来像补丁拼出来的。状态不保存。每次打开工具都要重新输入上一次的参数用两次就烦了。这一章的目标就是把这几个问题逐个解决掉。1.2 这一章选择的落地思路我的建议是新工具优先用 UI Toolkit老工具逐步迁移。不要上来就全面推翻重写而是挑一个你天天在用的窗口先用 UI Toolkit 重写一遍感受一下两者差异再把这里面的思路复制到其他窗口上。这里先给一个结论IMGUI 适合快速写一次性用的调试窗口UI Toolkit 适合做正式分发的内部工具。原因后面展开。2. 用 UI Toolkit 重写工具窗口从 IMGUI 到 UXML USS 的完整迁移UI Toolkit 是 Unity 官方主推的 UI 系统它对编辑器扩展开发者最大的意义是——你终于可以像写网站一样写编辑器界面了。结构写进 UXML样式写进 USS逻辑写进 C#三者互不干扰。这比 IMGUI 那种每帧重新画的模式可维护太多。2.1 一个适合练手的场景资源命名检查工具为了让整个迁移过程有一个具体载体我拿一个很常见的需求举例写一个工具扫描项目里所有资源根据关键词检查命名是否规范列出非法文件点击列表可以直接选中资源。在 IMGUI 版本里这个工具大概长这样一个窗口一个文本框一个按钮一个滚动列表。功能上没问题但界面真的简陋后续想加一个按资源类型过滤的下拉框就得手动管理更多布局。下面我直接用 UI Toolkit 从零重建它步骤分为 UXML、USS、C# 三个文件。2.2 第一步用 UXML 定义界面结构在项目里新建Assets/Editor/AssetNameCheckerWindow.uxml内容如下UXML xmlnsUnityEngine.UIElements xmlns:ueUnityEditor.UIElements VisualElement classcontainer Label textAsset Name Checker classheader-title / TextField labelKeyword namekeywordField / Button namecheckButton textRun Check classprimary-button / ScrollView nameresultScroll classresult-scroll / /VisualElement /UXML这里的结构信息量很简单但有两点值得展开name属性是给 C# 拿控件用的串联点后面root.QTextField(keywordField)就是通过这个名字定位到控件。class是给 USS 选择器用的样式只挂在 class 上不影响 C# 逻辑查找。2.3 第二步用 USS 定义样式再建一个Assets/Editor/AssetNameCheckerWindow.uss.container { flex-grow: 1; padding: 8px; background-color: rgb(45, 45, 48); } .header-title { font-size: 16px; -unity-font-style: bold; margin-bottom: 8px; } .primary-button { background-color: rgb(60, 120, 220); color: white; border-radius: 4px; margin-top: 6px; margin-bottom: 6px; -unity-font-style: bold; } .result-scroll { flex-grow: 1; border-top-width: 1px; border-top-color: rgb(80, 80, 85); }UI Toolkit 不能像传统 CSS 那样利用浏览器积累的样式所以有些属性名字比较特殊比如加粗是-unity-font-style圆角直接叫border-radius但语法上要比 Web CSS 简单。刚开始容易把写法搞混建议经常看一眼官方 USS 属性文档没必要全背下来。2.4 第三步C# 逻辑与控件绑定窗口脚本我放在Assets/Editor/AssetNameCheckerWindow.csusing UnityEditor; using UnityEngine; using UnityEngine.UIElements; public class AssetNameCheckerWindow : EditorWindow { [MenuItem(Tools/Asset Name Checker %#A)] public static void Open() { var wnd GetWindowAssetNameCheckerWindow(); wnd.titleContent new GUIContent(Asset Name Checker); wnd.minSize new Vector2(480, 320); wnd.Show(); } private TextField _keywordField; private ScrollView _resultScroll; private void CreateGUI() { var root rootVisualElement; root.Clear(); var visualTree AssetDatabase.LoadAssetAtPathVisualTreeAsset( Assets/Editor/AssetNameCheckerWindow.uxml); visualTree.CloneTree(root); var styleSheet AssetDatabase.LoadAssetAtPathStyleSheet( Assets/Editor/AssetNameCheckerWindow.uss); root.styleSheets.Add(styleSheet); _keywordField root.QTextField(keywordField); _resultScroll root.QScrollView(resultScroll); var button root.QButton(checkButton); button.clicked OnCheckClicked; } private void OnCheckClicked() { _resultScroll.Clear(); var keyword _keywordField.value.Trim(); if (string.IsNullOrEmpty(keyword)) { _resultScroll.Add(new Label(Please enter a keyword.)); return; } var guids AssetDatabase.FindAssets(); var count 0; foreach (var guid in guids) { var path AssetDatabase.GUIDToAssetPath(guid); var fileName System.IO.Path.GetFileNameWithoutExtension(path); if (fileName.Contains(keyword)) { var button new Button(() { Selection.activeObject AssetDatabase.LoadAssetAtPathObject(path); }) { text path }; _resultScroll.Add(button); count; } } if (count 0) { _resultScroll.Add(new Label(No matching assets.)); } } }几个非常关键的细节我在实际使用中才逐步理解写下来给你避坑第一CreateGUI是 UI Toolkit 风格下替代OnGUI的入口。IMGUI 里你写OnGUIUnity 每帧调用一次绘制UI Toolkit 里CreateGUI只会在窗口创建或者根元素需要重建的时候调用。这个差异是性能改善的根本原因。第二在CreateGUI开头写root.Clear()是一个好习惯。编辑器窗口支持反复编译、热重载如果不清空根节点可能导致重复 CloneTree 造成元素堆叠。第三按钮点击回调用了 lambda 捕获循环变量。这在 C# 5 之后通常是安全的因为 foreach 迭代变量每次循环都会被捕获为独立变量但如果你拿这段代码去改造把它换成for循环就一定要在循环体里面用一个局部变量复制索引否则点击之后选中的永远是最后一个。2.5 IMGUI 与 UI Toolkit 混用的边界从 IMGUI 迁到 UI Toolkit并不意味着所有旧代码都要作废。很多时候你是从一个 IMGUI 窗口迁移到 UI Toolkit 窗口但里面某个复杂操作流程已经有成熟的 IMGUI 实现这时候可以单独把那个部分保留下来。在 UI Toolkit 里调用 IMGUI 需要加一个IMGUIContainervar imguiContainer new IMGUIContainer(OnCustomIMGUISection); root.Add(imguiContainer); private void OnCustomIMGUISection() { GUILayout.Label(IMGUI section); if (GUILayout.Button(Legacy Action)) { ExecuteLegacyLogic(); } }我的建议是迁移初期可以这样过渡但长期来看还是尽量把内容全搬到 UI Toolkit 上。因为IMGUIContainer内部还是走老一套的布局计算和事件分发你在一个窗口里混用两种体系有时候会遇到一些诡异的交互问题比如 IMGUI 区域抢键盘焦点导致 UI Toolkit 的 TextField 无法输入。3. CustomPropertyDrawer在 Inspector 里画出真正好用的字段控件窗口工具做完以后另一个高频需求来自 Inspector。这里的问题通常是ScriptableObject数据类有很多字段Unity 默认画法是一个字段一行、全是默认样式遇到枚举就是下拉框遇到 bool 就是勾选框。当数据结构复杂起来默认画法会导致三个问题字段多的时候很难扫视也没法分组有些字段的值之间存在约束关系默认画法没法做联动视觉上没有层级不直观特别是给策划看的时候。CustomPropertyDrawer就是解决这些问题的主要手段。它和CustomEditor不一样。CustomEditor是控制整个MonoBehaviour或ScriptableObject的 Inspector 绘制CustomPropertyDrawer是控制某一个具体字段类型在 Inspector 里的显示方式粒度更细。3.1 典型场景把枚举画成按钮组先给一个最常见的例子一个数据类里有一个枚举字段你想让它在 Inspector 里从下拉框变成一排按钮策划不用点两下才能选到需要的模式。using System; using UnityEngine; [Serializable] public class ToolConfig { public ToolMode mode; public string path; public bool verbose; public int batchSize; } public enum ToolMode { None, Build, Test, Release }现在给这个枚举写一个 PropertyDrawerusing UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(ToolMode))] public class ToolModeDrawer : PropertyDrawer { private readonly string[] _options { None, Build, Test, Release }; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); var labelRect new Rect( position.x, position.y, EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight ); var buttonRect new Rect( position.x EditorGUIUtility.labelWidth, position.y, position.width - EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight ); EditorGUI.LabelField(labelRect, label); var newIndex GUI.Toolbar(buttonRect, property.enumValueIndex, _options); if (newIndex ! property.enumValueIndex) { property.enumValueIndex newIndex; } EditorGUI.EndProperty(); } }这里面有个非常重要的逻辑不要直接修改SerializedProperty背后的对象而是通过property.enumValueIndex来读写值。SerializedProperty是 Unity 序列化系统的抽象层绕过它直接改对象会破坏撤销记录和 Inspector 的自动刷新机制。新手最容易犯的错就是在 Drawer 里用反射或者类型转换拿原始对象操作完发现 CtrlZ 不好使了。EditorGUI.BeginProperty和EditorGUI.EndProperty这两个配对调用也要养成习惯。它做的事情包括绘制字段的上下文菜单右键菜单里有 Copy/Ping 等选项、处理多选对象的显示逻辑、正确渲染撤销快捷方式。很多自定义 Drawer 丢了这个包裹字段右键菜单消失用起来非常别扭。3.2 多字段可视化一个带进度条的自定义类型如果你有一个自定义类型比如一个带最小值、最大值和当前值的区间数据结构默认画法会把它展开成三个 float 字段。更好的画法是画一条进度条下面用两个滑块控制。[Serializable] public class RangedFloat { public float min; public float max; public float value; }对应的 Drawer[CustomPropertyDrawer(typeof(RangedFloat))] public class RangedFloatDrawer : PropertyDrawer { public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight * 2f 4f; } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); var minProp property.FindPropertyRelative(min); var maxProp property.FindPropertyRelative(max); var valueProp property.FindPropertyRelative(value); var minVal minProp.floatValue; var maxVal maxProp.floatValue; if (maxVal minVal) maxVal minVal; var value Mathf.Clamp(valueProp.floatValue, minVal, maxVal); var labelRect new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); var sliderRect new Rect(position.x, position.y EditorGUIUtility.singleLineHeight 4f, position.width, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(labelRect, label.text, $Range: {minVal:F2} - {maxVal:F2}); EditorGUI.MinMaxSlider(sliderRect, ref minVal, ref maxVal, 0f, 100f); valueProp.floatValue EditorGUI.Slider( new Rect(position.x, position.y EditorGUIUtility.singleLineHeight * 2f 8f, position.width, EditorGUIUtility.singleLineHeight), Value, value, minVal, maxVal); minProp.floatValue minVal; maxProp.floatValue maxVal; EditorGUI.EndProperty(); } }注意这里用了GetPropertyHeight并且返回了 2.5 倍行高。默认情况下 Inspector 认为每个字段高度是一行如果你在OnGUI里画了多行内容但不重写这个函数Unity 会按单行高度裁切导致内容显示不全。这个函数和OnGUI必须同步工作这是自定义 Drawer 最容易踩的坑之一。另外上面代码里的valueProp.floatValue EditorGUI.Slider(...)如果value原来不在[minVal, maxVal]范围内滑杆会先钳制它再显示但序列化值仍然是原始值。更稳妥的做法是var newValue EditorGUI.Slider(..., value, minVal, maxVal); if (!Mathf.Approximately(newValue, value)) { valueProp.floatValue newValue; }这样可以避免 Unity 在你的 Inspector 上画一条警告黄条数值超出范围。3.3 Drawer 与序列化引用之间的边界问题当你在 Drawer 里用FindPropertyRelative拿到子属性时要注意这些子属性在展开/折叠时的显示问题。如果你的自定义类型里面有引用类型的成员比如另一个ScriptableObject的引用那么FindPropertyRelative的结果可能是一个SerializedProperty的对象引用你需要用EditorGUI.ObjectField绘制var refProp property.FindPropertyRelative(target); EditorGUI.ObjectField( new Rect(position.x, position.y EditorGUIUtility.singleLineHeight, position.width, EditorGUIUtility.singleLineHeight), refProp, typeof(ScriptableObject), GUIContent.none );但这里有一个边界情况如果类型的引用目标是场景中的对象或项目资源ObjectField 第一个参数传refProp和在SerializedProperty上画对象引用还是有一点点区别的。传refProp时Unity 可以根据该字段的SerializeField类型自动过滤可赋值对象如果传了不匹配的类型Inspector 会警告。所以类型参数要明确不要稀里糊涂传typeof(Object)了事。4. 界面状态记忆让工具记得你是谁你上一次在干什么做编辑器工具做到中后期状态恢复往往是提升体验最明显的一步。尤其是那种每天都用的窗口如果每次打开都是默认状态用户心里的第一反应是又要重新填一遍参数这一节讲清楚状态存哪儿、怎么存、何时存最合适。4.1 EditorPrefs、SessionState、ScriptableObject 怎么选三者很容易混但定位完全不同存储方式生命周期适用场景EditorPrefs持久化存在操作系统注册表或配置文件中窗口尺寸、折叠状态、上次输入的关键词、个人偏好SessionState仅当前编辑器进程编译或重启后消失跨类共享的临时状态比如工具链传递数据ScriptableObject带CreateAssetMenu作为资产存到项目中可随版本库分发多用户共享的配置、预设、复杂数据结构个人经验是窗口 UI 的状态全放 EditorPrefs工具之间的流程中间量用 SessionState需要让全组统一使用的项目配置用 ScriptableObject。这三者的坑在于很多人不管什么数据都往 ScriptableObject 里塞导致项目版本库里多出一堆没意义的配置文件反过来也有人的工具一升级旧状态的 key 没变但格式变了读取时就抛异常。4.2 实例给资源检查工具加上记住上次输入的能力延续第2章的AssetNameCheckerWindow现在给它的状态加上保存和恢复。private const string PrefKeyword AssetNameChecker.Keyword; private const string PrefWindowSize AssetNameChecker.WindowSize; private void OnEnable() { var savedKeyword EditorPrefs.GetString(PrefKeyword, ); var savedSize EditorPrefs.GetString(PrefWindowSize, ); if (!string.IsNullOrEmpty(savedSize)) { var parts savedSize.Split(x); if (parts.Length 2 float.TryParse(parts[0], out var w) float.TryParse(parts[1], out var h)) { minSize new Vector2(w, h); } } } private void OnDisable() { EditorPrefs.SetString(PrefKeyword, _keywordField?.value ?? ); EditorPrefs.SetString(PrefWindowSize, ${position.width}x{position.height}); }注意两个细节第一_keywordField在OnDisable时可能为 null。编辑器工具的生命周期比很多人想象得复杂编译开始、脚本重载、窗口关闭都会触发OnDisable而且触发顺序并不总是先调OnEnable再调OnGUI。所以即使你在CreateGUI里一定给字段赋了值OnDisable里也必须加空判断。这一步不加脚本编译一次就会报一次 NullReferenceException虽然不致命但很烦。第二OnEnable比CreateGUI先执行这是 UI Toolkit 生命周期里被问得最多的顺序问题。OnEnable里你只能读取 EditorPrefs 保存到私有字段等CreateGUI执行时再把值赋给控件。反过来如果在OnEnable里直接_keywordField.value就会报错因为此时控件还没创建。4.3 与撤销系统协作时的状态保存策略自定义窗口状态的一个隐藏问题是你保存的状态和 Undo/Redo 视图不同步。举一个实际例子如果你的工具窗口可以编辑某个ScriptableObject的字段并且你用Undo.RecordObject保证可撤销用户在编辑器里改完值后按 CtrlZUnity 会恢复序列化数据。但如果你同时在OnGUI或 UI Toolkit 控件的回调里把当前值写进了 EditorPrefs那按 CtrlZ 的时候窗口里显示的可能是上一次保存的旧状态而 Inspector 里已经是撤销后的新状态两边就对不上了。解决思路是不要用一个每次变化都实时写入的监听器去同步 EditorPrefs 和界面而是只在OnDisable时保存窗口的 UI 偏好比如折叠状态、当前选区、关键字等不要保存正在编辑的业务数据字段。业务数据的持久化交给对象的序列化系统去管工具只负责展示和操作两者职责分开混乱就少一大半。这个原则我在多个项目里验证过非常管用。5. 编辑器界面性能改完 UI 之后变卡了怎么定位和修复这一部分是整个续篇里我最想写的。因为自定义编辑器界面本身不难但你一旦把窗口做得复杂起来就会遇到编辑器本身变卡、界面卡顿、甚至刷新闪烁的问题。你在网上搜很多中文教程基本很少聊这一块但它是实际项目中必现的坑。5.1 OnGUI 无法避免的高频调用和它带来的性能代价IMGUI 的OnGUI在编辑器里每一帧调用一次具体调用频率取决于你窗口是否脏或是否有交互。在 UI Toolkit 出现之前所有 EditorWindow 都走这套机制所以我们写插件时很容易在OnGUI里写一些昂贵操作private void OnGUI() { // 错误示范 var allGuids AssetDatabase.FindAssets(t:Prefab); foreach (var guid in allGuids) { var path AssetDatabase.GUIDToAssetPath(guid); // do something expensive } }这段代码的问题是AssetDatabase.FindAssets每次OnGUI都会扫描一次资产库。如果你的项目有几万资产编辑器会明显感到卡顿特别是拖动窗口或鼠标悬停在窗口时因为OnGUI会被更频繁地触发。修复思路很简单把耗时操作的结果缓存起来用一个“是否脏”标记决定何时重新扫描。在一开始那次扫描之后只有点击刷新按钮时才会重新扫描。UI Toolkit 虽然不会每帧无条件重绘但你在控件事件回调里写耗时逻辑同样会让编辑器变卡所以这条建议对两种 UI 体系都成立。5.2 用 Profiler 定位编辑器卡顿的完整排查链路如果你已经出现了编辑器卡顿但不确定是不是自己的工具窗口引起的可以按下面这个链路来排查第一步打开 Profiler 窗口Window Analysis Profiler在左上角的 Profiler 面板里把 Play Mode 改成Edit Mode。这是很多人容易忽略的地方默认显示的是 Play Mode 的 CPU 数据编辑器主循环的性能数据在 Edit Mode 里才看得到。第二步运行一段时间让卡顿复现然后点 CPU Usage 区域找到耗时最高的几个条目。如果你看到EditorGUI.DoTextField、GUIUtility.BeginGUI、IMGUIContainer.DoIMGUILayout这类条目耗时异常说明卡顿大概率来自某个自定义 IMGUI 窗口。第三步逐个禁用可疑窗口。这听起来很笨但非常有效把项目里你觉得可能有问题的 EditorWindow 脚本临时注释掉开项目看卡顿是否消失二分法排除比一直看代码更高效。第四步如果确定是OnGUI里触发了资产扫描或文件 IO记住三个原则不要在OnGUI里做扫描、不要每帧拿控件列表、不要在绘制路径里打开或保存文件。5.3 Repaint 失控窗口一直在偷偷重绘另一个经常遇到的性能杀手是Repaint 失控。症状是你的工具窗口没有进行任何交互但编辑器 CPU 占用持续很高打开 Profiler 看到Repaint事件一帧接一帧地刷。常见原因有两个第一个你在OnGUI或 UI Toolkit 的某个回调里调用了Repaint()而Repaint()又触发了下一次OnGUI形成了死循环private void OnGUI() { Repaint(); // 这是典型的 Repaint 风暴写法 }很多编辑器 API 如EditorApplication.QueuePlayerLoopUpdate也会触发额外的更新如果你在工具里定期刷新要记得设置间隔时间不要每帧都调用。第二个某些不可见的 EditorWindow 也会在后台持续Repaint。比如一个工具窗口被关闭了但如果它的脚本还持有EditorApplication.update事件并且该事件每次都会标记某个窗口需要重绘仍然会造成 CPU 消耗。排查时可以看看EditorApplication.update回调列表里有哪些你添加过但忘了移除的方法。在这里补充一个我个人的排查技巧打开Window General Console开启右上角的三条杠菜单里的Logging Info如果你每次打开某窗口就刷大量 Repainting ... window 日志那基本可以确定 Repaint 异常。5.4 与第三方插件窗口冲突时的处理思路当你自己的编辑器窗口和第三方插件同时存在时性能问题会变得更难定位。常见冲突有两种一是同一帧内多个窗口都在监听EditorApplication.update并各自触发自身Repaint造成叠加开销。这种情况无法通过改某个单点解决只能靠代码审查找出来。二是事件抢占。有的插件会重写OnSceneGUI或SceneView的绘制逻辑如果你的工具基于IMGUI在场景视图绘制手柄两者叠加可能造成手柄闪烁或者点击穿透。解决方案是尽量在工具里提供启用/禁用场景视图绘制的开关不要默认开着特别是在分发出去给多人用之前一定要确认它不会干扰别人常用的其他插件。从本团队实际使用情况看中间还有一个偏门但值得注意的点某些插件会修改EditorGUIUtility.labelWidth或注释掉其他窗口的样式尤其是不写BeginProperty就修改样式的代码。你的自定义窗口如果出现突然踩了别的窗口的样式可以先检查是否在OnGUI里用了全局静态 GUI 样式但没有还原。最后再补一个自己在实际项目里反复用到的习惯如果你看完前面内容准备马上动手改造一个工具窗口我建议你先别急着写代码花二十分钟把工具可能的扩展点列一遍。比如这个工具还会不会再增加筛选条件界面要不要给美术和策划用需不需要支持多开把这些可能的变化提前考虑进布局结构里会大幅减少你后续重构的成本。特别是多开这个点用它来决定状态是存实例字段还是统一存 EditorPrefs规划清楚后能避开很多麻烦。我自己在这几章内容上踩过的另一个坑就是开始时为了图省事把窗口状态全用静态类字段保存结果多个窗口同时打开时数据串来串去最后只好全部改成实例字段加 EditorPrefs 持久化白白改了好几轮。方向对了返工就少方向错了后面全是债。希望这一章能让你在深度定制编辑器界面这件事上少踩几个不必要的坑。

相关推荐

电脑卡顿别急着换机,6款免费软件帮你系统大扫除
电脑卡顿别急着换机,6款免费软件帮你系统大扫除

前几天帮朋友看一台用了三年的笔记本,整个开机过程比泡一碗面还久,打开浏览器要转上十几圈,C盘空间条红得发黑。他第一反应是把电脑扔给我,让我推荐一台新主机。我没急着下单,而是先花一下午把这台老机器重新收拾了一遍… · 2026/9/24 18:43:50

Orleans 网络拓扑与集群配置指南:端点、成员发现与生产环境网络规划
Orleans 网络拓扑与集群配置指南:端点、成员发现与生产环境网络规划

后端微服务 【免费下载链接】orleans Cloud Native application framework for .NET 项目地址: https://gitcode.com/gh_mirrors/or/orleans 点击查看 免费下载 Orleans 是一个面向 .NET 的云原生应用框架,其分布式运行时依赖一套清晰的网络拓扑来维持集… · 2026/9/24 18:43:50

延时触发复位芯片笔记:MCU跑飞后的最后一道保险
延时触发复位芯片笔记:MCU跑飞后的最后一道保险

上周排查一台扫地机,现场日志停在进入配网模式那一行再没动过,机器灯常亮、按键失灵,只能拔电池。这种就是典型的MCU跑飞,软件看门狗也没兜住。MCU跑飞常见几种。死循环卡在某个等待外设标志位的地方,中断被关、调度器… · 2026/9/24 18:43:50

JMeter接口加密参数实战:从sign签名到国密算法全解析
JMeter接口加密参数实战:从sign签名到国密算法全解析

这周最烦的一件事,是压测环境里所有请求突然开始报 sign 校验失败。开发那边给的说法很统一:安全要求,所有接口的请求参数必须带上加密签名。Jmeter 脚本里原来的参数直接暴露在请求里,现在必须把加密参数动态生成、动态塞进请求&… · 2026/9/24 19:19:38

JavaWeb课程设计实战:在线旅游网站从环境搭建到部署避坑全解析
JavaWeb课程设计实战:在线旅游网站从环境搭建到部署避坑全解析

简介:一份面向JavaWeb课程设计与期末大作业的在线旅游网站项目,内含完整源代码、数据库脚本及配套文档说明。项目基于Java服务端技术,实现了旅游线路展示、关键字搜索、路线详情等常用模块,并整合Bootstrap等前端资源,… · 2026/9/24 19:19:38

SpringBoot+Vue失物招领系统实战:从数据库设计到前后端分离部署
SpringBoot+Vue失物招领系统实战:从数据库设计到前后端分离部署

如果你正在做毕业设计或者课设,又恰好选了“校园失物招领系统”这种经典题目,那么用 SpringBoot Vue 这套组合算是走了一条稳路。这个项目表面上看是“发布失物、登记招领、管理员审核”的CRUD,但真正做完你会发现,它其实把后端接… · 2026/9/24 19:19:38

Presto 0.96 版本详解:try_cast 修复、分布式 Join 开关与 Hive DATE 分区支持
Presto 0.96 版本详解:try_cast 修复、分布式 Join 开关与 Hive DATE 分区支持

大数据数据库后端 【免费下载链接】presto The official home of the Presto distributed SQL query engine for big data 项目地址: https://gitcode.com/gh_mirrors/pre/presto 点击查看 免费下载 导读 本文围绕 Presto 发行说明 release-0.96.rst 展开&#xf… · 2026/9/24 19:19:31

SSM+Vue+微信小程序高校党费收缴系统:部署避坑与二次开发指南
SSM+Vue+微信小程序高校党费收缴系统:部署避坑与二次开发指南

简介:一套基于Java SSM框架的微信小程序高校党费收缴系统完整毕业设计源码包,面向计算机相关专业毕业生及需要快速搭建管理类小程序项目的人群。系统后台采用SSM框架,页面使用Vue,前端为微信小程序,搭配MySQL数据库与J… · 2026/9/24 19:19:31

Turnitin AI检测机制解析:从原理到论文降AIGC率的完整实践指南
Turnitin AI检测机制解析:从原理到论文降AIGC率的完整实践指南

我帮朋友处理过一篇被Turnitin标红的毕业论文,那次的经历让我意识到,很多人对AI检测的理解还停留在“改几个词就行”的阶段。等真正拿到检测报告,看到那一大片高亮标记,才明白这事没这么简单。这篇就围绕我实际折腾出来的经验&… · 2026/9/24 19:19:19

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码