I'm making a WPF text-editor using TextFormatter. I need to indent the second line in each paragraph.
The indentation width in the second line should be like the width of the first word on the first line, including the white space after the first word. Something like that:
Indent of second line in Indentation Inde
second line in Indentation Indenta
of second line in Indentation of second l
ine in Indentation of second line in Inde
ntation of second line in
Second thing: The last line in the paragraph should be in the center.
how to make this happen?
Thanks in advance!!
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"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
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}" />
</Border>
</StackPanel>
</Window>
I have chosen to write a custom control, but you could also build a geometry (like in the official 'TextFormatting' sample).
[ContentProperty(nameof(Text))]
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());
_lines.Clear();
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;
_lines.Add(line);
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);
}
else
{
_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
{
get
{
if (Text == null)
return null;
int pos = Text.IndexOf(' ');
if (pos < 0)
return Text;
while (pos < Text.Length && Text[pos] == ' ')
{
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,
index,
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:
Things I have not done:
PixelsPerDip
value all around.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With