パネルとピクチャボックスを管理するイメージコンテナを定義する

パネルには AutoScroll というプロパティがあります。この値を true にすると、パネルにピクチャボックスを内包させた場合、ピクチャボックスのサイズがパネルの領域より大きくなると自動でスクロールバーが出現して非常に便利です。これをうまく利用してパネルとピクチャボックスをイッキに管理してみようというのが、この記事の試みです。

ZoomableImageContainer というクラスを定義します。特に難しくはないのでトライする価値は十分にあります。クリックイベントなどもクラス側に持っていくと余計なコードを見なくていいのでとても気分がラクです。

まずは、フォームデザイナで Panel を配置して、その中に PictureBox を配置しましょう。名前は panel1 と pictureBox1 にしてください。

Fig. 1 フォームデザイナで PictureBox を内包した Panel を配置
Fig. 2 画像をパネルのサイズに合わせて表示
Fig. 3 ズーム率2倍で表示
Fig. 4 ズーム率4倍で表示

以下が Form1 のコードです。左クリック(人差し指クリック)でズーム率が2倍で表示です。右クリック(中指クリック)でズーム率が4倍です。

2倍、4倍は、ZoomableImageContainer のコンストラクタ引数で決定するので、Form1 でややこしいコードを書く必要はありません。

Form1_Load のコードが多いように見えますが、ほとんどパネルの位置やサイズを決めるコードですので、とりあえず試すのでしたらコメントアウトしてしまってかまいません。ただし AutoScroll = true; だけは必ず残しておいてください。

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

namespace aaa
{
	public partial class Form1 : Form
	{

		const double ZOOM_MUL0 = 2.0;
		const double ZOOM_MUL1 = 4.0;

		ZoomableImageContainer Zic;

		public Bitmap BmpOut;

		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load( object sender, EventArgs e )
		{

			// 適当なデバッグ用の画像ファイルを読み込む.
			BmpOut = new Bitmap( "c:/tmp/mikasa.jpg" );

			// 読み込んだ画像のアスペクト比を算出する.
			int w = BmpOut.Width;
			int h = BmpOut.Height;
			double asp = (double)( w ) / (double)( h );

			// パネルの左上隅の位置を( 0, 0 )よりすこしずらす.
			const int MARGIN_XY = 32;

			// パネルのサイズと位置を決める.
			int req_panel_width = this.ClientRectangle.Width - ( MARGIN_XY << 1 );
			panel1.Width = req_panel_width;
			int req_pcw = panel1.ClientRectangle.Width;
			int req_pch = (int)( Math.Round( (double)( req_pcw ) / (double)( asp ) ) );
			panel1.Size = new Size( req_pcw, req_pch );
			panel1.Location = new Point( MARGIN_XY, MARGIN_XY );

			// パネルが内包するピクチャボックスのサイズが大きくなったら自動でスクロールバーを表示する.
			panel1.AutoScroll = true;

			// パネル境界をわかりやすくしたいので外枠をつける.
			panel1.BorderStyle = BorderStyle.FixedSingle;

			// ピクチャボックスのサイズの初期値はパネルのクライアント領域と同じで左上隅に位置させる.
			pictureBox1.Size = panel1.ClientRectangle.Size;
			pictureBox1.Location = new Point( 0, 0 );

			// ここでカスタムのイメージコンテナを生成する.
			Zic = new ZoomableImageContainer( panel1, pictureBox1, w, h, ZOOM_MUL0, ZOOM_MUL1 );

			// ウインドウの高さを再調整する.
			int req_wnd_cw = this.ClientSize.Width;
			int req_wnd_ch = panel1.Height + ( MARGIN_XY << 1 );
			this.ClientSize = new Size( req_wnd_cw, req_wnd_ch );

		}

		private void pictureBox1_Paint( object sender, PaintEventArgs e )
		{

			if ( BmpOut == null )
			{
				return; // warning.
			}
			
			// 高画質に拡大したいとき.
			e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;

			// ピクセル感を残しながら拡大したいときはこちら.
			// e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;

			// 画像をピクチャボックスのサイズに合わせて出力する.
			e.Graphics.DrawImage( BmpOut, pictureBox1.ClientRectangle );

			// イメージコンテナからズーム率を取得する.
			this.Text = String.Format( "ズーム率 {0:f1}", Zic.ZoomRate );

		}
	}
}

以下が ZoomableImageContainer クラスのコードです。けっこう簡単なコードで実現できました。スクロールバーの位置の指定 AutoScrollPosition に与える new Point() は、ふたつとも負の値であることに注目してください。

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

namespace aaa
{

	public class ZoomableImageContainer
	{
		private Panel panel;
		private PictureBox pictureBox;

		bool m_flag_zoom_in = false;
		int m_original_data_width;
		int m_original_data_height;
		double m_zoom_mul0;
		double m_zoom_mul1;

		double m_zoom_rate;
		public double ZoomRate { get { return m_zoom_rate; }}

		public ZoomableImageContainer(
				Panel panel,
				PictureBox picture_box,
				int original_data_width,
				int original_data_height,
				double zoom_mul0,
				double zoom_mul1
				)
		{

			m_original_data_width = original_data_width;
			m_original_data_height = original_data_height;

			m_zoom_mul0 = zoom_mul0;
			m_zoom_mul1 = zoom_mul1;

			this.panel = panel;
			this.pictureBox = picture_box;

			// PictureBox の MouseDown イベントを登録
			this.pictureBox.MouseDown += PictureBox_MouseDown;

			// 外部から取得できるプロパティのデフォルト値を計算する.
			m_zoom_rate = (double)( pictureBox.Width )/(double)( original_data_width );

		}

		private void PictureBox_MouseDown( object sender, MouseEventArgs e )
		{

			if (( e.Button & MouseButtons.Left ) == MouseButtons.Left )
			{
				m_zoom_rate = m_zoom_mul0;
			}
			else if (( e.Button & MouseButtons.Right ) == MouseButtons.Right )
			{
				m_zoom_rate = m_zoom_mul1;
			}
			else
			{
				return;
			}

			int w = m_original_data_width;
			int h = m_original_data_height;

			m_flag_zoom_in = !m_flag_zoom_in;

			if ( m_flag_zoom_in )
			{
				ZoomIn( w, h, m_zoom_rate, e );
			}
			else
			{
				ZoomOut();

				m_zoom_rate = (double)( pictureBox.Width )/(double)( m_original_data_width );
			}

			pictureBox.Invalidate( true );

		}

		private void ZoomIn( int w, int h, double zoom_rate, MouseEventArgs e )
		{

			double mul = zoom_rate;

			int pw = pictureBox.Width;
			int ph = pictureBox.Height;

			double rate_x = (double)( w ) / (double)( pw );
			double rate_y = (double)( h ) / (double)( ph );
			double the_x = (double)( e.X * rate_x );
			double the_y = (double)( e.Y * rate_y );

			// PictureBox の新しいサイズを設定
			int req_pbx_w = (int)( Math.Round( w * mul ));
			int req_pbx_h = (int)( Math.Round( h * mul ));
			pictureBox.Size = new Size( req_pbx_w, req_pbx_h );

			// パネルのクライアント領域
			int pcw = panel.ClientRectangle.Width;
			int pch = panel.ClientRectangle.Height;

			// ズーム後の倍率を再計算
			rate_x = (double)( w ) / (double)( req_pbx_w );
			rate_y = (double)( h ) / (double)( req_pbx_h );

			// スクロール座標を計算
			double tmp_sx = -( the_x / rate_x );
			double tmp_sy = -( the_y / rate_y );

			// クライアント領域の中央を基準に補正
			double adj_x = ( pcw * 0.5 );
			double adj_y = ( pch * 0.5 );

			tmp_sx += adj_x;
			tmp_sy += adj_y;

			int sx = (int)( Math.Round( tmp_sx ) );
			int sy = (int)( Math.Round( tmp_sy ) );

			// 新しいスクロールポジション
			panel.AutoScrollPosition = new Point( -sx, -sy );

		}

		private void ZoomOut()
		{
			int req_pbx_w = panel.ClientRectangle.Width;
			int req_pbx_h = panel.ClientRectangle.Height;
			pictureBox.Size = new Size( req_pbx_w, req_pbx_h );

			// スクロール位置をゼロゼロにリセット
			panel.AutoScrollPosition = new Point( 0, 0 );
		}

	}
}

画像の拡大品質を変更したい場合、たとえばピクセル感たっぷりで表示するには Form1.cs において InterpolationMode を Bilinear から NearestNeighbor に変更してください。

Fig. 5 画像拡大補間モードを BiLinear で表示
Fig. 6 画像拡大の補間モードを NearestNeighbor で表示

余談ではありますが、画像はハイスクールフリートの街、横須賀にある三笠です。周りがコンクリートで固められているのが悲しい。この旗は "Z" を示し、アルファベット最後のZで後がないことから「皇国ノ興廃此ノ一戦二在リ各員一層奮励努力セヨ」を意味するそうです。これに関係する東郷ターンについても三笠の艦内で詳しく解説してあります。Z旗は "ぜかまし" でも有名ですね。