前回作成した1ピクセル単位で編集できる画像エディタはあまり実用性があるものとはいえませんでした。今回はその点を改善します。
まずクリックで置き換えることができる色を指定できないと使い物になりません。そこで以下のような機能を追加することにしました。
点だけでなく直線、矩形、楕円なども描画できるようにする。
色を指定できるようにする。ColorDialogを使わずに画像上に使われている色も指定対象にできるようにする
マウスが移動したりクリックしたときに、そこがBitmapのどのピクセル座標なのかわかるようにする
現在選択されている色がどの色かわかるようにする
点だけを変更するだけでもこれらの改善点が思いつきます。
Contents
色を指定できるようにする
まずフィールド変数とコンストラクタを示します。
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 partial class Form2 : Form { PictureBoxUserControl PictureBoxUserControl = null; public int CellSize = 10; List<Cell> Cells = new List<Cell>(); // クリックされた点の色 Color ClickedColor = Color.Empty; // 選択された色 Color SelectedColor = Color.Empty; public Form2(PictureBoxUserControl control) { InitializeComponent(); PictureBoxUserControl = control; // このうえにImageEditPictureBoxが貼り付けてられている panel1.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom; panel1.AutoScroll = true; // このうえに編集に必要なLabel、CheckBox、RadioButtonが貼り付けてられている panel2.Anchor = AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom; ImageEditPictureBox.MouseDown += ImageEditPictureBox_MouseDown; ImageEditPictureBox.MouseMove += ImageEditPictureBox_MouseMove; ImageEditPictureBox.MouseUp += ImageEditPictureBox_MouseUp; ImageEditPictureBox.Paint += ImageEditPictureBox_Paint; } } |
[色を生成する]をクリックすると編集に使える色を選択できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form2 : Form { // ColorDialogから編集に使う色を選択する private void ButtonCreateColor_Click(object sender, EventArgs e) { ColorDialog dialog = new ColorDialog(); if (dialog.ShowDialog() == DialogResult.OK) { SelectedColor = dialog.Color; SelectedPictureBox.BackColor = dialog.Color; } dialog.Dispose(); } } |
自分で色を作るのではなく隣のピクセルと同じ色にしたいときもあります。最後にクリックされた部分の色が保存されるので、[色をコピーする]をクリックことで選択された色として指定することができます。
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 |
public partial class Form2 : Form { // クリックされた点の色として保存されている色を、選択された色にコピーする private void ButtonColorCopy_Click(object sender, EventArgs e) { SelectedColor = ClickedColor; SelectedPictureBox.BackColor = ClickedColor; } private void ImageEditPictureBox_MouseDown(object sender, MouseEventArgs e) { // クリックされたBitmapのピクセル座標を表示する ClickedLabel.Text = String.Format("{0}, {1}", e.X / CellSize, e.Y / CellSize); Bitmap bitmap = PictureBoxUserControl.Bitmap; // RadioButtonで[範囲選択]が選択されている場合、クリックされた点の色を表示する if (RadioButtonRange.Checked) { Bitmap sourceBitmap = PictureBoxUserControl.Bitmap; ClickedColor = sourceBitmap.GetPixel(e.X / CellSize, e.Y / CellSize); ClickedPictureBox.BackColor = ClickedColor; } // RadioButtonで[点]が選択されている場合、選択された色でクリックされた点の色を置き換える if (RadioButtonPoint.Checked) MouseDownForDrawPoint(e.X, e.Y); } } |
RadioButtonで[点]が選択されている場合、クリックするとその部分の色が置き換わります。そのための処理を示します。
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) { // ImageEditPictureBox.Imageの変更 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")); // clientXとclientYをCellSizeの整数倍にする int x = clientX / CellSize * CellSize; int y = clientY / CellSize * CellSize; graphics.FillRectangle(solidBrush, new Rectangle(x, y, CellSize, CellSize)); graphics.Dispose(); ImageEditPictureBox.Invalidate(); // PictureBoxUserControlに表示されているBitmapも変更する Bitmap sourceBitmap = PictureBoxUserControl.Bitmap; sourceBitmap.SetPixel(clientX / CellSize, clientY / CellSize, SelectedColor); PictureBoxUserControl.Bitmap = sourceBitmap; } } |
マウスが移動したりボタンが離されたときの処理はとくにありませんが、他の機能を追加するときに必要になるので追加しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form2 : Form { // いまは使わないが、あとで使う private void ImageEditPictureBox_MouseMove(object sender, MouseEventArgs e) { } // いまは使わないが、あとで使う private void ImageEditPictureBox_MouseUp(object sender, MouseEventArgs e) { } } |
直線を描画する
点だけでなく直線の描画もしてみたいものです。直線を描画するための処理をForm2クラス内に書くとクラスが肥大化するので別のクラスを作成します。
DrawLineクラス
DrawLineクラスを作成します。クラス内部の処理で必要になるのはImageEditPanel、直線の開始座標、直線の色と太さ、セルの大きさです。
DrawLineクラスのコンストラクタとプロパティを示します。
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 |
public class DrawLine { public DrawLine(ImageEditPictureBox imageEditPictureBox, Point startPoint, Color color, int width, int cellSize) { ImageEditPictureBox = imageEditPictureBox; StartPoint = startPoint; Color = color; Width = width; CellSize = cellSize; Cells = new List<Cell>(); } public ImageEditPictureBox ImageEditPictureBox { get; } public Point StartPoint { get; } public Color Color { get; } public int Width { get; } protected int CellSize { get; } public List<Cell> Cells { protected set; get; } } |
マウスボタンが離されるまで仮の直線を描画する
マウスをドラッグすると開始点からその座標にむけて直線が引かれます。しかしこの線は仮の直線で確定したものではありません。マウスを動かすと古い仮の直線は消されて現在のマウス上の直線を描画させます。
処理としてはマウスが移動してCurPointにPointがセットされるとこれを記憶しておき、ImageEditPanel.Invalidateメソッドを呼び出すことです。これによって仮の直線が描画されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class DrawLine { Point _curPoint = Point.Empty; public Point CurPoint { set { _curPoint = value; ImageEditPictureBox.Invalidate(); } get { return _curPoint; } } } |
実際に仮の直線が描画される処理を示します。実際に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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public class DrawLine { public void DrawTempLine(Graphics graphics) { Pen pen = new Pen(Color.Black, Width); int x1 = StartPoint.X / CellSize; int y1 = StartPoint.Y / CellSize; int x2 = CurPoint.X / CellSize; int y2 = CurPoint.Y / CellSize; Bitmap bitmap = new Bitmap(Math.Abs(x2 - x1) + Width + 1, Math.Abs(y2 - y1) + Width + 1); Graphics g = Graphics.FromImage(bitmap); int startX = 0; int startY = ((x1 <= x2 && y1 <= y2) || (x1 > x2 && y1 > y2)) ? 0 : Math.Abs(y2 - y1); int endX = Math.Abs(x2 - x1); int endY = (x1 <= x2 && y1 > y2) || (x1 > x2 && y1 <= y2) ? 0 : Math.Abs(y2 - y1); g.DrawLine(pen, new Point(startX, startY), new Point(endX, endY)); pen.Dispose(); g.Dispose(); DrawCells(bitmap, graphics); bitmap.Dispose(); } protected void DrawCells(Bitmap tempBitmap, Graphics graphics) { Cells = new List<Cell>(); int x0 = GetLeftMargin(); int y0 = GetTopMargin(); SolidBrush solidBrush; if (Color != Color.Empty) solidBrush = new SolidBrush(Color); else solidBrush = new SolidBrush(Color.FromName("Control")); for (int x = 0; x < tempBitmap.Width; x++) { for (int y = 0; y < tempBitmap.Height; y++) { if (tempBitmap.GetPixel(x, y).ToArgb() == Color.Black.ToArgb()) { graphics.FillRectangle( solidBrush, new Rectangle((x + x0) * CellSize, (y + y0) * CellSize, CellSize, CellSize)); Cells.Add(new Cell(x + x0, y + y0, Color)); } } } solidBrush.Dispose(); } } |
GetLeftMarginメソッドとGetTopMarginメソッドは直線が描画されている部分の左と上にどれだけ空白部分があるかを求めるためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class DrawLine { protected int GetLeftMargin() { int x1 = StartPoint.X / CellSize; int x2 = CurPoint.X / CellSize; return x1 <= x2 ? x1 : x2; } protected int GetTopMargin() { int y1 = StartPoint.Y / CellSize; int y2 = CurPoint.Y / CellSize; return y1 <= y2 ? y1 : y2; } } |
それから位置が確定される前であれば直線を上下左右に移動することができるようにします。移動対象は始点と終点の両方、始点または終点の片方だけの3つから選べるようにします。
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 70 |
public class DrawLine { public void MoveLeft(bool start, bool end) { if (start) { Point point1 = StartPoint; point1.X -= CellSize; StartPoint = point1; } if (end) { Point point2 = _curPoint; point2.X -= CellSize; _curPoint = point2; } ImageEditPictureBox.Invalidate(); } public void MoveUp(bool start, bool end) { if (start) { Point point1 = StartPoint; point1.Y -= CellSize; StartPoint = point1; } if (end) { Point point2 = _curPoint; point2.Y -= CellSize; _curPoint = point2; } ImageEditPictureBox.Invalidate(); } public void MoveRight(bool start, bool end) { if (start) { Point point1 = StartPoint; point1.X += CellSize; StartPoint = point1; } if (end) { Point point2 = _curPoint; point2.X += CellSize; _curPoint = point2; } ImageEditPictureBox.Invalidate(); } public void MoveDown(bool start, bool end) { if (start) { Point point1 = StartPoint; point1.Y += CellSize; StartPoint = point1; } if (end) { Point point2 = _curPoint; point2.Y += CellSize; _curPoint = point2; } ImageEditPictureBox.Invalidate(); } } |
直線描画のためのマウスボタンが押されたときの処理
DrawLineクラスを用いた直線描画のための処理を示します。
まずはマウスボタンが押されたときの処理です。DrawLineのインスタンスを生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form2 : Form { DrawLine DrawLine = null; private void ImageEditPictureBox_MouseDown(object sender, MouseEventArgs e) { if (RadioButtonLine.Checked) MouseDownForDrawLine(e.X, e.Y); } void MouseDownForDrawLine(int clientX, int clientY) { DrawLine = new DrawLine(ImageEditPictureBox, new Point(clientX, clientY), SelectedColor, (int)numericUpDown1.Value, CellSize); } } |
直線描画のためのドラッグ時の処理
次にドラッグされているときの処理を示します。マウスの座標をCurPointプロパティにセットします。するとImageEditPaneの再描画がおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form2 : Form { private void ImageEditPictureBox_MouseMove(object sender, MouseEventArgs e) { if (RadioButtonLine.Checked && DrawLine != null) MouseMoveForDrawLine(e.X, e.Y); } void MouseMoveForDrawLine(int clientX, int clientY) { DrawLine.CurPoint = new Point(clientX, clientY); } } |
直線描画のためのドロップ時の処理
ドラッグが終わってドロップされたときに直線が確定するという仕様にしようと思ったのですが、予定を変更します。直線描画のためのドラッグが終わってもその段階では直線が確定せず、Enterキーが押されたときや別のところがクリックされたときに確定させることにします。そのためImageEditPictureBox_MouseUp内の処理はとくにありません。
確定時の処理
直線が確定したらそのときに色をつけるセルをChangeCellsに格納します。そのあとChangeCellsをつかって表示されているBitmapを変更し、ChangeCells内のデータをクリアします。ChangeCellsが空でないということはこのときに直線が確定したことを意味しています。そのあとDrawConfirmedメソッドを実行すれば直線が確定します。
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 |
public partial class Form2 : Form { List<Cell> ChangeCells = new List<Cell>(); void DecisionForDrawLine() { ChangeCells = DrawLine.Cells; DrawLine = null; } 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(); return true; } return false; } } |
ImageEditPictureBox_Paintにおける処理
描画処理の部分を示します。DrawLineがnullではなく残っている場合は直線は確定されていません。この場合はDrawLine.DrawTempLineメソッドを呼び出します。ここではPictureBoxUserControl.Bitmapの書き換えはおこなわれず、ImageEditPictureBox.Imageの変更もおこなわれません。
確定前の直線などを描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form2 : Form { private void ImageEditPictureBox_Paint(object sender, PaintEventArgs e) { // 確定前の直線などの描画 DrawProvisional(e.Graphics); // セル同士の境界線の描画 DrawBorderOfCells(e.Graphics); } } |
直線などが確定されるまえの仮の描画処理を示します。前述のDrawLine.DrawTempLineメソッドが呼び出され、仮の描画がおこなわれます。
1 2 3 4 5 6 7 8 |
public partial class Form2 : Form { void DrawProvisional(Graphics graphics) { if (RadioButtonLine.Checked && DrawLine != null) DrawLine.DrawTempLine(graphics); } } |
これはセル同士の境界線を描画するためのメソッドです。ImageEditPictureBox_Paintを長くしたくないので独立したメソッドをして作り直しました。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form2 : Form { void DrawBorderOfCells(Graphics graphics) { 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)); } } |
確定前の直線の移動処理
位置を確定させるまえであれば直線の位置を変更することができるようにします。Enterキーを押すと確定されます。方向キーで全体を移動、方向キー+Shiftキーは始点部分のみを移動、方向キー+Ctrlキーは終点部分のみを移動します。また別の場所をクリックした場合も確定処理がおこなわれます。
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 partial class Form2 : Form { protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Enter) DecisionDrawing(); if (e.KeyCode == Keys.Left) MoveLeftDrawing(!e.Control, !e.Shift); if (e.KeyCode == Keys.Up) MoveUpDrawing(!e.Control, !e.Shift); if (e.KeyCode == Keys.Right) MoveRightDrawing(!e.Control, !e.Shift); if (e.KeyCode == Keys.Down) MoveDownDrawing(!e.Control, !e.Shift); base.OnKeyDown(e); } private void ImageEditPictureBox_MouseDown(object sender, MouseEventArgs e) { // もし確定されていないのであれば確定させ、クリック時の処理は終わりにする // すでに未確定の直線が存在しない場合はfalseを返すので新しい直線の始点を設定することができる if (DecisionDrawing()) return; // 以下、略 // 新しい直線の始点設定の処理など } } |
確定処理をおこなうメソッドを示します。未確定の直線を確定させた場合はtrueを返します。ImageEditPictureBox_MouseDown内でこのメソッドがtrueを返した場合は確定処理以外はなにもしません。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form2 : Form { bool DecisionDrawing() { if (DrawLine != null) DecisionForDrawLine(); // 確定時の直線などの描画 return DrawConfirmed(); } } |
確定前の直線を移動させる処理を示します。第一引数がtrueなら始点が、第二引数が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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public partial class Form2 : Form { void MoveLeftDrawing(bool start, bool end) { if (RadioButtonLine.Checked && DrawLine != null) DrawLine.MoveLeft(start, end); if (RadioButtonRectangle.Checked && DrawRectangle != null) DrawRectangle.MoveLeft(start, end); if (RadioButtonRectangle.Checked && FillRectangle != null) FillRectangle.MoveLeft(start, end); if (RadioButtonEllipse.Checked && DrawEllipse != null) DrawEllipse.MoveLeft(start, end); if (RadioButtonEllipse.Checked && FillEllipse != null) FillEllipse.MoveLeft(start, end); } void MoveUpDrawing(bool start, bool end) { if (RadioButtonLine.Checked && DrawLine != null) DrawLine.MoveUp(start, end); if (RadioButtonRectangle.Checked && DrawRectangle != null) DrawRectangle.MoveUp(start, end); if (RadioButtonRectangle.Checked && FillRectangle != null) FillRectangle.MoveUp(start, end); if (RadioButtonEllipse.Checked && DrawEllipse != null) DrawEllipse.MoveUp(start, end); if (RadioButtonEllipse.Checked && FillEllipse != null) FillEllipse.MoveUp(start, end); } void MoveRightDrawing(bool start, bool end) { if (RadioButtonLine.Checked && DrawLine != null) DrawLine.MoveRight(start, end); if (RadioButtonRectangle.Checked && DrawRectangle != null) DrawRectangle.MoveRight(start, end); if (RadioButtonRectangle.Checked && FillRectangle != null) FillRectangle.MoveRight(start, end); if (RadioButtonEllipse.Checked && DrawEllipse != null) DrawEllipse.MoveRight(start, end); if (RadioButtonEllipse.Checked && FillEllipse != null) FillEllipse.MoveRight(start, end); } void MoveDownDrawing(bool start, bool end) { if (RadioButtonLine.Checked && DrawLine != null) DrawLine.MoveDown(start, end); if (RadioButtonRectangle.Checked && DrawRectangle != null) DrawRectangle.MoveDown(start, end); if (RadioButtonRectangle.Checked && FillRectangle != null) FillRectangle.MoveDown(start, end); if (RadioButtonEllipse.Checked && DrawEllipse != null) DrawEllipse.MoveDown(start, end); if (RadioButtonEllipse.Checked && FillEllipse != null) FillEllipse.MoveDown(start, end); } } |