WisdomSoft - for your serial experiences.

2.3 線で図形を描画

フォーム上に線や楕円を描画し、任意の図形を表示します。

2.3.1 フォーム上に絵を描く

どのようなゲームであれ、ゲームプログラミングで記述するコードの多くは描画処理に費やされます。また、ゲームの処理にかかるコンピュータへの負荷も描画処理が大部分を占めることになります。ゲームのようなマルチメディアアプリケーションを開発する場合、描画処理の知識がまず必要になります。

描画処理も出力デバイスに対する出力のひとつと考えられますが、IO におけるストリームの概念と同じように抽象化されています。ディスプレイに描画を行うには、ディスプレイを表す System.Drawing.Graphics クラスのオブジェクト取得しなければなりません。Windows では物理的なデバイスを抽象化してデバイスコンテキストと呼ばれる仮想オブジェクトを操作します。すなわち、Graphics オブジェクトは古い Win32 API 時代におけるデバイスコンテキストに相当するものです。

System.Drawing.Graphics クラス
System.Object 
    System.MarshalByRefObject 
        System.Drawing.Graphics
[ComVisible(false)]
public sealed class Graphics : MarshalByRefObject, IDisposable

Graphics クラスはコンストラクタを公開していないため、直接インスタンスを生成することはできません。Graphics クラスのインスタンスは、参照するデバイスに応じてシステムが適切なインスタンスを生成してくれます。通常、Graphics オブジェクトを取得して描画を行うには Control オブジェクトの描画イベントを処理する必要があります。

これまではフォームを表示するだけでしたが、この場ではさらに表示したフォームのクライアント領域に図形を描画します。図形の描画はアプリケーションが能動的に実行するのではなく、ウィンドウが表示され、Windows に描画が要求されるたびにコードを実行しなければなりません。GUI アプリケーションのようなイベントドリブン環境では、CUI のようにプログラムが制御の主導を握ることができません。必要とされるときに、必要な描画処理を行う必要があります。

ウィンドウの描画が必要になると Control クラスの OnPaint() メソッドが呼び出されます。OnPaint() メソッドはコントロール自身の Paint イベントを発生させています(イベントの仕組みについては後述します)。

Control クラス OnPaint() メソッド
protected virtual void OnPaint(PaintEventArgs e)

OnPaint() メソッドではパラメータに System.Windows.Forms.PaintEventArgs クラスのオブジェクトを受け取ります。このオブジェクトは描画イベントに関する情報を提供します。

System.Windows.Forms.PaintEventArgs クラス
System.Object 
    System.EventArgs 
        System.Windows.Forms.PaintEventArgs
public class PaintEventArgs : EventArgs, IDisposable

Form クラスを継承した独自のクラスで OnPaint() メソッドをオーバーライドし、OnPaint() メソッドのパラメータに与えられた PaintEventArgs オブジェクトから Graphics オブジェクトを取得することができます。PaintEventArgs クラスは Graphics オブジェクトを提供する Graphics プロパティを公開しています。

PaintEventArgs クラス Graphics プロパティ
public Graphics Graphics { get; }

PaintEventArgs の Graphics プロパティは、描画イベントが発生したコントロールを出力対象とするデバイスコンテキストに関連付けられています。すなわち、この Graphics オブジェクトを使って描画処理を行うと、対象のコントロールに絵が描かれるという仕組みになります。

まず最初に、単純な一本の線から描画しましょう。線は Graphics クラスの DrawLine() メソッドから描画することができます。このメソッドでは、線の開始座標と終端座標を指定し、その2つの座標を結ぶ線を描画します。

Graphics クラス DrawLine() メソッド
public void DrawLine( Pen pen, Point pt1, Point pt2 )
public void DrawLine( Pen pen, int x1, int y1, int x2, int y2 )

このメソッドはオーバーラードされているため、2点の座標を整数または Point オブジェクトで指定することができます。整数の場合 x1 パラメータと y1 パラメータに最初の点を、x2 パラメータと y2 パラメータに次の点の座標を指定します。Point オブジェクトで指定する場合は pt1 パラメータに最初の点を、pt2 パラメータに次の点を指定します。

DrawLine() メソッドの最初のパラメータで指定する pen は、System.Drawing.Pen クラスのオブジェクトです。Pen クラスはペンと呼ばれる線そのものの情報を表すクラスで、線の幅や色などを提供します。DrawLine() メソッドはこの Pen オブジェクトの情報に従って線を描画します。

System.Drawing.Pen クラス
System.Object
    System.MarshalByRefObject
        System.Drawing.Pen
public sealed class Pen : MarshalByRefObject, ICloneable, IDisposable

Pen クラスの詳細については後述します。DrawLine() メソッドで線を描画するには Pen オブジェクトが必要になるので、この場では Pen() コンストラクタを呼び出して Pen クラスのインスタンスを生成する方法だけを紹介します。

Pen クラスのコンストラクタ
public Pen( Color color )
public Pen( Color color, float width )

Pen コンストラクタでは、色のみを指定する方法と線の幅をあわせて指定する方法があります。color には線の色を、width には幅を指定します。width を省略した場合は 1 ピクセルの幅を持つペンが生成されます。

さて、改めて線を描画する手順をまとめると次のようになります。

  1. Control クラスの OnPaint() メソッドをオーバーライドする
  2. Pen クラスのインスタンスを生成する
  3. Graphics オブジェクトの DrawLine() メソッドを呼び出す

描画対象のコントロールの OnPaint() メソッドをオーバーライドする必要があるため、Control クラスまたはそのサブクラスの何らかのコントロールを継承する必要があるので注意してください。

コード1
using System.Drawing;
using System.Windows.Forms;

public class Test : Form
{
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint(e);
		Pen pen = new Pen(Color.Black);
		e.Graphics.DrawLine(pen, 10, 10, 200, 100);
	}

	static void Main() 
	{
		Application.Run(new Test());
	}
}
実行結果
コード1 実行結果

コード1は、Form クラスを継承して OnPaint() メソッドをオーバーライドしています。OnPaint() メソッドは、フォームに再描画が必要になったときにメッセージループから呼び出されます。Control クラスの OnPaint() メソッドは Paint イベントを発生させるという処理が行われいます。従来の役割を失わないために、オーバーライドした OnPaint() メソッドでは基本クラスの OnPaint() メソッドを呼び出しています。

Pen オブジェクトには、定義済みの色とされている Color.Black プロパティを渡しています。サイズは指定していないので、この Pen オブジェクトは 1 ピクセルの黒いペンを表します。

2.3.2 OnPaint() メソッドのタイミング

OnPaint() メソッドは、コントロールを再描画しなければならない時に呼び出されます。このメソッドがいつ呼び出されるかはシステムの都合です。再描画が必要なときとは、例えばウィンドウが他のウィンドウに隠された後、再び表示されようとしたときなどです。ウィンドウが最初に表示されるときも、コントロールの描画が必要なので OnPaint() メソッドが呼び出されます。

Windows は、ウィンドウが他のウィンドウに隠されたり、画面の外にはみ出したり、あるいは最小化されるなどの理由で隠されると、描画している内容をリセットしてしまいます。この状態で再びウィンドウが表示されようとすると、リセットされた領域を再び描画しようとして OnPaint() メソッドが呼び出されるのです。

では、表示されたときに一度だけ描画処理を行い、その後は OnPaint() メソッドで何も描画しないようにプログラムするとどうなるのでしょうか。この結果は、次のプログラムを実行して確認してみましょう。

コード2
using System.Drawing;
using System.Windows.Forms;

public class Test : Form
{
	private bool isFinished = false;
	protected override void OnPaint(PaintEventArgs e) 
	{
		base.OnPaint(e);
		if (isFinished) return;

		Pen pen = new Pen(Color.Black);
		for(int i = 0 ; i < 40 ; i++) 
		{
			e.Graphics.DrawLine(pen, 0, 10 * i, 400, 10 * i);
			e.Graphics.DrawLine(pen, 10 * i , 0 * i, 10 * i, 400);
		}
		isFinished = true;
	}
	static void Main() 
	{
		Test form = new Test();
		form.Size = new Size(450, 450);
		Application.Run(form);
	}
}
実行結果
コード2 実行結果

コード2は、DrawLine() メソッドを for 文の中で繰り返すことによって網模様の図形を描画しています。この OnPaint() メソッドでは過去に一度呼び出されたことがあるかどうかを判断するための isFinished 変数を調べ、一度描画されたことがある場合は描画処理を行わないようにプログラムしてあります。

つまり、このプログラムで OnPaint() メソッドによる網模様が描画されるのは表示される瞬間に呼び出されたときの一度きりということになります。その後、他のウィンドウなどによってフォームの一部が隠されてしまうと、その部分がリセットされてしまうことを確認できるでしょう。

図1 IEでウィンドウの一部を隠した場合
図1 IEでウィンドウの一部を隠した場合

ウィンドウの一部がリセットされると、再び OnPaint() メソッドが呼び出されますが、2度目以降の呼び出しでは描画処理を行わないため図の一部が欠けてしまいます。OnPaint() メソッドは、このように頻繁に呼び出されるメソッドなので、このメソッドの中で描画に関係の無いような処理や、負荷がかかるような処理はできるだけ避けて下さい。

2.3.3 矩形と楕円

DrawLine() メソッドのように、Graphics クラスの Pen オブジェクトを使って線による図形を描画するメソッドはすべて Draw~ から始まります。DrawLine() メソッドを何度も呼び出すことでどんな図を描画することもできますが、矩形や楕円などの基本的な図形は専用のメソッドで描画することができます。

矩形を描画するには DrawRectangle() メソッドを呼び出します。このメソッドはペンと長方形の左上隅の座標と幅と高さを指定します。

Graphics クラス DrawRectangle() メソッド
public void DrawRectangle(Pen pen, int x, int y, int width, int height)

pen パラメータには矩形の周りを描画する線に利用する Pen オブジェクトを指定します。x パラメータと y パラメータにはそれぞれ矩形の左上隅を表す X 座標と Y 座標を指定します。width パラメータには矩形の幅、height パラメータには高さを指定します。

楕円を描画する場合は DrawEllipse() メソッドを呼び出します。

Graphics クラス DrawEllipse() メソッド
public void DrawEllipse(Pen pen, int x, int y, int width, int height)

DrawEllipse() メソッドの使い方は DrawRectangle() メソッドと同じです。楕円は、指定した矩形に外接するように描画されます。

コード3
using System.Drawing;
using System.Windows.Forms;

public class Test : Form
{
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint(e);
		Pen pen = new Pen(Color.Black);
		e.Graphics.DrawRectangle(pen, 10, 10, 300, 200);
		e.Graphics.DrawEllipse(pen, 10, 10, 300, 200);
	}

	static void Main() 
	{
		Application.Run(new Test());
	}
}
実行結果
コード3 実行結果

コート3は、DrawRectangle() メソッドで長方形を描画しています。その後、まったく同じ座標とサイズで DrawEllipse() メソッドを呼び出しています。楕円が長方形の中に正しく納まっていることを確認できるでしょう。

2.3.4 円弧

DrawEllipse() メソッドは、指定した矩形に外接する閉じた円を描画しました。これ以外に DrawArc() メソッドを使えば、円の一部だけを描画するということができるようになります。

Graphics クラス DrawArc() メソッド
public void DrawArc(Pen pen, int x, int y, int width, int height, int startAngle, int sweepAngle)

5つ目までのパラメータまでは DrawEllipse() と同じです。pen パラメータに線を描画する Pen オブジェクトを設定し、x パラメータと y パラメータに円弧が外接する矩形の左上隅を表す X 座標と Y 座標を、width パラメータに幅、height パラメータに高さを指定します。startAngle パラメータには円弧の開始点の角度を度単位で指定し、sweepAngle パラメータには円弧の終了点を同じく度単位の角度で指定します。startAngle パラメータも sweepAngle パラメータも時計回りでの角度になるので注意してください。

また、DrawPie() メソッドを使うことで円の一部が欠けた図形を描画することができます。扇形やパックマンのような形と表現すれば分かりやすいでしょうか。

Graphics クラス DrawPie() メソッド
public void DrawPie(Pen pen, int x, int y, int width, int height, int startAngle, int sweepAngle)

DrawPie() メソッドのパラメータは DrawArc() メソッドと同じです。ただし、開始点と終了点から中心に向かって半径の線が引かれて円弧が閉じられます。この部分が DrawArc() メソッドと違います。

コード4
using System.Drawing;
using System.Windows.Forms;

public class Test : Form
{
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint(e);
		Pen pen = new Pen(Color.Black);
		e.Graphics.DrawArc(pen, 10, 10, 200, 200, 0, 235);
		e.Graphics.DrawPie(pen, 220, 10, 200, 200, 30, 300);
	}

	static void Main() 
	{
		Application.Run(new Test());
	}
}
実行結果
コード4 実行結果

コード4の実行結果を見れば、DrawArc() と DrawPie() メソッドの違いが確認できます。DrawArc() メソッドは開始点から終了点までの円を引くだけなのに対して、DrawPie() メソッドはそれに加えて開始点と終了点から円の中心に向かって半径を線で結びます。