Make a WPF line editor C#

[WPF line editor]

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 Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in algorithms, drawing, graphics, wpf and tagged , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

18 Responses to Make a WPF line editor C#

  1. azadmk says:

    hi dear
    how to create lines same like this project ,but different color & above’s length of line..pleas help me thanks..

    • RodStephens says:

      You’ll need to add a line editor (or hire me to build you one). For example, add a context menu so the user can right-click on a line to open the editor. The editor can allow the user to change the line’s color, thickness, etc.

      above’s length of line

      I don’t know what you mean by this.

      • azadmk says:

        thank you dear for your reply…
        My purpose is project like (Make a WPF line editor C#),..
        And change styles ,colors and thicknesses by comboBoxes but done effects as lines create before,and display length of lines
        on each lines.

        • RodStephens says:

          Ah. You’ll need to make an editor as I said before. Or you could put combo boxes at the top of the form and use them to manipulate the selected line.

          To display the lengths in WPF, you would need to add a label for each line and position it on each line’s midpoint.

          • azadmk says:

            thanks for your answer…
            I created this project and editor as comboBox..But have problem,when I change styles ,thickness or color ,,change All lines before created..
            when I change the color to Blue all lines before created changed colors..

          • RodStephens says:

            Make sure you’re only applying the new values to the currently selected line(s).

  2. Alejandro says:

    Hi RodStephens good day, thanks for you example, resolve my problem, regards Rod

  3. Alejandro says:

    Hi, good day RodStephens, have a questions, you know how I can add a rectangle at the end of the line, after you finish drawing the line, any suggestions, thanks.

    • RodStephens says:

      Do you mean like little drag handle boxes? You would need to keep a list of rectangles and their corresponding line segments. You could probably use a Dictionary to let you look up a segment’s rectangles easily.

      Then as you move the line’s end points you would need to move its rectangle.

      • Alejandro says:

        Yes, little boxes, for example in the method MouseIsOverEndpoint like add a System.Windows.Shapes.Rectangle(), is where is coordinates the line end, but in the moment that use: canvas.children.add(rectangle) , show a error in the foreach??

        • RodStephens says:

          I don’t know why the foreach loops should give you problems.

          You can add the rectangle to the canvas using code similar to the following.

          Rectangle rect = new Rectangle();
          rect.Width = 10;
          rect.Height = 10;
          rect.Stroke = Brushes.Black;
          rect.Fill = Brushes.White;
          rect.SetValue(Canvas.LeftProperty, location.X);
          rect.SetValue(Canvas.TopProperty, location.Y);
          canDrawing.Children.Add(rect);
          • Alejandro says:

            I can add rectangle in the end of line in the method canDrawing_MouseUp_Drawing, and for move the rectangle with the end line canDrawing_MouseMove_MovingEndPoint, and move rectangle with the line complete canDrawing_MouseMove_MovingSegment, but only does the last line

  4. Alejandro says:

    you remember your previous post:
    http://csharphelper.com/blog/2014/10/draw-and-move-line-segments-in-c/
    , I would like to do something like, draw circle o rectangle.

    • RodStephens says:

      That example drew the end points in Windows Forms. This one uses WPF objects to make the lines appear. That means you can’t really draw the end point boxes in the same way.

      You’ll need to make a structure such as a dictionary to hold the rectangles for all of the lines. When the user moves a line’s end point or the whole line, you need to also move the rectangles.

      • Alejandro says:

        at this time I could add a rectangle in the end line, but in the moent that move line out move point second the line, I duplicated the rectangle or as you could add an object at the end of line?

        • RodStephens says:

          You should only create the rectangles when you make the lines. Then move them when you move the lines.

          I don’t think you can easily “add” an object to the end of another object. You might be able to create the line and its rectangles as a single drawing object, perhaps a Path object, but then detecting when the mouse is over an end point and moving the object’s points could be hard.

Leave a Reply

Your email address will not be published. Required fields are marked *