HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Can calculation of word wrapping be made notably faster?

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
cancalculationmadefasterwordwrappingnotably

Problem

I need to re-wrap a text so that it fits a given width, font and DC. The result needs to be an array of lines. I use the following code:

private struct SplitInfo
{
    public string Word;

    public string SplitChar;

    public override string ToString()
    {
        return Word;
    }
}

private static float GetWidth(Graphics gr, string text, Font font)
{
    SizeF size = gr.MeasureString(text, font, 10000, StringFormat.GenericTypographic);
    return size.Width;
}

public static string[] WrapText(string text, IntPtr hdc, Font font, int textWidthInLoMetric)
{
    // Split words at space (functionality for splitting at multiple different chars omitted for simplicity).
    // This is also why it needs SplitInfo to store the words.
    List words = new List(text.Split(' ').Select(x => new SplitInfo { SplitChar = " ", Word = x }));

    StringBuilder resultText = new StringBuilder();
    string currentLine = string.Empty;
    SplitInfo lastWord = new SplitInfo { Word = string.Empty, SplitChar = string.Empty };

    using (Graphics gr = Graphics.FromHdc(hdc))
    {
        while (true)
        {
            string newString = (currentLine + lastWord.SplitChar).TrimStart(' ') + words[0].Word;

            if (currentLine != string.Empty && GetWidth(gr, newString, font) > textWidthInLoMetric)
            {
                // Word no longer fits in line.
                resultText.Append(currentLine.TrimEnd() + "\n");
                currentLine = string.Empty;
            }
            else
            {
                lastWord = words[0];
                words.RemoveAt(0);
                currentLine = newString;
            }

            if (words.Count == 0)
            {
                resultText.Append(currentLine.TrimEnd() + "\n");
                break;
            }
        }
    }

    return resultText.ToString().TrimEnd().Split('\n');
}


Performance measurement shows that MeasureString is the main time consumer here (~90%). Is there any clever

Solution

One possibility is to cache the measured length for each word. I do this, when the line breaks need to be recalculated (i.e. when document needs to be reflowed) frequently, for example because the user edits words or the user resizes the width of the control.

A second possibility is to use different APIs, for example:

  • A different StringFormat value passed as a parameter



  • A different method for example TextRenderer.MeasureText instead of Graphics.MeasureString



My code says ...

//http://msdn.microsoft.com/en-us/magazine/cc163630.aspx#S7 i.e. 
        //"Practical Tips For Boosting The Performance Of Windows Forms Apps" in MSDN says,
        //
        //It's better to use the TextRenderer method overloads that do not get IDeviceContext as an argument.
        //These methods are more efficient because they use a cached screen-compatible memory device context
        //rather than retrieving the native handle for device context from the internal DeviceContext and
        //creating an internal object to wrap it.
        //
        //My profiling shows that the method without the Graphics parameter takes 0.025 msec instead of 0.1 msec.
        //However, the resulting calculation is inaccurate.
        Size size = TextRenderer.MeasureText(
            m_graphics,
            text,
            paintStyle.font,
            proposedSize,
            textFormatFlags);


... and ...

static TextFormatFlags textFormatFlags =
        TextFormatFlags.NoPrefix |
        TextFormatFlags.NoPadding |
        TextFormatFlags.ExternalLeading |
        TextFormatFlags.Left |
        TextFormatFlags.Bottom;


This code recalculates dozens of pages in a fraction of a second (and because it caches the measured word sizes it only measures all words once, when the document is first loaded).

The corresponding method which I use for drawing the text is:

TextRenderer.DrawText(
            m_graphics,
            text,
            paintStyle.font,
            drawingPoint,
            color,
            textFormatFlags);

Code Snippets

//http://msdn.microsoft.com/en-us/magazine/cc163630.aspx#S7 i.e. 
        //"Practical Tips For Boosting The Performance Of Windows Forms Apps" in MSDN says,
        //
        //It's better to use the TextRenderer method overloads that do not get IDeviceContext as an argument.
        //These methods are more efficient because they use a cached screen-compatible memory device context
        //rather than retrieving the native handle for device context from the internal DeviceContext and
        //creating an internal object to wrap it.
        //
        //My profiling shows that the method without the Graphics parameter takes 0.025 msec instead of 0.1 msec.
        //However, the resulting calculation is inaccurate.
        Size size = TextRenderer.MeasureText(
            m_graphics,
            text,
            paintStyle.font,
            proposedSize,
            textFormatFlags);
static TextFormatFlags textFormatFlags =
        TextFormatFlags.NoPrefix |
        TextFormatFlags.NoPadding |
        TextFormatFlags.ExternalLeading |
        TextFormatFlags.Left |
        TextFormatFlags.Bottom;
TextRenderer.DrawText(
            m_graphics,
            text,
            paintStyle.font,
            drawingPoint,
            color,
            textFormatFlags);

Context

StackExchange Code Review Q#46718, answer score: 5

Revisions (0)

No revisions yet.