概要
何かしらの一覧を表示して、その詳細も見れるようにしたいことがある。
↑こんなイメージ
いつかの自分が実装に迷わないよう、作成手順を残しておく。
手順
1. 適当なプロジェクトを用意
2. テンプレート作成
自前でUXMLファイルを作ってもいいが、慣れていないうちはテンプレートから作ると実装のブレが少なくて楽
C#スクリプト => UIをスクリプトで制御することができる
`{}` => スタイルシートという見た目を指定できるファイル. CSSみたいなもの
`</>` => UXMLファイル(VisualTreeAsset). UIの構造を指定する.
UXMLファイルを3つコピペ
(Createの中にUXMLが見つからない)
名前を用途に合わせて変更
4. スクリプト
以下のように修正
コメントましましにしたので読めばわかるはず
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をセット
セットした後はウィンドウを開き直さないと反映されません
6. 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&guid=45f3cb131b09748238ee8214092a9491&type=3#ListAndDetailTool" />
</engine: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&guid=45f3cb131b09748238ee8214092a9491&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>
<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&guid=45f3cb131b09748238ee8214092a9491&type=3#ListAndDetailTool" />
<engine:Label text="Label" />
</engine:UXML>
7. 表示用データを作る
見たままstaticのListですが、リスト更新用のイベントも合わせて用意します
また、フィルタも実装したいので、そのためのメソッドも追加します
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を押した時に、適当なデータが追加されるようにします
このスクリプトをシーン上の適当なオブジェクトにつけておく
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. 完成!
エディタを再生モードにしてAを何回か押せば、テスト用のデータを確認することができる
未来の自分へTips
-
VisualTreeAssetをInstantiateした時はTemplateContainerという要素に入っている
「UI Builderでflex-growを1にしたのに要素が広がらない!」というときは、そっちを確認すること -
PaneはPanelの打ち間違えではなく、"ペイン"という要素!!!!
-
TwoPaneSplitViewの大きさを変える部分がバグってる
バーを左右ギリギリまで動かしたりすると、その後操作できなくなるのでウィンドウを開き直して直す -
UI Toolkit Debuggerがめっちゃ便利
UI Toolkit DebuggerのUI Toolkit Debuggerを開いたりできるので参考にすること