Frédéric Mélantois
WPF : Les transformations et les calculs matriciels
Le framework WPF permet d’effectuer un grand nombre de transformations sur des éléments héritant de la classe UIElement. Nous allons détailler les calculs matriciels.
Par Frédéric Mélantois publié le 16/11/2008 à 22:30, lu 3413 fois, 5 pages
 4 | Tentative d’amélioration de la performance
Nous allons tenter d’améliorer la performance et « encapsuler » l’animation de rotation de l’objet sur lui-même tout en effectuant une translation d’un point à un autre. L’idée est de fournir à la propriété « RenderTransform » de l’objet, en particulier Line, une seule transformation au lieu de la « TransformGroup » que nous avons pu utiliser au premier chapitre de cet article. La transformation la plus adéquate est la « MatrixTransform » puisqu’elle prend en charge une unique matrice. Ainsi la matrice théorique que nous avons calculée au deuxième chapitre pourrait l’alimenter.
Toutefois, nous ne faisons pas qu’une transformation, nous gérons aussi une animation. Il serait sans doute judicieux de regrouper la transformation et l’animation afin d’encapsuler toute l’animation de l’objet (Line). L’idée est donc de reconstruire tout d’abord une animation. Pour cela, rien n’est plus simple que d’hériter de la classe abstraite « AnimationTimeLine » de l’espace de nom « System.Windows.Media.Animation » :

public class RotateMatrixAnimation : AnimationTimeline

{

    public RotateMatrixAnimation() : base()

    {

    }

 

    public override Type TargetPropertyType

    {

        get { throw new NotImplementedException(); }

    }

 

    protected override Freezable CreateInstanceCore()

    {

        throw new NotImplementedException();

    }

}

Le type de la propriété cible « TargetPropertyType » devra être une « MatrixTransform » pour pouvoir alimenter la propriété « RenderTransform » de notre objet :

public override Type TargetPropertyType

{

    get { return typeof(MatrixTransform); }

}

Pour la méthode « CreateInstanceCore » , nous renvoyons une nouvelle instance :

protected override Freezable CreateInstanceCore()

{

    return new RotateMatrixAnimation();

}

Nous allons surcharger la méthode « GetCurrentValue » afin que celle-ci renvoie une transformation de type MatrixTransform :

public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue,

AnimationClock animationClock)

{

    return base.GetCurrentValue(defaultOriginValue, defaultDestinationValue, animationClock);

}

Cette méthode permet de renvoyer une valeur suivant un élément de départ et un élément de destination et/ou suivant le temps écoulé. Comme dans l’animation du premier chapitre de cet article, nous voulions faire évoluer notre objet (Line) d’un point à un autre suivant un angle de rotation de celui-ci sur lui-même, nous allons nous servir de cette méthode pour produire notre animation :

public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue,

AnimationClock animationClock)

{

    // en dur pour l'instant pour tester

    double fromVal = 0;

    double toVal = 2* Math.PI;

    Point pointTo = new Point(700,200);

    Point pointFrom = new Point(450,450);

 

    //Prise en compte de la progression du temps pour l'angle et le point central de l'objet

    //CurrentProgress.Value entre 0 et 1

    double angle = fromVal + (toVal - fromVal) * animationClock.CurrentProgress.Value;

    Point point = pointFrom + (pointTo - pointFrom) * animationClock.CurrentProgress.Value;

 

    double s = Math.Sin(angle);

    double c = Math.Cos(angle);

 

    //matrice théorique d'une rotation d'un objet autour d'un point, cet objet subissant une translation

    MatrixTransform m = new MatrixTransform(new Matrix(c, s, -s, c, (-pointFrom.X * c) + (pointFrom.Y * s) + point.X,

        (-pointFrom.Y * c) - (pointFrom.X * s) + point.Y));

    //Renvoie la matrice de transformation

    return m;

}

Jusqu’à présent, nous avons écrit très peu de code pour notre animation, devant gérer une rotation de l’objet sur lui-même du point (450, 450) au point (700,200). Nous pouvons mettre en œuvre notre animation dans le canvas :

Line l2 = new Line();

l2.Stroke = new SolidColorBrush(Colors.Red);

l2.StrokeThickness = 2;

l2.X1 = 300;

l2.X2 = 600;

l2.Y1 = 300;

l2.Y2 = 600;

canvas1.Children.Add(l2);

 

RotateMatrixAnimation r = new RotateMatrixAnimation();

r.Duration = new TimeSpan(0, 0, 20);

l2.BeginAnimation(Line.RenderTransformProperty, r);

Vous pouvez constater qu’il faut beaucoup moins de lignes de code que dans le chapitre 1 où nous gérions une « TransformGroup ». Il nous reste à rendre plus paramétrable notre animation et nous aurons économisé du temps de calcul. Nous aurons aussi permis d’avoir une syntaxe encore plus simplifiée pour mettre en œuvre notre animation d’objet.
Nous allons profiter de la particularité des éléments de WPF héritant de DependencyObject qui permet d’utiliser les Dependency Property. Leur mise en œuvre est relativement simple:

//Point de départ

public static readonly DependencyProperty PointFromProperty;

public Point PointFrom

{

    get

    {

        return (Point)GetValue(RotateMatrixAnimation.PointFromProperty);

    }

    set

    {

        SetValue(RotateMatrixAnimation.PointFromProperty, value);

    }

}

 

//Point de destination

public static readonly DependencyProperty PointToProperty;

public Point PointTo

{

    get

    {

        return (Point)GetValue(RotateMatrixAnimation.PointToProperty);

    }

    set

    {

        SetValue(RotateMatrixAnimation.PointToProperty, value);

    }

}

 

//Angle de départ

public static readonly DependencyProperty FromProperty;

public double From

{

    get

    {

        return (double)GetValue(RotateMatrixAnimation.FromProperty);

    }

    set

    {

        SetValue(RotateMatrixAnimation.FromProperty, value);

    }

}

 

//Angle d'arrivée

public static readonly DependencyProperty ToProperty;

public double To

{

    get

    {

        return (double)GetValue(RotateMatrixAnimation.ToProperty);

    }

    set

    {

        SetValue(RotateMatrixAnimation.ToProperty, value);

    }

}

Il faut toutefois enregistrer ces propriétés afin qu’elles puissent être utilisées :

//Enregistrement des propriétés

static RotateMatrixAnimation()

{

    FromProperty = DependencyProperty.Register("From", typeof(double),

        typeof(RotateMatrixAnimation));

    ToProperty = DependencyProperty.Register("To", typeof(double),

        typeof(RotateMatrixAnimation));

    PointToProperty = DependencyProperty.Register("PointTo", typeof(Point),

        typeof(RotateMatrixAnimation));

    PointFromProperty = DependencyProperty.Register("PointFrom", typeof(Point),

        typeof(RotateMatrixAnimation));

}

Nous n’avons plus qu’à modifier nos paramètres codés en dur dans la méthode « GetCurrentValue » :

double fromVal = (double)GetValue(RotateMatrixAnimation.FromProperty);

    double toVal = (double)GetValue(RotateMatrixAnimation.ToProperty);

    Point pointTo = (Point)GetValue(RotateMatrixAnimation.PointToProperty);

    Point pointFrom = (Point)GetValue(RotateMatrixAnimation.PointFromProperty);

Testons notre animation cette fois-ci en Xaml :

< Window x : Class ="WindowsFormsHostWPF.MatriceTransformation"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:dc="clr-namespace:WindowsFormsHostWPF"

   Height="900" Width="900">

    < Window.Resources >

        < Storyboard x : Key ="storyBoard1">

            < dc : RotateMatrixAnimation Duration ="0:0:20" PointFrom ="450,450" PointTo ="700,200" From ="0" To ="6.2831"

                                    Storyboard.TargetName="rotatetransform1"

                                    Storyboard.TargetProperty="RenderTransform"></dc:RotateMatrixAnimation>

        </ Storyboard >

    </ Window.Resources >

    < Window.Triggers >

        < EventTrigger RoutedEvent ="FrameworkElement.Loaded">

            < BeginStoryboard Storyboard ="{ StaticResource storyBoard1 }"/>

        </ EventTrigger >

    </ Window.Triggers >

    < Canvas Name ="canvas1">

        < Line X1 ="300" Y1 ="300" X2 ="600" Y2 ="600" Stroke ="Blue"

            StrokeThickness="2" x:Name="rotatetransform1">

        </ Line >

    </ Canvas >

</ Window >

Vous pouvez comparer avec le code du premier chapitre. Vous pourrez noter que nous réalisons plusieurs économies : nous n’utilisons qu’une seule transformation (nous évitons donc une multiplication de matrice pour chaque unité de temps de l’animation, ce qui n’est pas rien), nous nous passons aussi de l’instanciation d’une collection de transformations (TransformGroup) et de deux animations (DoubleAnimation).
Si l’objet que vous souhaitez animer ne nécessite pas d’avoir la richesse offerte par tout élément WPF héritant de « FrameworkElement », votre objet pourra alors hériter directement de « UIElement ». Il faut savoir que « FrameworkElement » offre des possibilités supplémentaires par rapport à UIElement, ce qui a un coût à prendre en compte surtout si vous n’en avez pas besoin :
  • Système de placement de l’objet (docking etc)
  • Système d’arborescence logique (notification de changement aux enfants etc)
  • Prise en charge d’événements tels que Initialized ou loaded
  • DataBinding
  • Utilisation de ressources dynamiques
  • Prise en charge des styles
Voici un exemple de création d’un élément « allégé » héritant de « UIElement ». Le code est en dur, vous pourrez aisément rendre paramétrable cet objet :

public class MyLine : UIElement

{

    public MyLine()

        : base()

    {

    }

 

    protected override void OnRender(DrawingContext drawingContext)

    {

        base.OnRender(drawingContext);

        drawingContext.DrawLine(new Pen(new SolidColorBrush(Colors.Coral), 2),

            new Point(300, 300), new Point(600, 600));

        RotateMatrixAnimation r = new RotateMatrixAnimation();

        r.From = 0;

        r.To = 2 * Math.PI;

        r.PointTo = new Point(700, 200);

        r.PointFrom = new Point(450, 450);

        r.Duration = new TimeSpan(0, 0, 20);

        this.BeginAnimation(UIElement.RenderTransformProperty, r);

    }

}

Dans ce code, nous agissons sur le rendu de l’objet grâce au « DrawingContext » qui permet de dessiner. On comprend bien que ce genre d’objets allégés est utile quand on fait de l’animation simple. Si nous devions mettre en place un scénario de poissons dans un aquarium, nous utiliserions sans doute cette méthode.
 
» Démarrer une discussion