【Unity】【UI Toolkit(旧UIElements)】独自のUXML要素(コントロール)を作る

UnityのUIElementsで独自のUXML要素を作る方法をまとめました。

Unity2019.3.0

はじめに

この記事ではUIElementsで独自のUXML要素を作る方法をまとめます。
UIElementsの基本的な使い方については以下の記事で説明しています。

light11.hatenadiary.com

本記事はUIElementsの基礎知識を前提としますので、必要に応じて上の記事を参照してください。

なお本記事の内容は執筆時点でちゃんとした公式ドキュメントがないのと、
UIElementsのランタイム対応時に一部変わりそうな予感がしているので、
誤りやうまく動かない点、記述が古い点などありましたらご指摘いただけますと幸いです。

やりたいこと

UIElementsにはデフォルトで様々なUXML要素が定義されています。

light11.hatenadiary.com

本記事ではこのUXML要素自体を自作する方法を紹介します。
実装例として以下のようにRGBA情報を表示するカラーピッカーを作成します。

カラーピッカー

VisualElementを作る

独自のUXML要素を作るにはVisualElementを継承したクラスを定義します。

using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace ExampleElements // 名前空間は定義しておいた方がいい
{
    public class ColorAndText : VisualElement // VisualElementを継承
    {        
        public Color Color
        {
            get => _colorField.value;
            set {
                _colorField.value = value;
                _label.text = _colorField.value.ToString();
            }
        }

        private readonly Label _label;
        private readonly ColorField _colorField;

        public ColorAndText()
        {
            // レイアウトを調整(横並び&上下中央に)
            style.flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row);
            style.alignItems = new StyleEnum<Align>(Align.Center);
            
            // ColorFieldを子要素として追加
            _colorField = new ColorField();
            SetMargin(_colorField, 0); // 子要素のMarginはUSSで変更されないようにスクリプトで定義
            SetPadding(_colorField, 0); // 子要素のPaddingはUSSで変更されないようにスクリプトで定義
            _colorField.style.width = new StyleLength(new Length(50, LengthUnit.Percent));
            Add(_colorField);
            
            // Labelを子要素として追加
            _label = new Label();
            SetMargin(_label, 0);
            SetPadding(_label, 0);
            _label.style.width = new StyleLength(new Length(50, LengthUnit.Percent));
            _label.text = _colorField.value.ToString();
            Add(_label);

            _colorField.RegisterValueChangedCallback(x => _label.text = x.newValue.ToString());
        }
        
        private static void SetMargin(VisualElement element, float px)
        {
            element.style.marginLeft = px;
            element.style.marginTop = px;
            element.style.marginRight = px;
            element.style.marginBottom = px;
        }

        private static void SetPadding(VisualElement element, float px)
        {
            element.style.paddingLeft = px;
            element.style.paddingTop = px;
            element.style.paddingRight = px;
            element.style.paddingBottom = px;
        }
    }
}

今回はColorFieldとLabelを持つUIを作りたいので、コンストラクタでこの二つを子要素として追加しています。

またこのUXML要素に対してUSSファイルが定義されたときにmarginやpaddingが変わってしまうと
見た目的におかしくなるためこれらはスクリプトで設定しています。
これは以下の記事で説明している通り、スクリプトから設定されたスタイルは最優先になる性質を利用しています。

light11.hatenadiary.com

ファクトリを定義する

次にこれをUXMLファイルで使えるようにするためにUxmlFactoryを継承したクラスを定義します。
これは先ほど定義したColorAndTextクラス内に定義する方法が推奨されています。

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

namespace ExampleElements
{
    public class ColorAndText : VisualElement // VisualElementを継承
    {
        // ファクトリとして使われるクラス
        public new class UxmlFactory : UxmlFactory<ColorAndText> {}
        ...(以下省略)

これでUXMLファイル内でこの要素を使い準備ができました。

定義した要素をUXMLで使う

さてそれでは前節までで作った要素をUXMLファイル内で使ってみます。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xmlns:ex="ExampleElements" <!-- 使用する名前空間を記述 -->
>
    
    <ex:ColorAndText></ex:ColorAndText> <!-- 定義した要素を使う -->

</engine:UXML>

これを読み込むと、正常に表示が行われていることが確認できます。

正常に表示されている

独自のUXML属性を追加する

さて次にこのUXML要素に独自の属性を追加してみます。
属性を追加するにはUxmlFactoryの他にVisualElement.UxmlTraitsを継承したクラスも定義します。

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

namespace ExampleElements
{
    public class ColorAndText : VisualElement // VisualElementを継承
    {
        // ジェネリックの二つ目の型にUxmlTraitsを指定
        public new class UxmlFactory : UxmlFactory<ColorAndText, UxmlTraits> {}
    
        // ファクトリによるColorAndTextの初期化時に使うクラス
        public new class UxmlTraits : VisualElement.UxmlTraits
        {
            // UXMLの属性を定義
            UxmlColorAttributeDescription _initialColor = new UxmlColorAttributeDescription { name = "initial-color" };
        
            // 子を持たない場合はこのように書く
            public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
            {
                get { yield break; }
            }
        
            // 初期化処理
            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                var colorAndText = ve as ColorAndText;

                // UXMLの属性に入っている値を代入する
                colorAndText.Color = _initialColor.GetValueFromBag(bag, cc);
                colorAndText._label.text = colorAndText._colorField.value.ToString();
            }
        }
        ...(以下省略)

これはUXML要素で使える属性を定義するためのものです。
上記ではinitial-colorという色の初期値を入力するための属性を定義しています。

属性の定義自体はUxmlColorAttributeDescriptionクラスで行います。
このクラスを_initialColor.GetValueFromBag(bag, cc);のように使うことで値を取り出しています。

さて属性を定義できたので次にこれをUXML内で使ってみます。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xmlns:ex="ExampleElements"
>

    <ex:ColorAndText initial-color="red"/> <!-- 定義した属性を指定 -->
    
</engine:UXML>

これで色の初期値が赤色になりました。

初期値が設定できた

なお今回は属性定義用のクラスにUxmlColorAttributeDescriptionを使いましたが、
全部で以下の種類が用意されています。

名前 説明
UxmlStringAttributeDescription string用
UxmlFloatAttributeDescription float用
UxmlDoubleAttributeDescription double用
UxmlIntAttributeDescription int用
UxmlLongAttributeDescription long用
UxmlBoolAttributeDescription bool用
UxmlColorAttributeDescription Color用
UxmlEnumAttributeDescription Enum

UXMLスキーマを作る

さてここまでで正常に動作するとは思いますが、UXMLスキーマを作成しておくと開発がよりスムーズになります。

UXMLスキーマを作るにはまず、適当なcsファイルに以下のようなUxmlNamespacePrefixアトリビュートを定義します。
名前は適当でいいですが今回はUxmlNamespacePrefix.csにしました。

using UnityEditor.UIElements;

[assembly: UxmlNamespacePrefix("ExampleElements", "example")]

次にAssets > Update UIElements SchemaからUXMLスキーマを更新します。

スキーマを更新

すると、プロジェクト直下(Assetsの一個上の階層)のUIElementsSchemaフォルダ内にファイルが作られます。
このファイルが作られると新しくUXMLファイルを作る際に最初から名前空間の記述が行われたり、補間が効くようになります。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xmlns:example="ExampleElements" <!-- 最初から記述されている -->
    xsi:noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd"
>
    <example:ColorAndText/> <!-- 補間が効く -->
    
</engine:UXML>

ちなみにスキーマの更新は新しくUXMLファイルを作るときにも行われます。
この挙動からするとUIElementsSchemaはバージョン管理対象外としていいフォルダなのかなと思います。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

参考

docs.unity3d.com