Title: Draw and move line segments in C#
This example lets the user draw and move line segments. It lets you perform three different operations depending on what is below the mouse.
- When the mouse is over a segment, the cursor changes to a hand. Then you can then click and drag to move the segment.
- When the mouse is over a segment's end point, the cursor changes to an arrow. Then you can then click and drag to move the end point.
- When the mouse is over nothing, you can click and drag to draw a new line segment.
The program handles all of these cases using MouseDown, MouseMove, and MouseUp events, but handling all of the possible combinations in one set of event handlers would be confusing. To make things easier to manage, the program uses separate MouseMove and MouseUp event handlers to perform its different tasks.
This post is divided into the following sections, which correspond to the program's basic states.
The program stores the coordinates of the segments' end points in the lists Pt1 and Pt2.
// The points that make up the line segments.
private List Pt1 = new List<Point>();
private List Pt2 = new List<Point>();
While you're drawing a new segment, the variable IsDrawing is true and the program stores the new segment's end points in variables NewPt1 and NewPt2.
// Points for the new line.
private bool IsDrawing = false;
private Point NewPt1, NewPt2;
The Paint event handler simply loops through the Pt1 and Pt2 lists, drawing the segments and their end points. It then draws the new line (if you're drawing one).
// Draw the lines.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
// Draw the segments.
for (int i = 0; i < Pt1.Count; i++)
{
// Draw the segment.
e.Graphics.DrawLine(Pens.Blue, Pt1[i], Pt2[i]);
}
// Draw the end points.
foreach (Point pt in Pt1)
{
Rectangle rect = new Rectangle(
pt.X - object_radius, pt.Y - object_radius,
2 * object_radius + 1, 2 * object_radius + 1);
e.Graphics.FillEllipse(Brushes.White, rect);
e.Graphics.DrawEllipse(Pens.Black, rect);
}
foreach (Point pt in Pt2)
{
Rectangle rect = new Rectangle(
pt.X - object_radius, pt.Y - object_radius,
2 * object_radius + 1, 2 * object_radius + 1);
e.Graphics.FillEllipse(Brushes.White, rect);
e.Graphics.DrawEllipse(Pens.Black, rect);
}
// If there's a new segment under constructions, draw it.
if (IsDrawing)
{
e.Graphics.DrawLine(Pens.Red, NewPt1, NewPt2);
}
}
If the mouse moves while you're not moving a segment or end point, the following event handler executes.
// The mouse is up. See whether we're over an end point or segment.
private void picCanvas_MouseMove_NotDown(object sender,
MouseEventArgs e)
{
Cursor new_cursor = Cursors.Cross;
// See what we're over.
Point hit_point;
int segment_number;
if (MouseIsOverEndpoint(e.Location, out segment_number,
out hit_point))
new_cursor = Cursors.Arrow;
else if (MouseIsOverSegment(e.Location, out segment_number))
new_cursor = Cursors.Hand;
// Set the new cursor.
if (picCanvas.Cursor != new_cursor)
picCanvas.Cursor = new_cursor;
}
This code calls the MouseIsOverEndPoint and MouseIsOverSegment methods described later to see if the mouse is over anything interesting. It then displays the appropriate cursor. (Arrow if over an endpoint, hand if over a segment, and cross if over nothing.)
If you're not moving anything and you press the mouse down, the following event handler executes.
// See what we're over and start doing whatever is appropriate.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
// See what we're over.
Point hit_point;
int segment_number;
if (MouseIsOverEndpoint(e.Location, out segment_number,
out hit_point))
{
// Start moving this end point.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_MovingEndPoint;
picCanvas.MouseUp += picCanvas_MouseUp_MovingEndPoint;
// Remember the segment number.
MovingSegment = segment_number;
// See if we're moving the start end point.
MovingStartEndPoint =
(Pt1[segment_number].Equals(hit_point));
// Remember the offset from the mouse to the point.
OffsetX = hit_point.X - e.X;
OffsetY = hit_point.Y - e.Y;
}
else if (MouseIsOverSegment(e.Location, out segment_number))
{
// Start moving this segment.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_MovingSegment;
picCanvas.MouseUp += picCanvas_MouseUp_MovingSegment;
// Remember the segment number.
MovingSegment = segment_number;
// Remember the offset from the mouse
// to the segment's first point.
OffsetX = Pt1[segment_number].X - e.X;
OffsetY = Pt1[segment_number].Y - e.Y;
}
else
{
// Start drawing a new segment.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_Drawing;
picCanvas.MouseUp += picCanvas_MouseUp_Drawing;
IsDrawing = true;
NewPt1 = new Point(e.X, e.Y);
NewPt2 = new Point(e.X, e.Y);
}
}
This method uses the MouseIsOverEndPoint and MouseIsOverSegment methods to see if the mouse is over anything interesting. If the mouse is over an end point or segment, the code starts moving that object.
Notice how the code uninstalls the picCanvas_MouseMove_NotDown event handler and installs new MouseMove and MouseUp event handlers for the operation it is starting.
The following code shows the MouseIsOverEndPoint and MouseIsOverSegment methods.
// See if the mouse is over an end point.
private bool MouseIsOverEndpoint(Point mouse_pt,
out int segment_number, out Point hit_pt)
{
for (int i = 0; i < Pt1.Count; i++ )
{
// Check the starting point.
if (FindDistanceToPointSquared(mouse_pt, Pt1[i]) <
over_dist_squared)
{
// We're over this point.
segment_number = i;
hit_pt = Pt1[i];
return true;
}
// Check the end point.
if (FindDistanceToPointSquared(mouse_pt, Pt2[i]) <
over_dist_squared)
{
// We're over this point.
segment_number = i;
hit_pt = Pt2[i];
return true;
}
}
segment_number = -1;
hit_pt = new Point(-1, -1);
return false;
}
// See if the mouse is over a line segment.
private bool MouseIsOverSegment(Point mouse_pt,
out int segment_number)
{
for (int i = 0; i < Pt1.Count; i++)
{
// See if we're over the segment.
PointF closest;
if (FindDistanceToSegmentSquared(
mouse_pt, Pt1[i], Pt2[i], out closest)
< over_dist_squared)
{
// We're over this segment.
segment_number = i;
return true;
}
}
segment_number = -1;
return false;
}
These methods simply call the FindDistanceToPointSquared and FindDistanceToSegmentSquared methods. FindDistanceToPointSquared is trivial. For a description of how FindDistanceToSegmentSquared works, see the post Find the shortest distance between a point and a line segment in C#.
The program tests the square of the distance so it doesn't need to calculate square roots, which are relatively slow. Note that x < y if and only if x2 < y2, so this test still determines whether an object is within the required distance of the mouse.
The following code shows the MouseMove and MouseUp event handlers that are active when you're drawing a new segment.
// We're drawing a new segment.
private void picCanvas_MouseMove_Drawing(object sender,
MouseEventArgs e)
{
// Save the new point.
NewPt2 = new Point(e.X, e.Y);
// Redraw.
picCanvas.Invalidate();
}
// Stop drawing.
private void picCanvas_MouseUp_Drawing(object sender,
MouseEventArgs e)
{
IsDrawing = false;
// Reset the event handlers.
picCanvas.MouseMove -= picCanvas_MouseMove_Drawing;
picCanvas.MouseMove += picCanvas_MouseMove_NotDown;
picCanvas.MouseUp -= picCanvas_MouseUp_Drawing;
// Create the new segment.
Pt1.Add(NewPt1);
Pt2.Add(NewPt2);
// Redraw.
picCanvas.Invalidate();
}
When the mouse moves, the MouseMove event handler updates the value of NewPt2 to hold the mouse's current position. It then invalidates the program's PictureBox so its Paint event handler draws the current segments and the new one in progress.
When you release the mouse, the MouseUp event handler restores the "not moving anything" event handlers, adds the new segment's points to the Pt1 and Pt2 lists, and invalidates the PictureBox to redraw.
The following code shows the MouseMove and MouseUp event handlers that are active when you're moving an end point.
// We're moving an end point.
private void picCanvas_MouseMove_MovingEndPoint(object sender,
MouseEventArgs e)
{
// Move the point to its new location.
if (MovingStartEndPoint)
Pt1[MovingSegment] =
new Point(e.X + OffsetX, e.Y + OffsetY);
else
Pt2[MovingSegment] =
new Point(e.X + OffsetX, e.Y + OffsetY);
// Redraw.
picCanvas.Invalidate();
}
// Stop moving the end point.
private void picCanvas_MouseUp_MovingEndPoint(object sender,
MouseEventArgs e)
{
// Reset the event handlers.
picCanvas.MouseMove += picCanvas_MouseMove_NotDown;
picCanvas.MouseMove -= picCanvas_MouseMove_MovingEndPoint;
picCanvas.MouseUp -= picCanvas_MouseUp_MovingEndPoint;
// Redraw.
picCanvas.Invalidate();
}
When the mouse moves, the MouseMove event handler updates the position of the point you are moving and then invalidates the PictureBox to make it redraw. The MouseUp event handler simply restores the "not moving anything" event handlers and redraws.
The following code shows the MouseMove and MouseUp event handlers that are active when you're moving an end point.
// We're moving a segment.
private void picCanvas_MouseMove_MovingSegment(object sender,
MouseEventArgs e)
{
// See how far the first point will move.
int new_x1 = e.X + OffsetX;
int new_y1 = e.Y + OffsetY;
int dx = new_x1 - Pt1[MovingSegment].X;
int dy = new_y1 - Pt1[MovingSegment].Y;
if (dx == 0 && dy == 0) return;
// Move the segment to its new location.
Pt1[MovingSegment] = new Point(new_x1, new_y1);
Pt2[MovingSegment] = new Point(
Pt2[MovingSegment].X + dx,
Pt2[MovingSegment].Y + dy);
// Redraw.
picCanvas.Invalidate();
}
// Stop moving the segment.
private void picCanvas_MouseUp_MovingSegment(object sender,
MouseEventArgs e)
{
// Reset the event handlers.
picCanvas.MouseMove += picCanvas_MouseMove_NotDown;
picCanvas.MouseMove -= picCanvas_MouseMove_MovingSegment;
picCanvas.MouseUp -= picCanvas_MouseUp_MovingSegment;
// Redraw.
picCanvas.Invalidate();
}
When the mouse moves, the MouseMove event handler updates the positions of the segment's end points and redraws to show the new position. The MouseUp event handler simply restores the "not moving anything" event handlers and redraws.
There are lots of other features you can add to a drawing program such as this one. You might want to add:
- Other drawing tools such as polylines, polygons, scribbles, rectangles, ellipses, and so forth.
- A different selection model so, for example, the user must select an object before seeing and moving its end points.
- Grab handles that let the user resize a selected object. (You don't need this if you're only drawing line segments and you can move their end points.)
- Snap-to-grid features.
- Alignment tools such as Align Tops and Align Middles.
- The ability to save and restore pictures.
- The ability to remove objects and change their stacking order.
- Shapes with different foreground and background colors.
I may get to some of these in future examples. (A better approach for those more flexible features would be to use classes to represent the objects in the drawing.)
Note also that building custom drawing tools is one of my favorite types of consulting, so if you want one built, email me.
Download the example to experiment with it and to see additional details.
|