HandleOperation

ハンドルとは

3D アプリケーションでは、3D ビューにマウスで操作可能な制御オブジェクトを表示することがあります。 例えば矢印オブジェクトを表示して、それをマウスでドラッグすると矢印方向に物体を移動できる、といったものです。 こういう、いわば3DビューでのGUIコントロールを、MoNo.RAILでは「ハンドル」と呼んでいます。

このハンドルを作成するための機能を提供するのが、 HandleOperation やその周辺のクラスとなります。

HandleOperation と Handle

HandleOperation クラスは、次のように Root プロパティで一つの Handle オブジェクトを保持しています。

namespace MoNo.Ctrl
{
  public class HandleOperation : Operation, ...
  {
        public Handle Root { get; }
        ...
  }
}

そして Handle クラスは次のように、子要素の Handle を複数保持できる構造となっています。

namespace MoNo.Ctrl
{
  public class Handle : Core.BreathObject, Graphics.ISceneHolder
  {
        public List<Handle> Handles { get { return _items; } }
        ...
  }
}

つまり、HandleOperation の下に Handle がツリー状にぶら下がるような構造となります。

Handle クラス

Handle クラスは、アプリケーションで必要となるハンドルを定義するための親クラスです。 このクラスを継承して、独自のハンドルを定義することが出来ます。 また、このクラスを継承した定義済みのハンドルクラス(HBall と HArrow)を利用することも出来ます。

外観の描画

ハンドルとは3Dビュー内のGUIコントロールですので、その外観を描画する必要があります。 その描画のためのシーンオブジェクトを Handle に登録することが出来ます。

上記の Handle クラスの定義を見ると ISceneHolder インターフェイスを実装していることが分かります。 ISceneHolder は次のように定義されたインターフェイスです。

namespace MoNo.Graphics
{
  /// <summary>
  /// ワールド座標系、カメラ座標系、スクリーン座標系のシーンを束ねるインターフェイスです。
  /// </summary>
  public interface ISceneHolder : Core.IBreath
  {
        /// <summary>
        /// ワールド座標系のシーンコレクションです。
        /// </summary>
        BoundarySceneCollection WorldScenes { get; }

        /// <summary>
        /// カメラ座標系のシーンコレクションです。
        /// </summary>
        SceneCollection CameraScenes { get; }

        /// <summary>
        /// スクリーン座標系の背景シーンコレクションです。
        /// </summary>
        SceneCollection BackgroundScenes { get; }

        /// <summary>
        /// スクリーン座標系の前景シーンコレクションです。
        /// </summary>
        SceneCollection ForegroundScenes { get; }
  }
}

この定義からわかるように、 Handle には各種座標系のシーンを持たせることが出来ます。 ここにハンドルの外観を描画するシーンを登録することになります。 Handle クラスを継承して独自のハンドルを定義している場合は、そのコンストラクタでシーンを登録すれば良いでしょう。

振る舞いの実装

ハンドルは、ユーザーのマウスオペレーションに反応して動作しなくてはなりません。 そのような振る舞いを実装するために、 Handle クラスには次のようにマウスイベントが定義されています。

public class Handle
{
  public event HandleMouseEventHandler MouseMovePreview;
  public event HandleMouseEventHandler MouseDownPreview;
  public event HandleMouseEventHandler MouseUpPreview;
  public event HandleMouseEventHandler MouseClickPreview;
  public event HandleMouseEventHandler MouseDoubleClickPreview;

  public event HandleMouseEventHandler MouseMove;
  public event HandleMouseEventHandler MouseDown;
  public event HandleMouseEventHandler MouseUp;
  public event HandleMouseEventHandler MouseClick;
  public event HandleMouseEventHandler MouseDoubleClick;

      ...
}

これらのイベントにハンドラを設定して、ハンドルの振る舞いを実装します。 ハンドルによっては振る舞いが複雑になり得ますが、簡便に振る舞いを実装できるような特別な仕組みはなく、イベントを拾って丹念に動きを実装するしかありません。

一つのコツとしては、複雑なハンドルは出来るだけ小さなハンドルの部品に分割して、複数のハンドル部品を組み立てるようにして作るのが良いでしょう。

Handle のマウスイベント

前節で示した Handle クラスの持つマウスイベントを見てみましょう。 着目点は次の2点です。

  • イベントの型が通常の System.Windows.Forms.MouseEventHandler ではなく、HandleMouseEventHandler 型となっている。
  • クリックに対応するイベントが MouseClick と MouseClickPreview の2種類ある。

まず HandleMouseEventArgs の定義を下記に示します。

namespace MoNo.Ctrl
{
  public class HandleMouseEventArgs : MouseEventArgs
  {
        public bool Handled { get; set; }

        public void Interrupt( IOperation operation )
        {
          this.Handled = true;
          OperationDriver.Default.Interrupt( operation );
        }

        ...
  }

  public delegate void HandleMouseEventHandler( Graphics.IView view, HandleMouseEventArgs e );
}

通常の System.Windows.Forms.MouseEventArgs に Handled というプロパティと Interrupt というメソッドが加わっています。 Interrupt については後で説明することとして、まずは Handled プロパティがどのように使われるかを見ていきます。

Handled プロパティと2種類のイベント

HandleOperation がマウスクリックを検知すると、そのイベントは次の関数で処理されます。

public class Handle
{
  ...
  protected internal void OnMouseClick( Graphics.IView sender, HandleMouseEventArgs e )
  {
    if ( this.TargetViews == null || this.TargetViews.Contains( sender ) ) {
      if ( !e.Handled ) this.MouseClickPreview.Fire( sender, e );
      if ( !e.Handled ) this.Handles.ForEach( handle => handle.OnMouseClick( sender, e ) );
      if ( !e.Handled ) this.MouseClick.Fire( sender, e );
    }
  }
}

次の順番でイベントが発火されることが分かります。

  1. まず MouseClickPreview イベントを発火
  2. 次に子ハンドルの OnMouseClick を呼び出す
  3. 最後に MouseClick イベントを発火

このイベントの流れを図示すると次のようになります。

../../_images/EventFlow.png

ハンドルのツリー構造において、まず MouseCLickPreview イベントがルートから末端に向かって伝播していき、 末端に達するとイベントが反射して逆向きに MouseClick イベントが伝播していく、という流れです。

そして、この流れは Handled プロパティが true になった時点で止まります。 Handled プロパティが true ということは、このイベントは既に処理済みだから次に伝播させなくて良いということを意味しているのです。

イベントハンドラを実装するときは、必要に応じてイベント引数の Handled プロパティを true にセットするようにして下さい。

Interrupt メソッド

前提知識として、 OperationDriver の Interrupt メソッドの解説に目を通しておいて下さい。

Handle における Interrupt メソッドは、マウスのドラッグ操作の実装が典型的な用途です。 ドラッグ操作を実装するには、まず MouseDown イベントでドラッグの開始を検知し、そこから MouseUp イベント待ち状態に入る必要があります。 このときに Interrupt メソッドを使って MouseUp 待ち状態を割り込ませます。

典型的なコードは次のような形式になります。

handle.MouseDown += (view, e) => {
  if ( e.Button == MouseButtons.Left) {
    var mouseUp = new MoNo.Ctrl.LButtonUp(view);
    mouseUp.MouseMove += ...; // ドラッグ操作中のMouseMoveでプレビュー表示を行うなど
    e.Interrupt(mouseUp);
  }
}