Страницы

Поиск по вопросам

среда, 20 февраля 2019 г.

Каким образом можно реализовать бегущую подсветку (выделение) многострочного текста?

Здесь уже был вопрос про закраску текста и на него был дан ответ
Вкратце суть решения состоит в создании FormattedText из содержимого скрытого TextBlock, а затем создается геометрия из FormattedText, которая отображается с помощью Path, который в свою очередь закрашивается с помощью анимации на нем.
И все прекрасно работает пока текст занимает одну строку, а вот когда строк несколько, получается не то, что хотелось бы.

XAML я повторять не буду, т.к. он полностью совпадает с таковым в выше указанном ответе. Мой код тоже мало чем отличается, но я его приведу для ясности вопроса.
private List _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]; }
Вот метод, заполняет черным отрисованный текст
///

/// Закрашивание рисованного текста /// /// начальная позиция слова /// число закрашиваемых букв в слове 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(); }


Ответ

Вы пошли правильным путём, разбить на отдельные Path'ы — хорошая идея.
Давайте её реализуем до конца.
Для начала, функциональность проигрывания уже достаточно сложна, так что вынесем её в отдельный UserControl. Затем, каждый контрол пусть отвечает за одну строку. Чтобы не искать куски по геометрии, просто отрежем эту строку при помощи Clip'а. На вход в UserControl будем подавать результаты разбора текста на геометрию (функцией Create).

В code-behind будет анимация:
public partial class SingleLine : UserControl { List boundingBoxes; double extent;
public SingleLine() { InitializeComponent(); }
public SingleLine(Geometry geo, List 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); // дождёмся конца анимации } } }
Зачем нам нужна такая сложность с Clip и totalExtent? К сожалению, я не нашёл метода выкусить только нужную часть геометрии. Поэтому мы даём на вход всю геометрию, а хотим показывать только текущую строку. Для этого мы вычисляем прямоугольник, соответствующий нужной части геометрии (текущей строке), и отрезаем отображение остального при помощи Clip'а. Но наши вычисления коэффициентов (b.Left / extent и т. д.) требуют процента от общей ширины Path'а, а не ширины текущей строки! (Напомню, наш Path получает геометрию всей строки, включая остальные строки тоже.) Поэтому приходится передавать ещё и общую ширину.
Теперь основной код. Он стал проще, так как часть функциональности отделилась. В нём мы не можем положить один, фиксированный Path, так как у нас количество строк не известно заранее. Поэтому контролы будем добавлять динамически.
Главное окно выглядит просто:

И code-behind:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += (o, args) => Create(); // вначале запустим Create PreviewKeyDown += (o, args) => Play(); // а по нажатию клавиши - Play }
// список контролов, отображающих строки List lineControls = new List();
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> boundingBoxesByLine = new List>(); List currentLine = null; double lastRectBottom = double.NegativeInfinity; foreach (var rect in boundingBoxes) { // проверка на новую строку. если верх текущего прямоугольника там же, // где низ предыдущего прямоугольника, или ещё ниже - новая строка, иначе нет if (rect.Top >= lastRectBottom) { // добавим старую строку в список строк if (currentLine != null) boundingBoxesByLine.Add(currentLine); // новый пустой контейнер прямоугольников для новой строки currentLine = new List(); } 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(); } }
Всё!

Результат:

Комментариев нет:

Отправить комментарий