Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Painting on a Panel

0.00/5 (No votes)
23 May 2011 1  
How to create a panel that can be drawn on
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.

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 various other projects with very little hassle and could be extended to include other non-standard drawing features like text.

The Classes

To accomplish this, we have two main classes, one of the classes 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 separate shape and shuffles on the effect.
                for (int n = i; n < _Shapes.Count; n++)
                {
                    _Shapes[n].ShapeNumber += 1;
                }
                //Go back a step so we don't miss a point.
                i -= 1;
            }
        }
    }
}

Class Shape

Variable Type Purpose
Colour Color Saves the Colour 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, it's 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, etc.

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 repetitive data.
private Color CurrentColour = Color.Black;     	//Default Colour
private float CurrentWidth = 10;              	//Default Pen width
private int ShapeNum = 0;                     	//record the shapes so they can be 
					//drawn separately.

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 augment 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, illustrate 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 it's not at the same place it was last time, 
		//saves on recording more data.
		if (LastPos != e.Location)
		{
			//set this position as the last position
			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 adjoining 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 adjoining points
			e.Graphics.DrawLine(p, T.Location, T1.Location);
			//get rid of the pen when finished
			p.Dispose();
		}
	}
}

Adding the Erasing Tool

Now that 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 augment our code slightly 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 program.

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

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

The use of the Brush variable changes a few things, most notably the MouseDown event handler. Instead of assigning the IsPainting variable as true when the mouse is down, we need to check weather it’s painting or erasing. 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, illustrate the shape being drawn 
		//and reset the last position
		IsPainting = true;
		ShapeNum++;
		LastPos = new Point(0, 0);
	}
        //but if we're erasing...
        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 Erasing.
	IsEraseing = false;
}

Drawing a Mouse Cursor

Removing and painting new lines onto a panel is great, but the task for the user is made more difficult by the fact that they cannot interactively see how big they are painting or removing lines. To do this, we need to make a final few tweaks to the program. This method is a little brute force for larger applications, but it will do the trick for smaller applications. An additional two variables are needed in order to make this work.

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

IsMouseing is a boolean which will be used to decide whether 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 finally 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 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 visible.

Points of Interest

This project was quite fun, nothing extremely challenging, but trying to optimize it from my original conception was interesting. It will be interesting to see what else can be done by pushing this code further by creating a small vector artwork package, or by introducing layers. For me, I’ve been using it to let users annotate node based graphs and images, and store the vector data with it.

History

  • 23rd May, 2011: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here