画像の加工をするときによくWindowsに標準搭載されているPaintを使いますが(Photoshop高いし・・・)、細かいところを修正するときは使いにくいです。小さな画像を加工するときはとくに使いにくい・・・、ということで自作することにしました。今回作成するのは高機能なエディタではなく、1ピクセル単位で編集することに特化した画像エディタです(需要あるかな?)。
まずは編集したい画像を普通に表示させ、編集したい部分を拡大表示させます。まずはここまでやってみましょう。
Contents
PictureBoxUserControlクラス
最初にユーザーコントロールを作成します。原寸大表示とコントロール内に収まるように縮小表示させるPictureBoxのようなものをつくります。実態はユーザーコントロールのうえにPictureBoxを貼り付けているだけです。
クラス名はPictureBoxUserControlにしました。原寸大表示をした場合、スクロールバーが表示されるようにします。
コンストラクタを示します。ユーザーコントロールとPictureBoxを同じサイズにしたいのですが、pictureBox1.Dock = DockStyle.Fillとやるとスクロールバーが表示されません。ユーザーコントロールとPictureBoxを同じサイズにする処理はDockStyle.Fill以外の方法でおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class PictureBoxUserControl : UserControl { public PictureBoxUserControl() { InitializeComponent(); this.AutoScroll = true; //this.pictureBox1.Dock = DockStyle.Fill;は不可 ShowActualSize = true; // プロパティ(後述) pictureBox1.MouseDown += PictureBox1_MouseDown; pictureBox1.MouseDoubleClick += PictureBox1_MouseDoubleClick; } } |
実寸大表示と縮小表示
ShowActualSizeプロパティは実寸大表示をするかコントロール内に収まるように縮小して表示するかを設定するためのものです。実寸大表示をさせるのであればPictureBoxのSizeModeはPictureBoxSizeMode.AutoSizeに、そうでない場合はPictureBoxSizeMode.Normalにします。
PictureBox.SizeModeを変更したらBitmapプロパティ(後述)を設定したあとAutoScrollのtrueとfalseを切り替えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public partial class PictureBoxUserControl : UserControl { Bitmap _bitmap; public bool ShowActualSize { set { if (value) { this.pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize; Bitmap = _bitmap; this.AutoScroll = true; } else { pictureBox1.Location = new Point(0, 0); pictureBox1.Size = this.Size; this.pictureBox1.SizeMode = PictureBoxSizeMode.Normal; Bitmap = _bitmap; this.AutoScroll = false; } } get { if (this.pictureBox1.SizeMode == PictureBoxSizeMode.AutoSize) return true; else return false; } } } |
縮小表示のとき縦横比を維持する
Bitmapプロパティを示します。実寸大表示をする場合はそのままPictureBox.Imageにセットしてしまっていいのですが、縮小表示の場合は縮小したBitmapを作成してこれをセットします。GetExpansionRateメソッドはどれだけ縮小するかを計算するためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public partial class PictureBoxUserControl : UserControl { // Bitmap _bitmap; 前述 public Bitmap Bitmap { set { _bitmap = value; if (ShowActualSize) this.pictureBox1.Image = _bitmap; else { double rate = GetExpansionRate(); Bitmap bitmap = new Bitmap(this.Width, this.Height); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; graphics.DrawImage(_bitmap, new Rectangle(0, 0, (int)(_bitmap.Width * rate), (int)(_bitmap.Height * rate))); graphics.Dispose(); this.pictureBox1.Image = bitmap; } } get { return _bitmap; } } } |
縮小表示のときにどれだけ縮小するかを計算するGetExpansionRateメソッドを示します。コントロールの幅高さとBitmapの幅高さから拡大率を求めます。そして少ないほうを採用します。この値が1を超えている場合は縮小する必要がないので1を返します。
1 2 3 4 5 6 7 8 9 10 |
public partial class PictureBoxUserControl : UserControl { double GetExpansionRate() { double rate1 = 1d * this.Height / Bitmap.Height; double rate2 = 1d * this.Width / Bitmap.Width; double rate = rate1 < rate2 ? rate1 : rate2; return rate >= 1 ? 1 : rate; } } |
リサイズに対応させる
コントロールがリサイズされたときの処理を示します。Bitmapが存在し、縮小表示されている場合はコントロールのサイズ変更に伴って縮小率が変わる場合があります。縮小率の計算はBitmapプロパティがやってくれるので、ここではpictureBoxのサイズ変更をして、もう一度Bitmapプロパティをセットしなおしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class PictureBoxUserControl : UserControl { protected override void OnResize(EventArgs e) { if (_bitmap != null && !ShowActualSize) { this.AutoScroll = false; pictureBox1.Location = new Point(0, 0); pictureBox1.Size = this.Size; this.pictureBox1.SizeMode = PictureBoxSizeMode.Normal; Bitmap = _bitmap; } base.OnResize(e); } } |
クリックイベントに対応させる
PictureBoxがクリックされたときにイベントを発生させてForm1クラスからこれに対する処理ができるようにしています。イベントハンドラの引数はクリックされた座標ではなくクリックされた場所にあるピクセルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class PictureBoxUserControl : UserControl { public delegate void PictureBoxMouseDownHandler(object sender, Point point); public event PictureBoxMouseDownHandler PictureBoxMouseDown; private void PictureBox1_MouseDown(object sender, MouseEventArgs e) { if (ShowActualSize) PictureBoxMouseDown?.Invoke(this, new Point(e.X, e.Y)); else { double rate = GetExpansionRate(); int x = (int)((e.X) / rate); int y = (int)(e.Y / rate); PictureBoxMouseDown?.Invoke(this, new Point(x, y)); } } } |
PictureBoxがダブルクリックされたときにも対応できるようにイベントを発生させています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class PictureBoxUserControl : UserControl { public delegate void PictureBoxMouseDoubleClickHandler(object sender, Point point); public event PictureBoxMouseDownHandler PictureBoxMouseDoubleClick; private void PictureBox1_MouseDoubleClick(object sender, MouseEventArgs e) { if (ShowActualSize) PictureBoxMouseDoubleClick?.Invoke(this, new Point(e.X, e.Y)); else { double rate = GetExpansionRate(); int x = (int)((e.X) / rate); int y = (int)(e.Y / rate); PictureBoxMouseDoubleClick?.Invoke(this, new Point(x, y)); } } } |
Form1クラスの処理
次にForm1クラスの処理を考えます。
最初は原寸大表示
コンストラクタを示します。ダブルクリックされたら編集用のフォームを表示させたいのでイベントハンドラを追加しています。また最初は「原寸大表示」が選択されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); // 最初は「原寸大表示」を選択する ShowActualSizeMenuItem.Checked = pictureBoxUserControl1.ShowActualSize; ShowReducedSizeMenuItem.Checked = !pictureBoxUserControl1.ShowActualSize; pictureBoxUserControl1.PictureBoxMouseDoubleClick += PictureBoxUserControl1_PictureBoxMouseDoubleClick; } } |
表示モードの切り替えに対応させる
メニューで原寸大表示または縮小表示が選択されたらPictureBoxUserControl.ShowActualSizeプロパティをtrueまたはfalseにして、メニューのチェック状態を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { private void ShowActualSizeMenuItem_Click(object sender, EventArgs e) { pictureBoxUserControl1.ShowActualSize = true; ShowActualSizeMenuItem.Checked = pictureBoxUserControl1.ShowActualSize; ShowReducedSizeMenuItem.Checked = !pictureBoxUserControl1.ShowActualSize; } private void ShowReducedSizeMenuItem_Click(object sender, EventArgs e) { pictureBoxUserControl1.ShowActualSize = false; ShowActualSizeMenuItem.Checked = pictureBoxUserControl1.ShowActualSize; ShowReducedSizeMenuItem.Checked = !pictureBoxUserControl1.ShowActualSize; } } |
画像ファイルを開く/保存する
メニューから開くが選択されたら選択されたファイルからBitmapを取得して、これをpictureBoxUserControl1.Bitmapプロパティにセットします。ファイルの保存が選択されたらpictureBoxUserControl1.BitmapプロパティからBitmapを取得してこれを保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public partial class Form1 : Form { private void OpenMenuItem_Click(object sender, EventArgs e) { OpenFileDialog dialog = new OpenFileDialog(); dialog.Filter = "PNG|*.png|JPEG|*.jpg|BMP|*.bmp|GIF|*.gif"; if (dialog.ShowDialog() == DialogResult.OK) { // 拡張子だけで画像ファイルと判断するのは危険なので例外処理をする try { // new Bitmap(dialog.FileName)をそのままセットすると // ファイルがロックされてしまうので、コピーをつくってこれを使う Bitmap bitmap = new Bitmap(dialog.FileName); pictureBoxUserControl1.Bitmap = new Bitmap(bitmap); bitmap.Dispose(); } catch { MessageBox.Show("このファイルは画像ファイルではありません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } } dialog.Dispose(); } private void SaveMenuItem_Click(object sender, EventArgs e) { SaveFileDialog dialog = new SaveFileDialog(); dialog.Filter = "PNG|*.png"; if (dialog.ShowDialog() == DialogResult.OK) { pictureBoxUserControl1.Bitmap.Save(dialog.FileName); } dialog.Dispose(); } } |
ダブルクリックされたらその部分を拡大して1ピクセル単位の編集ができるようにします。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { private void PictureBoxUserControl1_PictureBoxMouseDoubleClick(object sender, Point point) { // 後述 } } |
ImageEditPictureBoxクラス
1ピクセル単位の編集をするためのコントロールはどうやってつくればよいでしょうか?
PictureBoxに拡大した画像を描画してクリックするとその部分の色が変わるようなものをつくります。
外側にあるのは普通のPanelです。内側にあるのはPictureBoxを継承して作成したコントロールです。名前はImageEditPictureBoxとします。PictureBoxをそのままではなく継承したのはDoubleBufferedプロパティを使うためです。クラスの外からPanel.DoubleBuffered = true;とやるのはエラーになってしまうのです。
1 2 3 4 5 6 7 |
public class ImageEditPictureBox : PictureBox { public ImageEditPictureBox() { this.DoubleBuffered = true; } } |
ImageEditPictureBoxクラスはこれだけです。次にForm2クラスを考えます。
Form2クラスの処理
コンストラクタのなかでImageEditPictureBoxがクリックされたときと再描画されるときの処理ができるようにイベントハンドラを追加します。またImageEditPictureBoxのサイズが大きくなったときスクロールバーが表示されるようにImageEditPictureBoxの親のPanelにAutoScroll = trueを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public partial class Form2 : Form { PictureBoxUserControl PictureBoxUserControl = null; public int CellSize = 10; // 1ピクセルを10×10で表示する public Form2(PictureBoxUserControl control) { InitializeComponent(); PictureBoxUserControl = control; panel1.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom; panel1.AutoScroll = true; ImageEditPictureBox.MouseDown += ImageEditPictureBox_MouseDown; ImageEditPictureBox.Paint += ImageEditPictureBox_Paint; // ↓ いまは必要ないがこのあと必要になるので入れておく KeyPreview = true; } // ↓ いまは必要ないがこのあと必要になるので入れておく protected override bool ProcessDialogKey(Keys keyData) { return false; } // ↓ いまは必要ないがこのあと必要になるので入れておく protected override void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } // ↓ いまは必要ないがこのあと必要になるので入れておく protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); } } |
ImageEditPanelのサイズ変更
フォームがロードされたらPictureBoxUserControl.Bitmapを読み込んでCellのリストを作成します。それと同時にImageEditPictureBoxのサイズを変更します。PictureBoxUserControl.Bitmapのサイズの10倍です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public partial class Form2 : Form { // PictureBoxUserControlがダブルクリックされた座標に相当するピクセルの座標 public int CenterPixelX = 0; public int CenterPixelY = 0; // セルは縦横に何列必要か? int RowMax = 0; int ColumMax = 0; private void Form2_Load(object sender, EventArgs e) { // PictureBoxUserControl.Bitmapを読み込んでCellのリストを作成 List<Cell> cells = new List<Cell>(); for (int x = 0; x < PictureBoxUserControl.Bitmap.Width; x++) { for (int y = 0; y < PictureBoxUserControl.Bitmap.Height; y++) { cells.Add(new Cell(x, y, PictureBoxUserControl.Bitmap.GetPixel(x, y))); } } // ImageEditPictureBoxのサイズを計算 ColumMax = cells.Max(x => x.X) + 1; RowMax = cells.Max(x => x.Y) + 1; ImageEditPictureBox.ClientSize = new Size(ColumMax * CellSize, RowMax * CellSize); ImageEditPictureBox.Location = new Point(0, 0); // PictureBoxUserControlのダブルクリックされた部分が中心になるようにスクロールさせる int hScroll = CenterPixelX * CellSize - panel1.ClientSize.Width / 2; panel1.HorizontalScroll.Value = hScroll > 0 ? hScroll : 0; ; int vScroll = CenterPixelY * CellSize - panel1.ClientSize.Height / 2; panel1.VerticalScroll.Value = vScroll > 0 ? vScroll : 0; // PictureBoxUserControl.BitmapをCellSize倍に拡大してImageEditPictureBoxに表示させる Bitmap bitmap = new Bitmap(ColumMax * CellSize, RowMax * CellSize); Graphics graphics = Graphics.FromImage(bitmap); foreach (Cell cell in cells) graphics.FillRectangle(new SolidBrush(cell.Color), new Rectangle(cell.X * CellSize, cell.Y * CellSize, CellSize, CellSize)); graphics.Dispose(); ImageEditPictureBox.Image = bitmap; } } |
Cellクラスはこのようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class Cell { public int X; public int Y; public Color Color; public Cell(int x, int y, Color color) { X = x; Y = y; Color = color; } public override bool Equals(object o) { if (o == null || GetType() != o.GetType()) { return false; } if (X == ((Cell)o).X && Y == ((Cell)o).Y) return true; else return false; } public override int GetHashCode() { return 0; } } |
ImageEditPictureBoxがクリックされたときの処理
ImageEditPictureBoxでクリックされたときの処理を示します。クリックされた部分に相当するBitmapのピクセルを白に変更するとともにForm2に表示するPictureBox用のBitmapも変更しています。
ImageEditPictureBox.Invalidateメソッドを実行すると再描画がおこなわれ、変更されたBitmapが反映されます。
クリックされた部分を白に変更するだけではあまり実用的ではありませんが、次回以降、実用性のあるアプリケーションへと改良していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public partial class Form2 : Form { private void ImageEditPictureBox_MouseDown(object sender, MouseEventArgs e) { int x = e.X / CellSize; int y = e.Y / CellSize; Bitmap bitmap1 = PictureBoxUserControl.Bitmap; bitmap1.SetPixel(x, y, Color.White); PictureBoxUserControl.Bitmap = bitmap1; Bitmap bitmap2 = (Bitmap)ImageEditPictureBox.Image; Graphics graphics = Graphics.FromImage(bitmap2); graphics.FillRectangle(new SolidBrush(Color.White), new Rectangle(x * CellSize, y * CellSize, CellSize, CellSize)); graphics.Dispose(); ImageEditPictureBox.Invalidate(); } private void ImageEditPictureBox_Paint(object sender, PaintEventArgs e) { // セルの境界線を描画する for (int i = 0; i <= ColumMax; i++) graphics.DrawLine(Pens.Black, new Point(i * CellSize, 0), new Point(i * CellSize, RowMax * CellSize)); for (int i = 0; i <= RowMax + 1; i++) graphics.DrawLine(Pens.Black, new Point(0, i * CellSize), new Point(ColumMax * CellSize, i * CellSize)); } } |
最後にForm1クラスのPictureBoxUserControlがダブルクリックされたときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { private void PictureBoxUserControl1_PictureBoxMouseDoubleClick(object sender, Point point) { if (pictureBoxUserControl1.Bitmap == null) return; // point.Xとpoint.YはBitmap上のピクセル座標に変換されているので、そのまま使ってOK。 Form2 form2 = new Form2(pictureBoxUserControl1); form2.CenterPixelX = point.X; form2.CenterPixelY = point.Y; form2.ShowDialog(); } } |