4.1 マウス入力
4.1.1 イベントに反応する
アプリケーションを操作するユーザーは、アプリケーションウィンドウに対して何らかの要求をするとき、マウスカーソルをウィンドウに合わせてクリックしたり、キーボードの何らかのキーを押します。アプリケーションは、この情報を受け取ってユーザーからの要求を認識し、処理を進めます。
このように、ユーザーの操作によって発生した操作をイベントと呼びます。イベントの種類は様々ですが、基本はマウスとキーボードからの入力になります。他にも、ウィンドウサイズの変更、ウィンドウ座標の変更、再描画などもイベントです。こうしたイベントはプログラムが能動的に動くのではなく、ユーザーからの入力を待って、イベントが発生するとシステムによって呼び出されるという受動的な特性を持っています。こうした、イベントによってプログラムの機能が実行される GUI の特性をイベントドリブンと呼びます。
例えば、アクションゲームやシューティングゲームで、キャラクターを動かす方法を考えます。多くの場合は、キーボードの矢印キーか、マウスカーソルの移動を認識して、これに合わせてキャラクターを描画するという方法になります。プログラムはユーザーが入力するまでは何もせずに待ち続け(もしくは内部処理に専念)、イベントが発生した時点で必要な処理を瞬間的に行い、処理が終われば再びユーザーからの入力を待ちます。
ユーザーからの入力を認識するには、Control クラスのイベントに適切なデリゲートを追加する方法と、Control クラスのメソッドをオーバーライドする方法があります。イベントメンバを利用する方法は、外部からコントロールで発生したイベントを処理するときに使い、後者は Control クラスまたはそのサブクラスを継承して新しいコントロールを定義するときに使うことができます。
ユーザーがマウスやキーボードなどから入力を行うと、Windows がこれに反応してメッセージを生成します。Application クラスの Run() メソッドは、メッセージを受信すると、対象コントロールのメッセージを処理する適切なメソッドを呼び出します。一般に、このようなイベント処理メソッドをイベントハンドラと呼びます。System.Windows.Forms 名前空間のコントロールは、On から始まる protected 修飾子を指定しているメソッドがイベントハンドラです。
先に、メソッドをオーバーライドする方法を紹介します。この方法は、実は OnPaint() メソッドをオーバーライドした方法とまったく同じです。OnPaint() メソッドは、ウィンドウのクライアント領域が無効状態となり、描画処理が要求されたときに呼び出されるメソッドでした。これと同様に、マウス入力が発生したときにメッセージループから呼び出されるメソッドがあります。
ユーザーがコントロール上でマウスボタンを押した場合は OnMouseDown() メソッドが、マウスボタンを離した場合は OnMouseUp() メソッドが呼び出されます。これらのメソッドをオーバーライドして、ボタンが押されたときや離されたときのコードをメソッド内部に追加することで、コントロールはユーザーに反応することができます。
protected virtual void OnMouseDown(MouseEventArgs e)
protected virtual void OnMouseUp(MouseEventArgs e)
これらのメソッドに渡されるイベント引数 e パラメータは、System.Windows.Forms.MouseEventArgs クラスのオブジェクトです。このクラスは、発生したマウスイベントのカーソル座標や押されたボタンなどの情報を提供します。
System.Object System.EventArgs System.Windows.Forms.MouseEventArgs
[ComVisible(true)] public class MouseEventArgs : EventArgs
マウスイベントが発生したときのカーソルの X 座標は X プロパティに、Y 座標は Y プロパティに保存されています。
public int X { get; }
public int Y { get; }
例えば、OnMouseDown() メソッドをオーバーライドして、MouseEventArgs オブジェクトからカーソルの座標を取得します。その結果をフィールドに保存することで、ユーザーが最後にクリックした座標を他のメソッドに知らせることができます。この座標を OnPaint() メソッドで利用すれば、ユーザーがクリックした座標に動的に図形を描画する、ということができるようになります。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Point pt; protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown (e); this.pt = new Point(e.X, e.Y); this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Brush brush = new SolidBrush(Color.Black); e.Graphics.FillEllipse(brush, pt.X, pt.Y, 40, 40); } static void Main() { Application.Run(new Test()); } }
コード1では、Form を継承する Test クラスで OnMouseDown() メソッドをオーバーライドしています。このメソッドは、ウィンドウのクライアント領域がクリックされるとメッセージループから呼び出されるので、明示的に呼び出す必要はありません。
Test クラスでは、Point 構造体の pt フィールドに楕円を描画する座標を格納しています。最初は (0, 0) に楕円が描画されていますが、マウスをクリックすると OnMouseDown() メソッドが呼び出されて、pt の値をクリックされた座標に上書きします。ただし、これだけでは OnPaint() メソッドを呼び出して再描画することができないため、一度ウィンドウ全体を無効にして再描画するように仕掛ける必要があります。
コントロールを再描画するには OnMouseDown() メソッドの最後で呼び出している Invalidate() メソッドを使います。
public void Invalidate()
Invalidate() メソッドを呼び出すと、コントロールの描画領域が無効になるため、ウィンドウを一度隠して再描画させたときと同じように、OnPaint() メソッドが再び呼び出されます。マウスやキー入力を感知して処理を行い、その結果を表示するためにウィンドウを更新する処理には必ず必要になります。これを呼び出さなければ、pt フィールドを更新しても、それが即座にウィンドウの描画に反映されないので注意してください。
また、OnMouseDown() や OnMouseUp() などのイベント処理メソッドは、Control クラスでイベントメンバを呼び出しています。そのため、オーバーライドする場合は必ずスーパークラスのメソッドを呼び出すようにしてください。そうしなければ、次に紹介するイベントが呼び出されなくなってしまいます。
4.1.2 イベントにデリゲートを追加する
OnMouseDown() や OnMouseUp() メソッドをオーバーライドしてマウス入力に反応する方法は Control クラスをオーバーライドするクラスでしか使うことができません。また、オーバーライドの本来の目的はクラスが持っている機能や役割の拡張なので、発生したイベントを処理するという目的でメソッドをオーバーライドするのは設計としては不適切です。コントロールのマウス入力に対する挙動を変更するという目的以外で、OnMouseDown() や OnMouseUp() メソッドをオーバーライドするべきではありません。
単純にコントロールで発生したイベントの通知を受けるという目的であれば、Control クラスが公開しているイベントメンバにデリゲートを追加するという方法が適切です。イベントメンバの名前は、イベントを呼び出す protected なメソッドの On を省いた形です。例えば、コントロール上でマウスボタンが押されると OnMouseDown() メソッドが呼び出され、OnMouseDown() メソッドは自分自身の MouseDown イベントを発生させます。同様に、OnMouseUp() メソッドは MouseUp イベントを発生させています。
public event MouseEventHandler MouseDown
public event MouseEventHandler MouseUp
これらのイベントに登録するのは System.Windows.Forms.MouseEventHandler デリゲートです。
[Serializable] public delegate void MouseEventHandler(object sender, MouseEventArgs e)
sender パラメータには、このイベントを発生させたソースオブジェクトが、e パラメータにはマウスイベントの情報を提供する MouseEventArgs オブジェクトが渡されます。e パラメータに渡されるオブジェクトは OnMouseDown() メソッドのパラメータで受け取ったオブジェクトと同じです。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Point pt; private void Test_MouseDown(object sender, MouseEventArgs e) { this.pt = new Point(e.X, e.Y); this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Brush brush = new SolidBrush(Color.Black); e.Graphics.FillEllipse(brush, pt.X, pt.Y, 40, 40); } public Test() { this.MouseDown += new MouseEventHandler(Test_MouseDown); } static void Main() { Application.Run(new Test()); } }
コード2の結果はコード1と同じです。このプログラムはコード1とまったく同じ動作をしますが、イベントの処理方法が OnMouseDown() メソッドをオーバーライドするではなく、MouseDown イベントに Test_MouseDown() メソッドへのデリゲートを登録する方法を採用しています。
イベントメンバの可視性は public なので、外部から Control で発生したイベントを監視するという場合にもこの方法を採用します。
4.1.3 マウスの移動
マウスカーソルがコントロール上を移動すると OnMouseMove() メソッドが呼び出され、MouseMove イベントが発生します。マウスカーソルの位置でキャラクターを動かして、クリックで何らかの動作を指示するなど、マウスイベントを組み合わせることで簡単なゲームの基盤を作ることができます。
protected virtual void OnMouseMove(MouseEventArgs e)
public event MouseEventHandler MouseMove
OnMouseMove() メソッドのパラメータ e や MouseMove イベントのデリゲート型は OnMouseDown() メソッドや MouseDown イベントとまったく同じです。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Point pt; private Size size = new Size(40, 40); private void Test_MouseMove(object sender, MouseEventArgs e) { this.pt = new Point(e.X - (size.Width / 2), e.Y - (size.Height / 2)); this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Brush brush = new SolidBrush(ForeColor); e.Graphics.FillEllipse(brush, pt.X, pt.Y, size.Width, size.Height); } public Test() { this.MouseMove += new MouseEventHandler(Test_MouseMove); } static void Main() { Application.Run(new Test()); } }
コード3は、ウィンドウ上に描画される楕円がマウスの移動に合わせて追跡してくるプログラムです。例えば、ブロック崩しのようなミニゲームを考えた場合、ボールを跳ね返すバーの操作をマウスカーソルで行います。この実装には、MouseMove イベントを感知してバーの X 座標を設定すればよいのです。
4.1.4 マウスボタン
マウスボタンが押されたり離されたとき、どのボタンが押されたのかが重要になります。多くのプログラムでは、左クリックと右クリックそれぞれに専用の動作を対応させるので、MouseDown や MouseUp イベントを受信したときに、どのボタンによってこのイベントが発生したのかを調べる必要があります。
イベント発生時のマウスボタンの情報は MouseEventArgs クラスの Button プロパティから取得します。
public MouseButtons Button {get;}
このプロパティは、押されたボタンを表す System.Windows.Forms.MouseButtons 列挙体のメンバの値、またはその組み合わせを提供します。
[Flags] [Serializable] [ComVisible(true)] public enum MouseButtons
マウスの左ボタンは Left メンバ、右ボタンは Right メンバ、中央は Middle メンバ、押されていなければ None メンバなど、マウスボタンを表すメンバが提供されています。通常のマウスは 2 ボタン、または 3 ボタンですが、Windows 2000 以降は 5 つボタンマウスがサポートされています。5 つボタンのマウスでは、XButton1 メンバと XButton2 メンバを使います。
マウスの左ボタンと右ボタンの同時押しの動作など、複数のボタンの組み合わせを認識したい場合は | 演算子で組み合わせて使います。左ボタンと右ボタンの同時押しは MouseButtons.Left | MouseButtons.Right となります。
using System.Drawing; using System.Windows.Forms; public class Test : Form { private Point pt; private int size = 40; private void Test_MouseUp(object sender, MouseEventArgs e) { switch(e.Button) { case MouseButtons.XButton1: if (size > 0) size -= 10; break; case MouseButtons.XButton2: if (size < 100) size += 10; break; } this.Invalidate(); } private void Test_MouseMove(object sender, MouseEventArgs e) { switch(e.Button) { case MouseButtons.Left: this.ForeColor = Color.Red; break; case MouseButtons.Right: this.ForeColor = Color.Blue; break; case (MouseButtons.Left | MouseButtons.Right): this.ForeColor = Color.Green; break; default: this.ForeColor = Color.Black; break; } this.pt = new Point(e.X - (size / 2), e.Y - (size / 2)); this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Brush brush = new SolidBrush(ForeColor); e.Graphics.FillEllipse(brush, pt.X, pt.Y, size, size); } public Test() { this.MouseUp += new MouseEventHandler(Test_MouseUp); this.MouseMove += new MouseEventHandler(Test_MouseMove); } static void Main() { Application.Run(new Test()); } }
コード4は、MouseMove イベントと MouseUp イベントでどのボタンが押されているかによって処理を分岐させるプログラムです。まず、MouseMove イベントを処理する Test_MouseMove() メソッドでは、押されているボタンが、左ボタンか、右ボタンが、または両方押されているかで楕円の色を変更します。何もボタンを押していなければ黒、左ならば赤、右ならば青、両方ならば緑色になります。
MouseUp イベントを処理する Test_MouseUp() メソッドでは、XButton1 と XButton2 で、矩形のサイズを変更します。XButton1 と XButton2 を入力するには、5つボタンマウスを使う必要があります。ただし、これらのボタンは万人が使用しているものではないので重要な機能に割り当てるべきではありません。基本的な操作は左ボタンと右ボタンだけで行えるように設計し、頻繁に行われる動作の一部をショットカットするという目的でこうした拡張ボタンを利用するべきでしょう。
4.1.5 マウスホイール
すべてのマウスに標準ではありませんが、近年ではインターネットブラウザやビジネス文書アプリケーションなどの垂直スクロールを行うホイールボタンが一般的です。ホイールはマウスの中央ボタン部分に設置され、指で回転させることができます。垂直スクロールバーまでマウスカーソルを移動させてドラッグさせる手間を省略できるため、特にビジネスアプリケーションでは一般的に利用されています。また、水平スクロールも可能なマウスも存在します。
ホイールの回転は MouseDown や MouseUp イベントではなく、OnMouseWheel() メソッドから呼び出される MouseWheel イベントとして独立しています。
protected virtual void OnMouseWheel(MouseEventArgs e)
public event MouseEventHandler MouseWheel
OnMouseWheel() メソッドのパラメータや、イベントのデリゲート型は OnMouseDown() メソッドや MouseDown イベントとまったく同じです。ホイールの回転や方向は MouseEventArgs クラスの Delta プロパティから取得します。
public int Delta { get; }
ホイールは均等な間隔で1回分の移動と定める刻み目が存在します。この刻み目に当たる1移動が発生するとホイールイベントが発生し OnMouseWheel() が呼び出されます。Delta に格納されている値でユーザーがマウスに向かって前方に回転させたのか、後方に回転させたのかが分かります。
Windows は定数で、マウスホイールの1移動を 120 と定めていますが、この値はあまり重要なものではありません。通常は 120 以外の値が格納されることは無いと考えられますが、例えば、一回のホイール移動により精密な高解像度のマウスであれば 120 よりも小さな値が返るとされています。しかし、どちらにしても一般的に数値は重要ではありません。
Delta の値は、マウス前方に向かってホイールを回転させると正数に、後方に向かって回転させると負数になります。
using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; public class Test : Form { private int y; private void Test_MouseWheel(object sender, MouseEventArgs e) { if (e.Delta > 0) y -= 5; else if (e.Delta < 0) y += 5; this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Pen pen = new Pen(Color.Black, 16); pen.EndCap = LineCap.ArrowAnchor; e.Graphics.DrawLine(pen, Width / 2, Height / 2, Width / 2, Height / 2 + y); } public Test() { this.MouseWheel += new MouseEventHandler(Test_MouseWheel); } static void Main() { Application.Run(new Test()); } }
コード5は、マウスホイールを回転させるとウィンドウ上の矢印が回転した方向に従って伸びていくプログラムです。ホイールを前方に向かって動かすと Delta の値は正数(おそらくは 120)に、後方に向かって動かすと負数(おそらくは -120)が格納されています。