Let the user draw line segments, and then drag the segments or their end points in C#

When you move the mouse over objects, this program displays a hand cursor if you're over a line segment, an arrow if you're over a segment's end point, and a cross hair if you're not over anything.

If you press the mouse down over a line segment or an end point, you can move that object. If you press the mouse down over an unoccupied area, you can draw a new line.

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 could be confusing. To make things manageable, the program uses separate MouseMove and MouseUp event handlers to perform its different tasks.

This description is divided into the following sections, which correspond to the program's basic states.

Drawing

The program stores the coordinates of the segment points in variables Pt1 and Pt2. While you are drawing a new segment, the variable IsDrawing is true and the program stores that segment's end points in variables NewPt1 and 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).

// The points that make up the line segments.
private List Pt1 = new List();
private List Pt2 = new List();

// Points for the new line.
private bool IsDrawing = false;
private Point NewPt1, NewPt2;

// 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);
}
}

Not Moving Anything

When you're not moving a segment or end point and the the mouse moves, the following event handler executes. It uses the MouseIsOverEndPoint and MouseIsOverSegment methods to see if the mouse is over anything interesting and it displays an appropriate cursor.

// 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 mouse_pt = new Point(e.X, e.Y);
Point hit_point;
int segment_number;

if (MouseIsOverEndpoint(mouse_pt, out segment_number, out hit_point))
{
new_cursor = Cursors.Arrow;
}
else if (MouseIsOverSegment(mouse_pt, out segment_number))
{
new_cursor = Cursors.Hand;
}

// Set the new cursor.
if (picCanvas.Cursor != new_cursor)
{
picCanvas.Cursor = new_cursor;
}
}

If you're not moving anything and you press the mouse down, the following event handler executes. It also 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 it uninstalls the picCanvas_MouseMove_NotDown event handler and installs new MouseMove and MouseUp event handlers for the operation.

// The "size" of an object for mouse over purposes.
private const int object_radius = 3;

// We're over an object if the distance squared
// between the mouse and the object is less than this.
private const int over_dist_squared = object_radius * object_radius;

// 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 mouse_pt = new Point(e.X, e.Y);
Point hit_point;
int segment_number;

if (MouseIsOverEndpoint(mouse_pt, 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(mouse_pt, 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);
}
}

The following code shows the MouseIsOverEndPoint and MouseIsOverSegment methods. These simply call the FindDistanceToPointSquared and FindDistanceToSegmentSquared methods. FindDistanceToPointSquared is trivial. For a description of how FindDistanceToSegmentSquared works, see the entry 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 x * x < y * y so this test will still determine whether an object is within the required distance of the mouse.)

// 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;
}

// Calculate the distance squared between two points.
private int FindDistanceToPointSquared(Point pt1, Point pt2)
{
int dx = pt1.X - pt2.X;
int dy = pt1.Y - pt2.Y;
return dx * dx + dy * dy;
}

// Calculate the distance squared between
// point pt and the segment p1 --> p2.
private double FindDistanceToSegmentSquared(Point pt, Point p1, Point p2, out PointF closest)
{
float dx = p2.X - p1.X;
float dy = p2.Y - p1.Y;
if ((dx == 0) && (dy == 0))
{
// It's a point not a line segment.
closest = p1;
dx = pt.X - p1.X;
dy = pt.Y - p1.Y;
return dx * dx + dy * dy;
}

// Calculate the t that minimizes the distance.
float t = ((pt.X - p1.X) * dx + (pt.Y - p1.Y) * dy) / (dx * dx + dy * dy);

// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
closest = new PointF(p1.X, p1.Y);
dx = pt.X - p1.X;
dy = pt.Y - p1.Y;
}
else if (t > 1)
{
closest = new PointF(p2.X, p2.Y);
dx = pt.X - p2.X;
dy = pt.Y - p2.Y;
}
else
{
closest = new PointF(p1.X + t * dx, p1.Y + t * dy);
dx = pt.X - closest.X;
dy = pt.Y - closest.Y;
}

return dx * dx + dy * dy;
}

Drawing a New Segment

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.

Moving an End Point

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.

Moving a Line Segment

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.

What Next?

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 its end points.
  • Grab handles that let the user resize a selected object.
  • 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 heir 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.

   

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.