アンドゥ、リドゥ 1ピクセル単位で編集できる画像エディタ(6)
アンドゥ、リドゥの機能を追加します。
Contents
Undobufクラス
まずアンドゥバッファを管理するUndobufクラスを作成します。ここでかつて使用されていたBitmapを保存しておき、Undo、Redoされたら取り出して使います。
保存するBitmapはPictureBoxUserControlで表示させるものとImageEditPictureBoxで保存するものがあります。一方から他方をつくることも可能ですが、実際に動作させてみると元の画像ファイルが大きいと時間がかかります。そこで最初から2種類つくっておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class BitmapPair { public Bitmap ImageEditBitmap { get; } public Bitmap SourceBitmap { get; } public BitmapPair(Bitmap sourceBitmap, Bitmap imageEditBitmap) { ImageEditBitmap = imageEditBitmap; SourceBitmap = sourceBitmap; } public void Dispose() { ImageEditBitmap.Dispose(); SourceBitmap.Dispose(); } } |
新しくデータが追加されたらAddメソッドを実行します。すると引数で渡されたデータが現在のデータとなり、それまでCurBitmapに格納されていたデータはUndobufsのなかに格納されます。
Undoメソッドが実行されるとUndobufsのなかに一番最後に格納されていたデータがCurBitmapPairに格納され、それまでCurBitmapPairにあったデータはRedobufsに格納されます。Undobufsが空の時はUndoはできないのでfalseが返されます。
Redoメソッドが実行されるとRedobufsのなかに一番最後に格納されていたデータがCurBitmapPairに格納され、それまでCurBitmapPairにあったデータはUndobufsに格納されます。Redobufsが空の時はRedoはできないので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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class Undobuf { List<BitmapPair> Undobufs = new List<BitmapPair>(); List<BitmapPair> Redobufs = new List<BitmapPair>(); public BitmapPair CurBitmapPair = null; public Undobuf() { } public void Add(Bitmap newSourceBitmap, Bitmap newImageEditBitmap) { if (CurBitmapPair != null) Undobufs.Add(CurBitmapPair); CurBitmapPair = new BitmapPair(newSourceBitmap, newImageEditBitmap); foreach (BitmapPair bitmapPair in Redobufs) bitmapPair.Dispose(); Redobufs.Clear(); } public bool Undo() { if (Undobufs.Count == 0) return false; if (CurBitmapPair != null) Redobufs.Add(CurBitmapPair); CurBitmapPair = Undobufs.Last(); Undobufs.Remove(CurBitmapPair); return true; } public bool Redo() { if (Redobufs.Count == 0) return false; if (CurBitmapPair != null) Undobufs.Add(CurBitmapPair); CurBitmapPair = Redobufs.Last(); Redobufs.Remove(CurBitmapPair); return true; } public void AllClear() { foreach (BitmapPair bitmapPair in Undobufs) bitmapPair.Dispose(); foreach (BitmapPair bitmapPair in Redobufs) bitmapPair.Dispose(); Undobufs.Clear(); Redobufs.Clear(); CurBitmapPair = null; } public bool IsEmpty() { return (Undobufs.Count == 0 && Redobufs.Count == 0); } } |
UndobufクラスのインスタンスをForm1クラスのなかに作成します。またBitmapのペア(PictureBoxUserControl用とImageEditPictureBox用)を格納するためのメソッドもつくっておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { Undobuf Undobuf = new Undobuf(); public Form1() { InitializeComponent(); // 追加 } public void AddUndobuf(Bitmap sourceBitmap, Bitmap imageEditBitmap) { Undobuf.Add(sourceBitmap, imageEditBitmap); } } |
画像が編集されたときにUndobufを追加する処理
画像が編集されたときにUndobufを追加する処理が必要です。
ファイルが読み出されたときの処理
一番最初に追加しないといけないのはファイルが読み出されたときです。
Form1クラスでおこなう処理を示します。まずファイルが読み出されたときにおこなわれる処理を示します。これより前に編集されていた画像ファイルがあるかもしれないので、最初にUndobufをクリアします。
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 |
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 { Bitmap bitmap = new Bitmap(dialog.FileName); pictureBoxUserControl1.Bitmap = new Bitmap(bitmap); bitmap.Dispose(); // Undobufをクリア Undobuf.AllClear(); } catch { MessageBox.Show("このファイルは画像ファイルではありません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } } dialog.Dispose(); } } |
次にダブルクリックされたら編集用のフォームを表示させるのですが、このときにPictureBoxUserControlに表示されていたBitmapのコピーとImageEditPictureBoxに表示させるためのBitmapを生成してUndobufに追加します。
この処理がおこなわれるのは新たにファイルが開かれたあと最初に編集用のフォームを表示するときだけです。それ以外のときはすでに存在するUndobufを使います。Undobufが空ならファイルが開かれてから最初に編集用のフォームが表示されるときです(ファイルを開くときにUndobufを空にしたため)。
もうひとつ変更しなければならないのはShowDialogの引数です。Form2クラスからForm1クラスにアクセスできるように引数を渡しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { private void PictureBoxUserControl1_PictureBoxMouseDoubleClick(object sender, Point point) { Bitmap sourceBitmap = pictureBoxUserControl1.Bitmap; if (sourceBitmap == null) return; Form2 form2 = new Form2(pictureBoxUserControl1); form2.CenterPixelX = point.X; form2.CenterPixelY = point.Y; if (Undobuf.IsEmpty()) { Bitmap copyBitmap = new Bitmap(sourceBitmap); Undobuf.Add(copyBitmap, form2.GetBitmapImageEdit(copyBitmap)); } form2.ShowDialog(this); } } |
PictureBoxUserControlに表示されていたBitmapのコピーからImageEditPictureBoxに表示させるためのBitmapを生成するための処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form2 : Form { public Bitmap GetBitmapImageEdit(Bitmap sourceBitmap) { List<Cell> cells = new List<Cell>(); for (int x = 0; x < sourceBitmap.Width; x++) { for (int y = 0; y < sourceBitmap.Height; y++) cells.Add(new Cell(x, y, PictureBoxUserControl.Bitmap.GetPixel(x, y))); } Bitmap bitmap = new Bitmap(sourceBitmap.Width * CellSize, sourceBitmap.Height * 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(); return bitmap; } } |
画像が編集されたときの処理
画像が編集されたときにUndobufを追加する処理を示します。
画像が編集されたときに実行されるメソッドのなかでやればいいのですが、MouseDownForDrawPointメソッド(点が描画されたとき)、MouseDownForFillConnectedColorメソッド、MouseDownForFillEnclosedAreaメソッド(塗りつぶしの処理)、DrawConfirmedメソッド(それ以外の描画処理が確定したとき)の3つがあります。そこでそれぞれのなかでUndobufを追加する処理をおこないます。
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 |
public partial class Form2 : Form { bool DrawConfirmed() { if (ChangeCells != null && ChangeCells.Count != 0) { Bitmap bitmap1 = PictureBoxUserControl.Bitmap; Bitmap bitmap2 = (Bitmap)ImageEditPictureBox.Image; Graphics graphics = Graphics.FromImage(bitmap2); foreach (Cell cell in ChangeCells) { SolidBrush solidBrush; if (cell.Color != Color.Empty) solidBrush = new SolidBrush(cell.Color); else solidBrush = new SolidBrush(Color.FromName("Control")); if (0 <= cell.X && cell.X < bitmap1.Width && 0 <= cell.Y && cell.Y < bitmap1.Height) { graphics.FillRectangle(solidBrush, new Rectangle(cell.X * CellSize, cell.Y * CellSize, CellSize, CellSize)); bitmap1.SetPixel(cell.X, cell.Y, cell.Color); } } graphics.Dispose(); ImageEditPictureBox.Image = bitmap2; PictureBoxUserControl.Bitmap = bitmap1; ChangeCells.Clear(); // 追加するのはこの部分。 // PictureBoxUserControl.BitmapとImageEditPictureBox.Imageは編集されると変化していくので、 // bitmap1、bitmap2そのものではなくコピーを渡す ((Form1)this.Owner).AddUndobuf(new Bitmap(bitmap1), new Bitmap(bitmap2)); return true; } return 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 |
public partial class Form2 : Form { void MouseDownForDrawPoint(int clientX, int clientY) { Bitmap bitmap = (Bitmap)ImageEditPictureBox.Image; Graphics graphics = Graphics.FromImage(bitmap); SolidBrush solidBrush; if (SelectedColor != Color.Empty) solidBrush = new SolidBrush(SelectedColor); else solidBrush = new SolidBrush(Color.FromName("Control")); int x = clientX / CellSize * CellSize; int y = clientY / CellSize * CellSize; graphics.FillRectangle(solidBrush, new Rectangle(x, y, CellSize, CellSize)); graphics.Dispose(); ImageEditPictureBox.Invalidate(); Bitmap sourceBitmap = PictureBoxUserControl.Bitmap; sourceBitmap.SetPixel(clientX / CellSize, clientY / CellSize, SelectedColor); PictureBoxUserControl.Bitmap = sourceBitmap; // 追加するのはこの部分。bitmapのコピーを渡す ((Form1)this.Owner).AddUndobuf(new Bitmap(sourceBitmap), new Bitmap(bitmap)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form2 : Form { // 同じ色で繋がった領域を塗りつぶす void MouseDownForFillConnectedColor(int clientX, int clientY) { FillColor fillColor = new FillColor(PictureBoxUserControl, ImageEditPictureBox, CellSize); fillColor.FillConnectedColor(clientX, clientY, SelectedColor); // 追加するのはこの部分。bitmapのコピーを渡す ((Form1)this.Owner).AddUndobuf(new Bitmap(PictureBoxUserControl.Bitmap), new Bitmap(ImageEditPictureBox.Image)); } // 指定色で囲まれた領域を塗りつぶす void MouseDownForFillEnclosedArea(int clientX, int clientY) { FillColor fillColor = new FillColor(PictureBoxUserControl, ImageEditPictureBox, CellSize); fillColor.ForFillEnclosedArea(clientX, clientY, SelectedColor); // 追加するのはこの部分。bitmapのコピーを渡す ((Form1)this.Owner).AddUndobuf(new Bitmap(PictureBoxUserControl.Bitmap), new Bitmap(ImageEditPictureBox.Image)); } } |
実際にUndo、Redoを実行するための処理
実際にUndo、Redoを実行するためのメソッドもつくっておきます。それから各コントロール内にも取得されたUndobufから画像を復元するためのメソッドもつくります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { public bool Undo(Form2 form2) { if (!Undobuf.Undo()) return false; form2.SetBitmapFromUndobuf(Undobuf); pictureBoxUserControl1.SetBitmapFromUndobuf(Undobuf); return true; } public bool Redo(Form2 form2) { if (!Undobuf.Redo()) return false; form2.SetBitmapFromUndobuf(Undobuf); pictureBoxUserControl1.SetBitmapFromUndobuf(Undobuf); return true; } } |
これは取得されたUndobufからImageEditPictureBoxに表示させる画像を復元するためのメソッドです。
1 2 3 4 5 6 7 8 9 |
public partial class Form2 : Form { public void SetBitmapFromUndobuf(Undobuf undobuf) { // コピーをわたすようにしないとundobuf.CurBitmapPair.ImageEditBitmapが改ざんされてしまう ImageEditPictureBox.Image = new Bitmap(undobuf.CurBitmapPair.ImageEditBitmap) ; ImageEditPictureBox.Invalidate(); } } |
これは取得されたUndobufからPictureBoxUserControlに表示させる画像を復元するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public partial class PictureBoxUserControl : UserControl { public void SetBitmapFromUndobuf(Undobuf undobuf) { // コピーをわたすようにしないとundobuf.CurBitmapPair.SourceBitmapが改ざんされてしまう _bitmap = new Bitmap(undobuf.CurBitmapPair.SourceBitmap); // 実物大 if (ShowActualSize) this.pictureBox1.Image = _bitmap; else { // 縮小 double rate = GetExpansionRate(); Bitmap newBitmap = new Bitmap(this.Width, this.Height); Graphics graphics = Graphics.FromImage(newBitmap); 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 = newBitmap; } } } |
あとはForm2クラスでキー操作をしたときにUndoとRedoの処理ができるようにすれば完成です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public partial class Form2 : Form { protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Z) Undo(); if (e.KeyCode == Keys.Y) Redo(); base.OnKeyDown(e); } void Undo() { if (!((Form1)Owner).Undo(this)) MessageBox.Show("Undoできません", "", MessageBoxButtons.OK, MessageBoxIcon.Error); } void Redo() { if(!((Form1)Owner).Redo(this)) MessageBox.Show("Redoできません", "", MessageBoxButtons.OK, MessageBoxIcon.Error); } } |