There was already a question about filling the text and it was answered.

In short, the essence of the solution is to create a FormattedText from the contents of a hidden TextBlock , and then create a geometry from FormattedText , which is displayed using the Path , which in turn is filled with animation on it.

And everything works fine as long as the text takes one line, but when there are several lines, it turns out not what we would like.

An example of incorrect text filling

XAML I will not repeat, because it fully coincides with that in the above answer. My code is also not much different, but I will give it for clarity.

 private List<Rect> _RectsForFill; // ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠΈ с ΠΊΠ°ΠΆΠ΄Ρ‹ΠΌ символом private double _LengthFillText; // общая ΡˆΠΈΡ€ΠΈΠ½Π° рисованного тСкста //анимация Storyboard _Storyboard; DoubleAnimation _FromAnimation; DoubleAnimation _ToAnimation; private void CreateFillText() { //ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ тСкстовоС содСрТимоС TextBlock tb = this.textBlockHidden; var text = tb.Text; //создаСм экзСмпляр Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ тСкста FormattedText formattedText = new FormattedText( text, CultureInfo.GetCultureInfo("en-US"), FlowDirection.LeftToRight, new Typeface( tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch), tb.FontSize, Brushes.Black // конкрСтная ΠΊΠΈΡΡ‚ΡŒ Π½Π°ΠΌ Π½Π΅ Π²Π°ΠΆΠ½Π°, ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ); //Π±Π΅Ρ€Π΅ΠΌ пСрСносы строк Ρƒ эталонного тСкстблока formattedText.MaxTextWidth = this.textBlockHidden.Width; formattedText.MaxTextHeight = this.textBlockHidden.Height; // стащили Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ Ρƒ тСкста... var geo = formattedText.BuildGeometry(new Point()); // ...ΠΈ ΠΎΡ‚Π΄Π°Π»ΠΈ Π΅Ρ‘ Path'Ρƒ Target.Data = geo; //вычислим ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠΈ для заполнСния GetRectsForFill(text, formattedText); } private void GetRectsForFill(string text, FormattedText formattedText) { var bb = formattedText.BuildHighlightGeometry(new Point()); _LengthFillText = bb.Bounds.Width; // общая ΡˆΠΈΡ€ΠΈΠ½Π° //заполняСм ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΡŽ ΠΏΠΎΠ±ΡƒΠΊΠ²Π΅Π½Π½Ρ‹Ρ… боксов _RectsForFill = Enumerable.Range(0, text.Length) .Select(k => formattedText.BuildHighlightGeometry(new Point(), k, 1) .Bounds) .ToList(); //ссылки Π½Π° Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΡŽ для дальнСйшСй Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Π½Π΅ΠΉ _Storyboard = (Storyboard)Target.Resources["AnimationStoryboard"]; _FromAnimation = (DoubleAnimation)_Storyboard.Children[0]; _ToAnimation = (DoubleAnimation)_Storyboard.Children[1]; } 

Here is the method that fills the black text with black.

 /// <summary> /// Π—Π°ΠΊΡ€Π°ΡˆΠΈΠ²Π°Π½ΠΈΠ΅ рисованного тСкста /// </summary> /// <param name="startPos">Π½Π°Ρ‡Π°Π»ΡŒΠ½Π°Ρ позиция слова</param> /// <param name="count">число Π·Π°ΠΊΡ€Π°ΡˆΠΈΠ²Π°Π΅ΠΌΡ‹Ρ… Π±ΡƒΠΊΠ² Π² словС</param> public void FillTextPath(int startPos, int count) { if (count == 0) throw new ArgumentException(nameof(count)); //вычисляСм индСкс Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎΠ³ΠΎ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠ° int index = startPos + count; if (index >= _RectsForFill.Count) index = _RectsForFill.Count - 1; //Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹ΠΉ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊ Rect box = _RectsForFill[index]; //Π·Π°ΠΊΡ€Π°ΡˆΠΈΠ²Π°Π΅ΠΌ _FromAnimation.From = box.Left / _LengthFillText; _FromAnimation.To = box.Right / _LengthFillText; _ToAnimation.From = box.Left / _LengthFillText; _ToAnimation.To = box.Right / _LengthFillText; _Storyboard.Begin(); } 
  • Now that's the solution for the case of multi-line text. - VladD
  • @VladD, very much looking forward :) I already thought to make through creating separate Path for each necessary line, but I haven’t yet decided how to determine when the text in TextBlock got a transfer to another line. - Bulson

1 answer 1

You went the right way, break into separate Path 's - a good idea.

Let's implement it to the end.

For a start, the playback functionality is already quite complex, so we move it into a separate UserControl . Then, let each control be responsible for one line. In order not to look for pieces by geometry, simply cut this line with Clip 'a. At the entrance to the UserControl we will submit the results of the analysis of the text on the geometry (the Create function).

 <UserControl x:Class="KaraokeText.SingleLine" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Path Name="Target" Stroke="Black" StrokeThickness="0.5"> <Path.Fill> <LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5"> <GradientStop Color="Black" Offset="0" x:Name="TargetFrom"/> <GradientStop Color="White" Offset="0" x:Name="TargetTo" /> </LinearGradientBrush> </Path.Fill> <Path.Resources> <Storyboard x:Key="AnimationStoryboard"> <DoubleAnimation Duration="00:00:00.25" Storyboard.TargetName="TargetFrom" Storyboard.TargetProperty="Offset"> <DoubleAnimation.EasingFunction> <CubicEase EasingMode="EaseIn"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> <DoubleAnimation Duration="00:00:00.25" Storyboard.TargetName="TargetTo" Storyboard.TargetProperty="Offset"> <DoubleAnimation.EasingFunction> <CubicEase EasingMode="EaseOut"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </Path.Resources> </Path> </UserControl> 

In the code-behind will be an animation:

 public partial class SingleLine : UserControl { List<Rect> boundingBoxes; double extent; public SingleLine() { InitializeComponent(); } public SingleLine(Geometry geo, List<Rect> boundingBoxes, double totalExtent) : this() { Target.Data = geo; extent = totalExtent; Rect clip = boundingBoxes.Aggregate(Rect.Union); Clip = new RectangleGeometry(clip); this.boundingBoxes = boundingBoxes; } public async Task Play() { var storyboard = (Storyboard)Target.Resources["AnimationStoryboard"]; var fromAnimation = (DoubleAnimation)storyboard.Children[0]; var toAnimation = (DoubleAnimation)storyboard.Children[1]; foreach (var b in boundingBoxes) { await Task.Delay(250); // ΠΏΠ΅Ρ€Π΅Ρ€Ρ‹Π² ΠΌΠ΅ΠΆΠ΄Ρƒ Π±ΡƒΠΊΠ²Π°ΠΌΠΈ fromAnimation.From = b.Left / extent; fromAnimation.To = b.Right / extent; toAnimation.From = b.Left / extent; toAnimation.To = b.Right / extent; storyboard.Begin(); await Task.Delay(250); // доТдёмся ΠΊΠΎΠ½Ρ†Π° Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΠΈ } } } 

Why do we need such complexity with Clip and totalExtent ? Unfortunately, I did not find a method to bite out only the necessary part of the geometry. Therefore, we give the entire geometry to the input, and we want to show only the current line. To do this, we calculate the rectangle corresponding to the desired part of the geometry (the current line), and cut off the rest of the display using Clip 'a. But our calculations of the coefficients ( b.Left / extent , etc.) require a percentage of the total width of the Path ', and not the width of the current line! (Let me remind you that our Path receives the geometry of the entire line, including the other lines as well.) Therefore, it is also necessary to transmit the total width.

Now the main code. It has become easier as part of the functionality has separated. In it, we can not put a single, fixed Path , as we have the number of lines is not known in advance. Therefore, the controls will be added dynamically.

The main window looks simple:

 <Window x:Class="KaraokeText.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ВСст" Height="350" Width="525"> <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Name="Container"> <TextBlock Name="Source" FontSize="24" Visibility="Hidden" TextWrapping="Wrap" Text="А-Π°, Π² АфрикС Ρ€Π΅ΠΊΠΈ Π²ΠΎΡ‚ Ρ‚Π°ΠΊΠΎΠΉ ΡˆΠΈΡ€ΠΈΠ½Ρ‹ &#x000d;А-Π°, Π² АфрикС Π³ΠΎΡ€Ρ‹ Π²ΠΎΡ‚ Ρ‚Π°ΠΊΠΎΠΉ Π²Ρ‹ΡˆΠΈΠ½Ρ‹"/> </Grid> </Window> 

And code-behind:

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += (o, args) => Create(); // Π²Π½Π°Ρ‡Π°Π»Π΅ запустим Create PreviewKeyDown += (o, args) => Play(); // Π° ΠΏΠΎ Π½Π°ΠΆΠ°Ρ‚ΠΈΡŽ клавиши - Play } // список ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΎΠ², ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°ΡŽΡ‰ΠΈΡ… строки List<SingleLine> lineControls = new List<SingleLine>(); void Create() // https://msdn.microsoft.com/en-us/library/ms745816(v=vs.110).aspx { TextBlock tb = Source; var text = tb.Text; FormattedText formattedText = new FormattedText( text, CultureInfo.GetCultureInfo("en-US"), FlowDirection.LeftToRight, new Typeface( tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch), tb.FontSize, Brushes.Black); // установили ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½ΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ тСкст Π±Ρ‹Π» Ρ€Π°Π·Π±ΠΈΡ‚ Π½Π° части formattedText.MaxTextWidth = Source.ActualWidth; var boundingBoxes = // побуквСнная ΡˆΠΈΡ€ΠΈΠ½Π° ΠΈ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ Enumerable.Range(0, text.Length) .Where(k => !char.IsWhiteSpace(text[k])) .Select(k => formattedText.BuildHighlightGeometry(new Point(), k, 1) .Bounds) .ToList(); // вычисляСм ΠΎΡ…Π²Π°Ρ‚Ρ‹Π²Π°ΡŽΡ‰ΠΈΠΉ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊ всСх ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠΎΠ² var totalBb = boundingBoxes.Aggregate(Rect.Union); var totalExtent = totalBb.Width; List<List<Rect>> boundingBoxesByLine = new List<List<Rect>>(); List<Rect> currentLine = null; double lastRectBottom = double.NegativeInfinity; foreach (var rect in boundingBoxes) { // ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π° Π½ΠΎΠ²ΡƒΡŽ строку. Ссли Π²Π΅Ρ€Ρ… Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠ° Ρ‚Π°ΠΌ ΠΆΠ΅, // Π³Π΄Π΅ Π½ΠΈΠ· ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π΅Π³ΠΎ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠ°, ΠΈΠ»ΠΈ Π΅Ρ‰Ρ‘ Π½ΠΈΠΆΠ΅ - новая строка, ΠΈΠ½Π°Ρ‡Π΅ Π½Π΅Ρ‚ if (rect.Top >= lastRectBottom) { // Π΄ΠΎΠ±Π°Π²ΠΈΠΌ ΡΡ‚Π°Ρ€ΡƒΡŽ строку Π² список строк if (currentLine != null) boundingBoxesByLine.Add(currentLine); // Π½ΠΎΠ²Ρ‹ΠΉ пустой ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½ΠΈΠΊΠΎΠ² для Π½ΠΎΠ²ΠΎΠΉ строки currentLine = new List<Rect>(); } currentLine.Add(rect); lastRectBottom = rect.Bottom; } if (currentLine != null) // послСднюю строку Π½Π΅ тСряСм boundingBoxesByLine.Add(currentLine); // стащили Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ Ρƒ тСкста... var geo = formattedText.BuildGeometry(new Point()); // строим ΠΏΠΎ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Ρƒ для ΠΊΠ°ΠΆΠ΄ΠΎΠΉ строки: foreach (var line in boundingBoxesByLine) { // ... отдавая Π΅ΠΌΡƒ Π³Π΅ΠΎΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ: var lineControl = new SingleLine(geo, line, totalExtent); Container.Children.Add(lineControl); lineControls.Add(lineControl); } } async void Play() { // ΠΏΡ€ΠΎΠΈΠ³Ρ€Ρ‹Π²Π°Π΅ΠΌ просто построчно foreach (var line in lineControls) await line.Play(); } } 

Everything!


Result:

simple anima named Natasha

  • Thanks, it is necessary to sleep :) Tomorrow morning I will try to repeat. - Bulson
  • one
    @Bulson: Please! I hope everything grows together. - VladD
  • For me, the riddle algorithm for splitting lines starting with the line var totalBb = boundingBoxes.Aggregate(Rect.Union); and ending with if (currentLine != null) . I understand correctly that you first calculate the total width of the combined rectangles derived from letters? But after all, whitespace rectangles were thrown higher, so the total width obtained would be less real? Yes, and in the cycle, the principle of separation according to Top and Bottom is not at all clear to me. You could add explanatory comments to the code in response. Thanks again. - Bulson
  • And it is also not clear, we transfer to the SingleLine(geo, line, totalExtent) constructor SingleLine(geo, line, totalExtent) as the total width of the totalExtent , although this is not the width of this particular line, but the total width of all the lines, how so? - Bulson
  • Sorry, and here in the SingleLine constructor here is this line Clip = new RectangleGeometry(clip); whole puzzle Why do you need this pruning contour? Thanks if you help me to understand :) - Bulson