Painting on a Panel (C#) - Overview

In this article we look at how to design and implement the ability to paint onto a panel with the mouse, how to record this drawing data, draw the lines and create an eraser tool to remove these lines. What’s more is that we will also store additional information like the line size and line colour and finish everything off by letting users know the current colour and size of their drawing with. 

This approach to drawing graphics is more aligned to vector type graphics where the points are floating values that are interpolated rather than straight forward rasterized bitmap values.  

-> Creating the supporting classes and how they will be used.

-> Setting up the form and getting it ready for use.

-> Handleing the events and drawing the lines.

-> Extending it with the ablity to erase the lines

-> Adding a mouse cursor on the panel for user interface purposes.

This tutorial was orginaly submitted to CodeProjects and the orginal can be found here.

 

This is part of a 3 part series, finishing with the complition of a simple vector based art programe.

Painting on a Panel (C#) - Tutorial

Paint_on_a_panel.jpg

Introduction 

In this article we look at how to design and implement the ability to paint onto a panel with the mouse, how to record this drawing data, draw the lines and create an eraser tool to remove these lines. What's more is that we will also store additional information like the line size and line colour and finish everything off by letting users know the current colour and size of their drawing with. 

This approach to drawing graphics is more aligned to vector type graphics where the points are floating values that are interpolated rather than straight forward rasterized bitmap values.  

Background

This article is at a simple level, so no previous Drawing knowledge is necessary, but preferable for expanding on what is done here. 

Using the code 

The code here is done in such a way that it can be easily transplanted onto varies other projects with very little hassle and could be extended to include other non-standard drawing features like text. 

The Class's

To accomplish this we have two main classes, one of the class's deals with the storage, adding, removing and getting of the individual shape data, whereas the other class is used more as a structure and stores data such as the colour, line width, position and what shape segment it is with.

Classes.JPG

Class Shapes

Methods Functionality
GetShape() Returns the index shape.
NewShape() Creates a new shape with the function arguments (position, width, colour , shape segment index).
NumberOfShapes() Returns the number of shapes currently being stored.
RemoveShape() Removes any point within a certain threshold of the point, and re-sorts out the shape segment index
of the rest of the points so we don't have problems with joining up odd lines.
public class Shapes
{
    private List _Shapes;    //Stores all the shapes


    public Shapes()
    {
        _Shapes = new List();
    }
    //Returns the number of shapes being stored.
    public int NumberOfShapes()
    {
        return _Shapes.Count;
    }
    //Add a shape to the database, recording its position,

    //width, colour and shape relation information
    public void NewShape(Point L, float W, Color C, int S)
    {
        _Shapes.Add(new Shape(L,W,C,S));
    }
    //returns a shape of the requested data.

    public Shape GetShape(int Index)
    {
        return _Shapes[Index];
    }
    //Removes any point data within a certain threshold of a point.
    public void RemoveShape(Point L, float threshold)
    {
        for (int i = 0; i < _Shapes.Count; i++)
        {
            //Finds if a point is within a certain distance of the point to remove.

            if ((Math.Abs(L.X - _Shapes[i].Location.X) < threshold) && 
                (Math.Abs(L.Y - _Shapes[i].Location.Y)< threshold))
            {
                //removes all data for that number
                _Shapes.RemoveAt(i);

                //goes through the rest of the data and adds an extra
                //1 to defined them as a seprate shape and shuffles on the effect.

                for (int n = i; n < _Shapes.Count; n++)
                {
                    _Shapes[n].ShapeNumber += 1;
                }
                //Go back a step so we dont miss a point.
                i -= 1;
            }
        }
    }
}

Class Shape

Variable Type Purpose
Colour Color Saves the Colour at for this part of the line.
Location Point The position of the line.
ShapeNumber Int Which shape this part of the line belongs to.
Width Float The width to draw the line at this point.
 public class Shape
{
    public Point Location;          //position of the point

    public float Width;             //width of the line
    public Color Colour;            //colour of the line
    public int ShapeNumber;         //part of which shape it belongs to


    //CONSTRUCTOR
    public Shape(Point L, float W, Color C, int S)
    {
        Location = L;               //Stores the Location
        Width = W;                  //Stores the width

        Colour = C;                 //Stores the colour
        ShapeNumber = S;            //Stores the shape number
    }
}

The Setup

When drawing in C# or .net, its advised to force the drawing surface to use double-buffering to reduce the amount of flickering when re-drawing, but this does come as a memory cost. We should also define some more variables for dealing with conditions such as the mouse position, the current drawing shape, current colour ect.

private Shapes DrawingShapes = new Shapes();    //Stores all the drawing data
private bool IsPainting = false;                //Is the mouse currently down

private Point LastPos = new Point(0, 0);        //Last Position, used to cut down on repative data.
private Color CurrentColour = Color.Black;      //Deafult Colour

private float CurrentWidth = 10;                //Deafult Pen width
private int ShapeNum = 0;                       //record the shapes so they can be drawn sepratley.



public Form1()
{
    InitializeComponent();
    //Set Double Buffering
    panel1.GetType().GetMethod("SetStyle", 
      System.Reflection.BindingFlags.Instance |
      System.Reflection.BindingFlags.NonPublic).Invoke(panel1, 
      new object[]{ System.Windows.Forms.ControlStyles.UserPaint | 
      System.Windows.Forms.ControlStyles.AllPaintingInWmPaint | 
      System.Windows.Forms.ControlStyles.DoubleBuffer, true });
}

DrawingShapes is an instance of the shapes class, which will allow us to store all the necessary drawing information, and allow the re-drawing function to later use this variable to draw the lines.

The Events

With the ability to store the drawing point data, we will need to argument the drawing panel's event handlers to accommodate the functions for MouseDown (when the mouse button is pressed down), MouseMove (when the mouse is moving across the drawing panel) and MouseUp (when the mouse button is let go of).

When the mouse is pressed down on the drawing panel we want to start recording the data, and as the mouse moves while the mouse is pressed down, we want to record the position of the mouse, and when the mouse button is lifted, that is the end of the drawing line.

MouseDown:
private void panel1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
    //set it to mouse down, illatrate the shape being drawn and reset the last position

    IsPainting = true;
    ShapeNum++;
    LastPos = new Point(0, 0);
}

MouseMove:

protected void panel1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{

//PAINTING
    if (IsPainting)
    {
        //check its not at the same place it was last time, saves on recording more data.
        if (LastPos != e.Location)
        {
            //set this position as the last positon
            LastPos = e.Location;
            //store the position, width, colour and shape relation data

            DrawingShapes.NewShape(LastPos, CurrentWidth, CurrentColour, ShapeNum);
        }
    }
    //refresh the panel so it will be forced to re-draw.
    panel1.Refresh();
}

MouseUp:

private void panel1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
    if (IsPainting)
    {
        //Finished Painting.

        IsPainting = false;
    }
}

The last part now that we can store that point data, is to draw the lines. To do this another event handler is needed on the drawing panel for Paint (which is called every time the panel is being re-drawn). It is here that we use the collected data to connect the dots using the line width and line colours that we have saved. The shape number information comes in particular use here as we don't want to be drawing a line between two dots which are not connected.

Paint:

//DRAWING FUNCTION
private void panel1_Paint(object sender, PaintEventArgs e)
{
    //Apply a smoothing mode to smooth out the line.

    e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
    //DRAW THE LINES
    for (int i = 0; i < DrawingShapes.NumberOfShapes()-1; i++)
    {
        Shape T = DrawingShapes.GetShape(i);
        Shape T1 = DrawingShapes.GetShape(i+1);
        //make sure shape the two ajoining shape numbers are part of the same shape

        if (T.ShapeNumber == T1.ShapeNumber)
        {
            //create a new pen with its width and colour
            Pen p = new Pen(T.Colour, T.Width);
            p.StartCap = System.Drawing.Drawing2D.LineCap.Round;
            p.EndCap = System.Drawing.Drawing2D.LineCap.Round; 
            //draw a line between the two ajoining points
            e.Graphics.DrawLine(p, T.Location, T1.Location);
            //get rid of the pen when finished

            p.Dispose();
        }
    }
}

Adding the Erasing Tool

Now we can draw on the panel, it's time to look at erasing the lines in a similar fashion. To do this we need to argument our code slight to accommodate this new interaction. The way I'm implementing this is by adding 2 extra variables, both of which are defined at the start of the programme.

Brush is a boolean value, representing either the Painting or Eraseing tool currently in use.

IsErasing is a boolean value which is used in an identical fashion to the IsPainting varibal.

The use of the Brush varibal changes a few things, most notibaly the MouseDown event handeler. Instead of assinging the IsPainting varibal as true when the mouse is down, we need to check weather it's painting or eraseing. The function should look like this after:

private void panel1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
    //If we're painting...

    if (Brush)
    {
        //set it to mouse down, illatrate the shape being drawn and reset the last position
        IsPainting = true;
        ShapeNum++;
        LastPos = new Point(0, 0);
    }
        //but if we're eraseing...

        else
        {
            IsEraseing = true;
        }
}

The MouseMove function has an admen that looks like this:

if (IsEraseing)
{
    //Remove any point within a certain distance of the mouse

    DrawingShapes.RemoveShape(e.Location,10);
}

And the MouseUp:

if (IsEraseing)
{
    //Finished Earsing.
    IsEraseing = false;
}

Drawing a mouse cursor

Removeing and Painting new lines onto a panel is great, but the task for the user is made more difficutly by the fact they cannot interactivly see how big they are painting or removing lines. To do this we need to make a final few tweaks to the programe. This method is a little brute force for larger applcation, but it will do the trick for smaller applcation. An additional two varierbals are needed in order to make this work;

MouseLoc is a point varibal, which means it holds two interger values, very good for location coordinates, and will store the current mouse coordinates.

IsMouseing is a boolean which will be used to discied wheather to draw the mouse "Painting Pointer" or not.

Two event handlers for the drawing panel are used for good measure to hide and show the mouse curser as it enters or leaves the drawing panel and to tell the re-drawer to draw the "Painting Pointer".

MouseEnter (Hide the mouse Cursor):

private void panel1_MouseEnter(object sender, EventArgs e)
{
    //Hide the mouse cursor and tell the re-drawing function to draw the mouse

    Cursor.Hide();
    IsMouseing = true;
}

MouseLeave (Show the mouse Cursor):

private void panel1_MouseLeave(object sender, EventArgs e)
{
    //show the mouse, tell the re-drawing function

    //to stop drawing it and force the panel to re-draw.
    Cursor.Show();
    IsMouseing = false;
    panel1.Refresh();
}

Two small changes are needed to be made to update the MouseLoc and then to finaly draw it. Within the MouseMove function, the following line should be added.

MouseLoc = e.Location;

At the bottom of the Paint event, the following lines should be added, which will draw the center circle to to the tip of the mouse pointer and the circle will be as large as the drawing width.

if (IsMouseing)
{
    e.Graphics.DrawEllipse(new Pen(Color.White, 0.5f), 
      MouseLoc.X - (CurrentWidth / 2), 
      MouseLoc.Y - (CurrentWidth / 2), CurrentWidth, CurrentWidth);
}

It is important to note that if the drawing of the mouse cursor is done before the drawing of the lines, the lines will be drawn on top of the curser and it would not be visibal.

Points of Interest

Painting on a Panel (C#) - Supporting Files

The supporting files can be found at CodeProject here, and include all the source code as well as a final compiled exe application.