UI ToolkitのQ("...")を手書きしないため、UXMLから型付きElement参照を生成する
UI ToolkitでElementを取得するときは、普通にQ<T>()を使えば十分です。
Q.cs
var startButton = root.Q<Button>("StartButton");ただし、Element名が文字列なので、UXML側でStartButtonをリネームしてもC#はコンパイルエラーになりません。
その画面を実際に開くまで、名前がズレていることに気づけない可能性があります。
とても嫌ですね。
Element参照をクラスにまとめたい
Windowごとに、使用するElementをまとめるクラスを作ることにしました。
TestWindowElements.cs
public sealed class TestWindowElements
{
public Button StartButton { get; }
public Button OptionsButton { get; }
public Button QuitButton { get; }
public TestWindowElements(VisualElement root)
{
StartButton = root.Q<Button>("StartButton");
OptionsButton = root.Q<Button>("OptionsButton");
QuitButton = root.Q<Button>("QuitButton");
}
}利用側ではElement名を意識せずに済みます。
TestWindow.cs
var elements = new TestWindowElements(root);
elements.StartButton.clicked += OnStart;
elements.OptionsButton.clicked += OnOptions;これで文字列がWindowの実装中に散らばることはなくなりました。
しかし、このクラスを手書きするなら結局何も解決していません。
このクラスを自動生成できるようにします。
UXMLから自動生成する
というわけで、nameが付いているElementを取得し、参照クラスを生成するEditorスクリプトを作りました。
仕組みの中心部分はこんな感じです。
GetElements.cs
private static (string Name, Type Type)[] GetElements(string uxmlPath)
{
var names = XDocument.Load(uxmlPath)
.Descendants()
.Select(element => element.Attribute("name")?.Value)
.Where(name => !string.IsNullOrEmpty(name))
.ToArray();
var root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath).Instantiate();
return names.Select(name => (name, root.Q(name).GetType())).ToArray();
}XDocumentから明示的に書かれたname属性を取得します。
型については文字列から推測せず、VisualTreeAssetをInstantiateした結果から取得します。
あとは取得した名前と型からC#コードを生成するだけです。
全体
全体ではこんな感じ。
UXMLを保存したら自動でトリガーがかかるようにしたり、テストでちゃんと生成されてるか確認しても良いでしょう。
WindowElementsGenerator.cs
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using UnityEditor;
using UnityEngine.UIElements;
public static class WindowElementsGenerator
{
[MenuItem("Tools/UI Toolkit/Generate Window Elements")]
public static void GenerateAll()
{
foreach (var uxmlPath in FindWindowUxmlPaths())
{
File.WriteAllText(GetGeneratedPath(uxmlPath), GenerateSource(uxmlPath));
}
AssetDatabase.Refresh();
}
public static string[] FindWindowUxmlPaths()
{
return AssetDatabase.FindAssets("t:VisualTreeAsset", new[] { "Assets" })
.Select(AssetDatabase.GUIDToAssetPath)
.Where(path => path.EndsWith("Window.uxml"))
.ToArray();
}
public static string GetGeneratedPath(string uxmlPath)
{
return Path.ChangeExtension(uxmlPath, null) + "Elements.cs";
}
public static string GenerateSource(string uxmlPath)
{
var className = Path.GetFileNameWithoutExtension(uxmlPath) + "Elements";
var root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath).Instantiate();
var elements = XDocument.Load(uxmlPath)
.Descendants()
.Select(element => element.Attribute("name")?.Value)
.Where(name => !string.IsNullOrEmpty(name))
.Select(name => (Name: name, Type: root.Q(name).GetType()))
.ToArray();
return $@"// <auto-generated />
using UnityEngine.UIElements;
public sealed class {className}
{{
{string.Join("\n", elements.Select(element => $" public {GetTypeName(element.Type)} {element.Name} {{ get; }}"))}
public {className}(VisualElement root)
{{
{string.Join("\n", elements.Select(element => $" {element.Name} = root.Q<{GetTypeName(element.Type)}>(\"{element.Name}\");"))}
}}
}}
";
}
private static string GetTypeName(Type type)
{
return string.IsNullOrEmpty(type.Namespace) || type.Namespace == typeof(VisualElement).Namespace ? type.Name : $"global::{type.FullName.Replace('+', '.')}";
}
}まとめ
平和な型付ライフを!