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.
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;
public Shapes()
{
_Shapes = new List();
}
public int NumberOfShapes()
{
return _Shapes.Count;
}
public void NewShape(Point L, float W, Color C, int S)
{
_Shapes.Add(new Shape(L,W,C,S));
}
public Shape GetShape(int Index)
{
return _Shapes[Index];
}
public void RemoveShape(Point L, float threshold)
{
for (int i = 0; i < _Shapes.Count; i++)
{
if ((Math.Abs(L.X - _Shapes[i].Location.X) < threshold) &&
(Math.Abs(L.Y - _Shapes[i].Location.Y)< threshold))
{
_Shapes.RemoveAt(i);
for (int n = i; n < _Shapes.Count; n++)
{
_Shapes[n].ShapeNumber += 1;
}
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; public float Width; public Color Colour; public int ShapeNumber;
public Shape(Point L, float W, Color C, int S)
{
Location = L; Width = W; Colour = C; ShapeNumber = S; }
}
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(); private bool IsPainting = false; private Point LastPos = new Point(0, 0); private Color CurrentColour = Color.Black; private float CurrentWidth = 10; private int ShapeNum = 0;
public Form1()
{
InitializeComponent();
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)
{
IsPainting = true;
ShapeNum++;
LastPos = new Point(0, 0);
}
MouseMove
protected void panel1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (IsPainting)
{
if (LastPos != e.Location)
{
LastPos = e.Location;
DrawingShapes.NewShape(LastPos, CurrentWidth,
CurrentColour, ShapeNum);
}
}
panel1.Refresh();
}
MouseUp
private void panel1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (IsPainting)
{
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
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
for (int i = 0; i < DrawingShapes.NumberOfShapes()-1; i++)
{
Shape T = DrawingShapes.GetShape(i);
Shape T1 = DrawingShapes.GetShape(i+1);
if (T.ShapeNumber == T1.ShapeNumber)
{
Pen p = new Pen(T.Colour, T.Width);
p.StartCap = System.Drawing.Drawing2D.LineCap.Round;
p.EndCap = System.Drawing.Drawing2D.LineCap.Round;
e.Graphics.DrawLine(p, T.Location, T1.Location);
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 (Brush)
{
IsPainting = true;
ShapeNum++;
LastPos = new Point(0, 0);
}
else
{
IsEraseing = true;
}
}
The MouseMove
function has an admen that looks like this:
if (IsEraseing)
{
DrawingShapes.RemoveShape(e.Location,10);
}
And the MouseUp
:
if (IsEraseing)
{
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)
{
Cursor.Hide();
IsMouseing = true;
}
MouseLeave (Show the mouse Cursor)
private void panel1_MouseLeave(object sender, EventArgs e)
{
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