Страницы

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

вторник, 24 декабря 2019 г.

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

#c_sharp #wpf #path


Здесь уже был вопрос про закраску текста и на него был дан  ответ.

Вкратце суть решения состоит в создании 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();
} 

    


Ответы

Ответ 1



Вы пошли правильным путём, разбить на отдельные 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(); } } Всё! Результат:

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

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