Friday, January 16, 2009

How to transfer rich text from a FlowDocument to a FormattedText object

The FormattedText class provides low-level control for drawing text. It gives you high performance multi-line text rendering in which each character in the text can be individually formatted.

Other aspects of text in WPF handle text in the context of controls that are dedicated to text (TextBlock, TextBox, RichTextBox), implement the FlowDocument model. As far as I can tell, there is no direct way to transfer text from a FlowDocument to a FormattedText object. This post outlines how it can be done with a bit of jiggery poker.

The tricky part of this process is traversing through the runs and paragraphs in the FlowDocument:
private IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
{
    // use the GetNextContextPosition method to iterate through the
    // FlowDocument
    
    for (TextPointer position = doc.ContentStart;
        position != null && position.CompareTo(doc.ContentEnd) <= 0;
        position = position.GetNextContextPosition(LogicalDirection.Forward))
    {
        if (position.GetPointerContext(LogicalDirection.Forward) == 
            TextPointerContext.ElementEnd)
        {
            // return solely the Runs and Paragraphs. all other elements are 
            // ignored since they aren't supported by FormattedText.
            
            Run run = position.Parent as Run;

            if (run != null)
            {
                yield return run;
            }
            else
            {
                Paragraph para = position.Parent as Paragraph;

                if (para != null)
                {
                    yield return para;
                }
            }
        }
    }
}
The rest of process is fairly straightforward. Use the GetRunsAndParagraphs method and build up the formatting on a FormattedText object:
public FormattedText GetFormattedText(FlowDocument doc)
{
    if (doc == null)
    {
        throw new ArgumentNullException("doc");
    }

    FormattedText output = new FormattedText(
        GetText(doc),
        CultureInfo.CurrentCulture,
        doc.FlowDirection,
        new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
        doc.FontSize,
        doc.Foreground);

    int offset = 0;

    foreach (TextElement el in GetRunsAndParagraphs(doc))
    {
        Run run = el as Run;

        if (run != null)
        {
            int count = run.Text.Length;

            output.SetFontFamily(run.FontFamily, offset, count);
            output.SetFontStyle(run.FontStyle, offset, count);
            output.SetFontWeight(run.FontWeight, offset, count);
            output.SetFontSize(run.FontSize, offset, count);
            output.SetForegroundBrush(run.Foreground, offset, count);
            output.SetFontStretch(run.FontStretch, offset, count);
            output.SetTextDecorations(run.TextDecorations, offset, count);

            offset += count;
        }
        else
        {
            offset += Environment.NewLine.Length;
        }
    }

    return output;
}

private string GetText(FlowDocument doc)
{
    StringBuilder sb = new StringBuilder();

    foreach (TextElement el in GetRunsAndParagraphs(doc))
    {
        Run run = el as Run;
        sb.Append(run == null ? Environment.NewLine : run.Text);
    }
    return sb.ToString();
}
You can download the source code and demo here.



2 comments:

David Hollinshead said...

A handy bit of code.

My small suggestion is....

As an alternative to :

private string GetText(FlowDocument doc) {
....
}

you could use :

new TextRange(doc.ContentStart, doc.ContentEnd).Text

regards David

WPF Mentor said...

I found that TextRange performed very poorly when used in this way for large documents.

Sorry, I should have mentioned this in the article.

Thanks
Dan