版本:Unity 6 (6000.0)
语言:English
编辑器 UI 支持
创建自定义检查器

使用 C# 脚本创建自定义编辑器窗口

版本: 2022.3+

此示例演示如何使用 C# 脚本创建自定义编辑器窗口以响应用户输入,使UI(用户界面) 允许用户与您的应用程序交互。Unity 目前支持三种 UI 系统。 更多信息
参见 术语表
可调整大小,并处理热重载。

自定义编辑器窗口是一个从 EditorWindow 类派生的类。UI 工具包使用 CreateGUI 方法将控件添加到编辑器 UI,并且当需要显示窗口时,Unity 会自动调用 CreateGUI 方法。此方法的工作方式与 AwakeUpdate 等方法相同。

创建自定义编辑器窗口时,请遵循以下准则

  • 将依赖于 UXML/USS 加载的代码放在 CreateGUI 方法中,以确保所有必要的资源都可用。
  • 将事件注册代码保留在 CreateGUI 内部或 CreateGUI 调用之后。

下图显示了编辑器窗口的执行顺序

Editor window execution order
编辑器窗口执行顺序

有关更多信息,请参阅 EditorWindow 类文档。

示例概述

此示例创建了一个精灵2D 图形对象。如果您习惯于使用 3D,则精灵本质上只是标准纹理,但有一些特殊的技术可以结合和管理精灵纹理,以便在开发过程中提高效率和便利性。 更多信息
参见 术语表
浏览器,它查找并显示项目中的所有精灵,并将它们显示在列表中。如果选择列表中的精灵,则精灵的图像将显示在窗口右侧。

Custom sprite browser
自定义精灵浏览器

您可以在此 GitHub 存储库 中找到此示例创建的完整文件。

先决条件

本指南适用于熟悉 Unity 编辑器、UI 工具包和 C# 脚本的开发人员。在开始之前,请熟悉以下内容

创建编辑器窗口脚本

要向 UI 添加 UI 控件,请将视觉元素视觉树的一个节点,它实例化或派生自 C# VisualElement 类。您可以设置外观样式、定义行为,并将其显示在屏幕上作为 UI 的一部分。 更多信息
参见 术语表
添加到视觉树中。UI 工具包使用 VisualElement.Add() 方法将子元素添加到现有的视觉元素中,并通过 rootvisualElement 属性访问编辑器窗口的 视觉树

  1. 在 Unity 中使用任何模板创建一个项目。
  2. 在“项目”窗口中右键单击,然后选择“创建”>“UI 工具包”>“编辑器窗口”。
  3. 在“C#”框中,输入 MyCustomEditor
  4. 清除“UXML”和“USS”复选框。
  5. 选择“确认”。
  6. 从菜单中,选择“窗口”>“UI 工具包”>“MyCustomEditor”以打开窗口。窗口显示一个带有“Hello World! From C#”文本的标签。

创建精灵列表

为了呈现精灵列表,此示例使用 AssetDatabase 查找项目中的所有精灵。对于精灵浏览器,添加一个 TwoPaneSplitView 以将可用的窗口空间分成两个窗格:一个固定大小,一个灵活大小。当您调整窗口大小时,灵活窗格会调整大小,而固定大小的窗格保持相同大小。

  1. 在文件顶部,添加列表所需的以下指令

    using System.Collections.Generic;
    
  2. CreateGUI() 内部的代码替换为以下代码。这将枚举项目中的所有精灵。

    public void CreateGUI()
    {
        // Get a list of all sprites in the project
        var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
        var allObjects = new List<Sprite>();
        foreach (var guid in allObjectGuids)
        {
          allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
        }
    }
    
  3. CreateGUI() 内部,添加以下代码。这将创建一个 TwoPaneSplitview 并添加两个子元素作为不同控件的占位符。

    // Create a two-pane view with the left pane being fixed.
    var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
    
    // Add the view to the visual tree by adding it as a child to the root element.
    rootVisualElement.Add(splitView);
    
    // A TwoPaneSplitView needs exactly two child elements.
    var leftPane = new VisualElement();
    splitView.Add(leftPane);
    var rightPane = new VisualElement();
    splitView.Add(rightPane);
    
  4. 从菜单中,选择“窗口”>“UI 工具包”>“MyCustomEditor”以打开窗口。窗口显示一个带有两个空面板的拆分视图。移动分隔线以查看其工作原理。

    Window with two split panes
    带有两个拆分窗格的窗口

添加列表视图

对于精灵浏览器,左侧窗格将是一个包含项目中所有精灵名称的列表。 ListView 控件派生自 VisualElement,因此可以轻松修改代码以使用 ListView 而不是 VisualElement

ListView 控件显示一个可选择项目的列表。它经过优化,可以创建足够多的元素来覆盖可见区域,并在您滚动列表时池化和回收视觉元素。这优化了性能并减少了内存占用,即使在包含许多项目的列表中也是如此。

要利用此功能,请使用以下内容初始化 ListView

  • 数据项数组
  • 一个回调函数,用于在列表中创建单个视觉列表条目
  • 一个绑定函数,使用数据数组中的项目初始化视觉列表条目

您可以为列表中的每个元素创建复杂的 UI 结构。出于演示目的,此示例使用简单的文本标签来显示精灵名称。

  1. CreateGUI() 内部,将左侧窗格更改为 ListView 而不是 VisualElement

    public void CreateGUI()
    {
        ...
        var leftPane = new ListView();
        splitView.Add(leftPane);
        ...
    }
    
  2. CreateGUI() 的底部,添加以下代码以初始化 ListView

    public void CreateGUI()
    {
        ...
        // Initialize the list view with all sprites' names
        leftPane.makeItem = () => new Label();
        leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
        leftPane.itemsSource = allObjects;
    }
    
  3. 从菜单中,选择“窗口”>“UI 工具包”>“MyCustomEditor”以打开自定义编辑器窗口。窗口显示一个可滚动的列表视图和可选择的项目,类似于下面的图像。

    ListView with sprite names
    带有精灵名称的 ListView

添加回调

要从列表中选择精灵时在右侧面板上显示精灵的图像,请使用左侧窗格的 selectionChanged 属性并添加回调函数。

要显示图像,请为选定的精灵创建一个新的 Image 控件,并使用 VisualElement.Clear() 删除所有先前的内容,然后再添加控件。

提示:如果您丢失了窗口并且菜单无法重新打开,请通过“窗口”>“面板”>“关闭所有浮动面板”下的菜单关闭所有浮动面板,或重置窗口布局。

  1. 当左侧窗格中的列表选择发生变化时添加回调函数。

    public void CreateGUI()
    {
        ...
        // React to the user's selection
        leftPane.selectionChanged += OnSpriteSelectionChange;
    }
    
    private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
    {
    }
    
  2. 回调函数需要访问 TwoPaneSplitview 的右侧窗格。为此,将 CreateGUI() 内部创建的右侧窗格更改为成员变量

    private VisualElement m_RightPane;
    
    public void CreateGUI()
    {
        ...
        m_RightPane = new VisualElement();
        splitView.Add(m_RightPane);
        ...
    }
    
  3. 将以下代码添加到 OnSpriteSelectionChange 函数中。这将清除窗格中的所有先前内容,获取选定的精灵,并添加一个新的 Image 控件以显示精灵。

    private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
    {
        // Clear all previous content from the pane.
        m_RightPane.Clear();
    
        // Get the selected sprite and display it.
        var enumerator = selectedItems.GetEnumerator();
        if (enumerator.MoveNext())
        {
            var selectedSprite = enumerator.Current as Sprite;
            if (selectedSprite != null)
            {
                // Add a new Image control and display the sprite.
                var spriteImage = new Image();
                spriteImage.scaleMode = ScaleMode.ScaleToFit;
                spriteImage.sprite = selectedSprite;
    
                // Add the Image control to the right-hand pane.
                m_RightPane.Add(spriteImage);
            }
        }
    }
    
  4. 从菜单中,选择“窗口”>“UI 工具包”>“MyCustomEditor”以打开自定义编辑器窗口。当您从左侧列表中选择一个精灵时,精灵的图像将显示在窗口右侧,类似于下面的图像。

    Sprite browser in action
    精灵浏览器工作中

使 UI 可调整大小

编辑器窗口在其允许的最小和最大尺寸内可调整大小。要设置这些尺寸,请写入 EditorWindow.minSizeEditorWindow.maxSize 属性。要防止窗口调整大小,请为这两个属性分配相同的尺寸。

如果窗口尺寸太小而无法显示整个 UI,则可以使用 ScrollView 元素为窗口提供滚动功能。左侧窗格上的 ListView 在内部使用 ScrollView,但右侧窗格是常规 VisualElement。要使右侧窗格可调整大小,请将其更改为具有双向滚动的 ScrollView

  1. ShowMyEditor() 函数的底部添加以下代码以限制窗口的大小

    public static void ShowMyEditor()
    {
        ...
        // Limit size of the window.
        wnd.minSize = new Vector2(450, 200);
        wnd.maxSize = new Vector2(1920, 720);
    }
    
  2. CreateGUI() 内部,将右侧窗格 VisualElement 更改为具有双向滚动的 ScrollView

    public void CreateGUI()
    {
        ...
        m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
        splitView.Add(m_RightPane);
        ...
    }
    
  3. 从菜单中,选择“窗口”>“UI 工具包”>“MyCustomEditor”以打开自定义编辑器窗口。精灵浏览器窗口现在具有滚动条。调整窗口大小以查看滚动条的工作原理。

    Editor window with scrollbars
    带有滚动条的编辑器窗口

支持编辑器窗口中的热重载

脚本一段代码,允许您创建自己的组件、触发游戏事件、随时间推移修改组件属性并以任何您喜欢的方式响应用户输入。 更多信息
参见 术语表
重新编译或编辑器进入播放模式时,会发生 C# 域重载。在您刚刚创建的编辑器窗口中,打开精灵浏览器,选择一个精灵,然后进入播放模式。窗口将重置,并且选择将消失。

正确的编辑器窗口需要与 热重载 工作流配合使用。由于 VisualElement 对象不可序列化,因此您必须在每次发生重载时重新创建 UI。这意味着 CreateGUI() 方法在重载完成后被调用。这使您可以在重载之前通过将必要的数据存储在您的 EditorWindow 类中来恢复 UI 状态。

  1. MyCustomEditor 类添加一个成员变量以保存精灵列表中的选中索引。当您进行选择时,此成员变量将存储 ListView 的新选中索引。

    public class MyCustomEditor : EditorWindow
    {
        [SerializeField] private int m_SelectedIndex = -1;
        ....
    }
    
  2. CreateGUI() 的底部添加以下代码以存储和恢复选定的列表索引。

    public void CreateGUI()
    {
      ...
    
      // Restore the selection index from before the hot reload.
      leftPane.selectedIndex = m_SelectedIndex;
    
      // Store the selection index when the selection changes.
      leftPane.selectionChanged += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
    }
    
  3. 从菜单中选择**窗口** > **UI 工具包** > **MyCustomEditor** 以打开自定义编辑器窗口。从列表中选择一个精灵并进入播放模式以测试热重载。

作为参考,以下是完成的脚本

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class MyCustomEditor : EditorWindow
{
    [SerializeField] private int m_SelectedIndex = -1;
    private VisualElement m_RightPane;

    [MenuItem("Window/UI Toolkit/MyCustomEditor")]
    public static void ShowMyEditor()
    {
        // This method is called when the user selects the menu item in the Editor.
        EditorWindow wnd = GetWindow<MyCustomEditor>();
        wnd.titleContent = new GUIContent("My Custom Editor");

        // Limit size of the window.
        wnd.minSize = new Vector2(450, 200);
        wnd.maxSize = new Vector2(1920, 720);
  }

  public void CreateGUI()
  {
      // Get a list of all sprites in the project.
      var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
      var allObjects = new List<Sprite>();
      foreach (var guid in allObjectGuids)
      {
        allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
      }

      // Create a two-pane view with the left pane being fixed.
      var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);

      // Add the panel to the visual tree by adding it as a child to the root element.
      rootVisualElement.Add(splitView);

      // A TwoPaneSplitView always needs two child elements.
      var leftPane = new ListView();
      splitView.Add(leftPane);
      m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
      splitView.Add(m_RightPane);

      // Initialize the list view with all sprites' names.
      leftPane.makeItem = () => new Label();
      leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
      leftPane.itemsSource = allObjects;

      // React to the user's selection.
      leftPane.selectionChanged += OnSpriteSelectionChange;

      // Restore the selection index from before the hot reload.
      leftPane.selectedIndex = m_SelectedIndex;

      // Store the selection index when the selection changes.
      leftPane.selectionChanged += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
  }

  private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
  {
      // Clear all previous content from the pane.
      m_RightPane.Clear();

      var enumerator = selectedItems.GetEnumerator();
      if (enumerator.MoveNext())
      {
          var selectedSprite = enumerator.Current as Sprite;
          if (selectedSprite != null)
          {
              // Add a new Image control and display the sprite.
              var spriteImage = new Image();
              spriteImage.scaleMode = ScaleMode.ScaleToFit;
              spriteImage.sprite = selectedSprite;

              // Add the Image control to the right-hand pane.
              m_RightPane.Add(spriteImage);
          }
      }
  }
}

其他资源

编辑器 UI 支持
创建自定义检查器