============================================ HandleOperation ============================================ ハンドルとは ============================================ 3D アプリケーションでは、3D ビューにマウスで操作可能な制御オブジェクトを表示することがあります。 例えば矢印オブジェクトを表示して、それをマウスでドラッグすると矢印方向に物体を移動できる、といったものです。 こういう、いわば3DビューでのGUIコントロールを、MoNo.RAILでは「ハンドル」と呼んでいます。 このハンドルを作成するための機能を提供するのが、 ``HandleOperation`` やその周辺のクラスとなります。 HandleOperation と Handle ============================================ ``HandleOperation`` クラスは、次のように ``Root`` プロパティで一つの ``Handle`` オブジェクトを保持しています。 .. code-block:: csharp namespace MoNo.Ctrl { public class HandleOperation : Operation, ... { public Handle Root { get; } ... } } そして ``Handle`` クラスは次のように、子要素の ``Handle`` を複数保持できる構造となっています。 .. code-block:: csharp namespace MoNo.Ctrl { public class Handle : Core.BreathObject, Graphics.ISceneHolder { public List Handles { get { return _items; } } ... } } つまり、``HandleOperation`` の下に ``Handle`` がツリー状にぶら下がるような構造となります。 Handle クラス ==================================================== ``Handle`` クラスは、アプリケーションで必要となるハンドルを定義するための親クラスです。 このクラスを継承して、独自のハンドルを定義することが出来ます。 また、このクラスを継承した定義済みのハンドルクラス(``HBall`` と ``HArrow``)を利用することも出来ます。 外観の描画 ---------------------------- ハンドルとは3Dビュー内のGUIコントロールですので、その外観を描画する必要があります。 その描画のためのシーンオブジェクトを ``Handle`` に登録することが出来ます。 上記の Handle クラスの定義を見ると ``ISceneHolder`` インターフェイスを実装していることが分かります。 ``ISceneHolder`` は次のように定義されたインターフェイスです。 .. code-block:: csharp namespace MoNo.Graphics { /// /// ワールド座標系、カメラ座標系、スクリーン座標系のシーンを束ねるインターフェイスです。 /// public interface ISceneHolder : Core.IBreath { /// /// ワールド座標系のシーンコレクションです。 /// BoundarySceneCollection WorldScenes { get; } /// /// カメラ座標系のシーンコレクションです。 /// SceneCollection CameraScenes { get; } /// /// スクリーン座標系の背景シーンコレクションです。 /// SceneCollection BackgroundScenes { get; } /// /// スクリーン座標系の前景シーンコレクションです。 /// SceneCollection ForegroundScenes { get; } } } この定義からわかるように、 ``Handle`` には各種座標系のシーンを持たせることが出来ます。 ここにハンドルの外観を描画するシーンを登録することになります。 ``Handle`` クラスを継承して独自のハンドルを定義している場合は、そのコンストラクタでシーンを登録すれば良いでしょう。 振る舞いの実装 ---------------------------- ハンドルは、ユーザーのマウスオペレーションに反応して動作しなくてはなりません。 そのような振る舞いを実装するために、 ``Handle`` クラスには次のようにマウスイベントが定義されています。 .. code-block:: csharp 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`` の定義を下記に示します。 .. code-block:: csharp 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 がマウスクリックを検知すると、そのイベントは次の関数で処理されます。 .. code-block:: csharp 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`` イベントを発火 このイベントの流れを図示すると次のようになります。 .. image:: EventFlow.png ハンドルのツリー構造において、まず ``MouseCLickPreview`` イベントがルートから末端に向かって伝播していき、 末端に達するとイベントが反射して逆向きに ``MouseClick`` イベントが伝播していく、という流れです。 そして、この流れは ``Handled`` プロパティが ``true`` になった時点で止まります。 ``Handled`` プロパティが ``true`` ということは、このイベントは既に処理済みだから次に伝播させなくて良いということを意味しているのです。 イベントハンドラを実装するときは、必要に応じてイベント引数の ``Handled`` プロパティを ``true`` にセットするようにして下さい。 Interrupt メソッド ------------------------------------------- 前提知識として、 ``OperationDriver`` の ``Interrupt`` メソッドの解説に目を通しておいて下さい。 Handle における Interrupt メソッドは、マウスのドラッグ操作の実装が典型的な用途です。 ドラッグ操作を実装するには、まず MouseDown イベントでドラッグの開始を検知し、そこから MouseUp イベント待ち状態に入る必要があります。 このときに Interrupt メソッドを使って MouseUp 待ち状態を割り込ませます。 典型的なコードは次のような形式になります。 .. code-block:: csharp handle.MouseDown += (view, e) => { if ( e.Button == MouseButtons.Left) { var mouseUp = new MoNo.Ctrl.LButtonUp(view); mouseUp.MouseMove += ...; // ドラッグ操作中のMouseMoveでプレビュー表示を行うなど e.Interrupt(mouseUp); } }