Indentation of second line in WPF TextFormatter

前端 未结 1 1323
無奈伤痛
無奈伤痛 2021-01-02 08:10

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 w

相关标签:
1条回答
  • 2021-01-02 08:41

    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:

    • 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
    0 讨论(0)
提交回复
热议问题