Draw arc wedges in C#

[arc wedges]

This example defines several extension methods that draw various interesting items including the one that I originally set out to draw: arc wedges. The extension methods let you easily draw arc wedges, arcs with tic marks, line segments with tic marks and arrowheads, and arrowheads without line segments. Note that the wedges are isosceles trapezoids; their inner and outer edges are straight line segments and do not follow arcs of a circle.

I was inspired to draw arc wedges by a picture in an article that was trying to look like some sort of high-tech biometric identification system. Once I started on the final drawing, I also added arrowheads, arrows, and lines with tic marks.

When the program starts, it displays two forms. The picture here shows the main form. The second form shows samples of line segments with various arrowheads.

Before I show you the code to draw arc wedges, arrowheads, and lines with tic marks, let’s take a look at the main form’s code that uses those tools. When the form needs to draw, the following Paint event handler executes. The most interesting drawing code is highlighted in blue. I’ll describe those methods in later sections.

private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw inner arc tics.
    Point center = new Point(173, 89);
    float radius = 40;
    float start_angle = 40;
    float sweep_angle = 360 - 2 * start_angle;
    int num_tics = 24;
    float skip_angle = sweep_angle / num_tics;
    e.Graphics.DrawArcTics(
        Pens.Yellow, Pens.Yellow,
        center, radius, 10,
        start_angle, sweep_angle,
        skip_angle, 1, 1);

    // Draw outer wedges.
    float inner_radius2 = 60;
    float outer_radius2 = inner_radius2 + 40;
    int num_wedges = 12;
    float skip_degrees = sweep_angle /
        (2 * num_wedges + (num_wedges - 1));
    float draw_degrees = skip_degrees * 2;
    e.Graphics.DrawArcWedges(
        null, Pens.Black,
        center, inner_radius2, outer_radius2,
        start_angle, num_wedges,
        draw_degrees, skip_degrees);

    // Highlight one arc wedge.
    using (Pen pen = new Pen(Color.Purple, 2))
    {
        using (Brush brush = new SolidBrush(
            Color.FromArgb(80, pen.Color)))
        {
            start_angle += 3 * (draw_degrees + skip_degrees);
            e.Graphics.DrawArcWedges(
                brush, pen,
                center, inner_radius2, outer_radius2,
                start_angle, 1,
                draw_degrees, skip_degrees);
        }
    }

    // Draw the arrow pointing to the center.
    Brush arrow_brush = Brushes.Orange;
    PointF start_point = new PointF(center.X + 80, center.Y);
    e.Graphics.DrawSegment(
        start_point, center, Pens.Orange,
        Pens.Orange, 10, 10, 1, 2,
        Extensions.ArrowheadTypes.None,
        null, 0,
        Extensions.ArrowheadTypes.TriangleHead,
        Brushes.Orange, 8);

    // Draw the ticks above the pupil.
    PointF p1 = new PointF(center.X - 75, center.Y - 50);
    PointF p2 = new PointF(center.X + 75, center.Y - 50);
    e.Graphics.DrawSegment(
        p1, p2, Pens.White,
        Pens.White, 10, 15, 1, 1,
        Extensions.ArrowheadTypes.None, null, 0,
        Extensions.ArrowheadTypes.None, null, 0);
    PointF p3 = new PointF(center.X - 25, p1.Y - 20);
    PointF p4 = new PointF(center.X - 25, p1.Y - 10);
    e.Graphics.DrawArrowhead(Brushes.White, p3, p4, 10,
        Extensions.ArrowheadTypes.VHead);

    // Draw tic marks to the left.
    PointF p5 = new PointF(center.X - 75, center.Y - 50);
    PointF p6 = new PointF(center.X - 75, p5.Y + 15 * 7);
    e.Graphics.DrawSegment(
        p5, p6, Pens.White,
        Pens.White, 10, 15, 1, 1,
        Extensions.ArrowheadTypes.None, null, 0,
        Extensions.ArrowheadTypes.None, null, 0);
    PointF p7 = new PointF(p5.X - 20, center.Y + 20);
    PointF p8 = new PointF(p5.X - 10, center.Y + 20);
    e.Graphics.DrawArrowhead(Brushes.White, p7, p8, 10,
        Extensions.ArrowheadTypes.VHead);
}

The event handler sets the Graphics object’s SmoothingMode property, sets a few parameters, and then calls the DrawArcTics extension method. That method draws an arc with tic marks on it. In the picture at the top of the post, it draws the yellow arc surrounding the eye’s pupil.

[arc wedges]

Next, the code sets some more parameters and uses the DrawArcWedges extension method to draw the black arc wedges that list outside of the yellow arc. It then highlights one wedge in translucent purple. To place the wedge correctly, the code begins at the start angle used to draw all of the arc wedges and adds three times the number of degrees that the DrawArcWedges method skipped between wedges. The red arcs on the picture to the right show how updating start_angle moves the start angle to the beginning of the highlighted wedge. Most of the other parameters passed into the DrawArcWedges method are the same as those used before so the highlighted wedge falls on top of a previously drawn wedge. The only change is that the new call uses a purple pen and a translucent purple brush.

Now the program draws some arrows and arrowheads. It calls the DrawSegment extension method to draw the orange arrow that ends at the eye’s pupil. As you can see, the method draws the line segment, tic marks, and a triangular arrowhead.

The code then uses the same method to draw a white line segment with tic marks but no arrowheads above the pupil. It uses the DrawArrowhead extension method, which is also used by the DrawSegment method, to draw an arrowhead above the segment.

The event handler finishes repeating those steps to draw one more white line segment with tic marks and an arrowhead beside it to the left of the pupil.

That’s all there is to the Paint event handler. It’s relatively simple, although it contains a lot of statements that define parameters for the extension methods that it calls. The following sections describe the extension methods.

DrawArcTics

The following code shows the start of the DrawaArcTics extension method.

public static void DrawArcTics(this Graphics gr,
    Pen arc_pen, Pen tic_pen,
    Point center, float radius, float tic_length,
    float start_angle, float sweep_angle, float skip_degrees,
    int num_skip_start_tics, int num_skip_end_tics)

The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • arc_pen – The method uses this pen to draw the arc. Set this to null not draw the arc.
  • tic_pen – The method uses this pen to draw the tic marks. Set this to null to not draw the tic marks.
  • center – This is the center of the arc’s circle.
  • radius – This is the radius of the arc’s circle.
  • tic_length – This is the length of the tic marks. The method draws them centered on the arc.
  • start_angle – This is the angle where the arc should start.
  • sweep_angle – This is the amount that the arc should sweep.
  • skip_degrees – This is the number of degrees that the method should skip between tic marks.
  • num_skip_start_tics – Set this to the number of tic marks that the method should not draw at the beginning of the arc. Use this to make the arc begin with a stretch that does not include tic marks.
  • num_skip_end_tics – Set this to the number of tic marks that the method should not draw at the end of the arc.

The following code shows the method.

// Draw tic marks around an arc.
public static void DrawArcTics(this Graphics gr,
    Pen arc_pen, Pen tic_pen,
    Point center, float radius, float tic_length,
    float start_angle, float sweep_angle, float skip_degrees,
    int num_skip_start_tics, int num_skip_end_tics)
{
    if (arc_pen != null)
    {
        RectangleF rect =new RectangleF(
            center.X - radius,
            center.Y - radius,
            2 * radius, 2 * radius);
        gr.DrawArc(arc_pen, rect, start_angle, sweep_angle);
    }
    if (tic_pen == null) return;

    int num_tics = (int)(sweep_angle / skip_degrees) + 1;
    double theta = DegToRad(start_angle);
    double skip_rad = DegToRad(skip_degrees);

    theta += skip_rad * num_skip_start_tics;
    num_tics -= num_skip_start_tics;

    num_tics -= num_skip_end_tics;

    float inner_radius = radius - tic_length / 2f;
    float outer_radius = radius + tic_length / 2f;

    for (int i = 0; i < num_tics; i++)
    {
        PointF p1 =
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta)),
                (float)(center.Y + inner_radius * Math.Sin(theta)));
        PointF p2 =
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta)),
                (float)(center.Y + outer_radius * Math.Sin(theta)));
        gr.DrawLine(tic_pen, p1, p2);

        theta += skip_rad;
    }
}

If the arc_pen parameter is not null, then the code uses the Graphics object’s DrawArc method to draw the arc with that pen.

If tic_pen is null, the method is done so it returns.

Otherwise if tic_pen is not null, the method calculates the number of tic marks that it must draw. It then uses the DegToRad helper method to convert the angle parameters from degrees to radians. That method is straightforward so I won’t show it here. It simply multiples the angle in degrees by π and divides the result by 180.

Next the code adds to the angle theta to skip the desired number of tic marks at the arc’s start. It also subtracts from num_tics the number of tic marks that it should skip at the arc’s beginning and end.

The code then calculates the inner and outer radii where the tic marks should begin and end. It then enters a loop to draw the tic marks.

Inside the loop, the code uses cosines and sines to calculate the X and Y coordinates of the tic marks’ end points and draws them. After each tic mark, it increases the angle theta by the skip angle. These calculations are done in radians because that’s what Math.Sin and Math.Cos use.

DrawArcWedges

The following code shows the start of the DrawArcWedges extension method.

public static void DrawArcWedges(this Graphics gr,
    Brush brush, Pen pen,
    Point center, float inner_radius, float outer_radius,
    float start_angle, int num_wedges,
    float draw_degrees, float skip_degrees)

The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • brush – The method fills the arc wedges with this brush. Set this to null to make the method not fill the wedges.
  • pen – The method outlines the arc wedges with this pen. Set this to null to make the method not outline the wedges.
  • center – This is the center of the circle that defines the arc wedges.
  • inner_radius – This is the radius of the wedges’ inner corners.
  • outer_radius – This is the radius of the wedges’ outer corners.
  • start_angle – The method starts drawing at this angle.
  • num_wedges – This determines the number of arc wedges that the method draws.
  • draw_degrees – This gives the number of degrees that a wedge should span.
  • skip_degrees – This gives the number of degrees between wedges.

The following code shows the DrawArcWedges method.

// Draw arc wedges.
public static void DrawArcWedges(this Graphics gr,
    Brush brush, Pen pen,
    Point center, float inner_radius, float outer_radius,
    float start_angle, int num_wedges,
    float draw_degrees, float skip_degrees)
{
    double theta = DegToRad(start_angle);
    double draw_rad = DegToRad(draw_degrees);
    double skip_rad = DegToRad(skip_degrees);
    for (int i = 0; i < num_wedges; i++)
    {
        PointF[] points =
        {
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta)),
                (float)(center.Y + inner_radius * Math.Sin(theta))),
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta)),
                (float)(center.Y + outer_radius * Math.Sin(theta))),
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta + draw_rad)),
                (float)(center.Y + outer_radius * Math.Sin(theta + draw_rad))),
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta + draw_rad)),
                (float)(center.Y + inner_radius * Math.Sin(theta + draw_rad))),
        };
        if (brush != null) gr.FillPolygon(brush, points);
        if (pen != null) gr.DrawPolygon(pen, points);

        theta += draw_rad + skip_rad;
    }
}

The method first converts the angle parameters from degrees to radians and then loops through the desired number of arc wedges. For each wedge, the code uses Math.Cos and Math.Cos to calculate the positions of the wedge’s corners. It fills and outlines the wedge appropriately and then increases the angle theta by the angular size of the wedge and the gap between wedges.

DrawSegment

The DrawSegment draws a line segment with optional tic marks and arrowheads at both ends. The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • start_point – This is the point where the segment starts.
  • end_point – This is the point where the segment ends.
  • line_pen – The method draws the segment with this pen. Set this to null to not draw the segment.
  • tic_pen – The method draws the tic marks with this pen. Set this to null to not draw tic marks.
  • tic_length – This is the length of the tic marks. The method centers the tic marks across the segment and makes them perpendicular to it.
  • tic_spacing – This is the distance along the segment between tic marks.
  • num_skip_start_tics – This indicates the number of tic marks that the method should skip at the beginning of the segment.
  • num_skip_end_tics – This indicates the number of tic marks that the method should skip at the end of the segment.
  • start_arrowhead_type – This determines the type of arrowhead that should be drawn at the beginning of the segment. Set this to Extensions.ArrowheadTypes.None to not draw a starting arrowhead.
  • start_arrowhead_brush – This is the brush used to draw the starting arrowhead.
  • start_arrowhead_radius – This determines the size of the starting arrowhead. The exact effect depends on the arrowhead type, but this is basically half of the arrowhead’s width. Note that this is not scaled for different segment thicknesses. (The Graphics class’s DrawLine method scales end caps for segments of different thicknesses.)
  • end_arrowhead_type – This determines the type of arrowhead that should be drawn at the end of the segment. Set this to Extensions.ArrowheadTypes.None to not draw an ending arrowhead.
  • end_arrowhead_brush – This is the brush used to draw the ending arrowhead.
  • end_arrowhead_radius – This determines the size of the ending arrowhead.

The following code shows the DrawSegment method.

// Draw a lines segment with optional tic marks and arrowheads.
public static void DrawSegment(this Graphics gr,
    PointF start_point, PointF end_point,
    Pen line_pen,
    Pen tic_pen, float tic_length, float tic_spacing,
    int num_skip_start_tics, int num_skip_end_tics,
    ArrowheadTypes start_arrowhead_type,
    Brush start_arrowhead_brush, float start_arrowhead_radius,
    ArrowheadTypes end_arrowhead_type,
    Brush end_arrowhead_brush, float end_arrowhead_radius)
{
    // Draw the line.
    if (line_pen != null)
        gr.DrawLine(line_pen, start_point, end_point);

    // Draw the tic marks.
    if (tic_pen != null)
    {
        // Get unit vectors in the directions
        // of the segment and perpendicular.
        float dx = end_point.X - start_point.X;
        float dy = end_point.Y - start_point.Y;
        float length = (float)Math.Sqrt(dx * dx + dy * dy);
        dx /= length;
        dy /= length;
        float nx = dy;
        float ny = -dx;

        // Convert to the desired lengths.
        dx *= tic_spacing;
        dy *= tic_spacing;
        nx *= tic_length / 2f;
        ny *= tic_length / 2f;

        // Prepare if we should skip the first tic mark.
        PointF point = start_point;
        int num_tics = (int)(length / tic_spacing) + 1;

        // Skip starting tics if desired.
        num_tics -= num_skip_start_tics;
        point.X += num_skip_start_tics * dx;
        point.Y += num_skip_start_tics * dy;

        // Skip ending tics if desired.
        num_tics -= num_skip_end_tics;

        // Draw the tic marks.
        for (int i = 0; i < num_tics; i++)
        {
            PointF p1 = new PointF(
                point.X + nx,
                point.Y + ny);
            PointF p2 = new PointF(
                point.X - nx,
                point.Y - ny);
            gr.DrawLine(tic_pen, p1, p2);

            point.X += dx;
            point.Y += dy;
        }
    }

    // Draw the arowheads.
    DrawArrowhead(gr, start_arrowhead_brush,
        end_point, start_point, start_arrowhead_radius,
        start_arrowhead_type);
    DrawArrowhead(gr, end_arrowhead_brush,
        start_point, end_point, end_arrowhead_radius,
        end_arrowhead_type);
}

The method first checks the line_pen parameter and draws the line segment if that pen is not null.

Next, if tic_pen is not null, the method draws the tic marks. To do that it gets the difference in X and Y coordinates between the segment’s end and start points. It divides those differences by the segment’s length so the vector <dx, dy> has length one.

The code then switches those values and negates one to get a vector <nx, ny> = <dy, -dx> that has length one and that is perpendicular to the line segment.

The code will use the vector <dx, dy> to move along the line segment, so it multiplies dx and dy by tic_spacing so it will move the correct amount between tic marks.

Similarly the method will use the vector <nx, ny> to move from the segment to the tic marks’ end points. It will move half of the tic mark length to each side of the segment, so it multiplies nx and ny by half of tic_length.

The code then calculates the number of tic marks it would need to fill the entire segment. The code then subtracts the number of starting tic marks that it should skip and updates the start point accordingly. It also subtracts the number of ending tic marks that it should skip.

Finally, the code enters a loop to draw the tic marks. For each tic mark, it adds vector <nx, ny> to the current position on the segment to find the tic mark’s end points. It connects those points and then uses dx and dy to update the current position on the segment so it is ready to draw the next tic mark.

The method finishes by calling the DrawArrowhead method twice to draw the segment’s starting and ending arrowheads.

DrawArrowhead

The DrawArrowhead method draws an arrowhead pointing in a certain direction. The following enumeration defines the types of arrowheads that the method can draw.

// Arrowhead types.
public enum ArrowheadTypes
{
    None,
    TriangleHead,
    TriangleTail,
    VHead,
    VTail,
    Broadhead,
    Broadtail,
};

[arc wedges]

The picture on the right shows samples of each of the arrowhead and tail types. From top-to-bottom they demonstrate the None, Triangle, V, and Broad styles.

The following list describes the DrawArrowhead method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • brush – The method uses this brush to fill the arrowhead.
  • from_point – The arrowhead is drawn at the end of a line segment that starts at this point.
  • to_point – The arrowhead is drawn at the end of a line segment that ends at this point.
  • radius – This defines the size of the arrowhead. The exact effect depends on the arrowhead type, but this is basically half of the arrowhead’s width.
  • arrowhead_type – This specifies the type of arrowhead or tail to draw. Set this to ArrowheadTypes.None to draw nothing.

Note that this method only uses the segment defined by start_point and end_point to position and orient the arrowhead or tail. It does not actually draw the segment.

The following code shows the shell of the DrawArrowhead method.

// Draw an arrowhead at the indicated point.
public static void DrawArrowhead(this Graphics gr, Brush brush,
    PointF from_point, PointF to_point, float radius,
    ArrowheadTypes arrowhead_type)
{
    if (arrowhead_type == ArrowheadTypes.None) return;

    // Get the vectors that we need.
    float dx = to_point.X - from_point.X;
    float dy = to_point.Y - from_point.Y;
    float length = (float)Math.Sqrt(dx * dx + dy * dy);
    float ux = dx / length; // Unit distance along the arrow.
    float uy = dy / length;
    float rx = uy;    // Unit distance perpendicular to the arrow.
    float ry = -ux;

    // Generate the arrowhead's points.
    PointF[] points = null;
    switch (arrowhead_type)
    {
        case ArrowheadTypes.Broadhead:
            ...
            break;
        case ArrowheadTypes.Broadtail:
            ...
            break;
        case ArrowheadTypes.TriangleHead:
            ...
            break;
        case ArrowheadTypes.TriangleTail:
            ...
            break;
        case ArrowheadTypes.VHead:
            ...
            break;

        case ArrowheadTypes.VTail:
            ...
            break;
    }

    gr.FillPolygon(brush, points);
}

The method first checks arrowhead_type and returns if it is None.

The method then gets two the unit (length one) vectors much as the DrawSegment method did. The vector <ux, uy> points in the same direction as the line segment. The vector <rx, ry> points perpendicularly to the right of the segment as you look along the segment.

The code then declares an array of PointF objects and enters the switch statement that makes up the bulk of the method. The different switch cases use the <dx, dy> and <nx, ny> vectors to fill in the points array to define the different kinds of arrowheads and tails.

After the switch blocks end, the program simply uses the points in the points array to fill the polygon that defines the arrowhead shape.

The following code shows how the method defines the TriangleHead shape.

case ArrowheadTypes.TriangleHead:
    points = new PointF[]
    {
        new PointF(to_point.X, to_point.Y),
        new PointF(
            to_point.X - radius * ux + radius * rx,
            to_point.Y - radius * uy + radius * ry),
        new PointF(
            to_point.X - radius * ux - radius * rx,
            to_point.Y - radius * uy - radius * ry),
    };
    break;

[arc wedges]

The picture on the right shows how the code generates the points that it saves in the points array. Each of the vectors is multiplied by the radius, although I have omitted that from the picture to save space.

The code saves the line segment’s end point in the first array entry.

For the second point, it starts at the segment’s end point, subtracts the vector <ux, uy> (times the radius), and then adds the vector <rx, ry> (again, times the radius).

For the third point, the code starts at the segment’s end point, subtracts the vector <ux, uy> (times the radius), and then subtracts the vector <rx, ry> (all times the radius).

Together the three points define the arrowhead’s triangle.

The other switch cases define their arrow pieces similarly, although some of them are a bit more complicated. Download the example to see how they work.

Conclusion

This example defines some useful drawing extension methods. Even if you never need to draw arc wedges, line segments with arrowheads, or tic marks on an arc, the basic technique of adding extension methods to the Graphics class is extremely useful.

Download the example to experiment with the methods, to see additional details, or to try adding your own extension methods.


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.

2 Responses to Draw arc wedges in C#

  1. Hai says:

    Hi Rod,

    Would you please let me know that after using your book , can I develop graphic applications that similar to AutoCAD?

    Thanks

    Hai

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.