Let the user draw, move, and modify an arc in C#, Part 2

[arc]

My previous post Let the user draw, move, and modify an arc in C#, Part 1 describes an Arc class that allows a program to draw arcs. This post explains how the example program uses that class to let the user draw and modify arcs.

The example uses a very useful technique for managing mouse event handlers in a program that uses a complex collection of states. The following section describes that technique. The sections after that explain how the program allows the user to draw, move, and modify arcs.

Managing State

This example uses the mouse to perform the following tasks.

  • Change the cursor depending on what’s below the mouse
  • If the mouse is not over an arc, click and drag to create a new arc
  • If the mouse is over an arc’s starting point, click and drag to move the starting point
  • If the mouse is over an arc’s ending point, click and drag to move the ending point
  • If the mouse is over an arc’s body, click and drag to move the arc

One way to handle that is to use a single set of MouseDown, MouseMove, and MouseUp event handlers. Each of those would use a switch statement or series of if statements to determine which actions to take depending on the program’s current state. That works, but it makes the event handlers very confusing.

This example uses a different approach. It uses a different set of MouseDown, MouseMove, and MouseUp event handlers for each of its states. The following code shows the MouseMove event handler that is initially installed when the program starts.

// Used to draw new arcs.
private Arc NewArc = null;
private Point StartPoint;

// Used to draw existing Arcs.
private List Arcs = new List();

// The Arc and part of the Arc that the mouse is over.
private Arc ArcUnderMouse = null;
private Arc.Part PartUnderMouse = Arc.Part.None;

// Process MouseMove when the mouse is up.
private void picCanvas_MouseMove(object sender, MouseEventArgs e)
{
    // See what the mouse is over.
    foreach (Arc arc in Arcs)
    {
        Arc.Part part = arc.ArcPartAtPoint(e.Location, RADIUS);
        if (part != Arc.Part.None)
        {
            ArcUnderMouse = arc;
            PartUnderMouse = part;

            switch (part)
            {
                case Arc.Part.StartPoint:
                    picCanvas.Cursor = Cursors.Cross;
                    break;
                case Arc.Part.EndPoint:
                    picCanvas.Cursor = Cursors.Cross;
                    break;
                case Arc.Part.Body:
                    picCanvas.Cursor = Cursors.SizeAll;
                    break;
            }
            return;
        }
    }

    // We're not over any arc parts.
    ArcUnderMouse = null;
    PartUnderMouse = Arc.Part.None;
    picCanvas.Cursor = ArcCursor;
}

The NewArc field holds a reference to the new arc that the program is drawing, while it is drawing a new arc. The Arcs list holds references to the arcs that the program has already created. The ArcUnderMouse and PartUnderMouse fields track the part of the arc below the mouse when you move it.

The picCanvas_MouseMove event handler loops through the Arcs list. For each arc object in the list, the code calls the arc’s ArcPartAtPoint method (described in the previous post). If the mouse is over an arc, the code saves a reference to the arc and the part under the mouse. It then sets the program’s cursor accordingly.

If the program isn’t doing anything to an arc and you press the mouse down, the following event handler executes.

// Handle MouseDown.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
    // Install new arc event handlers
    // depending on what is under the mouse.
    switch (PartUnderMouse)
    {
        case Arc.Part.Body:
            // Move the Arc.
            StartPoint = e.Location;
            picCanvas.MouseDown -= picCanvas_MouseDown;
            picCanvas.MouseMove -= picCanvas_MouseMove;
            picCanvas.MouseMove += MoveArc_MouseMove;
            picCanvas.MouseUp += MoveArc_MouseUp;
            break;
        case Arc.Part.StartPoint:
            // Move the starting point.
            picCanvas.MouseDown -= picCanvas_MouseDown;
            picCanvas.MouseMove -= picCanvas_MouseMove;
            picCanvas.MouseMove += MoveStartPoint_MouseMove;
            picCanvas.MouseUp += MoveStartPoint_MouseUp;
            break;
        case Arc.Part.EndPoint:
            // Move the ending point.
            picCanvas.MouseDown -= picCanvas_MouseDown;
            picCanvas.MouseMove -= picCanvas_MouseMove;
            picCanvas.MouseMove += MoveEndPoint_MouseMove;
            picCanvas.MouseUp += MoveEndPoint_MouseUp;
            break;
        case Arc.Part.None:
        default:
            // Make a new Arc.
            picCanvas.MouseDown -= picCanvas_MouseDown;
            picCanvas.MouseMove -= picCanvas_MouseMove;
            picCanvas.MouseMove += NewArc_MouseMove;
            picCanvas.MouseUp += NewArc_MouseUp;
            StartPoint = e.Location;
            Rectangle bounds = new Rectangle(
                StartPoint, new Size(0, 0));
            NewArc = new Arc(bounds, 270, 90);
            break;
    }
}

This event handler examines the arc part below the mouse and then registers the appropriate event handlers for deal with that object. For example, if the mouse is over an arc’s body, the code saves the current mouse location in the field StartPoint. It unregsisters the picCanas_MouseDown and picCanvas_MouseMove event handlers so those event handlers no longer work. It also registers the MoveArc_MouseMove and MoveArc_MouseUp event handlers so they now take action when the mouse moves or when you release the mouse button.

The last case occurs when the mouse is not over an existing arc. In that case, the program switches event handlers as it does in the other cases. It also creates a new Arc object. The Paint event handler loops through the existing arcs to draw them and then draws the new arc with a different style. This is relatively straightforward so it isn’t described here. Download the example to see how it works.

The program keeps track of its internal state by properly un-registering and registering its event handlers. That means it must use more event handlers than the “one set of event handlers handles all” approach, but it makes each of those event handlers much simpler and easier to understand.

The following sections describe the event handlers that create a new arc, move an existing arc, or move an existing arc’s starting or ending point.

Creating a New Arc

The following code shows the event handlers that are active while the user is creating a new arc.

// MouseMove while creating a new Arc.
private void NewArc_MouseMove(object sender, MouseEventArgs e)
{
    NewArc.Bounds = GetRectangle(StartPoint, e.Location);
    picCanvas.Refresh();
}

// MouseUp while creating a new Arc.
private void NewArc_MouseUp(object sender, MouseEventArgs e)
{
    // Add the new arc if it has non-zero width and height.
    if ((NewArc.Bounds.Width > 0) &&
        (NewArc.Bounds.Height > 0))
    {
        Arcs.Add(NewArc);
    }

    // Restore original event handlers.
    picCanvas.MouseDown += picCanvas_MouseDown;
    picCanvas.MouseMove += picCanvas_MouseMove;
    picCanvas.MouseMove -= NewArc_MouseMove;
    picCanvas.MouseUp -= NewArc_MouseUp;

    NewArc = null;
    picCanvas.Refresh();
}

The NewArc_MouseMove

event handler simply updates the bounds of the new arc that it is drawing and then refreshes the program’s PictureBox to show the new arc in its new location.

The NewArc_MouseUp event handler first checks that the new Arc object has a non-zero width and height. If the width and height are both greater than zero, the code adds the new arc to the Arcs list.

The event handler then uninstalls the new arc event handlers and re-installs the “not doing anything” event handlers.

Moving an Arc

The following code shows the event handlers that are active when the user is moving an existing arc.

// MouseMove while moving an Arc.
private void MoveArc_MouseMove(object sender, MouseEventArgs e)
{
    int dx = e.Location.X - StartPoint.X;
    int dy = e.Location.Y - StartPoint.Y;
    ArcUnderMouse.Move(dx, dy);
    StartPoint = e.Location;
    picCanvas.Refresh();
}

// MouseUp while moving an Arc.
private void MoveArc_MouseUp(object sender, MouseEventArgs e)
{
    // Restore original event handlers.
    picCanvas.MouseDown += picCanvas_MouseDown;
    picCanvas.MouseMove += picCanvas_MouseMove;
    picCanvas.MouseMove -= MoveArc_MouseMove;
    picCanvas.MouseUp -= MoveArc_MouseUp;
}

The MoveArc_MouseMove event handler calculates the X and Y distances that the mouse has moved since the last time it was saved. It then calls the Move method of the Arc object that is being moved. That method was described in the previous post. The event handler then saves the current mouse location and refreshes the program’s PictureBox to show the arc at its new position.

The MoveArc_MouseUp event handler simple un-registers the “move arc” event handlers and re-installs the “not doing anything” event handlers.

Moving an Arc’s End Point

The following code shows the event handlers that are active when the user is moving an existing arc’s starting point.

// MouseMove while moving a starting point.
private void MoveStartPoint_MouseMove(object sender, MouseEventArgs e)
{
    ArcUnderMouse.MoveStartPoint(e.Location);
    picCanvas.Refresh();
}

// MouseUp while moving a starting point.
private void MoveStartPoint_MouseUp(object sender, MouseEventArgs e)
{
    // Restore original event handlers.
    picCanvas.MouseDown += picCanvas_MouseDown;
    picCanvas.MouseMove += picCanvas_MouseMove;
    picCanvas.MouseMove -= MoveStartPoint_MouseMove;
    picCanvas.MouseUp -= MoveStartPoint_MouseUp;
}

The MoveStartPoint_MouseMove event handler simply calls the MoveStartPoint method provided by the Arc object that is being modified, passing it the mouse’s current position. The MoveStartPoint method was described in the previous post.

The MoveStartPoint_MouseUp event handler simply unregisters the “move starting point” event handlers and re-installs the “not doing anything” event handlers.

The event handlers that deal with moving an existing arc’s ending point are almost exactly the same as those that move the starting point. Download the example to see how they work.

Summary

Believe it or not, that’s all there is to the application! Using different sets of event handlers to perform different tasks makes state management simple. The Arc class described in the previous post makes it relatively easy to create, move, or modify an Arc object.

There are still a lot of features that you might like to add to the program. Some examples include the following.

  • Allow the user to save and load drawings.
  • Provide copy, cut, and paste.
  • Provide undo and redo.
  • Allow object selection and multi-selection.
  • If the user presses Esc while performing an action, cancel the action.
  • Allow the user to resize an arc’s bounding rectangle.
  • Allow the user to rotate a drawing object.
  • Add other shapes such as lines, rectangles, circles, smooth curves, polylines, polygons, images, and so forth.
  • Provide print preview and printing.
  • Allow the user to export a drawing by producing C# or XAML code that draws the objects.
      I’m sure there are other features that you might like to add. If you think of others or if you add new features to the program, feel free to post in the comments below. (I actually tried to use Kickstarter to generate some interest in building this sort of object-enabled drawing program but very few people supported the project.)


      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 drawing, graphics, mathematics and tagged , , , , , , , , , , . Bookmark the permalink.

    Leave a Reply

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

    This site uses Akismet to reduce spam. Learn how your comment data is processed.