WPF TextFormatter中第二行的缩进

goo*_*dev 9 .net c# vb.net wpf

我正在使用TextFormatter制作WPF文本编辑器.我需要在每个段落中缩进第二行.

第二行中的缩进宽度应该类似于第一行上第一个单词的宽度,包括第一个单词后面的空格.像这样的东西:

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
Run Code Online (Sandbox Code Playgroud)

第二件事:段落的最后一行应该在中心.

如何实现这一目标?

提前致谢!!

Sim*_*ier 6

这远非易事.我建议你使用WPF的高级文本格式.

有一个官方(相对较差,但它是唯一的)样本:TextFormatting.

所以,我创建了一个带有文本框的小样本应用程序和一个特殊的自定义控件,可以按照您想要的方式同时呈现文本框中的文本(好吧,差不多,请看结尾处的注释).

<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>
Run Code Online (Sandbox Code Playgroud)

我选择编写一个自定义控件,但你也可以构建一个几何体(比如官方的'TextFormatting'样本).

[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();
}
Run Code Online (Sandbox Code Playgroud)

这是结果:

在此输入图像描述

我没做过的事情:

  • 这不支持编辑,它不是文本框.对于如此小的赏金来说,这是太多的工作:-)
  • 支持多个段落.我刚刚缩小了样本中的第二行.您需要解析文本以提取"段落",无论您认为是什么.
  • 应添加DPI感知支持(适用于.NET Framework 4.6.2或更高版本).这是在'TextFormatting'示例中完成的,您基本上需要随身携带PixelsPerDip值.
  • 在某些边缘情况下会发生什么(只有两条线等)
  • 在自定义控件上公开常用属性(FontFamily等)