See the question and my original answer on StackOverflow

This is far from being easy. I suggest you use WPF's Advanced Text Formatting.

There is an offical (relatively poor, but it's the only one) sample: TextFormatting.

So, I have created a small sample app with a textbox and a special custom control that renders the text from the textbox simultaneously, the way you want (well, almost, see remarks at the end).

<Window x:Class="WpfApp3.MainWindow"
        Title="MainWindow" Height="550" Width="725">
    <StackPanel Margin="10">
        <TextBox  Name="TbSource" AcceptsReturn="True" TextWrapping="Wrap" BorderThickness="1"
                 VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"></TextBox>
        <Border BorderThickness="1" BorderBrush="#ABADB3" Margin="0" Padding="0">
            <local:MyTextControl Margin="5" Text="{Binding ElementName=TbSource, Path=Text}" />

I have chosen to write a custom control, but you could also build a geometry (like in the official 'TextFormatting' sample).

public class MyTextControl : FrameworkElement
    // I have only declared Text as a dependency property, but fonts, etc should be here
    public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyTextControl),
        new FrameworkPropertyMetadata(string.Empty,
            FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure));

    private List<TextLine> _lines = new List<TextLine>();
    private TextFormatter _formatter = TextFormatter.Create();

    public string Text { get => ((string)GetValue(TextProperty)); set { SetValue(TextProperty, value); } }

    protected override Size MeasureOverride(Size availableSize)
        // dispose old stuff
        _lines.ForEach(l => l.Dispose());

        double height = 0;
        double width = 0;
        var ts = new MyTextSource(Text);
        var index = 0;
        double maxWidth = availableSize.Width;
        if (double.IsInfinity(maxWidth))
            // it means width was not fixed by any constraint above this.
            // we pick an arbitrary value, we could use visual parent, etc.
            maxWidth = 100;

        double firstWordWidth = 0; // will be computed with the 1st line

        while (index < Text.Length)
            // we indent the second line
            var props = new MyTextParagraphProperties(new MyTextRunProperties(), _lines.Count == 1 ? firstWordWidth : 0);
            var line = _formatter.FormatLine(ts, index, maxWidth, props, null);
            if (_lines.Count == 0)
                // get first word and whitespace real width (so we can support justification / whitespaces widening, kerning)
                firstWordWidth = line.GetDistanceFromCharacterHit(new CharacterHit(ts.FirstWordAndSpaces.Length, 0));

            index += line.Length;

            height += line.TextHeight;
            width = Math.Max(width, line.WidthIncludingTrailingWhitespace);
        return new Size(width, height);

    protected override void OnRender(DrawingContext dc)
        double height = 0;
        for (int i = 0; i < _lines.Count; i++)
            if (i == _lines.Count - 1)
                // last line centered (using pixels, not characters)
                _lines[i].Draw(dc, new Point((RenderSize.Width - _lines[i].Width) / 2, height), InvertAxes.None);
                _lines[i].Draw(dc, new Point(0, height), InvertAxes.None);
            height += _lines[i].TextHeight;

// this is a simple text source, it just gives back one set of characters for the whole string
public class MyTextSource : TextSource
    public MyTextSource(string text)
        Text = text;

    public string Text { get; }

    public string FirstWordAndSpaces
            if (Text == null)
                return null;

            int pos = Text.IndexOf(' ');
            if (pos < 0)
                return Text;

            while (pos < Text.Length && Text[pos] == ' ')

            return Text.Substring(0, pos);

    public override TextRun GetTextRun(int index)
        if (Text == null || index >= Text.Length)
            return new TextEndOfParagraph(1);

        return new TextCharacters(
           Text.Length - index,
           new MyTextRunProperties());

    public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int indexLimit) => throw new NotImplementedException();
    public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(int index) => throw new NotImplementedException();

public class MyTextParagraphProperties : TextParagraphProperties
    public MyTextParagraphProperties(TextRunProperties defaultTextRunProperties, double indent)
        DefaultTextRunProperties = defaultTextRunProperties;
        Indent = indent;

    // TODO: some of these should be DependencyProperties on the control
    public override FlowDirection FlowDirection => FlowDirection.LeftToRight;
    public override TextAlignment TextAlignment => TextAlignment.Justify;
    public override double LineHeight => 0;
    public override bool FirstLineInParagraph => true;
    public override TextRunProperties DefaultTextRunProperties { get; }
    public override TextWrapping TextWrapping => TextWrapping.Wrap;
    public override TextMarkerProperties TextMarkerProperties => null;
    public override double Indent { get; }

public class MyTextRunProperties : TextRunProperties
    // TODO: some of these should be DependencyProperties on the control
    public override Typeface Typeface => new Typeface("Segoe UI");
    public override double FontRenderingEmSize => 20;
    public override Brush ForegroundBrush => Brushes.Black;
    public override Brush BackgroundBrush => Brushes.White;
    public override double FontHintingEmSize => FontRenderingEmSize;
    public override TextDecorationCollection TextDecorations => new TextDecorationCollection();
    public override CultureInfo CultureInfo => CultureInfo.CurrentCulture;
    public override TextEffectCollection TextEffects => new TextEffectCollection();

This is the result:

enter image description here

Things I have not done:

  • This does not support edit, it's not a textbox. This is too much work for such a small bounty :-)
  • Support multiple paragraphs. I've just indented the second line in my sample. You would need to parse the text to extract "paragraphs" whatever you think that is.
  • DPI awareness support should be added (for .NET Framework 4.6.2 or above). This is done in the 'TextFormatting' sample, you basically need to carry the PixelsPerDip value all around.
  • What happens in some edge cases (only two lines, etc.)
  • Expose usual properties (FontFamily, etc...) on the custom control