WPF. Практика трансформаций

Все исходники /  Язык программирования C# /  OS Windows /  Desktop /  WPF программирование / WPF. Практика трансформаций

WPF Transforms

Трансформация одна из главных операций в графике приложений. Разработчики WPF существенно облегчили работу прикладным программистам предложив ряд готовых классов для графических преобразований. В приложениях WPF можно трансформировать всё что угодно. Визуальные элементы, контейнеры с дочерним содержимым, кисти, рисованные объекты и даже текст. Всё можно вращать, изменять в размерах и перемещать в любую точку окна приложения.

Трансформация графики в WPF обеспечивается классами происходящих от абстрактного класса System.Windows.Media.Transform определяющего общую функциональность для преобразований в двумерной плоскости.

Классы для графических трансформаций:
  • RotateTransform - вращение объекта вокруг назначенного центра.
  • ScaleTrasnform - масштабирование объекта. Изменение размеров осуществляется раздельно по осям X и Y.
  • TranslateTransform - перемещение объекта в указанную точку X,Y.
  • SkewTransform - деформирует объект, наклоняя его в ту или иную сторону.
  • MatrixTransform - для создания специфических графических преобразований используя непосредственные матричные вычисления.

Для создания составных трансформаций из вышеописанных классов используется класс TransformGroup.

RenderTransform vs LayoutTransform

RenderTransform vs LayoutTransform

Две одинаковые группы элементов синхронно вращаются пошагово в 30 градусов. Преобразование координат выполняется просто, благодаря готовому классу трансформации вращения RotateTransform. Для сравнения задавать трансформацию будем через разные свойства RenderTransform и LayoutTransform.

Для наглядности работы разных видов графических преобразований вокруг группы элементов создан бордюр. При вращении группы через свойство RenderTransform размер бордюра не изменяется. Контейнер Grid владеющей группой элементов выходит за пределы из ограждения Border, перерасчет компоновки не производится.

Группа элементов, использующее для вращения LayoutTransform изменяет размер бордюра. После каждого цикла такого преобразования происходит перерасчёт компоновки дочерних элементов. Border вынужден изменять свои размеры чтобы полностью вместить трансформированную группу. При поворотах габаритную длину и ширину увеличивают внутренние диагонали прямоугольной группы. При исследовании размеров ActualWidth и ActualHeight внешнего бордюра, они в действительности изменяются после каждого поворота.

Выбор между свойствами

RenderTransform служит только для визуализации преобразования координат элемента, не вмешиваясь в компоновку родительского контейнера. LayoutTransform, напротив, выполняет трансформацию объектов, создаёт сведения об изменённых размерах и заставляет родителя перераспределить расположения всех дочерних элементов.

При выборе вида преобразований между RenderTransform и LayoutTransform следует руководствоваться выгодой производительности. Если нет необходимости изменять компоновку элементов (возможно большого количества), рекомендуется использовать RenderTransform.

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

Псевдокод такого симбиоза:
Grid gridElement = new Grid();
TranslateTransform translate = new TranslateTransform();
RotateTransform rotate = new RotateTransform();

gridElement.RenderTransform = translate;
gridElement.LayoutTransform = rotate;

XAML разметка и программный код

Визуальная часть выполнена в файле XAML. Краткий листинг xaml кода одной группы элементов для исследования трансформации вращения RotateTransform различными графическими преобразованиями RenderTransform и LayoutTransform:
<Border x:Name="borderRender" VerticalAlignment="Center" Margin="20,0,0,0">
    <Border.BorderBrush>
        <SolidColorBrush Color="#FFB4B4B4"/>
    </Border.BorderBrush>
    <Grid Name="gridRenderTransform" RenderTransformOrigin="0.5 0.5">
        <Grid.Background>
            <SolidColorBrush Color="#FFE1E7F5"/>
        </Grid.Background>
        <TextBlock Width="240" HorizontalAlignment="Center" Margin="0,50,0,30">
            <TextBlock.Text>
                Программирование — процесс и искусство...
            </TextBlock.Text>
        </TextBlock>
        <Label Content="RenderTransform" HorizontalAlignment="Center"/>
    </Grid>
</Border>

Логика вращения написана в программном коде отдельным методом.

Листинг метода вращения групп элементов:
double angleUIElement = 30;
private void Grid_MouseDown_UIElement(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    // Для симметричности воздействия на края окна приложения
    // вращение групп происходит навстречу друг другу.
    var rotateRender = new RotateTransform(angleUIElement);
    var rotateLayout = new RotateTransform(angleUIElement * -1);

    // Выбираем разные графические преобразования.
    gridRenderTransform.RenderTransform = rotateRender;
    gridLayoutTransform.LayoutTransform = rotateLayout;

    // При щелчке мыши поворачиваем на 30 градусов.
    angleUIElement += 30;

    // Исследование влияния разных преобразований на 
    // размеры бордюров вокруг групп элементов. 
    /*Title = "layoutah=" + borderLayout.ActualHeight.ToString() + 
            ";layoutaw=" + borderLayout.ActualWidth.ToString();
    Title += ";renderah=" + borderRender.ActualHeight.ToString() + 
            ";renderaw=" + borderRender.ActualWidth.ToString();*/
}

Вращение и масштабирование текста

Вращение и масштабирование текста

Применение одновременно двух трансформаций для текста - вращение и масштабирование. Операции преобразования над текстом производятся с помощью тех же инструментов что и трансформации элементов. Это классы вращения RotateTransform и масштабирования ScaleTransform. Трансформация выполняется в виде красивой анимации.

Текстом владеет элемент TextBlock, стоит подчеркнуть, что в этом примере трансформируется только сам текст. Как птичка из гнезда, вылетает текст из области TextBlock и возвращается вновь. Для элементов TextBlock можно определить текстовые эффекты с помощью его свойства TextEffects. Данное свойство имеет тип TextEffectCollection и позволяет одновременно применять различные эффекты к тексту.

Составная трансформация

Элементам можно присвоить только одну трансформацию. При необходимости нескольких трансформаций одновременно можно создать составную трансформацию. Для этого в распоряжении библиотек WPF имеется класс комбинирования преобразований TransformGroup. Через свойство Children данного класса добавляются необходимые виды трансформаций. В результате TransformGroup создаёт единую MatrixTransform, объединяя все трансформации из коллекции Children.

XAML разметка визуальной части

Визуальная часть текстовых эффектов небольшая и построена в XAML файле.

Краткий листинг данного кода:
<Border BorderThickness="2,2,2,2">
    <Border.BorderBrush>
        <SolidColorBrush Color="#FFB4B4B4"/>
    </Border.BorderBrush>
    <TextBlock x:Name="textForEffect" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock.Background>
            <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlLightColorKey}}"/>
        </TextBlock.Background>
        <TextBlock.Text>
            Трансформация текста
        </TextBlock.Text>
    </TextBlock>
</Border>

TextEffect для управления текстом

Логика составной трансформации определена программным кодом в методе события MouseDown главного контейнера закладки TabControl. Для красивого эффекта необходимо чтобы текст вращался и изменялся в размерах относительно своей середины. Чтобы не измерять длину текста применена простая хитрость - габариты текста определяет TextBlock в режиме автоматической подгонки размера. Вращение и масштабирование текста осуществляется посредством объекта класса TextEffect, который непосредственно воздействует на символы. Не только на весь текст, но и на отдельные символы.

Программный код составной трансформации текста

Листинг метода составной трансформации текста:
private void Grid_MouseDown_TextEffects(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    // Вычисление центра координат текста.
    double centerX = textForEffect.ActualWidth / 2;
    double centerY = textForEffect.ActualHeight / 2;

    var transformGroup = new TransformGroup();

    var rotateTransform = new RotateTransform()
    {
        CenterX = centerX,
        CenterY = centerY
    };
    transformGroup.Children.Add(rotateTransform);

    var scaleTransform = new ScaleTransform()
    {
        CenterX = centerX,
        CenterY = centerY
    };
    transformGroup.Children.Add(scaleTransform);


    var textEffect = new TextEffect
    {
        Transform = transformGroup,
        // Если подставить закомментированные значения
        // будет вращаться только пятый символ.
        PositionCount = textForEffect.Text.Length, // 1
        PositionStart = 0 // 4
    };
    textForEffect.TextEffects.Add(textEffect);


    double time = 2;
    var daRotate = new DoubleAnimation
    {
        From = 0,
        To = 720,
        Duration = TimeSpan.FromSeconds(time)
    };
    rotateTransform.BeginAnimation(RotateTransform.AngleProperty, daRotate);

    double scaleFrom = 1;
    double scaleTo = 4;
    var daScaleX = new DoubleAnimation
    {
        From = scaleFrom,
        To = scaleTo,
        Duration = TimeSpan.FromSeconds(time / 2),
        AutoReverse = true
    };
    scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, daScaleX);

    var daScaleY = new DoubleAnimation
    {
        From = scaleFrom,
        To = scaleTo,
        Duration = TimeSpan.FromSeconds(time / 2),
        AutoReverse = true
    };
    scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, daScaleY);
}

Вращение LinearGradientBrush

Вращение градиентной кисти

Трансформировать можно и кисти для закраски заднего фона элементов. LinearGradientBrush наследует от абстрактного класса Brush два свойства трансформации: Brush.RelativeTransform и Brush.Transform.

К кистям можно применять любую трансформацию. Для наглядности эффектов от воздействия на цвета градиентной кисти взята трансформация вращения. RotateTransform плюс метод BeginAnimation() самой кисти создают завораживающую красотой анимацию.

RelativeTransform vs Transform

У кистей, в сравнении с элементами наследуемые от UIElement, нет свойства RenderTransformOrigin. Центр координат для трансформации вращения, масштабирования, наклона, необходимо определять в классе трансформации свойствами CenterX и CenterY.

При использовании Brush.Transform центр кисти образуется абсолютными размерами. Например: размер кисти 100х200 единиц, тогда CenterX = 50 единиц, CenterY = 100 единиц. Brush.RelativeTransform позволяет воспринимать размеры кисти как относительные - 1х1. Тогда центр координат для RotateTransform в этом случае определяется так CenterX = 0.5, CenterY = 0.5. Но иногда, при трансформации сложных кистей, RelativeTransform может создавать нежелательные артефакты.

XAML разметка и программный код

Листинг xaml кода определения визуальной части трансформации градиентной кисти контейнера Grid:
<Grid MouseDown="Grid_MouseDown_Brush" x:Name="gridBrush">
    <Grid.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0" 
		    MappingMode="RelativeToBoundingBox" SpreadMethod="Reflect" >
            <GradientStop Color="Red"/>
            <GradientStop Color="Blue" Offset="1"/>
            <GradientStop Color="Yellow" Offset="0.4"/>
            <GradientStop Color="Yellow" Offset="0.6"/>
        </LinearGradientBrush>
    </Grid.Background>
</Grid>
Программный код вращения кисти:
private void Grid_MouseDown_Brush(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    var lgbrush = gridBrush.Background as LinearGradientBrush;

    // Определение центра вращения кисти.
    var rotateBrush = new RotateTransform()
    {
        CenterX = 0.5,
        CenterY = 0.5
    };
    lgbrush.RelativeTransform = rotateBrush;

    var daAngle = new DoubleAnimation
    {
        From = 0,
        To = 720,
        Duration = TimeSpan.FromSeconds(3)
    };
    rotateBrush.BeginAnimation(RotateTransform.AngleProperty, daAngle);
}

Трансформация TranslateTransform

Трансформация элемента через TranslateTransform

TranslateTransform такая же частая, в обиходе программистов, операция трансформации, как и RotateTransform. Чтобы переместить элемент указывается конечная точка свойствами типа double X и Y. Элемент мгновенно оказывается в новой позиции. Трансформация почти всегда ногу в ногу с анимацией. В прилагаемом коде перемещение совмещено с анимацией.

Данные новой позиции сохраняются только в объекте класса TranslateTransform. Чтобы отслеживать позицию элемента необходимо создавать объект TranslateTransform глобальным на уровне приложения.

Для перемещения элементов можно использовать только свойство RenderTransform. Если попробовать использовать свойство LayoutTransform то перемещения не будет. Это связано с тем, что макет автоматически корректирует любые линейные смещения позиций своих дочерних элементов. По сравнению с локальными трансформациями вращения и масштабирования, TranslateTransform может переместить элемент даже за пределы видимой зоны окна приложения, а это лишает смысла адаптивную компоновку макета. LayoutTransform просто игнорирует трансформацию TranslateTransform.

XAML код элементов Border

XAML код определения визуальности четырёх элементов Border:
<Grid Background="Transparent" MouseDown="Grid_MouseDown_Translate">
    <Border x:Name="border1" HorizontalAlignment="Left" VerticalAlignment="Top"
    Width="80" Height="80" Background="Red" Margin="10,0,0,0" CornerRadius="10" 
    BorderBrush="#FFB9B8B8" BorderThickness="3"/>

    <Border x:Name="border2" HorizontalAlignment="Left" VerticalAlignment="Top" 
    Width="80" Height="80" Background="Yellow" Margin="100,0,0,0" CornerRadius="10" 
    BorderBrush="#FFB9B8B8" BorderThickness="3"/>

    <Border x:Name="border3" HorizontalAlignment="Left" VerticalAlignment="Top" 
    Width="80" Height="80" Background="DarkGreen" Margin="190,0,0,0" CornerRadius="10" 
    BorderBrush="#FFB9B8B8" BorderThickness="3"/>

    <Border x:Name="border4" HorizontalAlignment="Left" VerticalAlignment="Top" 
    Width="80" Height="80" Background="Blue" Margin="280,0,0,0" CornerRadius="10" 
    BorderBrush="#FFB9B8B8" BorderThickness="3"/>
</Grid>

Программный код для трансформации TranslateTransform

Программный код трансформации перемещения элементов состоит из двух методов. Вспомогательный метод AnimationBorder(...) значительно уменьшает повторяющийся код.

Листинг методов перемещения элементов посредством трансформации TranslateTransform:
private void Grid_MouseDown_Translate(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    double duration = 0.5;
    // Начало движения каждого элемента задерживается на данную величину.
    double begin = 0.3;
    int count = 0;
    AnimationBorder(border1, duration, begin * count++);
    AnimationBorder(border2, duration, begin * count++);
    AnimationBorder(border3, duration, begin * count++);
    AnimationBorder(border4, duration, begin * count++);
}

private void AnimationBorder(Border border, double duration, double begin)
{
    border.RenderTransform = new TranslateTransform();

    var daTranslateX = new DoubleAnimation
    {
        From = 0,
        // По оси X перемещение до середины.
        To = ((Grid)border.Parent).ActualWidth/2 - border.Width/2 - border.Margin.Left,
        Duration = TimeSpan.FromSeconds(duration),
        // Этой строчкой можно разнообразить эффекты перемещения.
        //AutoReverse = true,
        BeginTime = TimeSpan.FromSeconds(begin),
    };
    ((TranslateTransform)border.RenderTransform).
        BeginAnimation(TranslateTransform.XProperty, daTranslateX);

    var daTranslateY = new DoubleAnimation
    {
        From = border.Margin.Top,
        // По оси Y перемещение до нижнего края.
        To = ((Grid)border.Parent).ActualHeight - border.Height,
        Duration = TimeSpan.FromSeconds(duration),
        // Этой строчкой можно разнообразить эффекты перемещения.
        //AutoReverse = true,
        BeginTime = TimeSpan.FromSeconds(begin),
    };
    ((TranslateTransform)border.RenderTransform).
        BeginAnimation(TranslateTransform.YProperty, daTranslateY);
}

Трансформация графики класса Path

Вращение, перемещение и масштабирование Path Geometry

Класс Path создаёт графику из последовательности цветных линий и геометрических фигур. Свойство LayoutTransform класс Path наследует от FrameworkElement, свойство RenderTransform досталось ему от UIElement. Имея данные свойства графические объекты Path полноценно поддерживают все виды 2D трансформаций.

Для трансформации геометрических фигур создадим составную группу из трех видов: вращения, масштабирования и перемещения. Комбинированную матрицу всех трансформаций, экземпляр класса TransformGroup, присвоим свойству RenderTransform экземпляров Path. Поскольку здесь есть объект TranslateTransform свойство LayoutTransform использовать нельзя. Для визуальной выразительности преобразований трансформацию оформим в виде анимации.

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

Программные коды трансформации геометрических фигур

XAML код графики геометрических фигур класса Path:
<Grid MouseDown="Grid_MouseDown_Path" Background="Transparent">
    <Path x:Name="pathRed" Fill="Red" HorizontalAlignment="Left" 
	Margin="0,0,0,0" VerticalAlignment="Center">
        <Path.Data>
            <GeometryGroup FillRule="EvenOdd">
                <RectangleGeometry Rect="0,0,60,60"></RectangleGeometry>
                <EllipseGeometry Center="30,30" RadiusX="20" RadiusY="20"/>
            </GeometryGroup>
        </Path.Data>
    </Path>

    <Path x:Name="pathBlue" Fill="Blue" HorizontalAlignment="Left" 
	Margin="20,0,0,0" VerticalAlignment="Center">
        <Path.Data>
            <GeometryGroup>
                <RectangleGeometry Rect="0,0,20,20"/>
            </GeometryGroup>
        </Path.Data>
    </Path>
</Grid>

Программный код комплексной трансформации экземпляров Path состоит из двух методов для уменьшения строк кода.

Листинг программного кода:
private void Grid_MouseDown_Path(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    double time = 3;
    double rotatefrom = 0;
    // 3 оборота вперед + 3 назад.
    double rotateto = 1080;

    double scalefrom = 1;
    double scaleto = 3;

    // Начало перемещения - это текущая позиция элемента. 
    double translatefrom = pathRed.Margin.Left;

    // Конечная позиция правый край родителя. (Равен краю окна приложения)
    double translateto = ((Grid)pathRed.Parent).ActualWidth - pathRed.ActualWidth;
  
    // Прямоугольники вращаются навстречу друг другу.
    RotatePath(pathRed, translatefrom, translateto, rotatefrom, 
        rotateto, scalefrom, scaleto, time);
    RotatePath(pathBlue, translatefrom, translateto, rotatefrom, 
        rotateto * -1, scalefrom, scaleto, time);
}


private void RotatePath(Path path, double translatefrom, double translateto, 
double rotatefrom, double rotateto, double scalefrom, double scaleto, double time)
{
    var daScaleX = new DoubleAnimation
    {
        From = scalefrom,
        To = scaleto,
        Duration = TimeSpan.FromSeconds(time),
        AutoReverse = true
    };

    var daScaleY = new DoubleAnimation
    {
        From = scalefrom,
        To = scaleto,
        Duration = TimeSpan.FromSeconds(time),
        AutoReverse = true
    };

    var daRotate = new DoubleAnimation
    {
        From = rotatefrom,
        To = rotateto,
        Duration = TimeSpan.FromSeconds(time),
        AutoReverse = true
    };

    var daTranslateX = new DoubleAnimation
    {
        From = translatefrom,
        To = translateto,
        Duration = TimeSpan.FromSeconds(time),
        AutoReverse = true
    };


    path.RenderTransformOrigin = new Point(0.5, 0.5);
    var rotate = new RotateTransform();
    var scale = new ScaleTransform();
    var translate = new TranslateTransform();
    var tg = new TransformGroup();
    tg.Children.Add(scale);
    tg.Children.Add(rotate);
    tg.Children.Add(translate);
    // Присваиваем итог нескольких трансформаций.
    path.RenderTransform = tg;

    rotate.BeginAnimation(RotateTransform.AngleProperty, daRotate);
    scale.BeginAnimation(ScaleTransform.ScaleXProperty, daScaleY);
    scale.BeginAnimation(ScaleTransform.ScaleYProperty, daScaleX);
    translate.BeginAnimation(TranslateTransform.XProperty, daTranslateX);
}

Исходник трансформаций элементов WPF

В подкрепление к вышенаписанному прилагается исходный код различных видов трансформаций - вращения, масштабирование и перемещения. Исходник написан в среде MS Visual 2019. Для запуска приложения требуется установка .NET Core 3.1.

Скачать исходник