Draw text on a circle in C#

[text on a circle]

This example uses some of the techniques described in the post Measure character positions when drawing long strings in C# to draw text on a circle. The previous post explains how to determine where the characters in a string will be drawn. In particular, it shows how to determine the width and height of each of the characters in a string.

The DrawTextOnCircle method shown in the following code uses that information to draw text along the top and bottom of a circle.


// Draw text centered on the top and bottom of the circle.
private void DrawTextOnCircle(Graphics gr, Font font,
    Brush brush, float radius, float cx, float cy,
    string top_text, string bottom_text)
{
    // Use a StringFormat to draw the middle
    // top of each character at (0, 0).
    using (StringFormat string_format = new StringFormat())
    {
        string_format.Alignment = StringAlignment.Center;
        string_format.LineAlignment = StringAlignment.Far;

        // Used to scale from radians to degrees.
        double radians_to_degrees = 180.0 / Math.PI;

        // **********************
        // * Draw the top text. *
        // **********************
        // Measure the characters.
        List<RectangleF> rects =
            MeasureCharacters(gr, font, top_text);

        // Use LINQ to add up the character widths.
        var width_query = from RectangleF rect in rects
            select rect.Width;
        float text_width = width_query.Sum();

        // Find the starting angle.
        double width_to_angle = 1 / radius;
        double start_angle = -Math.PI / 2 -
            text_width / 2 * width_to_angle;
        double theta = start_angle;

        // Draw the characters.
        for (int i = 0; i < top_text.Length; i++)
        {
            // See where this character goes.
            theta += rects[i].Width / 2 * width_to_angle;
            double x = cx + radius * Math.Cos(theta);
            double y = cy + radius * Math.Sin(theta);

            // Transform to position the character.
            gr.RotateTransform((float)(radians_to_degrees *
                (theta + Math.PI / 2)));
            gr.TranslateTransform((float)x, (float)y,
                MatrixOrder.Append);

            // Draw the character.
            gr.DrawString(top_text[i].ToString(), font, brush,
                0, 0, string_format);
            gr.ResetTransform();

            // Increment theta.
            theta += rects[i].Width / 2 * width_to_angle;
        }

        // *************************
        // * Draw the bottom text. *
        // *************************
        // Measure the characters.
        rects = MeasureCharacters(gr, font, bottom_text);

        // Use LINQ to add up the character widths.
        width_query = from RectangleF rect in rects
            select rect.Width;
        text_width = width_query.Sum();

        // Find the starting angle.
        width_to_angle = 1 / radius;
        start_angle = Math.PI / 2 +
            text_width / 2 * width_to_angle;
        theta = start_angle;

        // Reset the StringFormat to draw above the drawing origin.
        string_format.LineAlignment = StringAlignment.Near;

        // Draw the characters.
        for (int i = 0; i < bottom_text.Length; i++)
        {
            // See where this character goes.
            theta -= rects[i].Width / 2 * width_to_angle;
            double x = cx + radius * Math.Cos(theta);
            double y = cy + radius * Math.Sin(theta);

            // Transform to position the character.
            gr.RotateTransform((float)(radians_to_degrees *
                (theta - Math.PI / 2)));
            gr.TranslateTransform((float)x, (float)y,
                MatrixOrder.Append);

            // Draw the character.
            gr.DrawString(bottom_text[i].ToString(), font, brush,
                0, 0, string_format);
            gr.ResetTransform();

            // Increment theta.
            theta -= rects[i].Width / 2 * width_to_angle;
        }
    }
}

The method does some preliminary work and then draws the text on the top of the circle.

The code calls the MeasureCharacters method described in the earlier post to get the characters’ widths. It uses LINQ to add up the widths so it knows how long the entire string would be if drawn normally.

Next the code divides the string’s total width by 2 (to get half of the width) and divides the result by the circle’s radius (to get the angular size of half of the string). It subtracts that angular width from -π/2 to get the angle where the text should start. It then sets the variable theta equal to this starting angle.

[draw text]

The code then loops through the string’s characters. For each character, the code adds half of the character’s angular width to theta to find the angular position of the horizontal middle of the character. It then uses the circle’s radius, together with the sine and cosine functions, to calculate the position of this point on the circle. This point is where the bottom center of the character should be. (See the red point in the picture on the right.)

The next step is to apply a transformation to the Graphics object to rotate and translate the character to the desired position. The code first creates a rotation transformation to rotate the character appropriately. The angle theta + π/2 is at right angles to the character’s position angle theta. The result of that calculation is in radians, but the Graphics object’s RotateTransform method works in degrees, so the code translates from radians to degrees and then creates the rotation transformation.

Next, the code adds a translation transformation so the origin (0, 0) is translated to the red point on the circle shown in the previous picture.

If you look back at the beginning of the method, you’ll see these two lines:

string_format.Alignment = StringAlignment.Center;
string_format.LineAlignment = StringAlignment.Far;

These lines align text that is printed at a point so it is centered horizontally and drawn above the point. That’s exactly the situation we need for the red point in the previous picture. The code simply draws the character at the origin (0, 0) and the transformations rotate and translate it into position.

The loop finishes by resetting the Graphics object’s transformation (to remove the current transformations) and adding half of the character’s angular width to theta to move to the beginning of the next character’s position on the circle.

[draw text]

Next, the code draws the text below the circle. The steps are very similar to those used to draw the text above the circle. The biggest change is that this time the code must draw the characters below the circle. (See the blue point in the picture on the right.) To do that, the code simply uses the following statement.

// Reset the StringFormat to draw below
// the drawing origin.
string_format.LineAlignment = StringAlignment.Near;

The code also decreases theta with each character (to move left-to-right across the bottom of the circle), and it rotates the characters differently so they don’t appear upside down. See the code for additional details.

Note that this kind of text that follows a path looks best if the path doesn’t curve too abruptly. In this example, that means the circle has a large radius.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in algorithms, fonts, geometry, graphics, strings and tagged , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

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