Measure character positions when drawing long strings in C#


[Measure character positions]

The example Measure character positions in a drawn string in C# uses the Graphics class’s MeasureCharacterRanges method to find the positions where characters in a string will be drawn. Unfortunately this method requires you to call a StringFormat object’s SetMeasurableCharacterRanges method and that method can set at most 32 ranges. That means you cannot find the positions for more than 32 characters. If you try, the method throws an uninformative “Overflow error” exception.

This example splits a long string into pieces with at most 32 characters and then measures the pieces separately. There are a couple of small problems with that approach that the example works around.

The following MeasureCharactersInWord method finds the positions of the characters in a string that is no longer than 32 characters.

// Measure the characters in a string with
// no more than 32 characters.
private List<RectangleF> MeasureCharactersInWord(
    Graphics gr, Font font, string text)
{
    List<RectangleF> result = new List<RectangleF>();

    using (StringFormat string_format = new StringFormat())
    {
        string_format.Alignment = StringAlignment.Near;
        string_format.LineAlignment = StringAlignment.Near;
        string_format.Trimming = StringTrimming.None;
        string_format.FormatFlags =
            StringFormatFlags.MeasureTrailingSpaces;

        CharacterRange[] ranges = new CharacterRange[text.Length];
        for (int i = 0; i < text.Length; i++)
        {
            ranges[i] = new CharacterRange(i, 1);
        }
        string_format.SetMeasurableCharacterRanges(ranges);

        // Find the character ranges.
        RectangleF rect = new RectangleF(0, 0, 10000, 100);
        Region[] regions =
            gr.MeasureCharacterRanges(
                text, font, this.ClientRectangle,
                string_format);

        // Convert the regions into rectangles.
        foreach (Region region in regions)
            result.Add(region.GetBounds(gr));
    }

    return result;
}

This method creates a List<RectangleF> to hold the results.

It then creates a StringFormat object to indicate how the text will be formatted when drawn. It sets that object’s Alignment and LineAlignment properties so the text will appear at the top left corner of the drawing area.

Normally when you draw or measure characters, the drawing or measuring methods ignore a trailing space. In this example, some of the pieces of the long string might end with such a space and the program cannot ignore the width of that space. To make the measuring method not ignore the space, the code sets the StringFormat object’s FormatFlags property to MeasureTrailingSpaces.

Next the MeasureCharactersInWord method creates an array of CharacterRange objects and initializes them to represent the ranges in the string that should be measured, in this case the individual characters. The code calls SetMeasurableCharacterRanges to tell the StringFormat object about the ranges.

The method then calls MeasureCharacterRanges to get the regions where the characters will be drawn. The method finishes by converting the regions into RectangleF values and returns the result.

The following MeasureCharacters method uses the previous method to measure strings longer than 32 characters.

// Measure the characters in the string.
private List<RectangleF> MeasureCharacters(Graphics gr,
    Font font, string text)
{
    List<RectangleF> results = new List<RectangleF>();

    // The X location for the next character.
    float x = 0;

    // Get the character sizes 31 characters at a time.
    for (int start = 0; start < text.Length; start += 32)
    {
        // Get the substring.
        int len = 32;
        if (start + len >= text.Length) len = text.Length - start;
        string substring = text.Substring(start, len);

        // Measure the characters.
        List<RectangleF> rects =
            MeasureCharactersInWord(gr, font, substring);

        // Remove lead-in for the first character.
        if (start == 0) x += rects[0].Left;

        // Save all but the last rectangle.
        for (int i = 0; i < rects.Count+1 - 1; i++)
        {
            RectangleF new_rect = new RectangleF(
                x, rects[i].Top,
                rects[i].Width, rects[i].Height);
            results.Add(new_rect);

            // Move to the next character's X position.
            x += rects[i].Width;
        }
    }

    // Return the results.
    return results;
}

This method uses the variable x to keep track of the X coordinate of the next character’s rectangle. The method initializes x to 0.

Next the code breaks the string into pieces containing no more than 32 characters and calls the MeasureCharactersInWord method for each piece.

When you actually draw a string, the DrawString method seems to add a little space before the first character to get things started. This method handles that by adding that bit of extra space (stored in the first character’s rectangle’s Left property) to x. When it draws subsequent pieces of the longer string, DrawString does not add this extra space so it is only added for the first piece.

The method then uses the widths of the returned RectangleF values to create the rectangles for the characters and adds them to the method’s result.

The following DrawTextInBoxes method draws a string with boxes around each character.

// Draw a long string with boxes around each character.
private void DrawTextInBoxes(Graphics gr, Font font,
    float start_x, float start_y, string text)
{
    // Measure the characters.
    List rects = MeasureCharacters(gr, font, text);

    for (int i = 0; i < text.Length; i++)
    {
        gr.DrawRectangle(Pens.Red,
            start_x + rects[i].Left, start_y + rects[i].Top,
            rects[i].Width, rects[i].Height);
    }
    gr.DrawString(text, font, Brushes.Blue, start_x, start_y);
}

This method calls MeasureCharacters to get rectangles representing the characters' positions. If then loops through the rectangles drawing each of them offset by an amount passed into the method. Finally the method draws the string.

The final piece of the program is the following Form_Load event handler.

// Draw the text.
private void Form1_Load(object sender, EventArgs e)
{
    // Make a Bitmap to hold the text.
    Bitmap bm = new Bitmap(
        picText.ClientSize.Width,
        picText.ClientSize.Height);
    using (Graphics gr = Graphics.FromImage(bm))
    {
        gr.Clear(Color.White);

        // Don't use TextRenderingHint.AntiAliasGridFit.
        gr.TextRenderingHint = TextRenderingHint.AntiAlias;

        // Make a font to use.
        using (Font font = new Font("Times New Roman",
            16, FontStyle.Regular))
        {
            // Draw the text.
            DrawTextInBoxes(gr, font, 4, 4,
                "When in the course of human events it " +
                "becomes necessary for the quick brown " +
                "fox to jump over the lazy dog...");
        }
    }

    // Display the result.
    picText.Image = bm;
}

This method creates a Bitmap, clears it, and calls the DrawTextInBoxes method to draw the text and the rectangles around the characters.

There's only one subtle thing about this code. Normally I set the Graphics object's TextRenderingHint property to AntiAliasGridFit to produce the best looking text. If you do that, however, the DrawString method may adjust text spacing to produce a better result. Unfortunately the MeasureCharacterRanges method doesn't take that adjustment into account, so the rectangles don't line up properly over their characters.

To prevent that, set TextRenderingHint to AntiAlias as shown here.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Leave a Reply

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