WisdomSoft - for your serial experiences.

6.8 メタファイル

Windows Metafile (WMF) 及び Enhanced Metafile (EMF) 形式の画像ファイルを読み込み描画する方法、及び実行時生成したイメージを出力する方法を解説します。 

6.8.1 ベクタフラフィックス形式のイメージ

ビットマップはピクセルごとに色情報を保存するラスタグラフィックスと呼ばれる形式であったのに対し、メタファイルはイメージを構成する一連の描画手順を記録したレコードと呼ばれるデータを使ってイメージを再現することができるベクタグラフィックスと呼ばれる形式です。現在、インターネットで取引されているイメージやゲームで使われているイメージの多くはラスタグラフィックスです。では、ベクタグラフィックスの役目は無いのでしょうか。

ベクタグラフィックスや写真のような小さな点の集合でイメージを構成する情報を表現することは得意ではありませんが、設計図や機械的な模様を表現することを得意としています。なにより、イメージをデータから再現するという方式なので、拡大や縮小を行っても、ビットマップのように荒くなったりピクセル情報が失われるということがありません。

例えば、ゲーム画面上に表示するウィンドウやボタンなど、独自の描画コンポーネントを表現する手段として適切なのはビットマップよりもメタファイルと考えられます。ウィンドウやボタンなどの描画コンポーネントはサイズを自由に変更して再利用できるような仕組みが好ましいと考えられます。例えば、キャラクターのセリフを表示するウィンドウの場合、文字数によってウィンドウのサイズを変えなければなりません。ビットマップでウィンドウをデザインした場合、ビットマップの既定サイズよりも大きなウィンドウを表示すると、ビットマップが拡大されイメージが荒くなります。しかし、ベクタグラフィックスならば、伸縮してもイメージが荒くなるようなことはありません。なぜならば、ベクタグラフィックスはレコードの手順に従って実行時に描画を行うからです。

6.8.2 メタファイルを読み込む

メタファイルと呼ばれるファイル形式は、Windows MetafileEnhanced Metafile と呼ばれる2つの形式が存在します。Windows Metafile は .wmf という拡張子のファイルで Windows 1.0 のころから存在している古いフォーマットです。単にメタファイル、または標準メタファイルと呼ばれることもあります。

Windows Metafile は 16 ビット時代に作られた古いフォーマットなので、Windows 95 から拡張メタファイルと呼ばれる Enhanced Metafile が登場しました。拡張メタファイルは .emf という拡張子のファイルです。過去に使っていた古いプログラムとの互換性を考える必要がある場合を除いて、通常は拡張メタファイル形式を利用するべきです。

.NET では、Windows Metafile、Enhanced Metafile の両方をサポートしていますが、.NET で生成するメタファイルは EMF+ と呼ばれる新しい形式となっています。EMF+ は従来の Enhanced Metafile と互換性を保ちながら、いくつかの新しい描画コマンドを追加した拡張形式です。EMF+ の拡張子は従来の .emf と同じで、EMF+ に対応していない古いアプリケーションでも開くことができます。

メタファイルは、これまでと同じように Image.FromFile() メソッドから開くことができます。メタファイルは Image クラスを継承する System.Drawing.Imaging.Metafile クラスのコンストラクタからインスタンスを生成することもできます。

Metafile クラスのコンストラクタ
public Metafile(string filename)
public Metafile(Stream stream)

filename パラメータにはディスク上に保存されているメタファイルのパスを指定します。stream パラメータの場合は、メタファイルへのデータストリームを指定します。これらのコンストラクタから得られるインスタンスは、Image クラスの FromFile() メソッドでメタファイルを指定した場合と同じです。FromFile() メソッドは Image クラス型のオブジェクトを返しますが、インスタンスを調べれば Metafile 型であることを確認できるでしょう。Metafile クラスは Image クラスを継承しているので、描画などの基本的な動作は Image クラスと同じように行うことができます。

ディスク上のメタファイルを読み込んで生成した Metafile インスタンスから Graphics クラスの FromImage() メソッドを使って Graphics オブジェクトを取得することはできません。メタファイルを Graphics から操作するには、新規作成した Metafile インスタンスを使います。

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

public class Test : Form
{
	private Metafile image;
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint (e);
		if (image == null) image = new Metafile("test.emf");

		e.Graphics.DrawImage(image, 0, 0);
	}

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

コード1は、ディスクからメタファイルを読み込んで Metafile オブジェクトを作成しています。Metafile を描画するには、これまでと同じように DrawImage() メソッドを使います。メタファイルはビットマップではないのでピクセルを持っていませんが、参考となるサイズを保有しています。この幅と高さはピクセルではなく、メタファイル内で定義されている図形の座標から決定されます。

メタファイルは詳細な描画手順を記録したものなので、ペンやブラシの情報も含んでいます。実行結果の図は 見れば、ペンの性質や色、ブラシの情報が含まれていることが確認できます。この点において、メタファイルは形状や領域を保存するグラフィックスパスやリージョンとは異なります。

6.8.3 メタファイルの情報

Metafile クラスのメソッドやプロパティのほとんどは Image クラスのものを継承しているだけで特に新しい機能はありません。メソッドではいくつかの追加機能がありますが、その中でも特に Metafile オブジェクトを操作する上で重要なものは GetMetafileHeader() メソッドです。

Metafile クラス getMetafileHeader() メソッド
public MetafileHeader GetMetafileHeader()

このメソッドは、Metafile オブジェクトの情報を提供する System.Drawing.Imaging.MetafileHeader クラスのオブジェクトを返します。

System.Drawing.Imaging.MetafileHeader クラス
System.Object
    System.Drawing.Imaging.MetafileHeader
public sealed class MetafileHeader

イメージのサイズや解像度、種類やバージョンなどの詳細情報を取得するには、この MetafileHeader オブジェクトを取得します。Metafile オブジェクトが Windows Metafile なのか Enhanced Metafile なのか、または EMF+ なのかを調べるには Type プロパティを使います。

MetafileHeader クラス Type プロパティ
public MetafileType Type { get; }

Type プロパティの値は System.Drawing.Imaging.MetafileType 列挙体のいずれかのメンバです。

System.Drawing.Imaging.MetafileType 列挙体
[Serializable]
public enum MetafileType

MetafileType 列挙体には、メタファイルの種類を表すメンバを定義しています。Windows Metafile ならば Wmf、Enhanced Metafile ならば Emf という具合になります。

このほかに、メタファイルの矩形を取得する Bounds プロパティ、水平方向の解像度を取得する DpiX プロパティ、垂直方向の解像度を取得する DpiY プロパティなどがあります。

MetafileHeader クラス Bounds プロパティ
public Rectangle Bounds { get; }
MetafileHeader クラス DpiX プロパティ
public float DpiX { get; }
MetafileHeader クラス DpiY プロパティ
public float DpiY { get; }

DpiX プロパティと DpiY プロパティが返す値は、インチあたりのピクセル数です。この値が大きいほど解像度が高く鮮明なイメージになります。サイズや解像度は Image クラスのプロパティから取得できる値と同じものです。

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

public class Test : Form
{
	private Metafile image;
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint (e);
		if (image == null) image = new Metafile("test.emf");

		Brush brush = new SolidBrush(Color.Black);
		Font font = new Font(Font.Name, 20);
		MetafileHeader header = image.GetMetafileHeader();
		e.Graphics.DrawImage(image, 0, 0, ClientSize.Width, ClientSize.Height);
		e.Graphics.DrawString(header.Bounds.ToString(), font, brush, 0, 0);
		e.Graphics.DrawString("DpiX=" + header.DpiX , font, brush, 0, font.Height);
		e.Graphics.DrawString("DpiY=" + header.DpiY , font, brush, 0, font.Height * 2);
		e.Graphics.DrawString("Type=" + header.Type , font, brush, 0, font.Height * 3);
	}

	protected override void OnResize(System.EventArgs e)
	{
		base.OnResize (e);
		Invalidate();
	}

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

コード2は、MetafileHeader オブジェクトを取得して、描画している Metafile イメージの情報を表示します。Type プロパティの結果は、EmfPlusDual となっていますが、これは EMF+ 型のメタファイルで、古い Enhanced Metafile 形式と互換性を保っていることを表します。

6.8.4 メタファイルを作成する

Metafile クラスのコンストラクタはオーバーロードされて数多く用意されています。前述したディスク上のメタファイルを読み込むコンストラクタ以外はすべて、新しく Metafile オブジェクトを作成します。メタファイルを Metafile コンストラクタから新規作成する方法は数多く存在するので、この場では特に代表的な方法のみを解説します。

どの方法でも、メタファイルを新規作成するにはグラフィックス出力デバイスの情報が求められます。グラフィックス出力デバイスは Graphics クラスで表されているため、本来ならば Graphics オブジェクトを渡すべきなのですが、Metafile コンストラクタではどういうわけかネイティブ Windows で使われていた出力デバイスのハンドルを要求します。Win32 を経験している方には HDC 型の値といえばすぐに分かるでしょう。

Metafile クラスのコンストラクタ
public Metafile(string fileName, IntPtr referenceHdc)
public Metafile(Stream stream, IntPtr referenceHdc)

fileName パラメータには作成するメタファイルのファイル名を、stream パラメータにはメタファイルのデータストリームを指定します。このコンストラクタの場合は、指定したファイルやストリームから読み込むのではなく、生成する Metafile オブジェクトのデータを格納先となります。referenceHdc パラメータには出力デバイスのハンドル、すなわち HDC 型の値を指定します。このコンストラクタで生成されるメタファイルは EmfPlusDual 型、すなわち古いメタファイルと互換性のある EMF+ 型のメタファイルとなります。

問題の referenceHdc パラメータに渡す値は、Graphics クラスの GetHdc() メソッドから取得することができます。このメソッドが返す値はネイティブ Windows で使われているデバイスコンテキストと呼ばれる出力デバイスを表すオブジェクトのハンドルです。ハンドルとは、Win32 API でオブジェクトを識別するために使われていた ID のようなものです。

Graphics クラス GetHdc() メソッド
public IntPtr GetHdc()

.NET Framework の System.Windows.Forms 名前空間のいくつかのクラスは、このような Win32 API と通信するためのいくつかの機能を提供しています。Win32 API では GUI 上のコントロールなどを操作するために、オブジェクトをハンドル型の値で識別していました。GetHdc() メソッドのように、ハンドルを取得したりハンドルから .NET 用のオブジェクトを構築するメソッドなどが提供されています。Win32 ネイティブの機能を .NET から使う場合に必要とします。

GetHdc() メソッドからデバイスコンテキストのハンドルを取得した場合、不要になった時点で ReleaseHdc() メソッドを呼び出して解放しなければなりません。GetHdc() メソッドと ReleaseHdc() メソッドは一組の関係となります。

Graphics クラス ReleaseHdc() メソッド
public void ReleaseHdc(IntPtr hdc)

これで、Metafile クラスのコンストラクタから新しいメタファイルを生成することができます。Metafile オブジェクトのイメージを操作するには Graphics クラスの FromImage() メソッドから Graphics オブジェクトを取得して描画を行います。ビットマップの場合は、描画を行うことでピクセルの値が変更されましたが、メタファイルの場合はレコードとして描画手順が記録されることになります。

ただし、Metafile オブジェクトの操作の場合、取得した Graphics オブジェクトを使い終わった時点で Dispose() メソッドでオブジェクトを破棄しなければなりません。

Graphics クラス Dispose() メソッド
public virtual void Dispose()

この操作はビットマップの場合は省略できましたが、Metafile の場合はこれを行わなければデータが保存されません。なぜならば、Metafile の場合は描画手順を記録するという性質があるため、Dispose() メソッドでオブジェクトが破棄された時点で記録を終了するという意味を持っているのです。

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

public class Test : Form
{
	private Metafile image;
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint (e);
		if (image == null) 
		{
			IntPtr hdc = e.Graphics.GetHdc();
			image = new Metafile("test.emf", hdc);
			e.Graphics.ReleaseHdc(hdc);

			Graphics g = Graphics.FromImage(image);
			Pen pen = new Pen(Color.Black, 10);
			g.DrawLine(pen, 100 , 0, 100, 100);
			g.DrawArc(pen, 0, 0, 200, 200, -60, 300);
			g.Dispose();
		}

		e.Graphics.DrawImage(image, 0, 0);
	}

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

コード3は、新規に Metafile オブジェクトを作成し、Metafile オブジェクトを操作する Graphics オブジェクトを取得して Metafile に直接描画を行っています。このプログラムでは DrawLine() メソッドと DrawArc() メソッドを使って図形を描画していますが、もちろんメタファイルなので描画手順が記録されているだけです。

実行結果を見ると、DrawArc() メソッドでは座標 (0, 0) ピクセルから円を描画しているはずなのに、左上隅に予期しない余白が作られています。ファイル名を指定した Metafile クラスのコンストラクタを使っているので、実行ファイルと同じディレクトリに .emf ファイルが生成されていますが、これをイメージビューアで表示しても同じような余白を確認できます。

図1 生成したメタファイルをビューアで表示
図1 生成したメタファイルをビューアで表示

この余白は、明示的にメタファイルの矩形を指定しないために発生しています。メタファイルは性質的にピクセルを持たないため、レコードで指定されている座標からイメージの境界を算出します。イメージのサイズを指定するには Metafile クラスの次のコンストラクタを使います。

Metafile クラスのコンストラクタ
public Metafile(Stream stream, IntPtr referenceHdc, Rectangle frameRect)
public Metafile(Stream stream, IntPtr referenceHdc, Rectangle frameRect, MetafileFrameUnit frameUnit)
public Metafile(string fileName, IntPtr referenceHdc, Rectangle frameRect)
public Metafile(string fileName, IntPtr referenceHdc, Rectangle frameRect, MetafileFrameUnit frameUnit)

第2パラメータまでは前述したコンストラクタと同じです。それに加えて、生成する Metafile オブジェクトの範囲を表す frameRect パラメータと、frameRect に設定されている数値の単位を frameUnit で指定します。

frameUnit パラメータに指定する値は System.Drawing.Imaging.MetafileFrameUnit 列挙体のメンバのいずれかです。

System.Drawing.Imaging.MetafileFrameUnit 列挙体
[Serializable]
public enum MetafileFrameUnit

frameUnit パラメータを省略した場合 1/100 ミリメートル単位を表す GdiCompatible メンバが設定されています。多くの場合、この設定は望まれるものではないでしょう。ピクセル単位で範囲を指定するには Pixel メンバを指定してください。

また、これまでの Metafile はディスクやストリームにメタファイルを保存しましたが、ビットマップで行ったメモリデバイスのように、メモリ上だけにメタファイルを一時的に生成したいだけであれば、次のコンストラクタを使うことができます。

Metafile クラスのコンストラクタ
public Metafile(IntPtr referenceHdc, Rectangle frameRect);
public Metafile(IntPtr referenceHdc, Rectangle frameRect, MetafileFrameUnit frameUnit);

このコンストラクタは、referenceHdc パラメータにデバイスコンテキストのハンドルを、frameRect パラメータにオブジェクトの範囲を、frameUnit パラメータに frameRect の単位を指定します。

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

public class Test : Form
{
	private Metafile image;
	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint (e);
		if (image == null) 
		{
			IntPtr hdc = e.Graphics.GetHdc();
			image = new Metafile(
				hdc, new Rectangle(-5, -5, 210, 210),
				MetafileFrameUnit.Pixel
			);
			e.Graphics.ReleaseHdc(hdc);

			Graphics g = Graphics.FromImage(image);
			Pen pen = new Pen(Color.Black, 10);
			g.DrawLine(pen, 100 , 0, 100, 100);
			g.DrawArc(pen, 0, 0, 200, 200, -60, 300);
			g.Dispose();
		}

		e.Graphics.DrawImage(image, 0, 0);
	}

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

コード4は、メタファイルの範囲を明示的に設定した Metafile オブジェクトを生成しています。このオブジェクトはメモリにメタファイルを保存するため、ディスク上にメタファイルは生成されません。メタファイルの範囲は、ペンのサイズを考慮して大きめに確保しています。範囲の外に描画された図形は表示されないので注意が必要です。