[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Make a WPF line editor C#

[Make a WPF line editor C#]

This example is a "simple" WPF line editor that lets you add, move, and delete Line objects. The example Draw, move, and delete line segments in C# is a Windows Forms application that does something. The WPF version is a tiny bit simpler. (At least for the most part. Setting the trash can Image control's Source property is much harder, despite the fact that I set it at design time. But I've ranted about WPF's handling of simple resources before. It still doesn't make much sense.)

The program uses a Grid that is filled with a Canvas object. An Image control sits in the Canvas's upper left corner. At design time I set its Width and Height, set its Source property to make it display the trash can image, and set its Stretch property to Uniform.

When you draw lines, the program adds them a children of the Canvas control. Unlike in Windows Forms, in WPF Line is a control of its own with its own properties, methods, and events. I tried using the Line objects to handle their own events, but it turned out to be more problem than it was worth. They had particular trouble tracking MouseMove events when you drag the cursor off of a Line control. You might be able to do it, but it's just easier to have the Canvas control handle the events.

When the program starts, the following code executes.

// Save the trash can dimensions. private double TrashWidth, TrashHeight; private void Window_Loaded(object sender, RoutedEventArgs e) { TrashWidth = imgTrash.ActualWidth; TrashHeight = imgTrash.ActualHeight; // The Canvas must have a non-transparent background // to make it receive mouse events. canDrawing.Background = Brushes.White; }

This code saves the dimensions of the trash can Image control for later use. It also sets the Canvas control's background to white. By default that control has a transparent background that prevents it from receiving mouse events.

The program lets you draw a line, move a line, or move a line's end points. To do that, the program needs to be able to figure out whether the mouse is over a line or an end point. The following sections describe those four sections of the code.


Figure out what's below the mouse

The first section of code determines what lies under the mouse. The program uses the following two constants to decide whether a point is close enough to a line or an end point.

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

The FindDistanceToPointSquared method uses those values to calculate the distance squared between two points. It's simple so it isn't shown here. See the code for details.

Similarly the FindDistanceToSegmentSquared method finds the distance squared between a point and a line segment. For information about how it works, see the code and the post Find the shortest distance between a point and a line segment in C#.

The following method determines whether a specific point is over a Line object.

// See if the mouse is over a line segment. private bool MouseIsOverLine(Point mouse_pt, out Line hit_line) { foreach (object obj in canDrawing.Children) { // Only process Lines. if (obj is Line) { Line line = obj as Line; // See if we're over this line. Point closest; Point pt1 = new Point(line.X1, line.Y1); Point pt2 = new Point(line.X2, line.Y2); if (FindDistanceToSegmentSquared( mouse_pt, pt1, pt2, out closest) < over_dist_squared) { // We're over this segment. hit_line = line; return true; } } } hit_line = null; return false; }

This method loops through the objects that are children of the Canvas control. If an object is a Line, the code calls FindDistanceToSegmentSquared to see how far it is from the target point. If the distance squared is less than over_dist_squared, the points is over that Line. The method saves the line in the hit_line output parameter and returns true to indicate that it found a hit.

The following code determines whether a target point is over a line's end point.

// See if the mouse is over an end point. private bool MouseIsOverEndpoint(Point mouse_pt, out Line hit_line, out bool start_endpoint) { foreach (object obj in canDrawing.Children) { // Only process Lines. if (obj is Line) { Line line = obj as Line; // Check the starting point. Point point = new Point(line.X1, line.Y1); if (FindDistanceToPointSquared(mouse_pt, point) < over_dist_squared) { // We're over this point. hit_line = line; start_endpoint = true; return true; } // Check the end point. point = new Point(line.X2, line.Y2); if (FindDistanceToPointSquared(mouse_pt, point) < over_dist_squared) { // We're over this point. hit_line = line; start_endpoint = false; return true; } } } hit_line = null; start_endpoint = false; return false; }

This code also loops through the objects that are children of the Canvas control and processes the Line objects. It calls the FindDistanceToPointSquared method for each line's end points.


Draw a New Line

When no drag is in progress and you move the mouse over the Canvas control, the following MouseMove event handler executes.

// The line we're drawing or moving. private Line SelectedLine; // True if we're moving the line's first starting end point. private bool MovingStartEndPoint = false; // The offset from the mouse to the object being moved. private double OffsetX, OffsetY; // The mouse is up. See whether we're over an end point or segment. private void canDrawing_MouseMove_NotDown(object sender, MouseEventArgs e) { Cursor new_cursor = Cursors.Cross; // See what we're over. Point location = (canDrawing); if (MouseIsOverEndpoint(location, out SelectedLine, out MovingStartEndPoint)) new_cursor = Cursors.Arrow; else if (MouseIsOverLine(location, out SelectedLine)) new_cursor = Cursors.Hand; // Set the new cursor. if (canDrawing.Cursor != new_cursor) canDrawing.Cursor = new_cursor; }

This code uses e.MouseDevice.GetPosition to figure out where the mouse is. (In Windows Forms, the mouse events include that information. In WPF, you need to take an extra step to get it.) It then uses the MouseIsOverEndpoint and MouseIsOverLine methods to see if the mouse is over a line or end point. It then sets the Canvas control's cursor to an arrow or hand respectively.

When no drag is in progress and you press the mouse down over the Canvas control, the following MouseDown event handler executes.

// See what we're over and start doing whatever is appropriate. private void canDrawing_MouseDown(object sender, MouseButtonEventArgs e) { // See what we're over. Point location = e.MouseDevice.GetPosition(canDrawing); if (MouseIsOverEndpoint(location, out SelectedLine, out MovingStartEndPoint)) { // Start moving this end point. canDrawing.MouseMove -= canDrawing_MouseMove_NotDown; canDrawing.MouseMove += canDrawing_MouseMove_MovingEndPoint; canDrawing.MouseUp += canDrawing_MouseUp_MovingEndPoint; // Remember the offset from the mouse to the point. Point hit_point; if (MovingStartEndPoint) hit_point = new Point(SelectedLine.X1, SelectedLine.Y1); else hit_point = new Point(SelectedLine.X2, SelectedLine.Y2); OffsetX = hit_point.X - location.X; OffsetY = hit_point.Y - location.Y; } else if (MouseIsOverLine(location, out SelectedLine)) { // Start moving this segment. canDrawing.MouseMove -= canDrawing_MouseMove_NotDown; canDrawing.MouseMove += canDrawing_MouseMove_MovingSegment; canDrawing.MouseUp += canDrawing_MouseUp_MovingSegment; // Remember the offset from the mouse // to the segment's first end point. OffsetX = SelectedLine.X1 - location.X; OffsetY = SelectedLine.Y1 - location.Y; } else { // Start drawing a new segment. canDrawing.MouseMove -= canDrawing_MouseMove_NotDown; canDrawing.MouseMove += canDrawing_MouseMove_Drawing; canDrawing.MouseUp += canDrawing_MouseUp_Drawing; SelectedLine = new Line(); SelectedLine.Stroke = Brushes.Red; SelectedLine.X1 = location.X; SelectedLine.Y1 = location.Y; SelectedLine.X2 = location.X; SelectedLine.Y2 = location.Y; canDrawing.Children.Add(SelectedLine); } }

This event handler does one of three things depending on what's under the mouse.

If the MouseIsOverEndpoint method indicates that the mouse is over an end point, the code removes the Canvas control's MouseMove event handler and installs new MouseMove and MouseUp event handlers to let you move the end point you've selected.

If the MouseIsOverEndpoint method indicates that the mouse is over an end point, the code removes the Canvas control's MouseMove event handler and installs new MouseMove and MouseUp event handlers to let you move the end point you've selected. It also saves the X and Y distances from the mouse position to the end point. It uses those later to move the end point.

If the MouseIsOverLine method indicates that the mouse is over a Line object, the method installs event handlers to deal with moving a line. It also saves the X and Y distances from the mouse position to the line's starting end point. It uses those later to move the line.

Finally if the mouse isn't over an end point or a line, the code installs event handlers to draw a new line. It also creates a new Line object, sets its color to red, and adds it to the Canvas control's Children collection.

Here's the code that executes when you're drawing a new line and you move the mouse.

// We're drawing a new segment. private void canDrawing_MouseMove_Drawing(object sender, MouseEventArgs e) { // Update the new line's end point. Point location = e.MouseDevice.GetPosition(canDrawing); SelectedLine.X2 = location.X; SelectedLine.Y2 = location.Y; }

This code simply sets the position of the new Line object's second end point to the mouse's current position.

When you release the mouse, the following code executes.

// Stop drawing. private void canDrawing_MouseUp_Drawing(object sender, MouseEventArgs e) { SelectedLine.Stroke = Brushes.Black; // Reset the event handlers. canDrawing.MouseMove -= canDrawing_MouseMove_Drawing; canDrawing.MouseMove += canDrawing_MouseMove_NotDown; canDrawing.MouseUp -= canDrawing_MouseUp_Drawing; // If the new segment has no length, delete it. if ((SelectedLine.X1 == SelectedLine.X2) && (SelectedLine.Y1 == SelectedLine.Y2)) canDrawing.Children.Remove(SelectedLine); }

This code changes the new line's color to black and re-installs the event handlers that the Canvas control should use while no operation is in progress. Then if the line's length is 0, the code removes it from the Canvas control's Children collection.


Move a Line's End Point

The following event handler executes while you're moving a line's end point.

// We're moving an end point. private void canDrawing_MouseMove_MovingEndPoint(object sender, MouseEventArgs e) { // Move the point to its new location. Point location = e.MouseDevice.GetPosition(canDrawing); if (MovingStartEndPoint) { SelectedLine.X1 = location.X + OffsetX; SelectedLine.Y1 = location.Y + OffsetY; } else { SelectedLine.X2 = location.X + OffsetX; SelectedLine.Y2 = location.Y + OffsetY; } }

This code sets the line's end point coordinates equal to the mouse's current position plus the offset it recorded in the MouseDown event handler. For example, when you pressed the mouse down, the mouse might have been 1 pixel above and 3 pixels to the left of the line's true end point. The offset values keep the end point that distance from the mouse as you move it so the end point doesn't jump abruptly when you start the move.

When you release the mouse, the following event handler executes.

// Stop moving the end point. private void canDrawing_MouseUp_MovingEndPoint(object sender, MouseEventArgs e) { // Reset the event handlers. canDrawing.MouseMove += canDrawing_MouseMove_NotDown; canDrawing.MouseMove -= canDrawing_MouseMove_MovingEndPoint; canDrawing.MouseUp -= canDrawing_MouseUp_MovingEndPoint; }

This code simple re-installs the event handlers that should work when no operation is in progress.


Move a Line

The following event handler executes when you move the mouse while dragging a line.

// We're moving a segment. private void canDrawing_MouseMove_MovingSegment(object sender, MouseEventArgs e) { // Find the new location for the first end point. Point location = e.MouseDevice.GetPosition(canDrawing); double new_x1 = location.X + OffsetX; double new_y1 = location.Y + OffsetY; // See how far we are moving that point. double dx = new_x1 - SelectedLine.X1; double dy = new_y1 - SelectedLine.Y1; // Move the line. SelectedLine.X1 = new_x1; SelectedLine.Y1 = new_y1; SelectedLine.X2 += dx; SelectedLine.Y2 += dy; }

This code uses the offset values to calculate the new location for the line's first end point. It subtracts the end point's current position from the new position to get dx and dy values. It then uses those values to update the line's second end point.

When you release the mouse while dragging a line, the following code executes.

// Stop moving the segment. private void canDrawing_MouseUp_MovingSegment(object sender, MouseEventArgs e) { // Reset the event handlers. canDrawing.MouseMove += canDrawing_MouseMove_NotDown; canDrawing.MouseMove -= canDrawing_MouseMove_MovingSegment; canDrawing.MouseUp -= canDrawing_MouseUp_MovingSegment; // See if the mouse is over the trash can. Point location = e.MouseDevice.GetPosition(canDrawing); if ((location.X >= 0) && (location.X < TrashWidth) && (location.Y >= 0) && (location.Y < TrashHeight)) { if (MessageBox.Show("Delete this segment?", "Delete Segment?", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { // Delete the segment. canDrawing.Children.Remove(SelectedLine); } } }

This code re-installs the event handlers that should work while no operation is taking place.

It then determines whether the mouse's current position is over the trash can image. If it is, the code asks whether you want to delete the line. If you click the Yes button, the code removes the selected line from the Canvas control's Children collection.

This is another part of the application where you might be able to use other objects' events instead of those provided by the Canvas control. For example, you might like to use the trash can Image control's MouseUp event, but I had problems making that work. The Image control received that event sometimes but not always, even if I made the Canvas control's event handler mark the event as not handled. You could probably use WPF's routed events system to make this work, but it doesn't seem like it's worth the effort. The events this program handles drag across the Line and Image objects, so it makes some sense to make the Canvas control handle them anyway.

Conclusion

You could add plenty of enhancements to this simple WPF line editor. For example, you could:

  • Add other shapes such as ellipses, polygons, images, and text
  • Let the user click and drag to select multiple objects
  • Let the user move, resize, and delete multiple objects
  • Provide a snap-to grid
  • Allow the user to save and load drawing files

Hopefully this example is enough to get you started.

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.