ゲームエフェクトデザイナーのブログ | A Real-Time VFX Artist's Blog

About Making Materials on UE, Making Tools with C#, etc

C# PictureBoxを2つ重ねるサンプル

フォーム上でPictureBoxを2つ重ねて、下のレイヤーは背景用・上のレイヤーは透明にして一部だけ描画したい場合があると思います。
(画像を読み込んでトリミングするために選択範囲を描画したい場合など)

こちらが実際にスクリーンショット画像を取り込んでPictureBox1に表示し、PictureBox2で赤いラインを引くのを試してみてうまくいった状態になります。
f:id:moko_03_25:20200311232257j:plain

しかし落とし穴がいくつかあるのでメモしておきたいと思います。

まずForm1上にPictureBox1を置いて想定する位置に配置し、PictureBox2は小さくても良いのでLocationを 0, 0 にしておきます。
f:id:moko_03_25:20200311232250j:plain

またPictureBox2のBackColorは「Color.Transparent」にして背景を透明にしますが、透明にならない場合はコントロールの親子の設定が関係してくるようです。なのでフォームのロードイベントで「pictureBox2.Parent = pictureBox1;」のように設定しています。
この点に関してこちらの記事を参考にさせていただきました。
ossannt.hatenablog.com

この時、PictureBox2はPictureBox1の 0, 0 を原点とした位置に移動します。
そこで2つピッタリ重ねるために先ほど書いたようにあらかじめPictureBox2のLocationを 0, 0 に設定しています。
最初はフォームのロードイベントでLocationが 0, 0 にするよう試しましたがダメでした(もしかしたら親子付けする前に移動すれば良かっただけかも‥)。

実際のソースコードはこちらのような感じです。
PictureBox1のサイズが固定である前提の書き方になっていますが、変動する場合はPictureBox1のSizeChangedイベントでPictureBox2のサイズが連動するように書くと良いです。

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace SampleDoubleDraw
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}

		// 変数
		Point startPoint;
		Point endPoint;
		Bitmap canvas;
		Graphics g;
		Pen linePen;

		// Formロード時
		private void Form1_Load(object sender, EventArgs e)
		{
			LoadSampleImage();

			// PictureBox2の設定
			pictureBox2.Parent = pictureBox1;
			pictureBox2.Width = pictureBox1.Width;
			pictureBox2.Height = pictureBox1.Height;

			// サイズだけ指定して透明のキャンバスにする
			// pictureBox1のサイズで生成する
			canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);

			// ImageオブジェクトのGraphicsオブジェクトを作成
			g = Graphics.FromImage(canvas);

			// Penオブジェクトの作成
			linePen = new Pen(Color.Red, 4);

			// スタイルを指定
			linePen.DashStyle = DashStyle.Solid;
		}

		// マウスクリック時
		private void PictureBox2_MouseDown(object sender, MouseEventArgs e)
		{
			// カーソルの開始座標を取得
			startPoint = CursorPos();
		}

		// マウスドラッグ時
		private void PictureBox2_MouseMove(object sender, MouseEventArgs e)
		{
			// マウスの左ボタンが押されている場合のみ処理
			if ((Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left)
			{
				// カーソルの終了座標を取得
				endPoint = CursorPos();

				// 描画
				DrawLine();
			}
		}

		// マウスドラッグ終了時
		private void PictureBox2_MouseUp(object sender, MouseEventArgs e)
		{
			// 何か処理が必要であれば書く
		}

		// 関数:ラインを描画
		private void DrawLine()
		{
			// 前回のライン描画を一旦クリア
			g.Clear(Color.Transparent);

			// ラインを描画
			g.DrawLine(linePen, startPoint, endPoint);

			// PictureBox2に表示
			pictureBox2.Image = canvas;
		}

		// 関数:カーソル位置を取得
		private Point CursorPos()
		{
			// 画面座標でカーソルの位置を取得
			Point p = Cursor.Position;
			// 画面座標からコントロール上の座標に変換
			Point cp = pictureBox2.PointToClient(p);

			return cp;
		}

		// 関数:サンプル画像を読み込んで表示
		private void LoadSampleImage()
		{
			Bitmap b = new Bitmap(@"sample.png");
			pictureBox1.Image = b;
		}
	}
}


マウスドラッグで選択範囲(ラバーバンド)を描画する場合に、前のフレーム結果にそのまま合成すると選択範囲の描画が残って重なっていってしまいます。そこで選択範囲を描画する前のImageを取得しておいて毎フレームPictureBoxを上書きしてから選択範囲を描画することでカバーすると、非常に重くなります。Winfows Formsは古いライブラリでありGPUを利用しないからと思われます。

この場合、OpenCLの利用やWPFに乗り換えるといった手があるようですが、今回のPictureBoxを2枚重ねる方法でかなり快適になりました。
f:id:moko_03_25:20200312021211g:plain

しかし限界を感じてきています。
そろそろWPFに手を出す時期か。。