[C#]コントロールの背面全てを透過

久々?にC#ネタ!

.Netでコントロールを自作するときに、
「背景を透過させたい!」ってコトが稀によくある。

その時によくやるのが、コントロールのコンストラクタに
SetStyle関数を呼び出して透過色を有効にする方法。

public class TransparentTestCtrl : Control
{
	public TransparentTestCtrl()
		: base()
	{
		SetStyle(ControlStyles.OptimizedDoubleBuffer |
			ControlStyles.AllPaintingInWmPaint |
			ControlStyles.UserPaint |
			ControlStyles.SupportsTransparentBackColor |
			ControlStyles.ResizeRedraw, true);
	}

	protected override void OnPaint(PaintEventArgs e)
	{
		base.OnPaint(e);

		using (Pen p = new Pen(ForeColor))
		{
			e.Graphics.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
		}
	}
}

こんな感じのコントロールを作って、

コントロール色の背景色を透過色に変えると・・・

こんな感じに。

一見これでも透過してるように見えるし、
大抵の場合はこれで事足りると思うんだけど、
試しに背面にピクチャボックスを貼って画像を出してみると・・・

親コントロールの内容が透けて描画されるだけで、
同じレベルのコントロールは無視されるみたい^^;
なので何とも気持ちの悪い感じに・・・(苦笑)

この状態で後ろのコントロールも
透けさせたいって内容のものがあって、
色々と調べていると、こんな記事を発見。

コントロールの背景を透過・透明にする」(IYouryellable様)

これの方法2のやつが正にそれ。
ただ内容を見ていると、透けさせたいコントロールの描画時に、
ビットマップを生成して、そこに背面のコントロールを描画させて、
そのビットマップをコントロールの背景に描画して・・・
ってのを背面のコントロール毎に処理して実現されてる。

流石に描画の度に何度もビットマップを生成するのは、重すぎる気が・・・
どうせなら直接コントロールに描画させる方法はないものか・・・
っと考えて、パッと思い付いたので実現してみた!

要は、背面のコントロールを描画するときに、
Graphicsの原点を背面のコントロールの位置にずらせれば、
直接コントロールに描画出来るはず。

↑の記事を参考にして、
同じようにOnPaintBackgroundに実装してみるー

protected override void OnPaintBackground(PaintEventArgs pevent)
{
	base.OnPaintBackground(pevent);

	// 親がいない場合は無視
	if (this.Parent == null) return;

	Point offset = new Point(this.Left, this.Top);

	// 原点を親コントロールの座標へ
	pevent.Graphics.TranslateTransform(
		-offset.X, -offset.Y);
	// 親コントロールを描画
	this.InvokePaintBackground(this.Parent, pevent);
	this.InvokePaint(this.Parent, pevent);
	// 原点の座標を戻す
	pevent.Graphics.TranslateTransform(offset.X, offset.Y);

	// 各背面コントロールを描画
	for (int i = this.Parent.Controls.Count - 1; i >= 0; --i)
	{
		Control c = this.Parent.Controls[i];

		if (c == this) break;	// 背面コントロールの描画終わり
		if (!c.Visible) continue;	// 対象のコントロールが非表示

		// 対象のコントロールが描画領域に含まれているか
		if (c.Bounds.IntersectsWith(this.Bounds))
		{
			// 原点を背面コントロールの座標へ
			offset.X = this.Left - c.Left;
			offset.Y = this.Top - c.Top;
			pevent.Graphics.TranslateTransform(
				-offset.X, -offset.Y);
			// 背面コントロールを描画
			this.InvokePaintBackground(c, pevent);
			this.InvokePaint(c, pevent);
			// 原点の座標を戻す
			pevent.Graphics.TranslateTransform(offset.X, offset.Y);
		}
	}
}

原点をTranslateTransform関数で移動させておいて、
そのままGraphicsをPaint関数に渡す方法で実現してみたー

実際にこっちの方が速いのかは微妙・・・(笑)
いちいちビットマップを生成したりしてないから速いと思うけど、
実際に両方を試したわけじゃないから何とも^^;
とりあえず背面コントロールが多いと重いのは確実。

因みに、どちらの方法にしても、
これなら背景色を透過色に変える必要もないから、
SetStyleで透過色を有効にする必要もないよー

ま、参考程度に・・・(笑)

じゃ、ゲームして寝るー
バイニー☆

~追記~
↑の方法だと、背面コントロールの
子コントロールが描画されて無かった。

private void DrawControl(Control c, PaintEventArgs pevent)
{
	Point offset = new Point(this.Left - c.Left, this.Top - c.Top);

	// 原点を背面コントロールの座標へ
	pevent.Graphics.TranslateTransform(
		-offset.X, -offset.Y);

	// コントロールを描画
	this.InvokePaintBackground(c, pevent);
	this.InvokePaint(c, pevent);

	// 子コントロールを描画
	for (int j = c.Controls.Count - 1; j >= 0; --j)
	{
		Control child = c.Controls[j];
		if (!child.Visible) continue;	// 対象のコントロールが非表示
		DrawControl(child, pevent);
	}

	// 原点の座標を戻す
	pevent.Graphics.TranslateTransform(
		offset.X, offset.Y);
}

protected override void OnPaintBackground(PaintEventArgs pevent)
{
	base.OnPaintBackground(pevent);

	// 親がいない場合は無視
	if (this.Parent == null) return;

	Point offset = new Point(this.Left, this.Top);

	// 原点を親コントロールの座標へ
	pevent.Graphics.TranslateTransform(-offset.X, -offset.Y);
	// 親コントロールを描画
	this.InvokePaintBackground(this.Parent, pevent);
	this.InvokePaint(this.Parent, pevent);
	// 原点の座標を戻す
	pevent.Graphics.TranslateTransform(offset.X, offset.Y);

	// 各背面コントロールを描画
	for (int i = this.Parent.Controls.Count - 1; i >= 0; --i)
	{
		Control c = this.Parent.Controls[i];

		if (c == this) break;	// 背面コントロールの描画終わり
		if (!c.Visible) continue;	// 対象のコントロールが非表示

		// 対象のコントロールが描画領域に含まれているか
		if (c.Bounds.IntersectsWith(this.Bounds))
		{
			// 背面コントロールを描画
			DrawControl(c, pevent);
		}
	}
}

再帰呼び出しにして、子コントロールも
全て描画されるようにしてみたー

〜追記(2014/7/8)〜
コメントで頂いた背面コントロールがLabelだと正常に描けない問題。
返信でも書いたとおりLabel以外にもButtonやCheckBoxなどなど
Windowsの標準コントロール系が尽く描けてなかった・・・orz
というわけで解決策を模索してみました。

↑で書いたコードのDrawControl関数部分を置き換えで・・・

// using System.Runtime.InteropServices;
// ↑が必要
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(
	HandleRef hWnd, int Msg, IntPtr wParam, IntPtr lParam);

private const uint PRF_NONCLIENT = 0x00000002;
private const uint PRF_CLIENT = 0x00000004;
private const uint PRF_ERASEBKGND = 0x00000008;
private const uint PRF_CHILDREN = 0x00000010;

private void DrawControl(Control c, PaintEventArgs pevent)
{
	// 一時的にビットマップを作成
	using (Bitmap bmp = new Bitmap(c.Width, c.Height))
	using (Graphics g = Graphics.FromImage(bmp))
	{
		IntPtr hDc = g.GetHdc();
		// コントロールをビットマップに描画
		SendMessage(new HandleRef(c, c.Handle),
			0x0317 /* WM_PRINT*/, (IntPtr)hDc,
			(IntPtr)(PRF_CHILDREN | PRF_CLIENT |
			PRF_ERASEBKGND | PRF_NONCLIENT));
		g.ReleaseHdc(hDc);

		// ビットマップを画面に描画
		pevent.Graphics.DrawImage(bmp,
			c.Left - this.Left, c.Top - this.Top);
	}

	// 子コントロールを描画
	for (int j = c.Controls.Count - 1; j >= 0; --j)
	{
		Control child = c.Controls[j];
		if (!child.Visible) continue;   // 対象のコントロールが非表示
		DrawControl(child, pevent);
	}
}

描画したい子コントロールにWM_PRINTを投げてビットマップに描画させて、
そのビットマップを描画する形に・・・
これなら標準コントロールでも正しく描画される!

やや手抜きで毎回ビットマップを生成する形にしちゃったけど、
グローバルで大きめのビットマップ1つ作っておいて使い回したり、
Windows標準コントロールじゃなければ元の方法で描画するようにしたり、
工夫できそうなポイントもありそう。

親コントロールにWM_PRINTを投げて
子コントロールを丸ごと描画する方法も試したけど、
領域外のコントロールまで描画処理が走ったり、
親がウィンドウだとフレームの分ズレたりで断念。

因みに↑の処理、Control.DrawToBitmapのソースを参考に実装。
DrawToBitmapなら描画できてるから、どうやってるのかなーと思って。
なのでMSDNのDrawToBitmapに書かれている制限がそのまま当てはまるはず・・・。
RichTextBox では、DrawToBitmap の機能が制限され、
ビットマップの境界しか描画されません。
とか・・・
ただAPIレベルで出来ないとなると、この辺が落とし所かなーっと。

・・・こんなところで良かったでしょうか?(汗

タグ:
test?

[C#]コントロールの背面全てを透過」への2件のフィードバック

    1. みつ(@@@) 投稿作成者

      コメントありがとうございますm(_ _)m

      どうやらWindows標準のコントロールだと、
      TranslateTransform+InvokePaintで
      正しく描画されないみたいですね・・・。

      標準コントロールのときだけIYouryellable様の記事で書かれていた
      (今は500エラーで見えない・・・?)1度ビットマップに描画して、
      そのビットマップを実際に描画するという方法を取るしかなさそうですが、
      肝心の標準コントロールかどうか確認する方法が難題です・・・
      あとやはり速度面も考慮すると、もう一捻り工夫が欲しいところ・・・。

      もし一先ずLabelのみの対応でよければ、
      機能を絞ったLabel程度なら簡単に実装できるので、
      いっそのことControlクラスを継承し
      文字を描画するだけの単純なコントロールを自作して、
      Labelと置き換えてしまった方が早いかもしれません(汗

      根本的な解決方法は直ぐに思い付きそうにないので、
      また考えておきます。。。

      〜追記〜
      なんとか解決策を見つけたので記事に追記しておきました!
      WM_PRINTの方法ならLabelもちゃんと描画されました!

      返信

コメントを残す