これまで作成してきたイメージエディタは大きなサイズの画像を編集しようとすると動作が遅いです。
たとえば範囲選択されている部分をドラッグで移動させる処理はこのようになっています。Bitmapのコピーを作成してそこに移動中のデータを書き込み、これをPictureBox.Imageにセットする、このような処理をマウスが動くたびに繰り返すわけですから遅くなるのは当然です。
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 UserControlImage : UserControl { void OnMouseMoveForMoveSelection(Point bitmapPoint) { if(BitmapRectangle == null) return; int xMove = bitmapPoint.X - rectangleMoveStartMousePosX; int yMove = bitmapPoint.Y - rectangleMoveStartMousePosY; BitmapRectangle.X = rectangleMoveStartLocationX + xMove; BitmapRectangle.Y = rectangleMoveStartLocationY + yMove; Bitmap newBitmap = new Bitmap(Bitmap); Graphics g = Graphics.FromImage(newBitmap); if(blankBitmap != null) { g.DrawImage(blankBitmap.Bitmap, blankBitmap.Location); } g.DrawImage(BitmapRectangle.Bitmap, BitmapRectangle.Location); g.Dispose(); Bitmap bitmap = DrawBoderRectangle(newBitmap); ShowBitmap(bitmap); newBitmap.Dispose(); bitmap.Dispose(); } } |
そこでPaintイベントを利用して処理を高速化することを考えます。
まずはPaintイベントを捕捉したときの処理です。
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 |
public partial class UserControlImage : UserControl { private void PictureBox1_Paint(object sender, PaintEventArgs e) { if(isMouseDown || BitmapRectangle != null) { ShowBitmap(e.Graphics, Bitmap); if(EditMode == EditMode.Selection) { DrawBoderRectangle(e.Graphics); } else if(EditMode == EditMode.SelectionFree) { if(isMouseDown) DrawFreeLine(e.Graphics, linePoints, new Pen(GetBoderBrush())); else DrawBoderRectangle(e.Graphics); } else if(EditMode == EditMode.DrawRectangle || EditMode == EditMode.FillRectangle || EditMode == EditMode.DrawEllipse || EditMode == EditMode.FillEllipse) DrawRectangle(e.Graphics); else if(EditMode == EditMode.FreeLine) DrawFreeLine(e.Graphics, linePoints, new Pen(GetBoderBrush())); else if(EditMode == EditMode.DrawLine) DrawLine(e.Graphics); else DrawBoderRectangle(e.Graphics); e.Graphics.DrawRectangle(new Pen(Color.Black), new Rectangle(-1, -1, Bitmap.Width - ScrollBarPosX + 1, Bitmap.Height - ScrollBarPosY + 1)); } else { if(Bitmap != null) { if(isMouseDownForFieldSizeChange) return; ShowBitmap(e.Graphics, Bitmap); if(EditMode == EditMode.SelectionFree) { if(linePoints != null) DrawFreeLine(e.Graphics, linePoints, new Pen(GetBoderBrush())); } DrawFieldGrip(Bitmap, e.Graphics); } } } } |
これまでビットマップをピクチャーボックス上に表示するために使ってきたShowBitmap(Bitmap bitmap)メソッドはInvalidatePictureBoxメソッドを呼ぶためだけに変更しました。
1 2 3 4 5 6 7 8 |
public partial class UserControlImage : UserControl { public void ShowBitmap(Bitmap bitmap) { OptimizationScrloolBar(bitmap); InvalidatePictureBox(); } } |
フィールド変数linePointsに格納されたデータが必要なのでOnMouseUpForSelectFreeAreaメソッドのなかでクリアするのはやめることにします。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 |
public partial class UserControlImage : UserControl { void OnMouseUpForSelectFreeArea(Point bitmapPoint) { isMouseDown = false; endPoint = bitmapPoint; linePoints.Add(bitmapPoint); // 自由曲線でかこまれた境界線を保存する borderPoints = GetBorderPointsFromFreeLine(Bitmap, linePoints); //linePoints.Clear(); ここではクリアしない startPoint = new Point(-1, -1); endPoint = new Point(-1, -1); InvalidatePictureBox(); } public Bitmap Bitmap { get { return _bitmap; } set { linePoints = null; // ここでクリアする if(_bitmap != value && _bitmap != null) { if(IsNewBitmap(_bitmap, value)) { UndoBufs.InsertUndoBuf(new UndoBuf(_bitmap, DragDropRectangle)); UndoBufs.ClearRedoBufs(); BitmapChanged?.Invoke(this, new EventArgs()); } } _bitmap = value; // 変更したら描画内容も変更する pictureBox1.Invalidate(); } } } |
では各メソッドについてそれぞれみていきましょう。
DrawBoderRectangleメソッドは選択されている部分の境界線を描画するメソッドです。引数がGraphicsになっているだけでやっていることはほとんど同じです。startPoint, endPointの2点から矩形を取得して描画しています。ビットマップに描画するのではなくピクチャーボックスの上に描画しています。
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 |
public partial class UserControlImage : UserControl { void DrawBoderRectangle(Graphics g) { Rectangle rect; if(BitmapRectangle == null) rect = GetRectangle(startPoint, endPoint); else rect = BitmapRectangle.Rectangle; rect.X -= ScrollBarPosX; rect.Y -= ScrollBarPosY; g.DrawRectangle(new Pen(GetBoderBrush()), rect); List<Rectangle> griperRects = new List<Rectangle>(); griperRects.Add(new Rectangle(new Point(rect.X, rect.Y), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.X + rect.Width / 2 - GriperSize / 2, rect.Y), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.Right - GriperSize, rect.Y), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.X, rect.Bottom - GriperSize), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.X + rect.Width / 2 - GriperSize / 2, rect.Bottom - GriperSize), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.Right - GriperSize, rect.Bottom - GriperSize), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.X, rect.Y + rect.Height / 2 - GriperSize / 2), new Size(GriperSize, GriperSize))); griperRects.Add(new Rectangle(new Point(rect.Right - GriperSize, rect.Y + rect.Height / 2 - GriperSize / 2), new Size(GriperSize, GriperSize))); g.FillRectangles(GetBoderBrush(), griperRects.ToArray()); } } |
DrawFreeLineメソッドはstartPoint, endPointの2点から直線を描画するメソッドです。
1 2 3 4 5 6 7 |
public partial class UserControlImage : UserControl { void DrawFreeLine(Graphics g) { g.DrawLine(new Pen(DrawColor), startPoint.X- ScrollBarPosX, startPoint.Y- ScrollBarPosY, endPoint.X- ScrollBarPosX, endPoint.Y- ScrollBarPosY); } } |
DrawFreeLineメソッドは記憶された点のリストから自由曲線を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class UserControlImage : UserControl { void DrawFreeLine(Graphics g, List<Point> points, Pen pen) { if(points.Count < 2) return; Point fromPoint = points[0]; Point[] points1 = points.ToArray(); int xScrollBarPos = ScrollBarPosX; int yScrollBarPos = ScrollBarPosY; foreach(Point toPoint in points1) { g.DrawLine(pen, fromPoint.X - xScrollBarPos, fromPoint.Y - yScrollBarPos, toPoint.X - xScrollBarPos, toPoint.Y - yScrollBarPos); fromPoint = toPoint; } } } |
DrawRectangleメソッドは矩形と楕円の描画(枠だけ、ぬりつぶしの両方)をするためのメソッドです。
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 UserControlImage : UserControl { void DrawRectangle(Graphics g) { Rectangle rect; if(BitmapRectangle == null) rect = GetRectangle(startPoint, endPoint); else rect = BitmapRectangle.Rectangle; rect.X -= ScrollBarPosX; rect.Y -= ScrollBarPosY; if(EditMode == EditMode.DrawRectangle) g.DrawRectangle(new Pen(DrawColor), rect); if(EditMode == EditMode.FillRectangle) g.FillRectangle(new SolidBrush(DrawColor), rect); if(EditMode == EditMode.DrawEllipse) g.DrawEllipse(new Pen(DrawColor), rect); if(EditMode == EditMode.FillEllipse) g.FillEllipse(new SolidBrush(DrawColor), rect); DrawBoderRectangle(g); } } |
最後にビットマップを描画するメソッドを示します。スクロールバー位置よりビットマップをトリミングして表示します。また選択されている部分の移動先が確定するまえは移動元の画像があった部分は空白で表示するのですが、自由曲線で囲まれた部分はSetPixelメソッドで処理をしていました。これでは時間がかかりすぎるのでBitmapReatangle型に変換して、Graphics.DrawImageメソッドで処理ができるようにしてます。
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 |
public partial class UserControlImage : UserControl { public void ShowBitmap(Graphics g, Bitmap bitmap) { OptimizationScrloolBar(bitmap); Bitmap bitmap1 = GetBitmapDrawPictureBox(bitmap); if(BitmapRectangle != null) { g.DrawImage(bitmap1, new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height)); if(blankBitmap != null) { Point point1 = blankBitmap.Location; point1.X -= ScrollBarPosX; point1.Y -= ScrollBarPosY; g.DrawImage(blankBitmap.Bitmap, point1); } if(blankColorPoints != null) { int x1 = blankColorPoints.Min(x => x.X); int y1 = blankColorPoints.Min(x => x.Y); int x2 = blankColorPoints.Max(x => x.X); int y2 = blankColorPoints.Max(x => x.Y); Bitmap blank = new Bitmap(x2 - x1+1, y2 - y1 + 1); foreach(var cp in blankColorPoints) blank.SetPixel(cp.X- x1, cp.Y- y1, cp.Color); Rectangle rect = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1); blankBitmap = new BitmapRectangle(blank, rect); blankColorPoints.Clear(); blankColorPoints = null; } Point point2 = BitmapRectangle.Location; point2.X -= ScrollBarPosX; point2.Y -= ScrollBarPosY; g.DrawImage(BitmapRectangle.Bitmap, point2); } else { g.DrawImage(bitmap1, new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height)); } } } |
DrawFieldGripメソッドはフィールドのサイズ変更のためのグリッパーを描画するためのものです。
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 UserControlImage : UserControl { void DrawFieldGrip(Bitmap bitmap, Graphics g) { if(pictureBox1.Image != null) pictureBox1.Image = null; int x = bitmap.Width - ScrollBarPosX - GriperSize; int y = bitmap.Height - ScrollBarPosY - GriperSize; g.DrawRectangle(new Pen(Color.Black), new Rectangle(x, y, GriperSize, GriperSize)); x = Bitmap.Width / 2 - ScrollBarPosX - GriperSize / 2; y = Bitmap.Height - ScrollBarPosY - GriperSize; g.DrawRectangle(new Pen(Color.Black), new Rectangle(x, y, GriperSize, GriperSize)); x = Bitmap.Width - ScrollBarPosX - GriperSize; y = Bitmap.Height / 2 - ScrollBarPosY - GriperSize / 2; g.DrawRectangle(new Pen(Color.Red), new Rectangle(x, y, GriperSize, GriperSize)); g.DrawRectangle(new Pen(GetBoderBrush()), new Rectangle(-1, -1, bitmap.Width + 1, bitmap.Height + 1)); } } |
つぎに呼び出し側ですが、ビットマップ上に描画するのをやめてPictureBox.Invalidateメソッドを呼び出して上記の処理をおこなわせています。
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
public partial class UserControlImage : UserControl { void OnMouseMoveForDrawFreeLine(Point bitmapPoint) { endPoint = bitmapPoint; linePoints.Add(bitmapPoint); InvalidatePictureBox(); } void OnMouseMoveForDrawLine(Point bitmapPoint) { endPoint = bitmapPoint; InvalidatePictureBox(); } void OnMouseMoveForDrawRectangle(Point bitmapPoint) { endPoint = bitmapPoint; InvalidatePictureBox(); } void OnMouseMoveForMoveFreeLineSelection(Point bitmapPoint) { int xMove = bitmapPoint.X - rectangleMoveStartMousePosX; int yMove = bitmapPoint.Y - rectangleMoveStartMousePosY; BitmapRectangle.X = rectangleMoveStartLocationX + xMove; BitmapRectangle.Y = rectangleMoveStartLocationY + yMove; InvalidatePictureBox(); } void OnMouseMoveForMoveSelection(Point bitmapPoint) { if(BitmapRectangle == null) return; int xMove = bitmapPoint.X - rectangleMoveStartMousePosX; int yMove = bitmapPoint.Y - rectangleMoveStartMousePosY; BitmapRectangle.X = rectangleMoveStartLocationX + xMove; BitmapRectangle.Y = rectangleMoveStartLocationY + yMove; InvalidatePictureBox(); } void OnMouseMoveForSelectFreeArea(Point bitmapPoint) { endPoint = bitmapPoint; linePoints.Add(bitmapPoint); InvalidatePictureBox(); } void OnMouseMoveForSelectRectangle(Point bitmapPoint) { endPoint = bitmapPoint; InvalidatePictureBox(); } void OnMouseMoveForSizeChangeSelection(Point bitmapPoint) { int xMove = bitmapPoint.X - rectangleMoveStartMousePosX; int yMove = bitmapPoint.Y - rectangleMoveStartMousePosY; int x, y, width, height; x = BitmapRectangle.X; y = BitmapRectangle.Y; width = BitmapRectangle.Width; height = BitmapRectangle.Height; if(dragGriper == Griper.East) width = BitmapRectangle.OldWidth + xMove; if(dragGriper == Griper.South) height = BitmapRectangle.OldHeight + yMove; if(dragGriper == Griper.SouthEast) { width = BitmapRectangle.OldWidth + xMove; height = BitmapRectangle.OldHeight + yMove; } if(dragGriper == Griper.North) { y = BitmapRectangle.OldY + yMove; height = BitmapRectangle.OldHeight - yMove; } if(dragGriper == Griper.West) { x = BitmapRectangle.OldX + xMove; width = BitmapRectangle.OldWidth - xMove; } if(dragGriper == Griper.NorthWest) { x = BitmapRectangle.OldX + xMove; width = BitmapRectangle.OldWidth - xMove; y = BitmapRectangle.OldY + yMove; height = BitmapRectangle.OldHeight - yMove; } if(dragGriper == Griper.NorthEast) { width = BitmapRectangle.OldWidth + xMove; y = BitmapRectangle.OldY + yMove; height = BitmapRectangle.OldHeight - yMove; } if(dragGriper == Griper.SouthWest) { x = BitmapRectangle.OldX + xMove; width = BitmapRectangle.OldWidth - xMove; height = BitmapRectangle.OldHeight + yMove; } if(width <= 1 || height <= 1) { isMouseDownForSizeChange = false; startPoint = new Point(BitmapRectangle.Rectangle.Left, BitmapRectangle.Rectangle.Top); endPoint = new Point(BitmapRectangle.Rectangle.Right, BitmapRectangle.Rectangle.Bottom); BitmapRectangle.UpdatePosition(); string str = String.Format("startPoint ({0},{1}), endPoint({2},{3})", startPoint.X, startPoint.Y, endPoint.X, endPoint.Y); Console.WriteLine(str); return; } BitmapRectangle.X = x; BitmapRectangle.Width = width; BitmapRectangle.Y = y; BitmapRectangle.Height = height; InvalidatePictureBox(); } } |
それから文字を挿入する処理も修正しました。ただし[範囲選択(矩形)]が選択されていないと移動させることができません。そこで文字を挿入したら自動的に[範囲選択(矩形)]が選択されるようにしました。
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 UserControlImage : UserControl { public bool InsertString(string str, Font font, Color color, Point point) { if(Bitmap == null) return false; if(str == "") return false; // 文字列が描画される矩形を求める Rectangle rect = GetStringRectangle(str, font, point); Bitmap stringBitmap = new Bitmap(rect.Width, rect.Height); Graphics g = Graphics.FromImage(stringBitmap); g.DrawString(str, font, new SolidBrush(color), point); g.Dispose(); BitmapRectangle = new BitmapRectangle(stringBitmap, rect); EditMode = EditMode.Selection; InvalidatePictureBox(); return 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 59 60 61 62 |
public partial class Form1 : Form { private void EditMenuItem_DropDownOpening(object sender, EventArgs e) { CheckMenu1(userControlImage1.EditMode); } void CheckMenu1(EditMode editMode) { SelectionMenuItem.Checked = false; SelectionFreeMenuItem.Checked = false; DrawFigureMenuItem.Checked = false; DrawRectangleMenuItem.Checked = false; FillRectangleMenuItem.Checked = false; DrawEllipseMenuItem.Checked = false; FillEllipseMenuItem.Checked = false; DrawLineMenuItem.Checked = false; FreeLineMenuItem.Checked = false; FillColorMenuItem.Checked = false; if(editMode == EditMode.Selection) SelectionMenuItem.Checked = true; else if(editMode == EditMode.SelectionFree) SelectionFreeMenuItem.Checked = true; else if(editMode == EditMode.DrawRectangle) { DrawFigureMenuItem.Checked = true; DrawRectangleMenuItem.Checked = true; } else if(editMode == EditMode.FillRectangle) { DrawFigureMenuItem.Checked = true; FillRectangleMenuItem.Checked = true; } else if(editMode == EditMode.DrawEllipse) { DrawFigureMenuItem.Checked = true; DrawEllipseMenuItem.Checked = true; } else if(editMode == EditMode.FillEllipse) { DrawFigureMenuItem.Checked = true; FillEllipseMenuItem.Checked = true; } else if(editMode == EditMode.DrawLine) { DrawFigureMenuItem.Checked = true; DrawLineMenuItem.Checked = true; } else if(editMode == EditMode.FreeLine) { DrawFigureMenuItem.Checked = true; FreeLineMenuItem.Checked = true; } else if(editMode == EditMode.FillColor) { FillColorMenuItem.Checked = true; } } } |
ところでPictureBox.Invalidateメソッドを引数なしで呼び出すとビットマップの範囲外であっても矩形が描画されてしまいます。もちろん描画されるだけで実際に移動できるわけではありません。そこで引数つきでPictureBox.Invalidateメソッドを呼び出しています。これならビットマップの範囲外には描画されません。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class UserControlImage : UserControl { public void InvalidatePictureBox() { if(Bitmap == null) return; if(pictureBox1.Width >= Bitmap.Width || pictureBox1.Height >= Bitmap.Height) pictureBox1.Invalidate(new Rectangle(0, 0, Bitmap.Width, Bitmap.Height)); else pictureBox1.Invalidate(); } } |
スクロールバーがクリックされたときもVScrollBar1_ScrollメソッドやHScrollBar1_Scroll内で特別な処理はしないで、PictureBox.Invalidateメソッドを呼び出して適切な描画をさせることにしました。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class UserControlImage : UserControl { private void VScrollBar1_Scroll(object sender, ScrollEventArgs e) { InvalidatePictureBox(); } private void HScrollBar1_Scroll(object sender, ScrollEventArgs e) { InvalidatePictureBox(); } } |
UndoとRedoに関する処理です。BitmapRectangle == nullでないときはこれを一体化させ、そうでないときはUndo、Redoの処理をしています。BitmapRectangleをクリアしたときとBitmapプロパティが変化したときはPictureBox.Invalidate()メソッドでこれをピクチャーボックスに描画させています。
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 |
public partial class UserControlImage : UserControl { public void Undo() { if(BitmapRectangle != null) { ClearBitmapRectangle(); pictureBox1.Invalidate(); return; } if(UndoBufs.CanUndo()) { if(_bitmap != null) UndoBufs.InsertRedoBuf(new UndoBuf(_bitmap, DragDropRectangle)); UndoBuf undoBuf = UndoBufs.RemoveUndoBuf(); _bitmap = undoBuf.Bitmap; startPoint = new Point(-1, -1); endPoint = new Point(-1, -1); if(_bitmap != null) { OptimizationScrloolBar(Bitmap); pictureBox1.Invalidate(); } } } public void Redo() { if(BitmapRectangle != null) { ClearBitmapRectangle(); pictureBox1.Invalidate(); return; } if(UndoBufs.CanRedo()) { UndoBufs.InsertUndoBuf(new UndoBuf(_bitmap, DragDropRectangle)); UndoBuf redoBuf = UndoBufs.RemoveRedoBuf(); _bitmap = redoBuf.Bitmap; startPoint = new Point(-1, -1); endPoint = new Point(-1, -1); if(_bitmap != null) { OptimizationScrloolBar(Bitmap); pictureBox1.Invalidate(); } } } } |