Unity 6.3
0 онлайн 106 гостей 3 в системе
Вход
UI системы Шаг 317 из 317

Создать TreeView с IMGUI

Примечание: Настоятельно рекомендуется использовать UI Toolkit для расширения Unity Editor, так как это более современное, гибкое и масштабируемое решение, чем IMGUI.

Информация на этой странице предполагает, что читатель имеет базовые знания о IMGUI (немедленном режиме GUI). Информацию о IMGUI и настройке окон Редактора см. в Расширение редактора и блоге IMGUI Unity.

TreeView — это IMGUI элемент управления, используемый для отображения иерархических данных, которые можно разворачивать и сжимать. Используйте TreeView для создания высоконастраиваемых списков и многосторонних таблиц для окон Редактора, которые можно использовать вместе с другими IMGUI элементами управления и компонентами.

См. документацию Unity API скриптов по TreeView для информации о доступных функциях TreeView API.

Пример TreeView с MultiColumnHeader и SearchField.
Пример TreeView с MultiColumnHeader и SearchField.

Примечание: TreeView не является модель данных дереваВы можете построить TreeView используя любую деревянную структуру данных, которую вы предпочитаете. Это может быть C# модель дерева, или Unity-основанная древесная структура, как Transform иерархии.

Отображение TreeView осуществляется путем определения списка расширенных элементов, называемых строками. Каждая строка представляет один TreeViewItem. Каждый TreeViewItem содержит родительскую и дочернюю информацию, которая используется TreeView для управления навигацией (ввод клавиш и мыши).

TreeView имеет один корень TreeViewItem, который скрыт и не отображается в Редакторе. Этот элемент является корнем всех других элементов.

Важные классы и методы

Наиболее важными классами помимо самого TreeView являются TreeViewItem и TreeViewState.

TreeViewState (TreeViewState) содержит сведения о состоянии, которое меняется при взаимодействии с полями TreeView в редакторе: состояние выделения, состояние развёрнутости, состояние навигации и состояние прокрутки. TreeViewState это единственное состояние, которое сериализированный. Сам TreeView не поддается сериализации — он восстанавливается из данных, которые он представляет, когда он конструируется или перезагружается. TreeViewState как поле в вашем EditorWindow-производный класс, чтобы изменённые пользователем состояния не терялись при перезагрузке скриптов или переходе в режим Play (см. документацию по расширение редактора сведения о том, как это сделать. Пример класса, содержащего TreeViewState поле, см. Пример 1: Простое TreeViewниже.

TreeViewItem (TreeViewItem) содержат данные об индивидуальном элементе TreeView и используются для создания представления структуры дерева в Редакторе. Каждый TreeViewItem должен создаваться с уникальным целочисленным идентификатором (уникальным среди всех элементов TreeView). Идентификатор используется для поиска элементов в дереве, а также для состояния выделения, состояния развёрнутости и навигации. Если дерево представляет объекты Unity, используйте GetInstanceID для каждого объекта в качестве ID для TreeViewItem. IDs используются в TreeViewState для сохранения изменённых пользователем состояний (таких как расширенные элементы) при перезагрузке скриптов или входе в режим воспроизведения в Редакторе.

Все TreeViewItems имеют свойство depth, которое указывает на визуальный отступ. Подробнее см. примеры Инициализация TreeView ниже.

BuildRoot (BuildRoot) является единственным абстрактным методом TreeView класс, который необходимо реализовать, чтобы создать TreeView. В этом методе создаётся корневой элемент дерева. Он вызывается каждый раз, когда Reload Для простых деревьев, которые используют небольшие наборы данных, создайте целое дерево из TreeViewItems под корневым пунктом в BuildRoot. Для очень больших деревьев создавать всё дерево при каждой перезагрузке неоптимально. В этом случае создайте корень, а затем переопределите BuildRows метод, чтобы создать элементы только для текущих строк. Для примера BuildRoot используется, см. Пример 1: Простое TreeView ниже.

BuildRows (BuildRows) — виртуальный метод, реализация которого по умолчанию строит список строк на основе полного дерева, созданного в BuildRoot. Если только корень был создан в BuildRoot, этот метод следует переопределить для обработки расширенных строк. См. Инициализация TreeView, ниже, для получения дополнительной информации.

Резюме порядка и повторения методов событий BuildRoot и BuildRows.
Резюме порядка и повторения методов событий BuildRoot и BuildRows.

На этой диаграмме обобщается порядок и повторение BuildRoot и BuildRows методы событий во время TreeViewОбратите внимание, что BuildRoot метод вызывается один раз каждый раз Reload называется. BuildRows вызывает чаще, потому что он вызывает один раз на Reload (сразу после BuildRoot) и каждый раз TreeViewItem разворачивается или сжимается.

Инициализация TreeView

TreeView инициализируется, когда метод Reload вызван из объекта TreeView.

Существует два способа настройки TreeView:

  1. Создание полного дерева — Создание TreeViewItemдля всех элементов в данных модели дерева. Это по умолчанию и требует меньше кода для настройки. Полное дерево создается, когда BuildRoot вызван из объекта TreeView.

  2. Создать только развернутые элементы — При таком подходе вам потребуется переопределить BuildRows вручную управлять показываемыми строками, и BuildRoot используется только для создания корня TreeViewItemЭтот подход наилучшим образом подходит для масштабирования больших наборов данных или данных, которые часто меняются.

Используйте первый подход для небольших наборов данных или для данных, которые часто не меняются. Используйте второй подход для больших наборов данных или данных, которые часто меняются, поскольку быстрее создать только развернутые элементы, чем полное дерево.

Есть три способа, которыми вы можете настроить TreeViewItems:

  • Создайте TreeViewItemс дочерними, родительским и глубинами, инициализованными с начала.

  • Создайте TreeViewItemс родителем и детьми, а затем используйте SetupDepthsFromParentsAndChildren для установки глубин.

  • Создайте TreeViewItemтолько с информацией о глубине и затем используйте SetupDepthsFromParentsAndChildren для установки родительских и дочерних ссылок.

Примеры

Чтобы просмотреть Project и исходный код для примеров, показанных ниже, загрузите TreeViewExamples.zip.

Пример 1: Простое TreeView

Пример простой иерархии с использованием животных в качестве имен-заместителей.
Пример простой иерархии с использованием животных в качестве имен-заместителей.

Чтобы создать TreeView, объявите класс, расширяющий TreeView класс и реализуйте абстрактный метод BuildRoot. В следующем примере создается простой TreeView.

class SimpleTreeView : TreeView
{
    public SimpleTreeView(TreeViewState treeViewState)
        : base(treeViewState)
    {
        Reload();
    }
        
    protected override TreeViewItem BuildRoot ()
    {
        // BuildRoot is called every time Reload is called to ensure that TreeViewItems 
        // are created from data. Here we create a fixed set of items. In a real world example,
        // a data model should be passed into the TreeView and the items created from the model.

        // This section illustrates that IDs should be unique. The root item is required to 
        // have a depth of -1, and the rest of the items increment from that.
        var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
        var allItems = new List<TreeViewItem> 
        {
            new TreeViewItem {id = 1, depth = 0, displayName = "Animals"},
            new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"},
            new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"},
            new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"},
            new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"},
            new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"},
            new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"},
            new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"},
            new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"},
        };
            
        // Utility method that initializes the TreeViewItem.children and .parent for all items.
        SetupParentsAndChildrenFromDepths (root, allItems);
            
        // Return root of the tree
        return root;
    }
}

В этом примере информация о глубине используется для построения TreeView. Наконец, вызов SetupDepthsFromParentsAndChildren устанавливает родительские и дочерние данные TreeViewItems.

Обратите внимание, что существует два способа настройки TreeViewItem: установить родитель и дочерей напрямую, или использовать метод AddChild, как показано в следующем примере:

protected override TreeViewItem BuildRoot()
{
    var root = new TreeViewItem      { id = 0, depth = -1, displayName = "Root" };
    var animals = new TreeViewItem   { id = 1, displayName = "Animals" };
    var mammals = new TreeViewItem   { id = 2, displayName = "Mammals" };
    var tiger = new TreeViewItem     { id = 3, displayName = "Tiger" };
    var elephant = new TreeViewItem  { id = 4, displayName = "Elephant" };
    var okapi = new TreeViewItem     { id = 5, displayName = "Okapi" };
    var armadillo = new TreeViewItem { id = 6, displayName = "Armadillo" };
    var reptiles = new TreeViewItem  { id = 7, displayName = "Reptiles" };
    var croco = new TreeViewItem     { id = 8, displayName = "Crocodile" };
    var lizard = new TreeViewItem    { id = 9, displayName = "Lizard" };

    root.AddChild(animals);
    animals.AddChild(mammals);
    animals.AddChild(reptiles);
    mammals.AddChild(tiger);
    mammals.AddChild(elephant);
    mammals.AddChild(okapi);
    mammals.AddChild(armadillo);
    reptiles.AddChild(croco);
    reptiles.AddChild(lizard);

    SetupDepthsFromParentsAndChildren(root);

    return root;
}

Альтернативный вариант BuildRoot метод для SimpleTreeView класс выше

В следующем примере показан EditorWindow, который содержит SimpleTreeView. TreeViews построены с экземпляром TreeViewState. Тот, кто реализует TreeView, должен решить, как обрабатывать состояние этого представления: должно ли оно сохраняться до следующего сеанса Unity или только переживать перезагрузку скриптов (при входе в режим воспроизведения либо при перекомпиляции скриптов). В этом примере TreeViewState сериализирован в EditorWindow, благодаря чему TreeView сохраняет своё состояние при закрытии и повторном открытии редактора.

using System.Collections.Generic;
using UnityEngine;
using UnityEditor.IMGUI.Controls;

class SimpleTreeViewWindow : EditorWindow
{
    // SerializeField is used to ensure the view state is written to the window 
    // layout file. This means that the state survives restarting Unity as long as the window
    // is not closed. If the attribute is omitted then the state is still serialized/deserialized.
    [SerializeField] TreeViewState m_TreeViewState;

    //The TreeView is not serializable, so it should be reconstructed from the tree data.
    SimpleTreeView m_SimpleTreeView;

    void OnEnable ()
    {
        // Check whether there is already a serialized view state (state 
        // that survived assembly reloading)
        if (m_TreeViewState == null)
            m_TreeViewState = new TreeViewState ();

        m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
    }

    void OnGUI ()
    {
        m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
    }

    // Add menu named "My Window" to the Window menu
    [MenuItem ("TreeView Examples/Simple Tree Window")]
    static void ShowWindow ()
    {
        // Get existing open window or if none, make a new one:
        var window = GetWindow<SimpleTreeViewWindow> ();
        window.titleContent = new GUIContent ("My Window");
        window.Show ();
    }
}

Пример 2: Многосторонний TreeView

Пример иерархии с различными столбцами.
Пример иерархии с различными столбцами.

Этот пример иллюстрирует многосторонний TreeView, который использует MultiColumnHeader class.

MultiColumnHeader поддерживает следующие функции: переименование элементов, многократный выбор, изменение порядка элементов и содержимого строк с помощью обычных элементов управления IMGUI (таких как ползунки и поля объектов), сортировка столбцов, фильтрация и поиск строк.

В этом примере создается модель данных с использованием классов TreeElement и TreeModel. TreeView получает данные из этого “TreeModel”. В этом примере TreeElement и TreeModel классы созданы, чтобы продемонстрировать возможности класса TreeView. Они включены в проект примеров TreeView (TreeViewExamples.zipПример также показывает, как структура дерева модели сериализуется в данном случае. ScriptableObject и хранил богатство.

[Serializable]
//The TreeElement data class is extended to hold extra data, which you can show and edit in the front-end TreeView.
internal class MyTreeElement : TreeElement
{
    public float floatValue1, floatValue2, floatValue3;
    public Material material;
    public string text = "";
    public bool enabled = true;

    public MyTreeElement (string name, int depth, int id) : base (name, depth, id)
    {
        floatValue1 = Random.value;
        floatValue2 = Random.value;
        floatValue3 = Random.value;
    }
}

Следующий класс ScriptableObject гарантирует, что при сериализации дерева данные сохранятся в ассете.

[CreateAssetMenu (fileName = "TreeDataAsset", menuName = "Tree Asset", order = 1)]
public class MyTreeAsset : ScriptableObject
{
    [SerializeField] List<MyTreeElement> m_TreeElements = new List<MyTreeElement> ();

    internal List<MyTreeElement> treeElements
    {
        get { return m_TreeElements; }
        set { m_TreeElements = value; }
    }
}

Создание класса MultiColumnTreeView

Следующий пример показывает фрагменты класса MultiColumnTreeView, которая иллюстрирует, как многоколонковый GUI Полный исходный текст можно найти в разделе TreeView Примеры Project (TreeViewExamples.zip).

public MultiColumnTreeView (TreeViewState state, 
                            MultiColumnHeader multicolumnHeader, 
                            TreeModel<MyTreeElement> model) 
                            : base (state, multicolumnHeader, model)
{
    // Custom setup
    rowHeight = 20;
    columnIndexForTreeFoldouts = 2;
    showAlternatingRowBackgrounds = true;
    showBorder = true;
    customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; 
    extraSpaceBeforeIconAndLabel = kToggleWidth;
    multicolumnHeader.sortingChanged += OnSortingChanged;
            
    Reload();
}

Настраиваемые изменения в приведенном выше примере кода вводят следующие корректировки:

  • rowHeight = 20: Изменить высоту по умолчанию (которая основана на EditorGUIUtility.singleLineHeight16 точек) до 20, чтобы добавить больше места для GUI управления.

  • columnIndexForTreeFoldouts = 2: В примере стрелки разворачивания отображаются в третьей колонке, так как это значение установлено на 2 (см. изображение выше). Если это значение не изменено, стрелки разворачивания отображаются в первой колонке, так как “columnIndexForTreeFoldouts” по умолчанию имеет значение 0.

  • showAlternatingRowBackgrounds = true: Включить чередующиеся цвета фона строк, так что каждая строка будет отличаться.

  • showBorder = true: Отображать TreeView с полем вокруг него, так, чтобы была показана тонкая граница, отделяющая его от остальной части содержимого

  • customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f: Центрировать раскладки вертикально в ряду — см. Настройка GUI ниже.

  • extraSpaceBeforeIconAndLabel = 20: Сделать пробел перед метками дерева, чтобы была показана кнопка переключения.

  • multicolumnHeader.sortingChanged += OnSortingChanged: Назначьте событию метод, который обнаруживает изменение сортировки в компоненте заголовка (при щелчке по столбцу заголовка), чтобы строки TreeView изменялись в соответствии с состоянием сортировки.

Настройка GUI

Если по умолчанию используется обработка RowGUI, то TreeView выглядит как SimpleTreeView пример выше — только со сворачиваемыми элементами и меткой. Когда для каждого элемента используется несколько значений данных, для их отображения необходимо переопределить метод RowGUI.

protected override void RowGUI (RowGUIArgs args)

Следующий пример кода представляет собой структуру аргументов RowGUIArgs struct.

protected struct RowGUIArgs
{
    public TreeViewItem item;
    public string label;
    public Rect rowRect;
    public int row;
    public bool selected;
    public bool focused;
    public bool isRenaming;

    public int GetNumVisibleColumns ()
    public int GetColumn (int visibleColumnIndex)
    public Rect GetCellRect (int visibleColumnIndex)
}

Вы можете расширить TreeViewItem и добавить дополнительные пользовательские данные (при этом создаётся класс, производный от TreeViewItem). Затем вы можете использовать эти данные пользователя в обратном вызове RowGUI. Пример этого приведен ниже. См. override void RowGUI - в этом примере вводный элемент переносится в TreeViewItem<MyTreeElement>.

Существует три метода, связанных с обработкой столбцов: GetNumVisibleColumns, GetColumnи GetCellRect. Вы можете вызвать их только тогда, когда TreeView построен с MultiColumnHeader, в противном случае возникает исключение.

protected override void RowGUI (RowGUIArgs args)
{
    var item = (TreeViewItem<MyTreeElement>) args.item;

    for (int i = 0; i < args.GetNumVisibleColumns (); ++i)
    {
        CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
    }
}
void CellGUI (Rect cellRect, TreeViewItem<MyTreeElement> item, MyColumns column, ref RowGUIArgs args)
{
    // Center the cell rect vertically using EditorGUIUtility.singleLineHeight.
// This makes it easier to place controls and icons in the cells.
    CenterRectUsingSingleLineHeight(ref cellRect);

    switch (column)
    {

        case MyColumns.Icon1:
            
            // Draw custom texture
GUI.DrawTexture(cellRect, s_TestIcons[GetIcon1Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Icon2:

//Draw custom texture 
            GUI.DrawTexture(cellRect, s_TestIcons[GetIcon2Index(item)], ScaleMode.ScaleToFit);
            break;

        case MyColumns.Name:

            // Make a toggle button to the left of the label text
            Rect toggleRect = cellRect;
            toggleRect.x += GetContentIndent(item);
            toggleRect.width = kToggleWidth;
            if (toggleRect.xMax < cellRect.xMax)
                item.data.enabled = EditorGUI.Toggle(toggleRect, item.data.enabled); 

            // Default icon and label
            args.rowRect = cellRect;
            base.RowGUI(args);
            break;

        case MyColumns.Value1:

// Show a Slider control for value 1
            item.data.floatValue1 = EditorGUI.Slider(cellRect, GUIContent.none, item.data.floatValue1, 0f, 1f);
            break;

        case MyColumns.Value2:

// Show an ObjectField for materials
            item.data.material = (Material)EditorGUI.ObjectField(cellRect, GUIContent.none, item.data.material, 
                                          typeof(Material), false);
            break;

        case MyColumns.Value3:

// Show a TextField for the data text string
            item.data.text = GUI.TextField(cellRect, item.data.text);
            break;
    }
}

TreeView FAQ

Q: В моем подклассе TreeView у меня есть функции BuildRoot и RowGUI. Вызывается ли RowGUI для каждого TreeViewItem, добавленного в функцию сборки, или только для элементов, видимых на экране в просмотре прокрутки?

A: RowGUI вызывает только элементы, видимые на экране. Например, если у вас есть 10000 элементов, только 20 видимых элементов на экране имеют свой RowGUI вызван.

Q: Могу ли я получить индексы строк, которые видны на экране?

А: Да. Используйте метод GetFirstAndLastVisibleRows.

Q: Могу ли я получить список строк, которые построены в BuildRows?

А: Да. Используйте метод GetRows.

Q: Обязательно ли для любой из переопределенных функций вызывать base.Method?

О: Только если метод имеет поведение по умолчанию, которое вы хотите расширить.

Q: Я просто хочу сделать список элементов (не дерево). Должен ли я создавать корень?

A: Да, вы всегда должны иметь root. Вы можете создать root элемент и установить root.children = rows для быстрой настройки.

Q: Я добавил переключатель в мою строку — почему выбор не переходит в эту строку, когда я нажимаю на него?

О: По умолчанию строка выбирается только в том случае, если курсор мыши не поглощается содержимым строки. Здесь событие Toggle поглощает событие. Чтобы исправить это, используйте метод SelectionClick перед вызовом кнопки Toggle.

Q: Есть ли методы, которые я могу использовать до или после всех RowGUI методы вызываются?

О: Да. См. документацию API по BeforeRowsGUI и AfterRowsGUI.

Q: Есть ли простой способ вернуть ключевой фокус на TreeView из API? Если я выберу FloatField в моей строке, выбор строки становится серым. Как я могу сделать его снова синим?

О: Синий цвет указывает, какая строка в настоящее время имеет ключевой фокус. Поскольку FloatField имеет фокус, TreeView теряет фокус, так что это предполагаемое поведение. Установите GUIUtility.keyboardControl = treeViewControlID, когда это необходимо.

Q: Как я могу преобразовать из id в TreeViewItem?

A: Используйте либо FindItem, либо FindRows.

Q: Как я могу получить обратный вызов, когда пользователь изменяет свой выбор в TreeView?

О: Переопределить метод SelectionChanged (другие полезные обратные вызова: DoubleClickedItemи ContextClickedItem).