Version: 2023.2
Support for Editor UI
Create a Custom Inspector

Create a custom Editor window

Custom Editor windows allow you to extend Unity by implementing your own editors and workflows. This guide covers creating an Editor window through code, reacting to user input, making the UI resizable and handling hot-reloading.

In this tutorial, you will create a sprite browser, which finds and displays all sprites inside the project, and shows them as a list. Selecting a sprite in the list will display the image on the right side of the window.

You can find the completed example in the Editor window script section.

自定义精灵浏览器
自定义精灵浏览器

先决条件

This guide is for developers familiar with Unity, but new to UI Toolkit. It’s recommended to have a basic understanding of Unity and C# scripting.

This guide also references the following concepts:

Content

Controls used in this guide:

In this guide, you’ll do the following:

  • Create the Editor window script.
  • Create a menu entry to open the window.
  • Add UI controls to the window.
  • Write code to respond to user selections.
  • Make the UI resizable.
  • Support hot-reloading in the Editor.

Create the Editor window script

Tip
You can generate the necessary code to create an Editor window script in the Unity Editor. From the Project window, right-click and select Create > UI Toolkit > Editor Window. For this guide please disable the UXML and USS checkboxes. You might also have to add additional using directives at the top of the file, as shown below.

You can create Editor windows through C# scripts in your project. A custom Editor window is a class that derives from the EditorWindow class.

Create a new script file MyCustomEditor.cs under the Assets/Editor folder. Paste the following code into the script:

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

public class MyCustomEditor : EditorWindow
{
}
注意
This is an Editor-only window that includes the UnityEditor namespace, so the file must be placed under the Editor folder, or inside an Editor-only Assembly Definition.

创建一个菜单项来打开窗口

To open the new Editor window, you must create an entry in the Editor menu.

Add the MenuItem attribute to a static method. In this example, the name of the static method is ShowMyEditor().

Inside ShowMyEditor(), call the EditorWindow.GetWindow() method to create and display the window. It returns an EditorWindow object. To set the window title, change the EditorWindow.titleContent property.

Add the following function inside the MyCustomEditor class created in the previous step.

[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
  // 当用户在编辑器中选择菜单项时调用此方法
  EditorWindow wnd = GetWindow<MyCustomEditor>();
  wnd.titleContent = new GUIContent("My Custom Editor");
}

Test your new window by opening it via the Unity Editor menu Tools > My Custom Editor.

具有自定义标题的编辑器窗口
具有自定义标题的编辑器窗口

向窗口添加 UI 控件

UI Toolkit uses the CreateGUI method to add controls to Editor UI, and Unity calls the CreateGUI method automatically when the window needs to display. This method works the same way as methods such as Awake or Update.

You can add UI controls to the UI by adding visual elements to the visual tree. The VisualElement.Add() method is used to add children to an existing visual element. The visual tree of an Editor window is accessed via the rootvisualElement property.

To get started, add a CreateGUI() function to your custom Editor class and add a ‘Hello’ label:

public void CreateGUI()
{
  rootVisualElement.Add(new Label("Hello"));
}
带有Hello标签的窗口
带有“Hello”标签的窗口
注意
To present a list of sprites, use AssetDatabase functions to find all sprites in a project.

Replace the code inside CreateGUI() with the code below to enumerate all sprites inside the project.

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)));
  }
}

For the sprite browser, the top-level visual element will be a TwoPaneSplitView. This control splits the available window space into two panes: one fixed-size and one flexible-size. When you resize the window, only the flexible pane resizes, while the fixed-size pane remains the same size.

For the TwoPaneSplitView control to work, it needs to have exactly two children. Add code inside CreateGUI() to create a TwoPaneSplitview, then add two child elements as placeholders for different controls.

// 创建一个包含两个窗格的视图,左窗格固定
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);

// 通过将视图作为子元素添加到根元素来将视图添加到可视化树中
rootVisualElement.Add(splitView);

// 一个 TwoPaneSplitView 总是需要两个子元素
var leftPane = new VisualElement();
splitView.Add(leftPane);
var rightPane = new VisualElement();
splitView.Add(rightPane);

下图显示了带有两个空面板的自定义窗口。分割栏可以移动。

带有两个拆分窗格的窗口
带有两个拆分窗格的窗口

For the sprite browser, the left pane will be a list containing the names of all sprites found in the project. The ListView control derives from VisualElement, so it’s easy to modify the code to use a ListView instead of a blank VisualElement.

Modify the code inside the CreateGUI() function to create a ListView control for the left pane instead of a VisualElement.

public void CreateGUI()
{
  ...
  var leftPane = new ListView();
  splitView.Add(leftPane);
  ...
}

The ListView control displays a list of selectable items. It’s optimized to create only enough elements to cover the visible area, and pool and recycle the visual elements as you scroll the list. This optimizes performance and reduces the memory footprint, even in lists that have many items.

To take advantage of this, the ListView must be properly initialized with the following:

  • An array of data items.
  • A callback function to create an individual visual list entry in the list.
  • A bind function that initializes a visual list entry with an item from the data array.

You can create complex UI structures for each element in the list, but this example uses a simple text label to display the sprite name.

Add code to the bottom CreateGUI() function that initializes the ListView.

public void CreateGUI()
{
  ...
  // 使用所有精灵的名称初始化列表视图
  leftPane.makeItem = () => new Label();
  leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
  leftPane.itemsSource = allObjects;
}

The image below shows the Editor window with a scrollable list view and selectable items.

带有精灵名称的 ListView
带有精灵名称的 ListView

作为参考,以下是 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)));
  }

  // Create a two-pane view with the left pane being fixed with
  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 exactly two child elements
  var leftPane = new ListView();
  splitView.Add(leftPane);
  var rightPane = new VisualElement();
  splitView.Add(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;
}

Add callbacks

When you select a sprite from the list in the left pane, its image must display on the right pane. To do this you need to provide a callback function that the ListView can call when the user makes a selection. The ListView control has an onSelectionChange property for this purpose.

回调函数接收一个列表,其中包含用户选择的一个或多个项目。可以将 ListView 配置为允许多选,但默认情况下,选择模式仅限于单个项目。

Add a callback function when the user changes the selection from the list in the left pane.

public void CreateGUI()
{
  ...

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

private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
}
注意
If you lose your window and the menu doesn’t reopen, close all floating panels through the menu under Window > Panels > Close all floating panels, or reset your window layout.

To display the image of the selected sprite on the right side of the window, the function needs to be able to access the right-hand pane of the TwoPaneSplitView. You can make this control a member variable of the class to be able to access it inside the callback function.

Turn the rightPane created inside CreateGUI() into a member variable.

private VisualElement m_RightPane;

public void CreateGUI()
{
  ...

  m_RightPane = new VisualElement();
  splitView.Add(m_RightPane);

  ...
}

With a reference to the TwoPaneSplitView, you can access the right pane via the flexedPane property. Before creating a new Image control on the right pane, use VisualElement.Clear() to remove the previous image. This method removes all children from an existing visual element.

Clear the right pane from all previous content and create a new Image control for the selected sprite.

private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
  // 清除窗格中之前的全部内容
  m_RightPane.Clear();

  // 获取选定的精灵
  var selectedSprite = selectedItems.First() as Sprite;
  if (selectedSprite == null)
    return;

  // 添加一个新的 Image 控件并显示精灵
  var spriteImage = new Image();
  spriteImage.scaleMode = ScaleMode.ScaleToFit;
  spriteImage.sprite = selectedSprite;

  // 将 Image 控件添加到右窗格
  m_RightPane.Add(spriteImage);
}
注意
Make sure to include using System.Linq; at the top of your file to use the First() method on the selectedItems parameter.

Test your sprite browser in the Editor. The image below shows the custom Editor window in action.

正在运行的精灵浏览器
正在运行的精灵浏览器

使 UI 可调整大小

Editor windows are resizable within their minimum and maximum allowed dimensions. You can set these dimensions in C# when creating the window by writing to the EditorWindow.minSize and EditorWindow.maxSize properties. To prevent a window from resizing, assign the same dimension to both properties.

Limit the size of your custom editor window by adding the following lines to the bottom of the ShowMyEditor() function:

[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
  // 当用户在编辑器中选择菜单项时调用此方法
  EditorWindow wnd = GetWindow<MyCustomEditor>();
  wnd.titleContent = new GUIContent("My Custom Editor");

  // 限制窗口的大小
  wnd.minSize = new Vector2(450, 200);
  wnd.maxSize = new Vector2(1920, 720);
}

For situations where the window dimensions are too small to display the entire UI, you must use a ScrollView element to provide scrolling for the window, or the content might become inaccessible.

The ListView on the left pane is using a ScrollView internally, but the right pane is a regular VisualElement. Changing this to a ScrollView control automatically displays scrollbars when the window is too small to fit the entire image in its original size.

Exchange the right pane VisualElement for a ScrollView with bidirectional scrolling.

public void CreateGUI()
{
  ...

  m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
  splitView.Add(m_RightPane);

  ...
}

下图显示了带有滚动条的精灵浏览器窗口。

具有滚动条的编辑器窗口
具有滚动条的编辑器窗口

Close and reopen your custom Editor window to test out the new size limits.

注意
In Unity 2021.2 and up, Unity doesn’t respect the minSize and maxSize properties when the window is docked. This allows the user to resize dock areas without limitations. Consider creating a ScrollView as one of your top-level elements and place all UI inside of it to make your UI as responsive as possible.

支持在编辑器中热重载

A proper Editor window must work with the hot-reloading workflow that happens in the Unity Editor. A C# domain reload occurs when scripts recompile or when the Editor enters Play mode. You can learn more about this topic on the Script Serialization page.

To see this in action in the Editor window you just created, open the sprite browser, select a sprite, and then enter Play mode. The window resets, and the selection disappears.

Since VisualElement objects aren’t serializable, the UI must be recreated every time a reload happens in Unity. This means that the CreateGUI() method is invoked after the reload has completed. This lets you restore the UI state before the reload by storing necessary data in your EditorWindow class.

Add a member variable to save the selected index in the sprite list.

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

    ....
}

When you make a selection, the new selection index of the list view can be stored inside this member variable. You can restore the selection index during creation of the UI inside the CreateGUI() function.

Add code to the end of the CreateGUI() function to store and restore the selected list index.

public void CreateGUI()
{
  ...

  // 恢复热重载前的选择索引
  leftPane.selectedIndex = m_SelectedIndex;

  // 选择更改时存储选择索引
  leftPane.onSelectionChange += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
}

Select a sprite from the list and enter Play mode to test hot-reloading.

Editor window script

The code below is the final script of the Editor window created during this guide. You can paste the code directly into a file called MyCustomEditor.cs inside the Assets/Editor folder to see it in the Unity Editor.

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

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

  [MenuItem("Tools/My Custom Editor")]
  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 with
    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 exactly 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.onSelectionChange += OnSpriteSelectionChange;

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

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

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

    // Get the selected sprite
    var selectedSprite = selectedItems.First() as Sprite;
    if (selectedSprite == null)
      return;

    // 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);
  }
}

Support for Editor UI
Create a Custom Inspector