Bitmapからリージョンの作成

今回のトピックスはBitmapから自動でリージョンを作成する方法です。
何をしたいのかというとBitmapと同じ形のコントロールを作ろうということです。


まずこれを実現する方法の一つに、
FormクラスにはTransparencyKeyというプロパティがあり、そこに任意の色を指定するなどゴニョゴニョすれば任意の形のフォームを作ることができます。
もちろん任意のBitmapと同じ形のフォームも作れます。
この詳しい方法を知りたい場合は各自ググってください。


つまり今回のトピックスではその方法は使わないということです。
なぜかというと、その方法はあくまでBitmapと同じ形のフォームが作れるだけで同じ形のコントロールが作れるわけではないからです。
残念ながらButtonクラスやPanelクラスにTransparencyKeyプロパティはありません。


なので今回は透明色を指定して自力でBitmapと同じ形のリージョンを作り、ControlクラスのプロパティであるRegionに指定してやります。
Regionプロパティにリージョンを指定してやればそれがクリッピングリージョンになり、
描画をその外側にできなくなったりMouseMoveイベントなどのイベントが発生しなくなります。
こうすることでBitmapと同じ形のコントロールを作ります。


と説明を長々としても意味はないのでそろそろコードを。

public static Region CreateRegionFromBitmap(Bitmap bitmap)
{
	if (!(bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ||
		bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb ||
		bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppRgb))
		return null;// 24bitか32bit以外は無理

	//ここの変数もっとまともな名前つけたい
	int channels = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8;
	int offset = 0;
	int count = channels;
	if (bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb)
	{//Format32bppArgbの場合はα値のみを確認する
		count = 1;
		offset = 3;
	}

	//ビットマップから生データを取得
	Rectangle rectBitmap = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
	System.Drawing.Imaging.BitmapData data = bitmap.LockBits(rectBitmap,
		System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
	byte[] values = new byte[data.Height * data.Stride];
	System.Runtime.InteropServices.Marshal.Copy(data.Scan0, values, 0, data.Height * data.Stride);
	bitmap.UnlockBits(data);

	//透明にする色を取得
	byte[] transparent = new byte[count];
	if (count == 1)
		transparent[0] = 0;
	else
	{
		for (int i = 0; i < count; ++i)
			transparent[i] = values[i];
	}

	Region region = new Region();
	region.MakeEmpty();

	Rectangle rect = new Rectangle();
	for (int y = 0; y < data.Height; ++y)
	{
		int offsetY = y * data.Stride;
		for (int x = 0; x < data.Width; ++x)
		{
			for (int c = 0; c < count; ++c)
			{
				if (transparent[c] != values[offsetY + x * channels + c + offset])
				{//透過色ではない
					rect.X= x;//透過色ではないスタート地点を保存
					for (++x; x < data.Width; ++x)
					{
						for (c = 0; c < count; ++c)
						{
							if (transparent[c] != values[offsetY + x * channels + c + offset])
								goto CONTINUE;	//透過色ではないので続ける
						}
						break;	//次の透過色を見つけたので脱出
					CONTINUE:
						;
					}
					rect.Width = x - rect.X;
					rect.Y = y;
					rect.Height =  1;

					//透過色でない部分をリージョンに追加する
					region.Union(rect);
					break;
				}
			}
		}
	}

	return region;
}

最大5重ループだったり平気にgotoを使ってたり画像データを一次元配列としてと扱っているためインデックス指定がやたら長かったりと、可読性がいいとは言えないコードですがそこは勘弁してください。


可読性が悪いことを自覚しつつも、要所要所にコメントを書いているので詳細なコードの解説はしません。
ですか気を付けてほしいところをいくつか。


メソッドの最初の部分にあるように、引数のBitmapのPixelFormatはFormat24bppRgb・Format32bppRgb・Format32bppArgbの三つの場合しか対応していません。
このようにした理由は

  • これだけで大体の場合いける
  • 対応するのがメンドウくさい
  • 自分で使う分にはこれだけで充分
というとってもテキトーなものです。
このような割とテキトーな作りなので、うまくいかないときはBitmapのPixelFormatを確認してください。


もうひとつ、
α値を持つ場合はα値のみを確認するようになり、
α値をもたない場合はGetPixel(0,0)と同じ場所の値を強制的に透明色にしています。
なので現状それ以外を透明色に指定する方法はありません。
このようにした理由は...以下略
まぁ、完全に上に書いた理由と同じです。


とまあ適当感の漂うコードですが、
このメソッドが返したリージョンをControlクラスから派生したクラスのRegionプロパティに指定して、BackgroundImageプロパティにメソッドの引数にしたビットマップを指定してやれば同じ形のコントロールができると思います。

Buttonなどものによっては、フォーカスが当たったり押された状態になったときにイメージの上に線などが描画される場合がありますが、完成度を上げるための細かい部分は各自頑張ってください。


というわけで最後まで適当な感じで今回は終わりです。
注意として挙げたように完全に汎用的なコードというわけではないので、用途に合った拡張が必要になることがあると思いますが出来るだけセルフサービスでお願いします。
ですが何度も書いているように可読性の良いコードではないので、どーしても機能を拡張しないといけないけどどーしても自分でできなければ、コメントとしてどのような機能の要求があるのか書いてください。
できるだけ対応します。


最後にと書いておきながら今回はもうひとつコードをのっけます。


出来る事は全く一緒で大枠も完全に一緒なのですが、リージョンを構成していく方法を変えています。
最初の方法は空のリージョンに一つずつ矩形を加えていく方法でしたが、次に紹介するコードではパスに対して矩形を加えていき最後にパスからリージョンを作成するような方法をとっています。
速度的な違いもほとんどないのでどちらを使っても変わりはないですが、
パスにさらに別なものを加えればまた違ったリージョンが作れるので、別な事に応用ができるのでは?ということで一例として載せておきます。

public static Region CreateRegionFromBitmap(Bitmap bitmap)
{
	if (!(bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ||
		bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb ||
		bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppRgb))
		return null;// 24bitか32bit以外は無理

	//ここの変数もっとまともな名前つけたい
	int channels = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8;
	int offset = 0;
	int count = channels;
	if (bitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb)
	{//Format32bppArgbの場合はα値のみを確認する
		count = 1;
		offset = 3;
	}

	//ビットマップから生データを取得
	Rectangle rectBitmap = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
	System.Drawing.Imaging.BitmapData data = bitmap.LockBits(rectBitmap,
		System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
	byte[] values = new byte[data.Height * data.Stride];
	System.Runtime.InteropServices.Marshal.Copy(data.Scan0, values, 0, data.Height * data.Stride);
	bitmap.UnlockBits(data);

	//透明にする色を取得
	byte[] transparent = new byte[count];
	if (count == 1)
		transparent[0] = 0;
	else
	{
		for (int i = 0; i < count; ++i)
			transparent[i] = values[i];
	}

	//リージョンを構成するパスのインスタンス
	System.Drawing.Drawing2D.GraphicsPath rgnPath = new System.Drawing.Drawing2D.GraphicsPath();

	Rectangle rect = new Rectangle();
	for (int y = 0; y < data.Height; ++y)
	{
		int offsetY = y * data.Stride;
		for (int x = 0; x < data.Width; ++x)
		{
			for (int c = 0; c < count; ++c)
			{
				if (transparent[c] != values[offsetY + x * channels + c + offset])
				{//透過色ではない
					rect.X = x;//透過色ではないスタート地点を保存
					for (++x; x < data.Width; ++x)
					{
						for (c = 0; c < count; ++c)
						{
							if (transparent[c] != values[offsetY + x * channels + c + offset])
								goto CONTINUE;	//透過色ではないので続ける
						}
						break;	//次の透過色を見つけたので脱出
					CONTINUE:
						;
					}
					rect.Width = x - rect.X;
					rect.Y = y;
					rect.Height = 1;

					//ここでパスにrectを加える
					rgnPath.AddRectangle(rect);
					break;
				}
			}
		}
	}

	//最後にパスからリージョンを作成
	return new Region(rgnPath);
}