1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりアドベントカレンダーAdvent Calendar 2024

Day 4

【コピペで簡単】UIToolkitで詳細の見れるリストを作る

Last updated at Posted at 2024-12-03

概要

何かしらの一覧を表示して、その詳細も見れるようにしたいことがある。

↑こんなイメージ

いつかの自分が実装に迷わないよう、作成手順を残しておく。

手順

1. 適当なプロジェクトを用意

2. テンプレート作成

自前でUXMLファイルを作ってもいいが、慣れていないうちはテンプレートから作ると実装のブレが少なくて楽

3つのファイルが生成される

ざっくり
C#スクリプト => UIをスクリプトで制御することができる
`{}` => スタイルシートという見た目を指定できるファイル. CSSみたいなもの
`</>` => UXMLファイル(VisualTreeAsset). UIの構造を指定する.
### 3. 必要なUXMLを作る

UXMLファイルを3つコピペ
(Createの中にUXMLが見つからない:cry:)

名前を用途に合わせて変更

4. スクリプト

以下のように修正
コメントましましにしたので読めばわかるはず

ListAndDetailTool.cs
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class ListAndDetailTool : EditorWindow
{
    // スクリプトファイルのインスペクタからパーツを指定する
    [SerializeField] private VisualTreeAsset listAndDetailToolVta;
    [SerializeField] private VisualTreeAsset listViewVta;
    [SerializeField] private VisualTreeAsset detailViewVta;
    [SerializeField] private VisualTreeAsset listContentVta;

    private VisualElement _leftPane;
    private VisualElement _rightPane;
    private string _filterString = "";

    [MenuItem("Tools/ListAndDetailTool")]
    public static void ShowWindow()
    {
        ListAndDetailTool wnd = GetWindow<ListAndDetailTool>();
        wnd.titleContent = new GUIContent("ListAndDetailTool");
    }

    // Windowが開かれたり閉じたりした時の処理はOnEnableとOnDisableで行う
    void OnEnable()
    {
        ListDataContainer.OnListDataListChanged += RefreshListView;
    }

    void OnDisable()
    {
        ListDataContainer.OnListDataListChanged -= RefreshListView;
    }

    public void CreateGUI()
    {
        // 検索用のヘッダーを追加
        var toolbar = new Toolbar();
        var searchField = new ToolbarSearchField();
        searchField.RegisterValueChangedCallback(e =>
        {
            // 検索文字列が変更されたらリストビューを更新
            _filterString = e.newValue;
            RefreshListView();
        });
        toolbar.Add(searchField);
        rootVisualElement.Add(toolbar);

        // ベースとなる要素を作成
        var baseEl = listAndDetailToolVta.Instantiate();
        // Instantiate時にTemplateContainerが親として生成される
        // この親のStyleはエディタ上から変更できないため、スクリプトから設定する必要がある:cry:
        baseEl.style.flexGrow = 1;
        rootVisualElement.Add(baseEl);

        // 水平分割のためにTwoPaneSplitViewを使う
        var splitView = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
        baseEl.Add(splitView);

        // 左右のペインを作成
        _leftPane = new VisualElement();
        _rightPane = new VisualElement();
        splitView.Add(_leftPane);
        splitView.Add(_rightPane);

        // 左ペインにリストビューを追加
        var listView = listViewVta.Instantiate();
        _leftPane.Add(listView);

        // 右ペインに詳細ビューを追加
        var detailView = detailViewVta.Instantiate();
        _rightPane.Add(detailView);

        // 初期化
        SetupListView();
        RefreshListView();
        RefreshDetailView();
    }

    private void SetupListView()
    {
        var listView = _leftPane.Q<ListView>();

        // 要素が選択された時の処理
        listView.selectionChanged += selections =>
        {
            var selection = selections.FirstOrDefault();

            // 何も選択されなかった時
            if (selection == null)
                return;

            // 選択されたら詳細ビューを更新
            var listData = (ListData)selection;
            RefreshDetailView(listData);
        };

        // 要素の生成をする
        // 要素自体は使いまわされるため、ここでデータを入れることはしない
        // 表示時にbindItemでデータをバインドされる
        listView.makeItem = () => listContentVta.Instantiate();

        // リストビューの要素にデータをバインド
        // indexごとにListDataContainer.ListDataListからデータを取得してバインドする
        listView.bindItem = (el, index) =>
        {
            // 検索時のフィルタ結果はitemsSourceに入れて使い回すようにする
            var listData = (ListData)listView.itemsSource[index];
            el.Q<Label>().text = listData.Name;
        };
    }

    private void RefreshListView()
    {
        var listView = _leftPane.Q<ListView>();
        // データ自体を指定
        // 内部で要素数の取得や、フッターによる要素の追加などに使われる
        listView.itemsSource = _filterString == "" ? ListDataContainer.ListDataList : ListDataContainer.FilterListData(_filterString);

        listView.Rebuild();
    }

    private void RefreshDetailView(ListData listData = null)
    {
        var detailView = _rightPane.Q<VisualElement>();
        detailView.Clear();

        // 選択されたデータがない場合は何も表示しない
        if (listData == null)
            return;

        // 詳細ビューの内容を更新
        var nameLabel = new Label("Name");
        var nameField = new TextField { value = listData.Name };
        // テキストフィールドの値が変更されたらデータも更新
        nameField.RegisterValueChangedCallback(e =>
        {
            listData.Name = e.newValue;
            RefreshListView();
        });

        // 余白用の要素
        var space = new VisualElement { style = { height = 10 } };

        var descriptionLabel = new Label("Description");
        var descriptionField = new TextField { value = listData.Description };
        descriptionField.RegisterValueChangedCallback(e =>
        {
            listData.Description = e.newValue;
        });

        detailView.Add(nameLabel);
        detailView.Add(nameField);
        detailView.Add(space);
        detailView.Add(descriptionLabel);
        detailView.Add(descriptionField);
    }
}

5. インスペクタからUXMLをセット

:warning: セットした後はウィンドウを開き直さないと反映されません

6. UXMLを書き換え

DatailView.uxml
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/Editor/ListAndDetailTool.uss?fileID=7433441132597879392&amp;guid=45f3cb131b09748238ee8214092a9491&amp;type=3#ListAndDetailTool" />
</engine:UXML>
ListView.uxml
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/Editor/ListAndDetailTool.uss?fileID=7433441132597879392&amp;guid=45f3cb131b09748238ee8214092a9491&amp;type=3#ListAndDetailTool" />
    <engine:ListView allow-add="false" allow-remove="false" show-bound-collection-size="true" virtualization-method="DynamicHeight" show-border="true" show-foldout-header="true" show-alternating-row-backgrounds="ContentOnly" reorderable="false" horizontal-scrolling="true" show-add-remove-footer="false" style="flex-grow: 1; min-height: auto; max-height: none;" />
</engine:UXML>
ListContent.uxml
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/Editor/ListAndDetailTool.uss?fileID=7433441132597879392&amp;guid=45f3cb131b09748238ee8214092a9491&amp;type=3#ListAndDetailTool" />
    <engine:Label text="Label" />
</engine:UXML>

7. 表示用データを作る

見たままstaticのListですが、リスト更新用のイベントも合わせて用意します
また、フィルタも実装したいので、そのためのメソッドも追加します

ListData.cs
using System.Collections.Generic;

public class ListData
{
    public string Name { get; set; } = "New ListData";
    public string Description { get; set; } = "New Description";
}

public static class ListDataContainer
{
    public static List<ListData> ListDataList { get; private set; } = new();

    // ListDataListの要素が変更された時に発火するイベント
    public static event System.Action OnListDataListChanged;

    public static void AddListData(ListData listData)
    {
        ListDataList.Add(listData);
        OnListDataListChanged?.Invoke();
    }

    public static List<ListData> FilterListData(string filter)
    {
        return ListDataList.FindAll(data => data.Name.Contains(filter));
    }
}

Aを押した時に、適当なデータが追加されるようにします
このスクリプトをシーン上の適当なオブジェクトにつけておく

DataAdder.cs
using UnityEngine;

public class DataAdder : MonoBehaviour
{
    void Update()
    {
        // Aが押されたら適当なデータを追加
        if (Input.GetKeyDown(KeyCode.A))
            ListDataContainer.AddListData(new ListData()
            {
                Name = $"{(int)(System.DateTimeOffset.Now.ToUnixTimeMilliseconds())}_{System.Guid.NewGuid().ToString().Substring(0, 5)}",
                Description = System.Guid.NewGuid().ToString().Substring(0, 30)
            });
    }
}

8. 完成!

画面上部のメニューからウィンドウを開く
スクリーンショット 2024-12-04 0.03.21.png

エディタを再生モードにしてAを何回か押せば、テスト用のデータを確認することができる
スクリーンショット 2024-12-04 0.03.09.png

未来の自分へTips

  • VisualTreeAssetをInstantiateした時はTemplateContainerという要素に入っている
    「UI Builderでflex-growを1にしたのに要素が広がらない!」というときは、そっちを確認すること

  • PaneはPanelの打ち間違えではなく、"ペイン"という要素!!!!

  • TwoPaneSplitViewの大きさを変える部分がバグってる
    バーを左右ギリギリまで動かしたりすると、その後操作できなくなるのでウィンドウを開き直して直す

  • UI Toolkit Debuggerがめっちゃ便利
    UI Toolkit DebuggerのUI Toolkit Debuggerを開いたりできるので参考にすること

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?