【Unity】UI Toolkitにおけるイベント伝播の仕組みとハンドリング方法

UI Toolkitにおけるイベント伝播の仕組みとハンドリング方法についてまとめました。

Unity2021.3.16

はじめに

UI Toolkitでは、マウスやキーボードからの入力などをイベントと呼びます。
ButtonなどのインタラクティブなUIはこのイベントをハンドリングすることで作られています。

本記事ではこのようなButtonなどの実装の背景にあるイベントの仕組みを理解することを目的とします。

まず準備として、下図のようなEditorWindowを作成しておきます。
赤色のルート要素(Root)の下に緑色の子要素(Child)を、さらに孫要素として青色の要素(Grandchild)を配置しています。

EditorWindow

コードとしては以下のようになります(もちろんUXMLやUSSを使って組み立ててもいいのですが今回はコードで)。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class EventExample : EditorWindow
{
    private void OnEnable()
    {
        var root = rootVisualElement;
        root.style.paddingTop = 10;
        root.style.paddingBottom = 10;
        root.style.paddingLeft = 10;
        root.style.paddingRight = 10;
        root.style.backgroundColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f));

        var child = new VisualElement
        {
            style =
            {
                paddingTop = 10,
                paddingBottom = 10,
                paddingLeft = 10,
                paddingRight = 10,
                height = new StyleLength(Length.Percent(100)),
                backgroundColor = new StyleColor(new Color(0.3f, 0.8f, 0.3f))
            }
        };

        var grandchild = new VisualElement
        {
            style =
            {
                paddingTop = 10,
                paddingBottom = 10,
                paddingLeft = 10,
                paddingRight = 10,
                height = new StyleLength(Length.Percent(100)),
                backgroundColor = new StyleColor(new Color(0.3f, 0.3f, 0.8f))
            }
        };

        child.Add(grandchild);
        root.Add(child);
    }

    [MenuItem("Window/EventExample")]
    public static void ShowWindow()
    {
        GetWindow<EventExample>("EventExample");
    }
}

イベントのコールバックを登録

次に、前節で作成した孫要素に、要素上でマウスが押下された時のコールバックを登録します。
コールバックを登録するには、受け取りたいイベントの型を指定しつつ RegisterCallback を使います。

grandchild.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on grandchild");
});

これで要素上でマウスが押下された時のイベントを登録できました。
青い要素上でクリックするとログ出力されることを確認できます。

次に、一つ親の要素である赤い要素に対して同様にマウス押下イベントを登録してみます。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log($"mouse down on child");
});

さてこの状態でもう一度青い要素の上でマウスを押下すると、childgrandchildの両方のイベントのコールバックが呼ばれます。

コールバックが呼ばれる

つまり、一番子の要素を押下した場合、その親にある要素のイベントも呼ばれる仕様であることがわかります。

しかしながら今回は、見た目的には青い要素をクリックしただけなので、青い要素(grandchild)だけが反応するように制御したいです。
このような制御を行う場合には、UI Toolkitにおけるイベント伝播の仕組みを正確に理解する必要があります。

イベント伝播の概念

UI Toolkitでは、OSからマウス押下イベントを受け取ると、まずそのイベントはルートのVisual Elementに送られます。
次にその子へ、さらに子へと伝播されていきます。
このように、ルート要素から末端の要素の一個手前の要素までイベントを伝えていく段階をトリクルダウンフェーズと呼びます。

トリクルダウンフェーズ

次に、末端の要素(ターゲット)にイベントが伝播されます。
これをターゲットフェーズと呼びます。

※ ターゲットは必ずしも末端の要素じゃない場合がありますが、マウスダウンイベントの文脈では末端の要素になります

ターゲットフェーズ

最後に、末端の要素の一個手前の要素からルート要素まで遡るようにイベントを伝播していきます。
これをバブルアップフェーズと呼びます。

バブルアップフェーズ

イベント伝播の挙動を確認する

さて、このイベント伝播の情報は、イベントのコールバック内で以下のように取得できます。

element.RegisterCallback<MouseDownEvent>(x =>
{
    x.propagationPhase; // 伝播フェーズ
    x.target; // ターゲット(末端の要素)
    x.currentTarget; // この時点でイベントを処理している要素
});

したがって、前述の「見た目的にクリックした要素だけ反応するようにしたい」を実現するためには以下のようにイベント伝播フェーズを意識した処理に書き換える必要があります。

grandchild.RegisterCallback<MouseDownEvent>(x =>
{
    if (x.propagationPhase != PropagationPhase.AtTarget)
        return;
    Debug.Log("mouse down on grandchild");
});
child.RegisterCallback<MouseDownEvent>(x =>
{
    if (x.propagationPhase != PropagationPhase.AtTarget)
        return;
    Debug.Log("mouse down on child");
});

これで、見た目的にクリックした要素だけが反応するようになりました。

なお話は変わりますが、上述の通り、ターゲット要素以外の要素においては、トリクルダウンフェーズとバブルアップフェーズの2回イベントが処理されることになります。
RegisterCallbackメソッドではこのどちらのフェーズにおける処理を行うかを第二引数で指定することができます。
デフォルトはバブルアップフェーズのイベントを受け取り、引数にTrickleDownを指定するとトリクルダウンフェーズのイベントを受け取るようになります。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child");
}, TrickleDown.TrickleDown);

イベントの伝播を止める

以下のようにStopPropagationを使うことでイベントの伝播を途中で止めることもできます。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child and stop propagation");
    x.StopPropagation();
});

ただしこの場合、以下のように同じタイミングに登録したイベントは全て呼ばれます。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child 1 and stop propagation");
    x.StopPropagation();
});
child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child 2"); // 伝播を止めてもこれは呼ばれる
});

同じタイミングに登録したイベントも呼ばれないようにしたい場合には、代わりにStopImmediatePropagationを使います。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child 1 and stop immediate propagation");
    x.StopImmediatePropagation(); // StopImmediatePropagation
});
child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child 2"); // 呼ばれなくなる
});

ちなみに他の記事にまとめる予定ですが、このようにして伝播を止めても、各UI要素にデフォルトアクションとして定義されている処理は実行されます。
デフォルトアクションの処理を行わないようにするためにはPreventDefaultを使います。

child.RegisterCallback<MouseDownEvent>(x =>
{
    Debug.Log("mouse down on child and prevent default");
    x.PreventDefault();
});

まとめ

以上、UI Toolkitにおけるイベントの仕組みと、コールバックの登録を登録してそれらをハンドリングする方法についてまとめました。
今回は基礎的な内容なので割愛していますが、イベントのコールバックで処理する内容が複雑な場合には、Manipulatorという仕組みもあります。
このような関連するテーマについては他の記事で解説する予定です。

参考

docs.unity.cn