WisdomSoft - for your serial experiences.

4.11 直接描画

WPF では図形を UIElement (Shape) オブジェクトの構造で表現できますが、図の構造を記録する必要がない場合、グラフィックスデバイスに図形を直接描画することもできます。

4.11.1 線の描画

Shape オブジェクトによる図形の描画は、木構造に配置されたの論理的な図形オブジェクトの関係に従って WPF が描画してくれるというものでした。この作業は極めてオブジェクト指向的で、図形の表現や管理をより自由にしてくれるものです。この仕組みによって、開発者は再描画メッセージの受信を待ち構え、図形を描画する手順や座標の管理、バッファリングなどの作業から解放されました。

しかし、それでも GDI や GDI+ を経験してきた開発者はこの仕組みを疑問に思うかもしれません。単純な矩形を描画するには、木構造の UIElement オブジェクトの関係を作らなければなりません。場合によっては、この作業は冗長に感じることでしょう。

WPF では、GDI の仕組みを完全に捨てたわけではありません。UIElement の木構造は、最終的にはデバイスに描画しなければならないことに変わりはないのです。これまでのプログラムでは、各種コントロールや Shpae オブジェクトは、オブジェクトが提供する情報に基づいて、最終的に WPF によって描画されていたのです。UIElement の内部では、Windows32 API と同じように、再描画が要求された時点でデバイスに対して図形の描画を行っています。

もちろん、この作業は開発者が独自に拡張することも可能です。開発者が独自の UIElement を実装する場合、従来のプログラミングモデルと同様に再描画要求を待機し、再描画要求が発生した時点でデバイスに対して何らかの描画命令を発行します。この開発スタイルは Windows 32 API の WM_PAINT メッセージの処理や、System.Windows.Forms のコントロールの OnPaint() メソッドのオーバーライドと同様です。

WPF の UIElement には、OnRender() という protected メソッドが存在します。OnRender() メソッドは、オブジェクトが描画されようとしたときに WPF から呼び出されるメソッドで、対象の UIElement オブジェクトの見た目を生成する役割を持ちます。

UIElement クラス OnRender() メソッド
protected virtual void OnRender (DrawingContext drawingContext)

このメソッドは、パラメータに System.Windows.Media.DrawingContext クラスのオブジェクトを受け取ります。このオブジェクトは、仮想デバイスに対して任意の図形やテキストを直接描画する機能を提供します。従来の GDI における HDC 型のハンドル、GDI+ における Graphics クラスに相当するデバイスコンテキストの一種です。

System.Windows.Media.DrawingContext クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
    System.Windows.Media.DrawingContext
public abstract class DrawingContext : DispatcherObject, IDisposable

DrawingContext クラスは抽象クラスですが、このクラスのインスタンスをアプリケーションが生成する必要はありません。DrawingContext は抽象化された出力デバイスを操作するために使われるため、アプリケーションにとって実装は重要ではありません。重要なのは、OnRender() メソッドに渡された DrawingContext が、ディスプレイ上の UIElement の描画領域を参照しているということです。

DrawingContext クラスを用いた描画は、UIElement を使った図形やコントロールを表すオブジェクトを設定するものではなく、メソッドを用いて直接図形を出力する作業になります。描画用のメソッドには Draw~ から始まる名前が付けられています。例えば、1 本の線を描画するには DrawLine() メソッドを呼び出します。

DrawingContext クラス DrawLine() メソッド
public abstract void DrawLine (
    Pen pen,
    Point point0, Point point1
)

pen パラメータには、線の描画に利用するブラシや線の太さ、スタイルなどを保存する Pen オブジェクトを設定します。point0 には線の開始点を表す座標を、point1 には終了点を表す座標を指定してください。

コード
using System;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Pen pen = new Pen(Brushes.Black, 10);
		Point pt1 = new Point(10, 10);
		Point pt2 = new Point(200, 100);
		drawingContext.DrawLine(pen, pt1, pt2);
	}
}
実行結果
コード1 実行結果

コード1は、Line オブジェクトを用いるのではなく、OnRender() メソッドをオーバーライドし、DrawingContext オブジェクトから直接描画するプログラムです。このプログラムの構造は、System.Windows.Forms 名前空間、および System.Drawing 名前空間による GUI アプリケーション開発で一般的に使われていたものです。アプリケーションが独自に UIElement を拡張したい場合は、この方法で描画する必要があります。

このプログラムでは、OnRender() メソッドから図形を描画するために、独自の新しいコンポーネントととして構築しています。Test クラスは FrameworkElement を継承しているため、描画要素として扱うことができます。Window オブジェクトの Content プロパティに、Test クラスのオブジェクトを設定しているのはそのためです。

FrameworkElement クラスは UIElement を継承している WPF の描画要素のルートクラスです。WPF の描画要素に共通する基本的なプロパティやイベント、メソッドを提供しています。通常、WPF を拡張する新しい要素を開発するにはこのクラスを継承します。Shape や Control クラスもまた、このクラスから派生していることを思い出してください。

4.11.2 長方形と楕円

長方形や楕円家も、DrawLine() メソッドと同様に、メソッドに対して長方形を描画するための情報を渡して描画することができます。長方形を描画するには DrawRectangle() メソッドを使います。

DrawingContext クラス DrawRectangle() メソッド
public abstract void DrawRectangle (
    Brush brush, Pen pen, Rect rectangle
)

brush パラメータには、長方形の内部を塗りつぶすためのブラシを、pen パラメータには長方形の輪郭線を描画するためのペンを指定します。

描画する長方形の位置とサイズは rectangle パラメータに指定した情報に基づきます。

コード2
using System;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Pen pen = new Pen(Brushes.Black, 10);
		Rect rect = new Rect(10, 10, 400, 200);
		drawingContext.DrawRectangle(Brushes.Red, pen, rect);
	}
}
実行結果
コード2 実行結果

コード2は、黒いブラシを用いたペンで長方形の枠を描画し、赤いブラシで内部を塗りつぶしています。因みに、このような Draw~ という名前で始まるメソッドに渡す Brush オブジェクトや Pen オブジェクトは null を指定することも可能です。例えば、内部を塗りつぶしたくない場合はブラシを null に設定します。

角の丸い長方形を描画するには DrawRoundedRectangle() メソッドを使います。

DrawRoundedRectangle() メソッド
public abstract void DrawRoundedRectangle (
    Brush brush, Pen pen, Rect rectangle,
    double radiusX, double radiusY
)

brush パラメータ、pen パラメータ、rectangle パラメータまでは DrawRectangle() メソッドと同じです。

radiusX パラメータには水平方向の丸み、radiusY パラメータには垂直方向の丸みのピクセル数をそれぞれ指定します。これらの値の意味は、Rectangle クラスにおける RadiusX プロパティと RadiusY プロパティと同じです。

コード3
using System;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Pen pen = new Pen(Brushes.Black, 10);
		Rect rect = new Rect(10, 10, 400, 200);
		drawingContext.DrawRoundedRectangle(Brushes.Red, pen, rect , 100, 50);
	}
}
実行結果
コード3 実行結果

コード3では、DrawRoundedRectangle() メソッドを用いて角の丸い長方形を描画しています。

楕円形を描画するには DrawEllipse() メソッドを使います。

DrawEllipse() メソッド
public abstract void DrawEllipse (
    Brush brush, Pen pen, Point center,
    double radiusX, double radiusY
)

brush パラメータには楕円の内部を塗りつぶすブラシを、pen パラメータには楕円の輪郭を引くペンを指定します。

center パラメータは楕円の中心を表す座標を指定します。

radiusX パラメータは楕円の水平方向の半径、radiusY パラメータは垂直方向の半径を指定します。

コード4
using System;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Pen pen = new Pen(Brushes.Black, 10);
		Point pt = new Point(210, 110);
		drawingContext.DrawEllipse(Brushes.Red, pen, pt, 200, 100);
	}
}
実行結果
コード4 実行結果

コード4は、DrawEllipse() メソッドを用いて楕円を描画しています。

4.11.3 模様の描画

DrawRectangle() や DrawEllipse() を使えば、簡単な長方形や楕円を描画することは可能ですが、より複雑な図形を表現したい場合は Shape における Path クラスに相当する処理が必要になってきます。そこで、DrawingContext では Geometry オブジェクトを描画する DrawGeometry() メソッドを用意しています。

DrawingContext クラス DrawGeometry() メソッド
public abstract void DrawGeometry (
    Brush brush, Pen pen, Geometry geometry
)

brush パラメータには内部を塗りつぶすブラシを、pen パラメータには輪郭線を引くペンを指定します。

geometry パラメータには描画する模様を表す任意の Geometry オブジェクトを指定することができます。Geometry の表現力については前述した通りのなので、このメソッドを使えば、事実上あらゆる形を表現することができます。

コード5
using System;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Rect rect1 = new Rect(10, 10, 200, 200);
		Rect rect2 = new Rect(110, 10, 200, 200);
		
		EllipseGeometry ellipse1 = new EllipseGeometry(rect1);
		EllipseGeometry ellipse2 = new EllipseGeometry(rect2);
		
		CombinedGeometry combine = new CombinedGeometry(ellipse1, ellipse2);
		combine.GeometryCombineMode = GeometryCombineMode.Xor;

		drawingContext.DrawGeometry(Brushes.Black, null, combine);
	}
}
実行結果
コード5 実行結果

コード5は、2 つの楕円形を Xor で結合させた CombinedGeometry オブジェクトを描画しています。Geometry は非常に強力な表現力を持っているため、このメソッドを使えば曲線や幾何学的な模様を一度に描画することができます。

4.11.4 テキストの描画

これまで、テキストを描画するには、コントロールのコンテンツとして文字列を指定するか、TextBlock など、テキストを表示する専用のコントロールを使いました。しかし、テキストも他の図形の描画と同様に、コントロールに直接描画することができます。テキストの描画には DrawText() メソッドを使います。

DrawingContext クラス DrawText() メソッド
public void DrawText (
    FormattedText formattedText,
    Point origin
)

DrawText() メソッドは TextBlock クラスのオブジェクトのように文字列を渡せば表示することができるというほど単純なものではありません。formattedText パラメータに描画するテキストの文字列、フォント、地域情報などを組み合わせて提供する System.Windows.Media.FormattedText クラスのオブジェクトを指定しなければなりません。origin パラメータに、描画するテキストの座標を指定します。

System.Windows.Media.FormattedText クラス
public class FormattedText

FormattedText クラスのコンストラクタは、オーバーロードされています。最も単純なコンストラクタでも、次のように多くのパラメータを渡さなければなりません。

FormattedText クラスのコンストラクタ
public FormattedText (
    string textToFormat, CultureInfo culture,
    FlowDirection flowDirection, Typeface typeface,
    double emSize, Brush foreground
)

textToFormat パラメータは、表示するテキストを表す文字列を指定します。

culture パラメータには、System.Globalization 名前空間にある地域情報を提供する CultureInfo クラスのオブジェクトを渡します。このオブジェクトは、テキストの地域情報を表します。例えば、日本語のアプリケーションであれば "ja" カルチャを表すオブジェクトを設定することになります。

flowDirection パラメータは、文字が流れる方向を表す System.Windows.FlowDirection 列挙体のいずれかのメンバを設定します。

System.Windows.FlowDirection 列挙体
[LocalizabilityAttribute(LocalizationCategory.None, Readability=Readability.Unreadable)] 
public enum FlowDirection

この列挙体には、コンテンツが左から右に流れることを表す LeftToRight メンバと、右から左に流れることを表す RightToLeft メンバがあります。通常、英語や日本語など、多くの言語は左から右に流れるので LeftToRight を指定します。アラビア語など、右から左に流れる言語は RightToLeft を指定することになります。

typeface パラメータには、フォントの各種情報を 1 つにまとめる System.Windows.Media.Typeface クラスのオブジェクトを設定します。このクラスは、Pen クラスや Rect クラスなどと同様に、フォント名やスタイル情報など、これまで個別のプロパティに設定していた値を統合します。

System.Windows.Media.Typeface クラス
System.Object 
  System.Windows.Media.Typeface
public class Typeface

このクラスのコンストラクタはオーバーロードされています。この場では、最も単純なフォント名を指定するコンストラクタを使います。

Typeface クラスのコンストラクタ
public Typeface (string typefaceName)

必要であれば、このクラスのオブジェクトにはプロパティから、斜体や太字など、そのほかのフォントの設定を行うことができますが、この場では割愛します。DrawText() メソッドは、この Typeface クラスのオブジェクトが表すフォントを使ってテキストを描画します。

最後の emSize パラメータにはフォントサイズを、foreground パラメータにはテキストを塗りつぶすブラシを設定します。

コード6
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		FormattedText text = new FormattedText("ごきげんよう、お姉さま。",
			CultureInfo.GetCultureInfo("ja"),
          		FlowDirection.LeftToRight,
          		new Typeface("MS Pゴシック"), 30, Brushes.Black
		);
		Point pt = new Point(10, 10);
		drawingContext.DrawText(text, pt);
	}
}
実行結果
コード6 実行結果

コード6は、DrawText() メソッドを用いてテキストを UIElement 上に描画しています。FormattedText オブジェクト生成までの流れが少々煩雑ですが、DrawText() メソッド自体の作業は簡単なものです。

4.11.5 イメージの描画

イメージは、ImageSource オブジェクトを用いて、ImageSource オブジェクトが表すイメージを指定した長方形に描画します。イメージを描画するには DrawImage() メソッドを使います。

DrawingContext クラス DrawImage() メソッド
public abstract void DrawImage (
    ImageSource imageSource,
    Rect rectangle
)

imageSource パラメータに表示するイメージを表す ImageSource オブジェクトを、rectangle パラメータにイメージを表示する座標とサイズを格納した Rect オブジェクトを指定します。

コード7
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

class Test : FrameworkElement {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.Content = new Test();

		Application app = new Application();
		app.Run(wnd);
	}

	protected override void OnRender(DrawingContext drawingContext) {
		base.OnRender(drawingContext);

		Uri uri = new Uri("test.jpg", UriKind.RelativeOrAbsolute);
		BitmapImage image = new BitmapImage(uri);
		Rect rect = new Rect(10 , 10 , image.Width, image.Height);
		drawingContext.DrawImage(image, rect);
	}
}
実行結果
コード7 実行結果

コード7は、DrawImage() メソッドを使って ImageSource を直接描画しています。