[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: Let the user edit polygons in WPF and C#

[Let the user edit polygons in WPF and C#]

The post Let the user draw a polygon in WPF and C# shows how you can let the user draw polygons in a WPF application. This post shows how you can let the user edit them.

Individually the program's pieces aren't too complicated, but there are a lot of them so the final result is somewhat involved.

XAML Code

The following XAML code builds the program's interface.

<Window x:Class="howto_wpf_polygon_editor.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="howto_wpf_polygon_editor" Height="300" Width="300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="20"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal"> <RadioButton Content="New" Margin="5" Name="radNew" IsChecked="True" /> <RadioButton Content="Edit" Margin="5" Name="radEdit" Click="radEdit_Click" /> </StackPanel> <Border Grid.Row="1" Grid.Column="0" Margin="10" BorderBrush="Gray" BorderThickness="1"> <Canvas Name="canDraw" Background="Transparent" ClipToBounds="True" MouseDown="canDraw_MouseDown" MouseMove="canDraw_MouseMove" MouseUp="canDraw_MouseUp"/> </Border> </Grid> </Window>

This is fairly similar to the code used by the previous example with just a couple of changes. The most obvious changes are that this example displays two radio buttons in a stack panel. This code also references a MouseUp event handler that the previous example didn't need.

This example also sets the canDraw Canvas control's ClipToBounds property to true. The earlier example didn't need this because any mouse down events occurred within the canDraw control so the polygon had to stay inside the control.

However, when you're editing in the new example, you can drag a polygon or one of its vertices off of the control. In that case the polygon will draw partly outside of the canvas and that just looks weird. (Actually you can see a similar weird effect in the previous program if you create a polygon and then resize the window so the polygon lies partly outside of the Canvas control. Set the ClipToBounds property if you want to prevent that.)

Setting ClipToBounds to true makes the program only draw the parts of the polygon that fit within the control.

Drawing Polygons

This program uses radio buttons to let you decide whether you're drawing or editing polygons. When you move the mouse or press a mouse button, the program checks the radio buttons to see which mode it should use.

The following code shows the program's MouseDown and MouseMove event handlers.

private void canDraw_MouseDown(object sender, MouseButtonEventArgs e) { if (radNew.IsChecked.Value) ModeNew_MouseDown(sender, e); else ModeEdit_MouseDown(sender, e); } private void canDraw_MouseMove(object sender, MouseEventArgs e) { if (radNew.IsChecked.Value) ModeNew_MouseMove(sender, e); else ModeEdit_MouseMove(sender, e); }

When you press a mouse button, the code checks the radNew radio button to see whether you are trying to draw a new polygon. It then calls ModeNew_MouseDown or ModeEdit_MouseDown accordingly.

Similarly when you move the mouse, the program checks the radio button and calls ModeNew_MouseMove or ModeEdit_MouseMove.

The code that draws new polygons is similar to the code used by the earlier post so I won't show it here. Look at that post for more information.

Editing Variables

The program uses the following variables to keep track of the polygon that it is editing.

private bool Editing = false; private Extensions.PolygonHitTypes EditPolygonHitType = Extensions.PolygonHitTypes.None; private Polygon EditPolygon = null; private int EditPolygonPartIndex = -1; private Point EditLastPoint = new Point( double.NegativeInfinity, double.NegativeInfinity);

These variables have the following purposes.

  • Editing - This is true while we are editing a polygon.
  • EditPolygonHitType - This enumerated value indicates whether we are editing a polygon's vertex, edge, or nothing.
  • EditPolygon - This is the polygon that we are editing.
  • EditPolygonPartIndex - This gives the index of the polygon vertex or edge that we are editing.
  • EditLastPoint - This stores the last position of the mouse while we are moving a vertex or edge.

Mouse Movement

The following code executes when the user moves the mouse while the program is in editing mode.

private void ModeEdit_MouseMove(object sender, MouseEventArgs e) { if (Editing) { if (EditPolygonHitType == Extensions.PolygonHitTypes.Vertex) { // Move the vertex. MoveVertex(e); } else if (EditPolygonHitType == Extensions.PolygonHitTypes.Edge) { // Move the polygon. MovePolygon(e); } } else { Cursor new_cursor = null; // See if we're over a Polygon. SetEditPolygon(e.GetPosition(canDraw)); if (EditPolygonHitType == Extensions.PolygonHitTypes.Vertex) { new_cursor = Cursors.Cross; } else if (EditPolygonHitType == Extensions.PolygonHitTypes.Edge) { new_cursor = Cursors.SizeAll; } // Update the cursor. if (canDraw.Cursor != new_cursor) canDraw.Cursor = new_cursor; } }

If variable Editing is true, then the user is currently editing a polygon. In that case, the program checks the EditPolygonHitType variable to see whether it is editing a polygon vertex or edge. It then calls the MoveVertex or MovePolygon method accordingly. I'll describe those methods shortly.

If Editing is false, then the program is not currently editing a polygon. In that case, the code should display an appropriate cursor. The code sets the new cursor to a default value of null. It then calls SetEditPolygon (described later) to see what the mouse is over. If the mouse is over a polygon vertex or edge, the code updates the new cursor accordingly. Finally, if the drawing Canvas control is not already displaying the desired cursor, the code sets its cursor to the new one.

MoveVertex

If the user is editing a polygon vertex and the mouse moves, the program calls the following MoveVertex method.

// Move the editing vertex. private void MoveVertex(MouseEventArgs e) { Point cur_point = e.GetPosition(canDraw); double dx = cur_point.X - EditLastPoint.X; double dy = cur_point.Y - EditLastPoint.Y; Point new_point = new Point( EditPolygon.Points[EditPolygonPartIndex].X + dx, EditPolygon.Points[EditPolygonPartIndex].Y + dy); EditPolygon.Points[EditPolygonPartIndex] = new_point; EditLastPoint = cur_point; }

This method calls e.GetPosition to get the mouse's current position with respect to the canDraw Canvas control that contains the polygons. It then subtracts the X and Y coordinates of the mouse's previously recorded position stored in variable EditLastPoint from the mouse's new position to see how far the mouse has moved.

The code then updates the position of the vertex that the user is moving. The polygon is stored in variable EditPolygon. The polygon stores its vertices in its Points collection. Finally, the variable EditPolygonPartIndex holds the index of the vertex that the user is moving. So the program needs to update EditPolygon.Points[EditPolygonPartIndex].

Unfortunately you cannot modify the points in the polygon's Points collection. If you try, you get the following compile-time error.

Cannot modify the return value of 'System.Windows.Media.PointCollection.this[int]' because it is not a variable

Fortunately you can set the Points entry equal to a new Point that has the correct coordinates, so that's what this method does. It calculates the new X and Y coordinates that the vertex should have, uses them to make a new Point, and sets the Points entry.

The method finishes by saving the current mouse position in variable EditLastPoint so it is ready for the next mouse move.

MovePolygon

When the user clicks and drags over a polygon edge, the program does not move only the edge. Instead it moves the whole polygon. (You could modify it to move only the edge if you prefer.) The following MovePolygon method moves the editing polygon.

// Move the editing polygon. private void MovePolygon(MouseEventArgs e) { Point cur_point = e.GetPosition(canDraw); double dx = cur_point.X - EditLastPoint.X; double dy = cur_point.Y - EditLastPoint.Y; int num_points = EditPolygon.Points.Count; for (int i = 0; i < num_points; i++) { Point new_point = new Point( EditPolygon.Points[i].X + dx, EditPolygon.Points[i].Y + dy); EditPolygon.Points[i] = new_point; } EditLastPoint = cur_point; }

Like the MoveVertex method, this code finds the mouse's current position and determines how far the mouse has moved since the last time it was recorded. It then loops through the polygon's Points collection and sets each vertex equal to a new Point with an updated position. As in the MoveVertex method, this code cannot modify the points' X and Y coordinates, so it sets the entries equal to new points.

SetEditPolygon

When the user moves the mouse across the drawing canvas, the program calls the following SetEditPolygon method to see what is under the mouse.

// See if a polygon is at this point. // Set EditPolygon, EditPolygonHitType, and // EditPolygonPartIndex. private void SetEditPolygon(Point point) { EditPolygon = null; EditPolygonHitType = Extensions.PolygonHitTypes.None; EditPolygonPartIndex = -1; // See if we're over a Polygon. foreach (UIElement element in canDraw.Children) { Polygon polygon = element as Polygon; if (polygon == null) continue; if (polygon.IsAt(point, out EditPolygonHitType, out EditPolygonPartIndex)) { EditPolygon = polygon; return; } } }

This code assumes that the mouse is not over any polygon. It then loops through the elements inside the canDraw Canvas control's Children collection. Note that a Polygon is not a control, it is a UIElement.

For each item in the Children collection, the code tries to convert the item into a Polygon. If the item is not a Polygon, then the conversion fails and variable polygon is null. In that case the code uses a continue statement to skip the rest of the loop and continue with the next item.

If the item is a Polygon, the code calls the IsAt extension method described shortly to see whether the mouse is over the item. If the mouse is over the item, the code saves the item in the EditPolygon variable and returns.

When the SetEditPolygon method is finished, the variables EditPolygon, EditPolygonHitType, and EditPolygonPartIndex are set to indicate the object under the mouse.

Extension Methods

The SetEditPolygon method described in the preceding section calls the IsAt extension method. That method is part of the public static class Extensions.

The following code shows the PolygonHitTypes enumerated type defined by that class.

public enum PolygonHitTypes { None, Vertex, Edge, };

This enumeration indicates the part of a polygon that lies below the mouse.

The following code shows the IsAt method.

public static bool IsAt(this Polygon polygon, Point point, out PolygonHitTypes hit_type, out int item_index) { const double hit_radius = 2; int num_points = polygon.Points.Count; // See if the point is at a vertex. for (int i = 0; i < num_points; i++) { if (point.DistanceToPoint(polygon.Points[i]) < hit_radius) { hit_type = PolygonHitTypes.Vertex; item_index = i; return true; } } // See if the point is on an edge. Point closest; for (int i = 0; i < num_points; i++) { int j = (i + 1) % num_points; if (point.DistanceToSegment( polygon.Points[i], polygon.Points[j], out closest) < hit_radius) { hit_type = PolygonHitTypes.Edge; item_index = i; return true; } } hit_type = PolygonHitTypes.None; item_index = -1; return false; }

This method extends the Polygon class. It first sets the constant hit_radius equal to 2. If the mouse is within two pixels of a polygon vertex or edge, then it is considered above that vertex or edge. You can make this constant larger if you want to make it easier to grab parts of the polygons.

The code then loops through the polygon's vertices and uses the DistanceToPoint extension method to see how far the vertex is from the mouse position. If the vertex is within distance hit_radius of the mouse, the code sets the hit type and item index, and then returns true.

If none of the vertices is close enough to the mouse position, the method loops through the vertices again, this time looking at the edge that leads out of each vertex. For each edge, the code calls the DistanceToSegment extension method to see how far the mouse's position is from the edge. If the mouse is close enough to the edge, the code sets the hit type and edge index, and then returns true.

If none of the vertices or edges is close enough to the mouse's position, the method returns false.

The following code showos the DistanceToPoint extension method.

public static double DistanceToPoint(this Point from_point, Point to_point) { double dx = to_point.X - from_point.X; double dy = to_point.Y - from_point.Y; return Math.Sqrt(dx * dx + dy * dy); }

This method extends the Point structure. (Point is a struct not a class.) It simply calculates the distance between the start point and another point and returns the result.

The DistanceToSegment method also extends the Point structure. It uses the method described in the post Find the shortest distance between a point and a line segment in C# to find the distance between the point and a line segment. See that post and download this example to see the details.

MouseDown and MouseUp

The previous sections explain how the program moves a polygon or a vertex when you move the mouse. Those movements start and stop when you press and release the mouse.

The following code executes when you press the mouse button while in editing mode.

private void ModeEdit_MouseDown(object sender, MouseButtonEventArgs e) { // See if we are over a polygon. if (EditPolygon == null) return; EditLastPoint = e.GetPosition(canDraw); Editing = true; canDraw.CaptureMouse(); }

This code checked the EditPolygon variable to see if the mouse is over a polygon. (That variable will have been set by the ModeEdit_MouseMove method.) If the mouse is not over a polygon, the MouseDown event handler returns.

If the mouse is over a polygon, the event handler saves the mouse's current position in variable EditLastPoint and sets Editing = true.

The code also captures the mouse for the canDraw to make future mouse events go to that control. If you don't do that, then weird things can happen when you move the mouse off of the control. First, the MouseMove event does not fire so the program stops editing the polygon until the mouse returns to the control.

That's a bit strange, but not a big deal. A worse problem occurs if you release the mouse button while the mouse is not over the control. In that case, the mouse button is up, but the MouseUp event didn't fire so the program thinks it is still in editing mode. That means you cannot stop editing until you press and release the mouse again, this time over the control.

Capturing the mouse makes the MouseUp event go to the control even if the mouse is not over it so the program can finish editing.

This has the annoying side effect that you can drag a polygon off of the top or left side of the control where you cannot get it back. You can restrict the positions where you can move a polygon and its vertices if you like.

When the user moves the mouse after this point, the ModeEdit_MouseMove method adjusts the polygon. When the user releases the mouse button, the following event handler stops editing the polygon.

private void canDraw_MouseUp(object sender, MouseButtonEventArgs e) { Editing = false; canDraw.ReleaseMouseCapture(); }

This code sets Editing = false and releases its earlier mouse capture. After this point the EditMode_MouseMove method no longer edits a polygon and instead updates the cursor if necessary to show what's under the mouse.

Switching Modes

That concludes the polygon editing code, but there's one more odd case to handle. If you are in the middle of creating a new polygon and you check the Edit radio button, the program must stop drawing the new polygon so it can enter editing mode. To make that possible, this example moved the code used by the previous example to finish creating a polygon into a new FinishPolygon method. When you right-click to stop drawing a new polygon, or when you click the Edit radio button, the program calls this method.

For example, when you click the Edit radio button, the following event handler executes.

// If we're making a new Polygon, finish it. private void radEdit_Click(object sender, RoutedEventArgs e) { FinishPolygon(); }

// Remove the temporary polyline. canDraw.Children.Remove(NewPolyline); NewPolyline = null; }

The FinishPolygon method uses code similar to the code used by the previous example so I won't show it here. Download this example or read the previous post to see the details.

Conclusion

Individually the pieces of this example aren't too complicated, but it does take a bit of work to see how all of the pieces fit together. Download the example to experiment with it and to see additional details.

You can also enhance the program if you like. For example, you could add a trashcan icon and delete any polygons that you drag over the icon. Or you could allow the user to drag polygon edges instead of entire polygons. You could even let the user right-click a polygon to display a popup menu where the user could change the polygon's stroke and fill colors and styles. If you build something interesting and post it somewhere, let us know in a comment below.

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

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