WisdomSoft - for your serial experiences.

4.1 マウス入力

ウィンドウ上でマウスカーソルが移動したり、マウスのボタンを押すなどされたときに反応するプログラムを作成します。マウス入力を感知するには、コントロールで発生したイベントを処理します。

4.1.1 イベントに反応する

アプリケーションを操作するユーザーは、アプリケーションウィンドウに対して何らかの要求をするとき、マウスカーソルをウィンドウに合わせてクリックしたり、キーボードの何らかのキーを押します。アプリケーションは、この情報を受け取ってユーザーからの要求を認識し、処理を進めます。

このように、ユーザーの操作によって発生した操作をイベントと呼びます。イベントの種類は様々ですが、基本はマウスとキーボードからの入力になります。他にも、ウィンドウサイズの変更、ウィンドウ座標の変更、再描画などもイベントです。こうしたイベントはプログラムが能動的に動くのではなく、ユーザーからの入力を待って、イベントが発生するとシステムによって呼び出されるという受動的な特性を持っています。こうした、イベントによってプログラムの機能が実行される GUI の特性をイベントドリブンと呼びます。

例えば、アクションゲームやシューティングゲームで、キャラクターを動かす方法を考えます。多くの場合は、キーボードの矢印キーか、マウスカーソルの移動を認識して、これに合わせてキャラクターを描画するという方法になります。プログラムはユーザーが入力するまでは何もせずに待ち続け(もしくは内部処理に専念)、イベントが発生した時点で必要な処理を瞬間的に行い、処理が終われば再びユーザーからの入力を待ちます。

図1 イベントの流れ
図1 イベントの流れ

ユーザーからの入力を認識するには、Control クラスのイベントに適切なデリゲートを追加する方法と、Control クラスのメソッドをオーバーライドする方法があります。イベントメンバを利用する方法は、外部からコントロールで発生したイベントを処理するときに使い、後者は Control クラスまたはそのサブクラスを継承して新しいコントロールを定義するときに使うことができます。

ユーザーがマウスやキーボードなどから入力を行うと、Windows がこれに反応してメッセージを生成します。Application クラスの Run() メソッドは、メッセージを受信すると、対象コントロールのメッセージを処理する適切なメソッドを呼び出します。一般に、このようなイベント処理メソッドをイベントハンドラと呼びます。System.Windows.Forms 名前空間のコントロールは、On から始まる protected 修飾子を指定しているメソッドがイベントハンドラです。

先に、メソッドをオーバーライドする方法を紹介します。この方法は、実は OnPaint() メソッドをオーバーライドした方法とまったく同じです。OnPaint() メソッドは、ウィンドウのクライアント領域が無効状態となり、描画処理が要求されたときに呼び出されるメソッドでした。これと同様に、マウス入力が発生したときにメッセージループから呼び出されるメソッドがあります。

ユーザーがコントロール上でマウスボタンを押した場合は OnMouseDown() メソッドが、マウスボタンを離した場合は OnMouseUp() メソッドが呼び出されます。これらのメソッドをオーバーライドして、ボタンが押されたときや離されたときのコードをメソッド内部に追加することで、コントロールはユーザーに反応することができます。

Control クラス OnMouseDown() メソッド
protected virtual void OnMouseDown(MouseEventArgs e)
Control クラス OnMouseUp() メソッド
protected virtual void OnMouseUp(MouseEventArgs e)

これらのメソッドに渡されるイベント引数 e パラメータは、System.Windows.Forms.MouseEventArgs クラスのオブジェクトです。このクラスは、発生したマウスイベントのカーソル座標や押されたボタンなどの情報を提供します。

System.Windows.Forms.MouseEventArgs クラス
System.Object
    System.EventArgs
        System.Windows.Forms.MouseEventArgs
[ComVisible(true)]
public class MouseEventArgs : EventArgs

マウスイベントが発生したときのカーソルの X 座標は X プロパティに、Y 座標は Y プロパティに保存されています。

MouseEventArgs クラス X プロパティ
public int X { get; }
MouseEventArgs クラス Y プロパティ
public int Y { get; }

例えば、OnMouseDown() メソッドをオーバーライドして、MouseEventArgs オブジェクトからカーソルの座標を取得します。その結果をフィールドに保存することで、ユーザーが最後にクリックした座標を他のメソッドに知らせることができます。この座標を OnPaint() メソッドで利用すれば、ユーザーがクリックした座標に動的に図形を描画する、ということができるようになります。

コード1
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 実行結果

コード1では、Form を継承する Test クラスで OnMouseDown() メソッドをオーバーライドしています。このメソッドは、ウィンドウのクライアント領域がクリックされるとメッセージループから呼び出されるので、明示的に呼び出す必要はありません。

Test クラスでは、Point 構造体の pt フィールドに楕円を描画する座標を格納しています。最初は (0, 0) に楕円が描画されていますが、マウスをクリックすると OnMouseDown() メソッドが呼び出されて、pt の値をクリックされた座標に上書きします。ただし、これだけでは OnPaint() メソッドを呼び出して再描画することができないため、一度ウィンドウ全体を無効にして再描画するように仕掛ける必要があります。

コントロールを再描画するには OnMouseDown() メソッドの最後で呼び出している Invalidate() メソッドを使います。

Control クラス 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 イベントを発生させています。

Control クラス MouseDown イベント
public event MouseEventHandler MouseDown
Control クラス MouseUp イベント
public event MouseEventHandler MouseUp

これらのイベントに登録するのは System.Windows.Forms.MouseEventHandler デリゲートです。

System.Windows.Forms.MouseEventHandler デリゲート
[Serializable]
public delegate void MouseEventHandler(object sender, MouseEventArgs e)

sender パラメータには、このイベントを発生させたソースオブジェクトが、e パラメータにはマウスイベントの情報を提供する MouseEventArgs オブジェクトが渡されます。e パラメータに渡されるオブジェクトは OnMouseDown() メソッドのパラメータで受け取ったオブジェクトと同じです。

コード2
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 イベントが発生します。マウスカーソルの位置でキャラクターを動かして、クリックで何らかの動作を指示するなど、マウスイベントを組み合わせることで簡単なゲームの基盤を作ることができます。

Control クラス OnMouseMove() メソッド
protected virtual void OnMouseMove(MouseEventArgs e)
Control クラス MouseMove() イベント
public event MouseEventHandler MouseMove

OnMouseMove() メソッドのパラメータ e や MouseMove イベントのデリゲート型は OnMouseDown() メソッドや MouseDown イベントとまったく同じです。

コード3
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 プロパティから取得します。

MouseEventArgs クラス Button プロパティ
public MouseButtons Button {get;}

このプロパティは、押されたボタンを表す System.Windows.Forms.MouseButtons 列挙体のメンバの値、またはその組み合わせを提供します。

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 となります。

コード4
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 実行結果

コード4は、MouseMove イベントと MouseUp イベントでどのボタンが押されているかによって処理を分岐させるプログラムです。まず、MouseMove イベントを処理する Test_MouseMove() メソッドでは、押されているボタンが、左ボタンか、右ボタンが、または両方押されているかで楕円の色を変更します。何もボタンを押していなければ黒、左ならば赤、右ならば青、両方ならば緑色になります。

MouseUp イベントを処理する Test_MouseUp() メソッドでは、XButton1 と XButton2 で、矩形のサイズを変更します。XButton1 と XButton2 を入力するには、5つボタンマウスを使う必要があります。ただし、これらのボタンは万人が使用しているものではないので重要な機能に割り当てるべきではありません。基本的な操作は左ボタンと右ボタンだけで行えるように設計し、頻繁に行われる動作の一部をショットカットするという目的でこうした拡張ボタンを利用するべきでしょう。

4.1.5 マウスホイール

すべてのマウスに標準ではありませんが、近年ではインターネットブラウザやビジネス文書アプリケーションなどの垂直スクロールを行うホイールボタンが一般的です。ホイールはマウスの中央ボタン部分に設置され、指で回転させることができます。垂直スクロールバーまでマウスカーソルを移動させてドラッグさせる手間を省略できるため、特にビジネスアプリケーションでは一般的に利用されています。また、水平スクロールも可能なマウスも存在します。

ホイールの回転は MouseDown や MouseUp イベントではなく、OnMouseWheel() メソッドから呼び出される MouseWheel イベントとして独立しています。

Control クラス OnMouseWheel メソッド
protected virtual void OnMouseWheel(MouseEventArgs e)
Control クラス MouseWheel イベント
public event MouseEventHandler MouseWheel

OnMouseWheel() メソッドのパラメータや、イベントのデリゲート型は OnMouseDown() メソッドや MouseDown イベントとまったく同じです。ホイールの回転や方向は MouseEventArgs クラスの Delta プロパティから取得します。

MouseEventArgs クラス Delta プロパティ
public int Delta { get; }

ホイールは均等な間隔で1回分の移動と定める刻み目が存在します。この刻み目に当たる1移動が発生するとホイールイベントが発生し OnMouseWheel() が呼び出されます。Delta に格納されている値でユーザーがマウスに向かって前方に回転させたのか、後方に回転させたのかが分かります。

Windows は定数で、マウスホイールの1移動を 120 と定めていますが、この値はあまり重要なものではありません。通常は 120 以外の値が格納されることは無いと考えられますが、例えば、一回のホイール移動により精密な高解像度のマウスであれば 120 よりも小さな値が返るとされています。しかし、どちらにしても一般的に数値は重要ではありません。

Delta の値は、マウス前方に向かってホイールを回転させると正数に、後方に向かって回転させると負数になります。

コード5
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 実行結果

コード5は、マウスホイールを回転させるとウィンドウ上の矢印が回転した方向に従って伸びていくプログラムです。ホイールを前方に向かって動かすと Delta の値は正数(おそらくは 120)に、後方に向かって動かすと負数(おそらくは -120)が格納されています。