1

I am coding my own Paint in C# using the System.Drawing namespace and basically everything is going well so far except for one thing, the eraser tool.

Right now I the eraser works just by drawing a line with the same color as the background so it appears like it's erasing something.

However, I want to make a new Eraser tool, perhaps improved. This new Eraser would be able to DELETE an element with a single click if the click is within it's bounds. I know that if one thing has already been drawn it's there and nothing can be done but I was thinking about creating a string array and I'm going to add new elements to the array. For example when I add a line and a rectangle the first two elements of array would be:

line startPoint endPoint

rectangle height width x y

Something like that. And when the Erase tool is used, just compare the coordinates.

Is there an easier approach to this?

Thanks a lot!

  • Is there an easier approach? Maybe, depends on the code, how you are storing shapes, etc. Without seeing your code, I can't determine what is easy and what is difficult, so this question is at best unclear about what you are asking, or opinion based, there are a hundred ways to skin a cat, without knowing what kind of cat you have, I can't even suggest how to start. – Ron Beyer Nov 02 '15 at 18:19
  • __WinForms Draing Rule Nr One__: Anything drawn __needs__ to be drawn using a List of data (or some other data structure) and if deleting an element is all you want things will be easy. Things get (a lot) harder if you want to erase pixels. See [here](http://stackoverflow.com/questions/32919918/how-to-draw-line-and-select-it-in-panel/32920894?s=1|3.6623#32920894) for identifying a Line! Curves will be harder, maybe picking from a ListView is better.. – TaW Nov 02 '15 at 18:22
  • @TaW: Interesting that we both came to approximately the same code for this ... just in different questions and answers ;-) – Joey Nov 03 '15 at 09:43

1 Answers1

4

Yes, there is. What you're planning to do is essentially retained mode rendering in a way. You keep a list (or other data structure) of objects that are on the canvas and you can reorder or change that list in any way, e.g. by removing or adding objects. After that you just re-create the drawing, that is, clear your drawing area and then draw each object in your list in order. This is necessary because once you have drawn something you only have pixels, and if your line and rectangle intersect, you may have trouble separating line pixels from rectangle pixels.

With GDI+ this is the only approach, since you don't get much more than a raw drawing surface. However, other things exist which already provide that rendering model for you, e.g. WPF.

However, a string[] is a horrible way of solving this. Usually you would have some kind of interface, e.g.

public interface IShape {
  public void Draw(Graphics g);
  public bool IsHit(PointF p);
}

which your shapes implement. A line would keep its stroke colour and start/end coordinates as state which it would then use to draw itself in the Draw method. Furthermore, when you want to click on a shape with the eraser, you'd have the IsHit method to determine whether a shape was hit. That way each shape is responsible for its own hit-testing. E.g a line could implement a little fuzziness so that you can click a little next to the line instead of having to hit a single pixel exactly.

That's the general idea, anyway. You could expand this as necessary to other ideas. Note that by using this approach your core code doesn't have to know anything about the shapes that are possible (comparing coordinates can be a bit cumbersome if you have to maintain an ever-growing switch statement of different shapes). Of course, for drawing those shapes you still need a bit more code because lines may need a different interaction than rectangles, ellipses or text objects.

I created a small sample that outlines above approach here. The interesting parts are as follows:

interface IShape
{
    Pen Pen { get; set; }
    Brush Fill { get; set; }
    void Draw(Graphics g);
    bool IsHit(PointF p);
}

class Line : IShape
{
    public Brush Fill { get; set; }
    public Pen Pen { get; set; }
    public PointF Start { get; set; }
    public PointF End { get; set; }

    public void Draw(Graphics g)
    {
        g.DrawLine(Pen, Start, End);
    }

    public bool IsHit(PointF p)
    {
        // Find distance to the end points
        var d1 = Math.Sqrt((Start.X - p.X) * (Start.X - p.X) + (Start.Y - p.Y) * (Start.Y - p.Y));
        var d2 = Math.Sqrt((End.X - p.X) * (End.X - p.X) + (End.Y - p.Y) * (End.Y - p.Y));

        // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
        var dx = End.X - Start.X;
        var dy = End.Y - Start.Y;
        var length = Math.Sqrt(dx * dx + dy * dy);
        var distance = Math.Abs(dy * p.X - dx * p.Y + End.X * Start.Y - End.Y * Start.X) / Math.Sqrt(dy * dy + dx * dx);

        // Make sure the click was really near the line because the distance above also works beyond the end points
        return distance < 3 && (d1 < length + 3 && d2 < length + 3);
    }
}

public partial class Form1 : Form
{
    private ObservableCollection<IShape> shapes = new ObservableCollection<IShape>();
    private static Random random = new Random();

    public Form1()
    {
        InitializeComponent();
        pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
        shapes.CollectionChanged += Shapes_CollectionChanged;
    }

    private void Shapes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        Redraw();
    }

    public void Redraw()
    {
        using (var g = Graphics.FromImage(pictureBox1.Image))
        {
            foreach (var shape in shapes)
            {
                shape.Draw(g);
            }
        }
        pictureBox1.Invalidate();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        shapes.Add(new Line
        {
            Pen = Pens.Red,
            Start = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height)),
            End = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height))
        });
    }

    private void pictureBox1_SizeChanged(object sender, EventArgs e)
    {
        pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
        Redraw();
    }

    private IShape FindShape(PointF p)
    {
        // Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
        foreach (var shape in shapes.Reverse())
        {
            if (shape.IsHit(p))
                return shape;
        }
        return null;
    }

    private void button1_MouseClick(object sender, MouseEventArgs e)
    {
        var shape = FindShape(e.Location);
        if (shape != null)
        {
            shape.Pen = Pens.Blue;
            Redraw();
        }
    }
}

Clicking the button creates a random line and redraws. Redrawing is as simple as

public void Redraw()
{
    using (var g = Graphics.FromImage(pictureBox1.Image))
    {
        foreach (var shape in shapes)
        {
            shape.Draw(g);
        }
    }
    pictureBox1.Invalidate();
}

Clicking the picture will try finding a shape at the click point, and if it finds one, it colours it blue (along with a redraw). Finding the item works as follows:

private IShape FindShape(PointF p)
{
    // Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
    foreach (var shape in shapes.Reverse())
    {
        if (shape.IsHit(p))
            return shape;
    }
    return null;
}

So as you can see, the fundamental parts of actually drawing things and maybe selecting them again, are fairly easy that way. Of course, the different drawing tools are another matter, although that has solutions, too.

Joey
  • 344,408
  • 85
  • 689
  • 683
  • I see, thanks a lot. So using an Interface/Class would be better than using a plain old string array? What happens when I click on a line that has been drawn with a "pen" tool? – Bozhidar Grujoski Nov 02 '15 at 18:32
  • Well, the state you need to keep there would be essentially a large list of points. You can smooth it and make things more complex (but better-looking), but for a start, a pen tool just creates a polyline. So you have a number of line segments and testing whether a point lies on the polyline (or near it) is essentially just the same like for a single line – just try for every single segment. – Joey Nov 02 '15 at 18:40
  • Basically what you're saying is just storing every X,Y of the polyline? – Bozhidar Grujoski Nov 02 '15 at 18:42
  • Exactly. Start simple here. You can make things more complicated later. – Joey Nov 02 '15 at 18:51
  • @BozhidarGrujoski: I added a bit more, including a small sample application. – Joey Nov 02 '15 at 19:25
  • Wow! That's an amazing thing of you to do. I really appreciate it man. Helped me a lot! – Bozhidar Grujoski Nov 03 '15 at 06:31
  • @BozhidarGrujoski: It just so happens that I work in a fairly similar setting in our codebase at work currently. So whipping up that sample was fairly easy. I hope I could convince you that there are easier ways than string arrays for handling this ;-) – Joey Nov 03 '15 at 07:47
  • I just finished coding everything of my new eraser for ELLIPSE and RECTANGLE too, both filled or not. What I have left is the PenTool so I was thinking about adding a Dictionary that's going to hold every coordinate of the line that's been drawn. How does that sound like? – Bozhidar Grujoski Nov 03 '15 at 12:01
  • Make it a `List` instead. Dictionary would imply that you want to *map* integers to other integers. That's not really how a list of points works. – Joey Nov 03 '15 at 12:18
  • Thanks a lot, I'm still having troubles with that but I'll solve it later. I have one more question, when a Rectangle is clicked (not filled) it's supposed to check if the click is NEAR the boundaries (edge) and not INSIDE (Contains()) the rectangle. How would I approach this? – Bozhidar Grujoski Nov 03 '15 at 17:38
  • You'd check whether the fill is `null` or not and react accordingly. If the rectangle is filled, you'd just check whether the click point is inside it. If it's not filled, you check whether it's in a slightly larger rectangle and *not* in a slightly smaller rectangle. By definition that will be near the edge, then. – Joey Nov 03 '15 at 17:46
  • Great I got it all sorted out now. Except for the PenTool. The current method works by initializing the MouseUp and MouseDown events so I can't tell when they're done with drawing the line so I can add the points. Do you suggest using GraphicsPath or? – Bozhidar Grujoski Nov 03 '15 at 19:25
  • Just making sure you saw my last comment I don't think I mentioned you, @Joey – Bozhidar Grujoski Nov 03 '15 at 19:38
  • Well, you're done when you get the MouseUp event, while you set up everything needed during creation of the pen shape at the beginning of the MouseDown event. I don't think I have enough information here to know what goes wrong. In any case, you *could* just ask another question by now ;-) – Joey Nov 04 '15 at 17:05