3

I'm trying to create a simple drawing application with undo and redo features. I assume you can add what you are drawing into a list and calling upon the list to draw everything. Then undoing should just remove the last added item and redraw everything again. The problem is, how do I add what I've drawn into a list and use that list to undo?

I'm using the bitmap redraw method. This is how I draw:

    Point start, end;
    bool painting;
    private List<PointF> myPoints = new List<PointF>();

    private void pnlMain_MouseDown(object sender, MouseEventArgs e)
    {
        start = e.Location;
        painting = true;
    }

    private void pnlMain_MouseUp(object sender, MouseEventArgs e)
    {
        painting = false;
    }

    private void pnlMain_MouseMove(object sender, MouseEventArgs e)
    {
        if (painting == true)
        {
            end = e.Location;
            g.DrawLine(p, start, end);
            myPoints.Add(e.Location);
            pnlMain.Refresh();
            start = end;
        }
    }

    private void btnUndo_Click(object sender, EventArgs e)
    {
        g.Clear(cldFill.Color);
        if (myPoints.Count > 2)
        {
            myPoints.RemoveAt(myPoints.Count - 1);
            g.DrawCurve(p, myPoints.ToArray());
        }
        pnlMain.Refresh();
        //This works but you have to spam it to get rid of
        //a line and does some weird connections.
    }
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
Alex Diamond
  • 586
  • 3
  • 27
  • You need to collect the coordinates you draw in a `List` . For code on how to collect them see my [comments here](http://stackoverflow.com/questions/38280801/smoother-graphics-than-smoothingmode-antialias#38280801)! Also: You paint in the wrong way. You seem to cache a Graphics object. This is wrong! Draw everything in the Paint event! – TaW Jul 10 '16 at 21:47
  • Yes, no problem. If you implement the code I gave in [(all) my comments I linked to](http://stackoverflow.com/questions/38280801/smoother-graphics-than-smoothingmode-antialias#38280801) all you need is to add a button click with `curves.Remove(curves.Last)); pnlMain.Invalidate(); ` – TaW Jul 10 '16 at 22:47

1 Answers1

7

You need to store lines in a List<List<Point>>. Each element of the list contains points of a drawing which you draw using a down, move and up. The next line which you draw, will store in the next element of list. Each undo, will remove the last drawing.

Put an instance of this control on your form and it will handle the drawing for you. Also to perform undo, call its Undo method.

using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public class DrawingSurface : Control
{
    public DrawingSurface() { this.DoubleBuffered = true; }
    List<List<Point>> Lines = new List<List<Point>>();
    bool drawing = false;
    protected override void OnMouseDown(MouseEventArgs e) {
        Lines.Add(new List<Point>());
        Lines.Last().Add(e.Location);
        drawing = true;
        base.OnMouseDown(e);
    }
    protected override void OnMouseMove(MouseEventArgs e) {
        if (drawing) { Lines.Last().Add(e.Location); this.Invalidate(); }
        base.OnMouseMove(e);
    }
    protected override void OnMouseUp(MouseEventArgs e) {
        if (drawing) {
            this.drawing = false;
            Lines.Last().Add(e.Location);
            this.Invalidate();
        }
        base.OnMouseUp(e);
    }
    protected override void OnPaint(PaintEventArgs e) {
        e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        foreach (var item in Lines)
            e.Graphics.DrawLines(Pens.Black, item.ToArray()); /*or DrawCurve*/
    }
    public void Undo() {
        if (Lines.Count > 0) { this.Lines.RemoveAt(Lines.Count - 1); this.Invalidate(); }
    }
}

Note

  • Using this logic, you can simply implement redo using an other List<List<Point>>. It's enough to copy the last item before undo to redo list using RedoBuffer.Add(Lines.Last());. Then for each redo command, it's enough to add the last item of redo buffer to Lines and remove it from redo buffer. You should also clear the redo buffer after each mouse down.
  • You can use either of DrawLines or DrawCurve based on your requirement. DrawLines draws a poly-line, while DrawCurve draws a more smooth curve.

  • I prefer to encapsulate Lines.Count > 0 in a property like bool CanUndo and make it accessible from outside of control.

  • It's just an example and you can simply extend the solution. For example, instead of List<List<Point>> you can create a Shape class containing List<Point>, LineWidth, LineColor, etc and perform task using List<Shape>.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • That's a very nice class! Is it possible for you to show me an example of how I can talk to the class and tell it to draw rectangles or circles? – Alex Diamond Jul 11 '16 at 00:05
  • To extend the example, you can create a `Shape` class containing an abstract `Draw` method and some derived class like `Rectangle` and `Circle` implementing the method and containing required properties for coordinates. Then you can use a `List` to store objects which you want to draw. To draw a shape you should have a flag that indicates what the shape you are drawing and based in this flag, add the desired shape to that list. To paint them, in `OnPaint` method, call `Draw` method of each shape. – Reza Aghaei Jul 11 '16 at 00:12
  • This is nearly the whole idea. It's simple, but needs more code. Also it can be done in different ways, so if you start writing the application, feel free to ask questions related to your implementation. Hope this answer and comments help you :) – Reza Aghaei Jul 11 '16 at 00:32
  • To state the obvious: This is an __unlimited__ undo :-) - See [here for an example of saving the work](http://stackoverflow.com/questions/38281588/save-my-mouse-event-drawing-for-future-modification/38282099#38282099) – TaW Jul 11 '16 at 01:01