9. Arrowheads

This problem is relatively straightforward, but I'm going to describe the solution in some detail because it demonstrates some techniques that will be useful for later problems. This problem has two parts: allowing the user to draw a segment and drawing the segment as an arrow.

The first problem is relatively simple, at least as long as you've seen this sort of thing before. The Arrowhead example solution uses the following code to let the user select the segment:

// The segment's endpoint. If they are the same, there's no segment.
private Point StartPoint, EndPoint;

// True while dragging.
private bool Drawing = false;

// Start drawing.
private void arrowPictureBox_MouseDown(object sender, MouseEventArgs e)
{
Drawing = true;
StartPoint = e.Location;
EndPoint = e.Location;
arrowPictureBox.Refresh();
}

// Continue drawing.
private void arrowPictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (!Drawing) return;

EndPoint = e.Location;
arrowPictureBox.Refresh();
}

// Stop drawing.
private void arrowPictureBox_MouseUp(object sender, MouseEventArgs e)
{
    if (!Drawing) return;
Drawing = false;

EndPoint = e.Location;
arrowPictureBox.Refresh();
}

The StartPoint and EndPoint fields hold the segment's endpoints. The program needs some way to tell whether the user has made a selection. This program does that by checking whether the two endpoints are the same.

The Drawing field is true while the user is in the process of drawing a segment. When the user presses the mouse button down to start drawing a segment, the MouseDown event handler executes. It sets Drawing to true so the program knows that a draw is in progress. It sets StartPoint and EndPoint to the mouse's current location and refreshes the program's PictureBox control. You'll see shortly how that makes the program draw the arrow.

When the user moves the mouse over the program's PictureBox, the MouseMove event handler executes. If Drawing is false, then the user is moving the mouse with the mouse button up, so no draw is in progress. In that case, the event handler simply exits. However, if Drawing is true, the code updates EndPoint to hold the mouse's new location and refreshes the PictureBox.

When the user releases the mouse button, the MouseUp event handler executes. The program again checks Drawing to see whether a draw is in progress and exits if it is not. It then sets Drawing to false to indicate that the current draw is over. It updates EndPoint and refreshes the program's PictureBox.

When the PictureBox needs to redraw itself, for example when it is refreshed, the following Paint event handler executes:

// Draw the arrow.
private void arrowPictureBox_Paint(object sender, PaintEventArgs e)
{
if (StartPoint == EndPoint) return;

e.Graphics.Clear(Color.White);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

// Make a thick pen.
using (Pen pen = new Pen(Color.Red, 5))
{
pen.LineJoin = LineJoin.Round;
pen.EndCap = LineCap.Round;
pen.StartCap = LineCap.Round;

// Draw the segment.
e.Graphics.DrawLine(pen, StartPoint, EndPoint);

// Draw the arrowhead and tail.
DrawArrowPart(e.Graphics, pen, StartPoint, EndPoint, 15,
false);
DrawArrowPart(e.Graphics, pen, EndPoint, StartPoint, 15, true);
}
}

If  StartPoint and EndPoint are the same, then no segment has been defined, so the method simply returns. Otherwise, if a segment has been defined, the code clears the drawing area and sets the Graphics object's SmoothingMode property to produce smooth lines.

Next, the code creates a thick red pen, setting its properties so line joins and end caps are rounded.

The program then draws a line segment connecting StartPoint and EndPoint. It finishes by calling the DrawArrowPart method described shortly to draw the arrow's head and tail. Before I describe that method, however, I'll explain how it works.

A vector, <vx, vy>, represents a direction. For example, the vector <1, 2> means you should move one unit to the right in the X direction and two units up in the Y direction—sort of north-northeast.

As I mentioned in the hint, if <vx, vy> is a vector, then <vy, -vx> and <-vy, vx> are two vectors perpendicular to the original vector. The following diagram shows a vector in bold and its two perpendicular vectors created by this method:

This is all a bit upside down if you remember that Y coordinates increase downward in C#. For now, think of using a normal mathematical coordinate system where Y increases upward. If you add two vectors, the result is a third vector. The result is the same as if you had followed one vector and then the other.

For this problem, you can make an arrowhead or tail by adding a vector parallel to the arrow segment plus a vector perpendicular to it. For example, to create the right side of the arrowhead, you find the right-pointing perpendicular vector shown in the preceding diagram. You add that vector to a vector pointing in the direction opposite that of the line segment to get the dashed vector shown in the following diagram:

You then add the dashed vector's components to the endpoint of the line segment to get the point at the tip of that side of the arrowhead.

To get the left side of the arrowhead, you repeat the same operations but using the other perpendicular vector on the left side of the original segment.

To create an arrow's tail, you perform similar operations except you reverse the parallel vector's direction.

The final technique that you need to use to make nice arrowheads and tails is scaling a vector. If you multiply a vector's x and y components by a number, you scale its length by that amount. In particular, if you want a vector to have a specific length, L, you can divide it by its current length and then multiply it by L. The result is a vector pointing in the same direction as the original one but with the desired length.

The following code shows how the Arrowhead example solution uses these techniques to add an arrowhead or tail to a line segment:

// Draw an arrowhead wedge at point p1.
// If reversed is true, make an arrow tail.
private void DrawArrowPart(Graphics gr, Pen pen, Point p0, Point p1,
float sideLength, bool reversed)
{
// Get a vector along the arrow's length.
float vx = p1.X - p0.X;
float vy = p1.Y - p0.Y;

// If this should be a tail, reverse the vectors.
if (reversed)
{
vx = -vx;
vy = -vy;
}

// Get perpendicular vectors.
float p0x = vy;
float p0y = -vx;
float p1x = -vy;
float p1y = vx;

// Get arrowhead/tail vectors.
float headX0 = p0x - vx;
float headY0 = p0y - vy;
float headX1 = p1x - vx;
float headY1 = p1y - vy;

// Set the vectors' lengths.
float length0 = (float)Math.Sqrt(headX0 * headX0 + headY0 *
headY0);
headX0 *= sideLength / length0;
headY0 *= sideLength / length0;
float length1 = (float)Math.Sqrt(headX1 * headX1 + headY1 *
headY1);
headX1 *= sideLength / length1;
headY1 *= sideLength / length1;

// Draw it.
PointF[] points =
{
new PointF(p1.X + headX0, p1.Y + headY0),
p1,
new PointF(p1.X + headX1, p1.Y + headY1),
};
gr.DrawLines(pen, points);
}

The method starts by finding a vector <vx, vy> that points along the user's line segment from point p0 to point p1. If the method should draw a tail instead of an arrowhead, the code reverses that vector.

Next, the code finds perpendicular vectors <p0x, p0y> and <p1x, p1y>. (Here, the p stands for perpendicular.)

The method then adds the parallel and perpendicular vectors as described earlier to make the vectors that give the sides of the arrowhead or tail. It calculates the lengths of those vectors, divides them by their lengths, and multiplies them by the method's sideLength parameter to give them the desired final lengths.

Next, the method adds the vectors to the segment's endpoint to find the locations of the arrowhead/tail's points. It saves the points in an array and uses the array to draw the arrowhead/tail.

These are the main pieces of the program. Download the Arrowhead example solution to see additional details.

The following list shows the key techniques that you should take from this example:

  • You can use the MouseDown, MouseMove, and MouseUp event handlers to let the user draw. (You should also consider MouseClick if you only need to let the user select points instead of drawing.)
  • If you subtract the components of point p0 from point p1, you get the components for a vector pointing from p0 to p1.
  • If <vx, vy> is a vector, then <vy, -vx> and <-vy, vx> are perpendicular to it.
  • You can scale a vector by multiplying its components by a number. In particular, you can divide the vector by its length to get a vector of length one. Then, you can multiply it by the desired length if you like.
  • If you add a point's coordinates to a vector's components, you get a new point as if you had started at the original point and then moved in the direction and distance given by the vector.

You may find these techniques handy for solving later problems and when facing everyday programming challenges.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.158.36