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 );
}
}
}
次の順番でイベントが発火されることが分かります。
- まず
MouseClickPreview
イベントを発火 - 次に子ハンドルの
OnMouseClick
を呼び出す - 最後に
MouseClick
イベントを発火
このイベントの流れを図示すると次のようになります。
ハンドルのツリー構造において、まず 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);
}
}