Исходные коды программ и игр

Программирование - работа и хобби

Анимация графики WPF. Прозрачность, цвет и градиент

Язык программирования C#

Анимация графики WPF

Новая платформа программирования Windows Presentation Foundation (WPF) позволяет создавать гораздо более привлекательные приложения по сравнению с разработкой окон на базе её старшей сестры Windows Forms. DirectX в основе обработки графики обеспечивает оживлённый интерфейс без замедления богатых графических эффектов. WPF имеет богатейший набор средств для украшения визуальных объектов приложений.

Анимация - вершина любых украшений, позволяет изготавливать приложения с визуальной реакцией на действия пользователя. Например, при наведении курсора на текст плавно увеличивается шрифт, при покидании размер шрифта восстанавливается. В WPF анимация является если не главной, то одной из самых важных частей. Созданию и управлению анимацией отведено отдельное пространство имён System.Windows.Media.Animation с множеством классов. На базе этих классов можно создавать анимацию любых видимых элементов.

Только базовых классов анимации насчитываются более 20, которые анимируют значения от простейших типов Boolean до сложных Matrix, Quaternion, Rotation3D, Vector3D. Мощная поддержка анимации позволяет создавать эффекты только путем создания объектов и настроек их свойств. Это избавляет программистов от трудоёмкой низкоуровневой работы.

Линейная интерполяция

Анимация – это последовательность визуальных событий, изменяющихся в какую-либо сторону с установленным шагом и продолжительностью. В данной статье речь будет идти об анимации, использующей линейную интерполяцию. Представители данного типа анимаций – классы DoubleAnimation, ColorAnimation, ThicknessAnimation, PointAnimation, RectAnimation и другие. Классы происходят от базового TimeLine, определяющий отрезок времени (временную шкалу, временную линию).

В WPF нет событий OnPaint() для перерисовки недействительной части окна. Более совершенная графическая модель отслеживает изменения визуальных элементов и мгновенно перерисовывает элемент, часть или полностью окно. Анимация в WPF – это плавное или дискретное изменение свойств зависимостей (свойств, изменения которых отслеживает система WPF). Все классы линейной интерполяции имеют интуитивно понятный набор параметров: начальное значение From, конечное To и продолжительность Duration. Для более сложных анимаций предусмотрены и другие свойства: By, EasingFunction, IsAdditive, IsCumulative.

От теории к практике

Исходник, подготовленный для этой статьи, демонстрирует общие принципы создания и настройки анимационных свойств. Для создания анимаций используются методы элементов UIElement.BeginAnimation, классы DoubleAnimation, ColorAnimation, PointAnimation, ThicknessAnimation. Анимация применяется к свойствам прозрачности, цвета, градиентной окраски и толщины бордюра элементов. Все действующие модели анимаций компактно оформлены в одном приложении с помощью элемента TabControl.

Каждая демонстрация анимации инкапсулирована в отдельном классе. Общие члены классов объявлены в абстрактном классе, который и наследуют демонстрационные. Из названий классов интуитивно понятно на какие свойства воздействует анимационное преобразование:
  • CommonMembers
  • OpacityAnimation
  • GradientAnimation
  • ColorBrushAnimation
  • BorderAnimation

Opacity Animation – анимация прозрачности

WPF, Анимация прозрачностиСамый простой способ анимировать свойства объекта — это использование метода элемента BeginAnimation(OpacityProperty op, DoubleAnimation da). Создаётся шкала времени (DoubleAnimation) и указывается на какое свойство зависимости она будет воздействовать (OpacityProperty). Вызов этого метода немедленно запускает анимацию. Данный способ применения анимации экономичен при создании разовой, недлительной анимации, например изменение цвета элемента при наведении и покидании курсора, увеличение шрифта при выделении пункта и т.п. Остановить такую анимацию можно повторным вызовом метода с значением временной шкалы null: BeginAnimation(op, null).

Класс OpacityAnimation использует анимационные возможности кнопки методом, доставшегося ей от базового UIElement. Получившаяся анимация похожа на игру. На поле находится кнопка, при нажатии на неё она плавно увеличивает свою прозрачность, до полного исчезновения. В прозрачном, невидимом состоянии кнопка скачкообразно перемещается в случайное место на поле и далее восстанавливает свою видимость. При следующем нажатии цикл повторяется.

Роль игрового поля играет панель Canvas. Данный вид контейнера позволяет размещать дочерние элементы используя абсолютную систему координат. По завершении анимации прозрачности кнопки генерируется событие Complected, в котором далее вызываются методы перемещения и отложенной анимации "всплытия" кнопки.

Листинг класса OpacityAnimation:
class OpacityAnimation : CommonMembers
{
    // Элемент, прозрачность которого будет изменяться.
    private FrameworkElement element;
    // Получаем ссылку на панель размещения элемента.
    public OpacityAnimation(Canvas parent) : base(parent) { }

    // Метод анимации исчезновения кнопки.
    private void Vanish()
    {
        double time = 0.5;

        var opacity = new DoubleAnimation
        {
            From = 1.0,
            To = 0.0,
            Duration = new Duration(TimeSpan.FromSeconds(time))
        };

        opacity.Completed += Opacity_Completed;

        element.BeginAnimation(UIElement.OpacityProperty, opacity);
    }

    // После исчезновения перемещаем кнопку,
    // затем показываем.
    private void Opacity_Completed(object sender, EventArgs e)
    {
        Moving();
        Emersion();
    }

    // Начало анимационных действий.
    public void Start(FrameworkElement element)
    {   
        // Получаем ссылку на элемент и сохраняем её.
        this.element = element;

        Vanish();
    }

    // Случайное перемещение по полю родителя.
    readonly Random random = new Random();
    void Moving()
    {
        double x = random.Next(0, (int)(parent.Width - element.ActualWidth));
        double y = random.Next(0, (int)(parent.Height - element.ActualHeight));

        Canvas.SetLeft(element, x);
        Canvas.SetTop(element, y);
    }


    // Всплытие из глубины окна приложения.
    // Выполняется после установленной задержки.
    void Emersion()
    {
        double time = 0.3;
        double delay = 1;

        var opacity = new DoubleAnimation
        {
            From = 0.0,
            To = 1.0,
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            BeginTime = TimeSpan.FromSeconds(delay)
        };

        element.BeginAnimation(Button.OpacityProperty, opacity);
    }
}

ColorBrush Animation – анимация цвета

WPF, Анимация цветаСледующий класс приложения ColorBrushAnimation. Класс выполняет анимацию цвета 4-х прямоугольников. Назначенные цвета интерполируя переходят от одной фигуры к другой. Запускается анимация щелчком мыши по вкладке TabControl.

Управление цветом осуществляет объект класса ColorAnimation, специально созданным для анимации цвета. Свойства класса From и To имеют тип System.Windows.Media.Color. Каждому прямоугольнику для закрашивания назначается сплошная цветная кисть SolidColorBrush. Для управления свойством ColorProperty, каждой кисти назначается имя. Класс SolidColorBrush не имеет в своём составе свойства Name, поэтому уникальное имя регистрируется вспомогательным методом RegisterName(имя, кисть) в пределах области окна приложения.

Для каждого прямоугольника используется анимация из 4-х цветов. Управление 16-ю временными линиями ColorAnimation доверено раскадровке Storyboard. С целью унификации независимую анимацию для каждого прямоугольника создаёт единственный метод. Если не предпринять меры, то при щелчках мыши в раскадровку будут добавляться каждый раз по 16 элементов. Для мониторинга заполнения раскадровки, объектам ColorAnimation присваиваются уникальные имена. Перед вставкой проверяется наличие в раскадровке элементов с идентичными параметрами, если выявлен повтор, вставка не происходит.

Листинг класса анимации цвета:
class ColorBrushAnimation : CommonMembers
{

    public ColorBrushAnimation(FrameworkElement parent) : base(parent) { }

    public void ColorRectangleAnimation(Shape shape, Color init, Color to1, Color to2, Color to3)
    {
        var colorInit = init;
        double time = 3;

        SolidColorBrush scb = (SolidColorBrush)shape.Fill;

        // Если объект замороженный, то его нельзя модифицировать.
        // Сплошная кисть назначенная в XAML заморожена.
        // Поэтому необходимо создать кисть заново,
        // или использовать её клон.
        // Примечательно, что градиентная кисть в подобном случае
        // не замораживается.
        if (shape.Fill.IsFrozen == true)
        {
            // Создаём новую кисть для управления цветом фигуры.
            // или клон который можно модифицировать.
            //SolidColorBrush scb = new SolidColorBrush();
            scb = ((SolidColorBrush)shape.Fill).Clone();

            shape.Fill = scb;
        }

        // Теперь можно управлять цветной кистью фигуры.
        shape.Fill = scb;

        // Объекту анимации создаём уникальное имя
        // и регистрируем его в области 
        string regname = "anim_" + shape.Name;
        parent.RegisterName(regname, scb);


        var color1 = new ColorAnimation
        {
            From = colorInit,
            To = to1,
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            // Уникальное имя временной шкалы.
            Name = "color1_" + shape.Name
        };

        // Контроль исключения повторного добавления
        // color1 с идентичным именем.
        if (storyboard.Children.FirstOrDefault(e => e.Name == color1.Name) == null)
        {
            Storyboard.SetTargetName(color1, regname);
            Storyboard.SetTargetProperty(color1, new PropertyPath(SolidColorBrush.ColorProperty));
            storyboard.Children.Add(color1);
        }

        var color2 = new ColorAnimation
        {
            From = to1,
            To = to2,
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            BeginTime = TimeSpan.FromSeconds(time),
            Name = "color2_" + shape.Name
        };

        if (storyboard.Children.FirstOrDefault(e => e.Name == color2.Name) == null)
        {
            Storyboard.SetTargetName(color2, regname);
            Storyboard.SetTargetProperty(color2, new PropertyPath(SolidColorBrush.ColorProperty));
            storyboard.Children.Add(color2);
        }

        var color3 = new ColorAnimation
        {
            From = to2,
            To = to3,
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            BeginTime = TimeSpan.FromSeconds(time * 2),
            Name = "color3_" + shape.Name
        };

        if (storyboard.Children.FirstOrDefault(e => e.Name == color3.Name) == null)
        {
            Storyboard.SetTargetName(color3, regname);
            Storyboard.SetTargetProperty(color3, new PropertyPath(SolidColorBrush.ColorProperty));
            storyboard.Children.Add(color3);
        }

        var color4 = new ColorAnimation
        {
            From = to3,
            To = colorInit,
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            BeginTime = TimeSpan.FromSeconds(time * 3),
            Name = "color4_" + shape.Name
        };

        if (storyboard.Children.FirstOrDefault(e => e.Name == color4.Name) == null)
        {
            Storyboard.SetTargetName(color4, regname);
            Storyboard.SetTargetProperty(color4, new PropertyPath(SolidColorBrush.ColorProperty));
            storyboard.Children.Add(color4);
        }

        // Запуск анимации. 
        storyboard.Begin(parent);
    }
}

Gradient Animation – анимация градиента

WPF, Анимация градиентаКласс GradientAnimation управляет градиентной анимацией двух прямоугольников. Два цвета, две точки градиентного раздела. Синхронно, циклично границы между цветами одного и другого прямоугольника плавно скользят в горизонтальной плоскости. В классе GradientAnimation демонстрируется анимация для типа System.Windows.Point. Используемая временная линия так и называется PointAnimation. В качестве объектов анимации участвуют градиентные кисти LinearGradientBrush. Изменение значений свойств зависимостей startPoint и endPoint создаёт эффектную анимацию градиентов на поверхностях прямоугольников.

Метод элементов BeginAnimation(…) обеспечивает полноценную анимацию, но малоуправляемую. Кроме того, она применима только для набора свойств зависимостей одного элемента. В классе GradientAnimation, в отличие от других классов анимации приложения, используется управляемая анимация одновременно двух элементов. Её можно приостановить, продолжить, остановить и начать заново. Эффективно управлять анимацией, изменять её режимы работы, обеспечивать синхронное изменение свойств группы элементов под силу только контейнеру для временных шкал Storyboard.

Листинг класса GradientAnimation:
class GradientAnimation : CommonMembers
{
    public GradientAnimation(FrameworkElement parent) : base(parent) { }

    public void Start(Shape shape)
    {
        LinearGradientBrush brush = (LinearGradientBrush)shape.Fill;

        // Если объект замороженный, то его нельзя модифицировать.
        if (shape.Fill.IsFrozen == true)
        {
            // Создаём клон для модифицирования
            brush = ((LinearGradientBrush)shape.Fill).Clone();

            shape.Fill = brush;
        }


        // Подбираем уникальное имя для элемента brush.
        // Уникальность достигается благодаря уже регистрированному имени
        // плюс префикс.
        // Разные элементы не могут иметь идентичные имена.
        string regname = "gradientbrush_" + shape.Name;
        parent.RegisterName(regname, brush);

        double time = 4;

        var startPoint = new PointAnimation
        {
            From = new Point(-2, 0),
            To = new Point(1, 0),
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            Name = "startpoint_" + shape.Name
        };

        var endPoint = new PointAnimation
        {
            From = new Point(0, 0),
            To = new Point(2, 0),
            Duration = new Duration(TimeSpan.FromSeconds(time)),
            Name = "endpoint_" + shape.Name
        };

        // Исключаем раздувание раскадровки идентичными временными линейками.
        if (storyboard.Children.FirstOrDefault(e => e.Name == startPoint.Name)  == null)
        {
            Storyboard.SetTargetName(startPoint, regname);
            Storyboard.SetTargetProperty(startPoint, 
                new PropertyPath(LinearGradientBrush.StartPointProperty));
            storyboard.Children.Add(startPoint);
        }

        if (storyboard.Children.FirstOrDefault(e => e.Name == endPoint.Name) == null)
        {
            Storyboard.SetTargetName(endPoint, regname);
            Storyboard.SetTargetProperty(endPoint, 
                new PropertyPath(LinearGradientBrush.EndPointProperty));
            storyboard.Children.Add(endPoint);
        }

        // Запускаем бесконечную, реверсивную анимацию.
        storyboard.RepeatBehavior = RepeatBehavior.Forever;
        storyboard.AutoReverse = true;
        // Анимация будет управляемой.
        storyboard.Begin(parent, true /*isControllable*/);
    }

    public void Stop()
    {
        storyboard.Stop(parent);
    }
    
    // Один метод для паузы и продолжения анимации.
    public void PauseResume()
    {
        if (storyboard.GetIsPaused(parent) == false) storyboard.Pause(parent);
        else storyboard.Resume(parent);
    }
}

Border Animation – анимация границы элемента

WPF, Анимация толщины рамки вокруг элементаЕщё одна анимация приложения использует временную шкалу для изменения толщины декоративного элемента Border. Толщина рамки элемента задаётся структурой Thickness. В WPF и для этого случая есть класс анимации ThicknessAnimation (временная шкала). Задавая различные сочетания значений From и To можно анимировать границы по отдельности или плавно изменять толщину у всех одновременно.

Запускается анимация щелчком по прямоугольнику, вокруг которого обернут бордюр. Толщина рамки плавно увеличивается по часовой стрелке и затем, в этом же направлении, уменьшается до первоначального размера. По завершению анимационного круга вызывается событие Completed и раскадровка очищается от исполненных временных шкал.

Листинг класса BorderAnimation:
class BorderAnimation : CommonMembers
{
    public BorderAnimation(FrameworkElement parent) : base(parent) 
    {
        storyboard.Completed += Storyboard_Completed;
    }

    private void Storyboard_Completed(object sender, EventArgs e)
    {
        // Очищаем раскадровку по завершении анимации.
        storyboard.Children.Clear();
    }

    public void BeginAnimation(Border border, bool reverse)
    {
        double speed = 0.4;
        double bt = 30;

        // Задержка запусков отдельных линий времени анимации.
        double delay = 1.5;

        // Появление от первой
        var tnLeft = new ThicknessAnimation
        {
            From = new Thickness(0, 0, 0, 0),
            To = new Thickness(bt, 0, 0, 0),
            Duration = new Duration(TimeSpan.FromSeconds(speed))

        };
        Storyboard.SetTargetName(tnLeft, border.Name);
        Storyboard.SetTargetProperty(tnLeft, 
            new PropertyPath(Border.BorderThicknessProperty));
        storyboard.Children.Add(tnLeft);


        var tnTop = new ThicknessAnimation
        {
            From = new Thickness(bt, 0, 0, 0),
            To = new Thickness(bt, bt, 0, 0),
            Duration = new Duration(TimeSpan.FromSeconds(speed)),
            BeginTime = TimeSpan.FromSeconds(speed)
        };
        Storyboard.SetTargetName(tnTop, border.Name);
        Storyboard.SetTargetProperty(tnTop, 
            new PropertyPath(Border.BorderThicknessProperty));
        storyboard.Children.Add(tnTop);


        var tnRight = new ThicknessAnimation
        {
            From = new Thickness(bt, bt, 0, 0),
            To = new Thickness(bt, bt, bt, 0),
            Duration = new Duration(TimeSpan.FromSeconds(speed)),
            BeginTime = TimeSpan.FromSeconds(speed * 2)

        };
        Storyboard.SetTargetName(tnRight, border.Name);
        Storyboard.SetTargetProperty(tnRight, 
            new PropertyPath(Border.BorderThicknessProperty));
        storyboard.Children.Add(tnRight);


        var tnBottom = new ThicknessAnimation
        {
            From = new Thickness(bt, bt, bt, 0),
            To = new Thickness(bt, bt, bt, bt),
            Duration = new Duration(TimeSpan.FromSeconds(speed)),
            BeginTime = TimeSpan.FromSeconds(speed * 3)

        };
        Storyboard.SetTargetName(tnBottom, border.Name);
        Storyboard.SetTargetProperty(tnBottom, 
            new PropertyPath(Border.BorderThicknessProperty));
        storyboard.Children.Add(tnBottom);



        // Второй круг - исчезновения бордюра.
        // reverse определяет вид исчезновения: по кругу или обратно по кругу
        if (reverse == false)
        {
            // Исчезновение начиная с первой.
            tnLeft = new ThicknessAnimation
            {
                From = new Thickness(bt, bt, bt, bt),
                To = new Thickness(0, bt, bt, bt),
                Duration = new Duration(TimeSpan.FromSeconds(speed)),
                BeginTime = TimeSpan.FromSeconds(speed * 4 * delay)

            };
            Storyboard.SetTargetName(tnLeft, border.Name);
            Storyboard.SetTargetProperty(tnLeft, 
                 new PropertyPath(Border.BorderThicknessProperty));
            storyboard.Children.Add(tnLeft);


            tnTop = new ThicknessAnimation
            {
                From = new Thickness(0, bt, bt, bt),
                To = new Thickness(0, 0, bt, bt),
                Duration = new Duration(TimeSpan.FromSeconds(speed)),
                BeginTime = TimeSpan.FromSeconds(speed * 5 * delay)

            };
            Storyboard.SetTargetName(tnTop, border.Name);
            Storyboard.SetTargetProperty(tnTop, 
                new PropertyPath(Border.BorderThicknessProperty));
            storyboard.Children.Add(tnTop);


            tnRight = new ThicknessAnimation
            {
                From = new Thickness(0, 0, bt, bt),
                To = new Thickness(0, 0, 0, bt),
                Duration = new Duration(TimeSpan.FromSeconds(speed)),
                BeginTime = TimeSpan.FromSeconds(speed * 6 * delay)

            };
            Storyboard.SetTargetName(tnRight, border.Name);
            Storyboard.SetTargetProperty(tnRight, 
                new PropertyPath(Border.BorderThicknessProperty));
            storyboard.Children.Add(tnRight);


            tnBottom = new ThicknessAnimation
            {
                From = new Thickness(0, 0, 0, bt),
                To = new Thickness(0, 0, 0, 0),
                Duration = new Duration(TimeSpan.FromSeconds(speed)),
                BeginTime = TimeSpan.FromSeconds(speed * 7 * delay)

            };
            Storyboard.SetTargetName(tnBottom, border.Name);
            Storyboard.SetTargetProperty(tnBottom, 
                new PropertyPath(Border.BorderThicknessProperty));
            storyboard.Children.Add(tnBottom);
        }
        else
        {
            storyboard.AutoReverse = true;
        }

        storyboard.Begin(parent);
    }
}

Приложение анимации прозрачности, цвета и градиента

Приложение создано в среде программирование MS Visual Studio 2019 v.16.8. Требование NET Core 3.1
Файл: WpfAnimation-vs16.zip
Размер: 127 Кбайт
Загрузки: 8