Unity编辑器扩展:从EditorStyles到自定义GUIStyle的UI美化实战

1. 项目概述:为什么Unity编辑器样式值得深究?

如果你在Unity里做过编辑器扩展,无论是简单的工具窗口,还是复杂的资产管理器,肯定都遇到过界面美化的问题。Unity自带的默认控件,比如EditorGUILayout.TextField或者EditorGUILayout.Toggle,功能上没问题,但样式上往往千篇一律,和你的项目风格格格不入。更头疼的是,当你需要把多个控件组合成一个复杂的UI块,或者想实现一个带图标、颜色醒目的按钮时,光靠默认控件就显得捉襟见肘了。

这就是EditorStyles和自定义样式登场的时刻。它们不是锦上添花,而是决定你工具专业度和易用性的关键。一个布局清晰、视觉反馈明确、风格统一的编辑器工具,能极大提升开发者的使用体验和工作效率。想象一下,你花一周写了个强大的资源批处理工具,结果因为界面丑陋、操作反直觉,团队没人愿意用,这该多可惜。

网络上关于Unity编辑器扩展的教程很多,但大多集中在功能实现上,对样式的讲解往往浅尝辄止,或者只给出一段“魔法代码”而不解释其所以然。这导致很多开发者要么忍受默认的“灰盒子”界面,要么从网上东拼西凑样式代码,一旦出问题就无从调试。本指南的目的,就是彻底拆解Unity编辑器UI的样式系统,从内置的EditorStyles库开始,一步步深入到完全自定义的GUIStyle,让你不仅能“用”样式,更能“懂”样式,最终打造出独一无二的、高度定制化的编辑器界面。

2. 核心概念解析:GUIStyle与EditorStyles的基石

要玩转编辑器样式,必须先理解两个核心类:GUIStyleEditorStyles。它们是所有UI绘制的底层支撑。

2.1 GUIStyle:样式的原子单位

你可以把GUIStyle理解为一个UI控件的“皮肤”或“样式模板”。它不仅仅定义了颜色和字体,而是一个包含了控件所有视觉和布局状态的完整描述符。一个GUIStyle对象主要由以下属性构成:

  • normal / hover / active / focused / onNormal...: 这些是GUIStyleState对象,分别定义了控件在不同交互状态(正常、鼠标悬停、按下、获得焦点、开启状态等)下的视觉效果,最核心的是其background(背景纹理)和textColor(文字颜色)。
  • border: 一个RectOffset对象,定义了背景纹理的“九宫格”拉伸边界。这对于创建可伸缩的按钮、窗口边框至关重要。它指定了上、下、左、右四个方向的不拉伸区域。
  • margin / padding: 同样是RectOffset对象。margin是此样式控件外部与其他控件之间的间隔;padding内部内容(如文字)与控件边缘之间的间隔。
  • alignment:TextAnchor枚举,控制文本在控件矩形区域内的对齐方式(如左上、居中、右下等)。
  • fontStyle:FontStyle枚举,控制文本是否加粗、倾斜等。
  • fixedWidth / fixedHeight, stretchWidth / stretchHeight: 控制控件的尺寸行为。是固定大小,还是水平/垂直拉伸以填充可用空间。
  • wordWrap: 布尔值,控制文本是否自动换行。

理解GUIStyle的关键在于,Unity的IMGUI(即时模式GUI)系统在绘制每一个控件(如GUI.Button(rect, “text”, style))时,都会查询传入的GUIStyle对象,根据控件当前的状态,选择对应的GUIStyleState,并结合border进行九宫格渲染,最终将文本按照alignmentpadding等属性绘制在正确的位置。

2.2 EditorStyles:Unity为你准备好的工具箱

EditorStyles是一个静态类,里面预定义了海量在Unity编辑器内部使用的GUIStyle实例。这些样式保证了编辑器本身(如Inspector、Project窗口)视觉上的一致性。对于我们开发者来说,它是快速构建专业编辑器UI的宝藏库。

EditorStyles中的样式大致可以分为几类:

  1. 标签类:如label(普通标签)、boldLabel(粗体标签)、miniLabel(小号标签),用于显示只读文本。
  2. 按钮类:如button(普通按钮)、miniButton(小按钮)、toolbarButton(工具栏按钮)。
  3. 文本框类:如textField(单行文本框)、textArea(多行文本区域)、numberField(数字输入框)。
  4. 工具栏与标题类:如toolbar(工具栏背景)、helpBox(帮助信息框)、largeLabel(大标题)、headingLabel(节标题)。
  5. 特殊控件类:如foldout(折叠箭头)、toggle(复选框)、popup(下拉菜单样式)、objectField(对象引用字段)。

直接使用这些样式,能让你的工具窗口瞬间拥有“原生Unity”的观感。例如,使用EditorStyles.toolbarButton来制作工具条,其悬停、按下效果与Unity顶部的工具栏完全一致。

注意EditorStyles中的样式是共享的、全局的。你不应该去修改它们(如EditorStyles.label.normal.textColor = Color.red;),这会影响编辑器其他部分的显示,行为是不可预测的。正确的做法是以它们为蓝本,创建你自己的GUIStyle副本进行修改。

3. 内置EditorStyles实战应用与技巧

知道有哪些样式只是第一步,如何在正确的场景下选用合适的样式,并组合它们,才是体现功力的地方。

3.1 常用样式场景化选用指南

不要盲目选择样式。根据UI元素的用途和上下文来选择,能使界面逻辑更清晰。

  • 展示信息时

    • 主标题:使用EditorStyles.largeLabelEditorStyles.boldLabel
    • 节标题/分组标签:使用EditorStyles.label并搭配GUI.backgroundColor设置一个浅色背景,或者使用EditorStyles.helpBox将整个分组框起来。
    • 说明性文字:首选EditorStyles.helpBox。它的灰色背景和边距非常适合展示提示、警告或描述性文本。你也可以使用EditorStyles.miniLabel来显示辅助性的、字号较小的说明。
    • 数值/状态显示:如果只是只读显示,用EditorStyles.label。如果想突出显示,可以创建一个自定义样式,将normal.textColor设置为高亮色(如绿色表示正常,红色表示错误)。
  • 接收输入时

    • 单行文本:EditorStyles.textField
    • 多行文本:EditorStyles.textArea
    • 数字输入:虽然可以用textField,但更专业的做法是使用EditorGUILayout.IntField/FloatField等方法,它们内部使用了优化过的样式。
    • 对象拖拽赋值:使用EditorGUILayout.ObjectField方法,它会自动渲染为带picker按钮的样式,无需手动处理样式。
    • 复选框/开关:EditorStyles.toggle。用于布尔值。
    • 下拉菜单:EditorStyles.popup。通常与EditorGUILayout.Popup方法配合使用。
  • 触发操作时

    • 主要操作按钮:EditorStyles.miniButtonGUI.Button(使用默认或自定义样式)。对于重要操作,可以加大尺寸或使用醒目的背景色。
    • 工具栏按钮组:EditorStyles.toolbarButton。多个按钮并排时,使用这个样式能获得完美的工具栏视觉体验,按钮之间无缝衔接。
    • 图标按钮:为GUIStyle设置normal.background为一个Texture2D图标,并清除fixedWidth/Height,让按钮尺寸适应图标。

3.2 样式组合与布局实战

一个好看的UI是样式和布局共同作用的结果。EditorGUILayoutEditorGUI是布局的两大帮手。

示例:创建一个带标题和行动按钮的设置面板

using UnityEditor; using UnityEngine; public class StyleDemoWindow : EditorWindow { [MenuItem("Tools/Style Demo")] static void ShowWindow() { GetWindow<StyleDemoWindow>("样式演示"); } private string inputText = "请输入"; private bool toggleState = false; private int selectedIndex = 0; private string[] options = { "选项A", "选项B", "选项C" }; void OnGUI() { // 1. 主标题区域 EditorGUILayout.LabelField("配置面板", EditorStyles.largeLabel); EditorGUILayout.Space(); // 增加一点间距 // 2. 第一个设置分组 - 使用HelpBox视觉分组 EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("基础设置", EditorStyles.boldLabel); inputText = EditorGUILayout.TextField("文本输入:", inputText); toggleState = EditorGUILayout.Toggle("启用功能:", toggleState); selectedIndex = EditorGUILayout.Popup("选择模式:", selectedIndex, options); EditorGUILayout.EndVertical(); EditorGUILayout.Space(10); // 3. 第二个分组 - 使用背景色视觉分组 Color defaultBgColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.9f, 0.95f, 1.0f); // 浅蓝色背景 EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 这里依然用helpBox,但背景色被覆盖了 GUI.backgroundColor = defaultBgColor; // 恢复默认背景色 EditorGUILayout.LabelField("操作区域", EditorStyles.boldLabel); EditorGUILayout.HelpBox("点击下方按钮执行相应操作。", MessageType.Info); // 4. 按钮行 - 灵活运用布局 EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); // 将按钮推到右侧 if (GUILayout.Button("取消", EditorStyles.miniButton, GUILayout.Width(60))) { this.Close(); } // 给主要按钮一个不同的样式(这里用加粗标签模拟一个更突出的按钮) GUIStyle primaryButtonStyle = new GUIStyle(EditorStyles.miniButton); primaryButtonStyle.fontStyle = FontStyle.Bold; if (GUILayout.Button("确认保存", primaryButtonStyle, GUILayout.Width(80))) { Debug.Log($"保存配置:{inputText}, 状态:{toggleState}, 模式:{options[selectedIndex]}"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } }

这段代码的要点解析:

  1. EditorGUILayout.Space():这是最常用的间距控制方法,比硬编码Rect位置灵活得多。
  2. BeginVertical(EditorStyles.helpBox):利用helpBox样式自带的边框和内边距,快速创建一个视觉上的分组容器。这是实现分组最简洁有效的方式之一。
  3. 临时修改GUI.backgroundColor:通过临时改变背景色来高亮某个区域,操作结束后立即恢复,这是一种非常实用的局部样式技巧。
  4. BeginHorizontal()GUILayout.FlexibleSpace():这是实现右对齐按钮组的经典模式。FlexibleSpace()会填充所有可用空间,从而将后续的控件“推”到右侧。
  5. 动态创建GUIStyle:代码中基于EditorStyles.miniButton创建了一个新的primaryButtonStyle,并仅修改了其fontStyle。这是自定义样式的安全做法。

4. 从零到一:深度自定义GUIStyle

当内置样式无法满足需求时,就需要亲手打造自定义GUIStyle。这分为两种情况:基于现有样式修改,和从头创建。

4.1 基于现有样式的修改与继承

这是最推荐、最高效的方式。以EditorStyles.label为蓝本,创建一个只属于你当前窗口或控件的样式副本。

// 在类中定义,避免每次OnGUI都重新创建 private GUIStyle customLabelStyle; void OnGUI() { if (customLabelStyle == null) { customLabelStyle = new GUIStyle(EditorStyles.label); customLabelStyle.normal.textColor = Color.blue; customLabelStyle.fontSize = 14; customLabelStyle.fontStyle = FontStyle.BoldAndItalic; customLabelStyle.alignment = TextAnchor.MiddleCenter; // 添加文字阴影(通过绘制两次实现) // customLabelStyle.normal.textColor = Color.white; // customLabelStyle.hover.textColor = Color.white; // 阴影效果通常需要更复杂的绘制,这里仅作示意。 } EditorGUILayout.LabelField("这是一个自定义样式的标签", customLabelStyle); }

关键技巧:样式初始化时机

  • 避免在OnGUI内频繁newOnGUI每帧可能调用多次,频繁创建GUIStyle对象会引发GC(垃圾回收),导致性能问题。最佳实践是在脚本的OnEnable方法中,或者像上面一样使用懒加载模式(检查是否为null再创建)来初始化你的自定义样式。
  • GUIStyle的构造函数new GUIStyle(string name)可以按名称查找内置样式;new GUIStyle(GUIStyle other)是复制构造函数,最常用。

4.2 从头创建与九宫格(Border)艺术

当你需要独特的背景,比如圆角按钮、渐变标签时,就需要自己处理背景纹理和九宫格。

步骤1:准备背景纹理在Unity中导入一张背景图(如PNG),确保Texture Type设置为Editor GUI and Legacy,并关闭Generate Mip Maps。假设我们有一张名为custom_button_bg的圆角矩形纹理。

步骤2:在代码中创建样式

private GUIStyle customButtonStyle; private Texture2D buttonNormalTex; private Texture2D buttonActiveTex; void OnEnable() { // 加载纹理资源 buttonNormalTex = EditorGUIUtility.Load("custom_button_bg.png") as Texture2D; buttonActiveTex = EditorGUIUtility.Load("custom_button_bg_active.png") as Texture2D; // 按下状态的纹理 customButtonStyle = new GUIStyle(); // 1. 设置状态纹理 customButtonStyle.normal.background = buttonNormalTex; customButtonStyle.hover.background = buttonNormalTex; // 悬停也可以用同一张,或换色 customButtonStyle.active.background = buttonActiveTex; customButtonStyle.onNormal.background = buttonActiveTex; // 开启状态 // 2. 设置九宫格拉伸边界(Border) // 假设你的圆角矩形纹理的圆角半径为12像素,图片尺寸为100x50 // 那么border的left, right, top, bottom都应设置为12,确保圆角部分在拉伸时不变形 customButtonStyle.border = new RectOffset(12, 12, 12, 12); // 3. 设置内边距(Padding),让文字不要紧贴边缘 customButtonStyle.padding = new RectOffset(15, 15, 8, 8); // 左,右,上,下 // 4. 设置文本属性 customButtonStyle.normal.textColor = Color.white; customButtonStyle.hover.textColor = Color.yellow; customButtonStyle.active.textColor = Color.gray; customButtonStyle.alignment = TextAnchor.MiddleCenter; customButtonStyle.fontSize = 13; // 5. 固定高度,宽度自适应内容 customButtonStyle.fixedHeight = 50; customButtonStyle.stretchWidth = true; // 宽度会拉伸填充可用空间 } void OnGUI() { if (GUILayout.Button("九宫格按钮", customButtonStyle)) { // 按钮逻辑 } }

九宫格(Border)原理深度解析:这是自定义样式中最容易出错的部分。RectOffset定义了纹理的四个边界。leftrighttopbottom的值表示从纹理对应边缘向内多少像素的区域是“不拉伸”的。

  • border = new RectOffset(12, 12, 12, 12)为例,它将纹理分为9个区域:四个角(各12x12像素)、四个边(可拉伸)、一个中心(可拉伸)。
  • 当按钮尺寸变化时,四个角的像素保持不变,四条边只进行单向拉伸,中心区域进行双向拉伸
  • 这样就能保证无论按钮拉多大,它的圆角都能保持原样,只有中间平整的部分被拉伸,视觉上不会失真。

实操心得:确定border值的最佳方法是在Photoshop等工具中测量你设计的背景图。确保你的圆角或特殊边框位于“不拉伸”区域内。如果设置不当,拉伸时会出现圆角变形或边框粗细不均的问题。

5. 高级样式技巧与性能优化

掌握了基础创建后,一些高级技巧能让你的UI更加出彩和高效。

5.1 动态样式与状态反馈

样式不是一成不变的。根据数据状态动态改变样式,能提供极强的交互反馈。

private bool isWarning = false; private string userInput = ""; void OnGUI() { // 根据输入内容动态改变文本框样式 GUIStyle dynamicTextFieldStyle = new GUIStyle(EditorStyles.textField); if (string.IsNullOrEmpty(userInput)) { dynamicTextFieldStyle.normal.textColor = Color.gray; } else if (userInput.Length < 5) { dynamicTextFieldStyle.normal.textColor = Color.red; // 输入过短警告 isWarning = true; } else { dynamicTextFieldStyle.normal.textColor = EditorStyles.textField.normal.textColor; // 恢复默认 isWarning = false; } userInput = EditorGUILayout.TextField("用户名:", userInput, dynamicTextFieldStyle); // 在输入框下方显示动态提示 if (isWarning) { EditorGUILayout.HelpBox("用户名长度至少为5个字符", MessageType.Warning); } }

5.2 使用GUISkin实现样式主题化管理

当你的工具拥有大量自定义样式时,散落在代码各处会难以维护。GUISkin是一个资产文件(.guiskin),可以集中存储和管理多个GUIStyle。虽然它更常用于运行时UI,但也可以用于编辑器扩展。

  1. 在Project窗口右键Create -> GUI Skin
  2. 将创建好的.guiskin文件拖入你的编辑器脚本所在目录。
  3. 在代码中加载并使用:
public GUISkin myEditorSkin; // 在Inspector中赋值 private GUIStyle skinButtonStyle; void OnEnable() { if (myEditorSkin != null) { skinButtonStyle = myEditorSkin.FindStyle("MyCustomButton"); // 或者通过名称直接获取 myEditorSkin.button // 注意:GUISkin中的样式名是其在Inspector中显示的名称 } else { // 备用方案 skinButtonStyle = new GUIStyle(EditorStyles.miniButton); } }

使用GUISkin的好处是,美术或设计师可以直接在Unity编辑器中修改这个皮肤文件,调整颜色、字体等,而无需修改代码。

5.3 性能陷阱与优化策略

编辑器扩展的UI性能同样重要,糟糕的代码会导致工具窗口卡顿。

  • 陷阱1:在OnGUI中创建GUIStyle。如前所述,这会产生大量垃圾对象。务必OnEnableAwake或静态构造函数中初始化并缓存你的样式。
  • 陷阱2:过度复杂的自定义样式。每个使用独特纹理和复杂borderGUIStyle都会带来额外的绘制开销。尽量复用样式,对于简单颜色变化,优先考虑修改GUI.colorGUI.backgroundColor,它们是轻量级的全局状态设置。
  • 陷阱3:不合理的布局计算GUILayout系统非常方便,但过度嵌套BeginHorizontal/Vertical以及滥用FlexibleSpace()GUILayout.Width()会导致布局计算复杂化。在性能关键的循环中,考虑使用GUI而非GUILayout,通过精确计算Rect来定位,可以避免布局系统的开销。
  • 优化策略:样式池。对于完全动态生成的UI元素(如列表项),可以创建一个样式池,根据需要启用/禁用,而不是不断创建和销毁。

6. 常见问题排查与调试实录

即使理解了原理,在实际操作中仍会遇到各种诡异的问题。下面是我踩过的一些坑和解决方案。

6.1 样式不生效或渲染异常

问题现象可能原因排查步骤与解决方案
自定义样式完全没效果,显示为空白或默认样式。1. 纹理加载失败。
2.GUIStyle对象未正确初始化或为null。
3. 样式属性(如normal.background)在绘制后被意外修改。
1.检查纹理路径和类型:使用Debug.Log(EditorGUIUtility.Load(“path”))确认纹理是否加载成功。确保纹理类型正确。
2.确认初始化时机:在OnEnable中打断点或打印日志,确认样式已创建。
3.使用临时颜色测试:先将normal.textColor设为Color.red,看文字颜色是否改变,快速判断样式是否被应用。
背景纹理拉伸后严重失真,圆角变成椭圆。GUIStyle.border属性设置错误。1.测量纹理:在图像软件中打开背景图,精确测量圆角或不变形区域的像素宽度。
2.设置正确的RectOffset:确保border的四个值覆盖了所有不希望拉伸的区域。对于对称圆角,四个值通常相等。
3.使用GUIStyle.clipping:如果只是简单显示,可以尝试设置clipping = TextClipping.Clip,但这不是根本解决方案。
文字显示不完整,或被截断。GUIStyle.padding设置过小,或者fixedWidth/Height太小,不足以容纳文字。1.检查Padding:适当增加padding的左右和上下值。
2.检查固定尺寸:如果设置了fixedWidth/Height,尝试暂时注释掉,让控件自适应大小。
3.使用GUIContent并检查尺寸GUIStyle.CalcSize(new GUIContent(“your text”))可以计算给定文本所需的最小尺寸。
按钮等控件没有悬停、按下反馈。只设置了normal.background,未设置hover.backgroundactive.backgroundhoveractive状态也分配相应的纹理或颜色。如果想用颜色变化,可以只设置normal.textColorhover.textColor,背景纹理可以共用。

6.2 布局与样式冲突

有时样式看起来没问题,但一放到复杂布局里就乱套。

  • 问题:控件重叠或间距诡异。

    • 原因marginpadding理解混淆,或者GUILayout与绝对Rect混用导致计算冲突。
    • 解决:牢记margin是“外部间距”,影响控件与邻居的关系;padding是“内部间距”,影响控件内容与边框的关系。在GUILayout体系中,margin通常由布局组自动管理,手动设置可能无效。尽量避免在同一UI树中混用GUILayout和基于RectGUI方法。
  • 问题:滚动视图(ScrollView)内的自定义样式错位。

    • 原因:在BeginScrollViewEndScrollView之间使用GUILayout时,滚动视图需要计算总高度,而复杂的自定义样式可能让布局计算出现偏差。
    • 解决:尝试为滚动区域内的自定义样式明确设置fixedHeight,或者使用GUILayout.Height()强制指定高度,帮助布局系统进行准确计算。

6.3 调试利器:EditorWindow的“Inspect”功能

Unity编辑器自带一个强大的调试工具。在游戏运行时这个功能不明显,但在编辑器扩展开发中极其有用。

  1. 在你的自定义EditorWindow处于焦点状态时,点击Unity编辑器右上角的“Development”下拉菜单(如果没有,需要在Preferences -> General中开启Developer Mode)。
  2. 选择Inspect -> Inspector (Editor)
  3. 会弹出一个特殊的Inspector窗口,里面实时显示了当前焦点窗口的整个GUI层级树、每个元素的Rect、使用的GUIStyle等信息。
  4. 你可以点击树中的元素,查看其详细的样式属性,这对于定位哪个控件用了什么样式、尺寸位置是否正确,有奇效。

我个人在开发复杂编辑器工具时,几乎离不开这个“Inspect”功能。它能让你像用浏览器开发者工具检查DOM元素一样,来检查你的编辑器UI,是解决样式和布局问题的终极法宝。

7. 实战:构建一个风格统一的编辑器工具窗口

让我们综合运用以上所有知识,从头构建一个简单的“项目笔记”编辑器工具。这个工具将包含标题、可折叠的分组、带样式的文本输入和自定义按钮。

using UnityEditor; using UnityEngine; using System.Collections.Generic; public class ProjectNotesWindow : EditorWindow { [MenuItem("Window/Project Notes")] static void Init() { GetWindow<ProjectNotesWindow>("项目笔记").Show(); } // 自定义样式缓存 private GUIStyle headerStyle; private GUIStyle sectionHeaderStyle; private GUIStyle noteBoxStyle; private GUIStyle saveButtonStyle; // 数据 private Dictionary<string, bool> sectionFoldouts = new Dictionary<string, bool>(); private Dictionary<string, string> sectionNotes = new Dictionary<string, string>(); private Vector2 scrollPos; void OnEnable() { // 初始化数据 string[] sections = { "设计思路", "技术难点", "待办事项", "临时备忘" }; foreach (var sec in sections) { if (!sectionFoldouts.ContainsKey(sec)) sectionFoldouts[sec] = true; if (!sectionNotes.ContainsKey(sec)) sectionNotes[sec] = PlayerPrefs.GetString($"Note_{sec}", ""); } // 创建并缓存样式 CreateStyles(); } void CreateStyles() { // 1. 主标题样式 headerStyle = new GUIStyle(EditorStyles.largeLabel); headerStyle.alignment = TextAnchor.MiddleCenter; headerStyle.fontSize = 18; headerStyle.normal.textColor = new Color(0.2f, 0.3f, 0.6f); // 深蓝色 // 2. 分区标题样式 (可折叠) sectionHeaderStyle = new GUIStyle(EditorStyles.foldout); sectionHeaderStyle.fontStyle = FontStyle.Bold; sectionHeaderStyle.fontSize = 14; // 3. 笔记文本框样式 noteBoxStyle = new GUIStyle(EditorStyles.textArea); noteBoxStyle.wordWrap = true; noteBoxStyle.padding = new RectOffset(10, 10, 10, 10); // 设置一个柔和的背景色(通过修改文本区域背景) noteBoxStyle.normal.background = MakeTex(2, 2, new Color(0.97f, 0.97f, 0.98f, 1f)); // 4. 保存按钮样式 (基于miniButton,但更突出) saveButtonStyle = new GUIStyle(EditorStyles.miniButton); saveButtonStyle.fontStyle = FontStyle.Bold; saveButtonStyle.normal.textColor = Color.white; // 创建一个简单的绿色背景纹理 saveButtonStyle.normal.background = MakeTex(2, 2, new Color(0.3f, 0.7f, 0.3f, 1f)); saveButtonStyle.hover.background = MakeTex(2, 2, new Color(0.4f, 0.8f, 0.4f, 1f)); saveButtonStyle.active.background = MakeTex(2, 2, new Color(0.2f, 0.6f, 0.2f, 1f)); saveButtonStyle.padding = new RectOffset(12, 12, 6, 6); saveButtonStyle.border = new RectOffset(5, 5, 5, 5); // 小圆角效果 } // 辅助方法:创建纯色纹理 private Texture2D MakeTex(int width, int height, Color col) { Color[] pix = new Color[width * height]; for (int i = 0; i < pix.Length; ++i) { pix[i] = col; } Texture2D result = new Texture2D(width, height); result.SetPixels(pix); result.Apply(); return result; } void OnGUI() { // 开始滚动视图 scrollPos = EditorGUILayout.BeginScrollView(scrollPos); // 主标题 EditorGUILayout.LabelField("项目开发笔记", headerStyle, GUILayout.Height(40)); EditorGUILayout.Space(20); // 遍历所有分区 foreach (var section in sectionNotes.Keys) { // 可折叠的分区标题 sectionFoldouts[section] = EditorGUILayout.Foldout(sectionFoldouts[section], section, sectionHeaderStyle); if (sectionFoldouts[section]) { // 分区内容区域,稍微缩进 EditorGUILayout.BeginHorizontal(); GUILayout.Space(20); // 缩进 EditorGUILayout.BeginVertical(); // 使用自定义样式的文本区域 string oldNote = sectionNotes[section]; sectionNotes[section] = EditorGUILayout.TextArea(oldNote, noteBoxStyle, GUILayout.MinHeight(80)); // 如果内容改变,显示保存提示 if (oldNote != sectionNotes[section]) { EditorGUILayout.HelpBox("内容已修改,请记得保存。", MessageType.Info); } EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); } } EditorGUILayout.EndScrollView(); // 底部操作栏 EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.FlexibleSpace(); if (GUILayout.Button("全部折叠", EditorStyles.toolbarButton)) { foreach (var key in sectionFoldouts.Keys) sectionFoldouts[key] = false; } if (GUILayout.Button("全部展开", EditorStyles.toolbarButton)) { foreach (var key in sectionFoldouts.Keys) sectionFoldouts[key] = true; } GUI.enabled = true; // 确保按钮可用 if (GUILayout.Button("保存所有笔记", saveButtonStyle, GUILayout.Width(120))) { SaveAllNotes(); } EditorGUILayout.EndHorizontal(); } void SaveAllNotes() { foreach (var kvp in sectionNotes) { PlayerPrefs.SetString($"Note_{kvp.Key}", kvp.Value); } PlayerPrefs.Save(); Debug.Log("项目笔记已保存!"); // 这里可以替换为更持久的存储方式,如ScriptableObject或文件。 } void OnDestroy() { // 窗口关闭时自动保存 SaveAllNotes(); } }

这个综合案例的关键点:

  1. 样式初始化:所有自定义样式在OnEnable中创建并缓存,性能最优。
  2. 样式复用noteBoxStyle基于EditorStyles.textArea,只修改了背景色和内边距。saveButtonStyle基于EditorStyles.miniButton,但改变了颜色和边框,创建了更醒目的“主操作按钮”。
  3. 动态反馈:在文本修改后,动态显示一个HelpBox提示用户保存。
  4. 布局组合:综合运用了ScrollViewFoldout、水平垂直布局组、FlexibleSpaceSpace来控制整体结构。
  5. 工具条风格:底部操作栏使用了EditorStyles.toolbartoolbarButton,使其与Unity编辑器其他工具条风格一致。
  6. 实用功能:实现了折叠/展开、保存等实际功能,使这个示例不仅仅是一个样式演示。

通过这个完整的例子,你应该能够看到,将EditorStyles的便捷性与自定义GUIStyle的灵活性结合起来,完全可以打造出既专业又独特的编辑器工具界面。记住,好的编辑器UI是深思熟虑的样式设计与清晰的信息架构共同作用的结果。多观察、多模仿Unity编辑器自身的界面设计,然后大胆地用代码实现你的想法,这是提升编辑器扩展开发能力的最佳路径。