Vous êtes sur la page 1sur 306

Programmation C#

La
Programmation
Graphique 2D
de WPF 4
Avec l’environnement
de développement
.NET Framework 4 et
Visual Studio 2010
de MICROSOFT
La
programmation
graphique 2D
de
WPF 4

Patrice REY
Toutes les marques citées ont été déposées par leurs propriétaires respectifs.

La loi du 11 Mars 1957 n’autorisant aux termes des alinéas 2 et 3 de l’article 41,
d’une part, que les «copies ou reproductions strictement réservées à l’usage privé
du copiste et non destinées à une utilisation collective», et, d’autre part, que les
analyses et les courtes citations dans un but d’exemple et d’illustration, «toute
représentation ou reproduction intégrale, ou partielle, faite sans le consentement
de l’auteur ou de ses ayants droit ou ayant cause, est illicite» (alinéa 1er de l’article
40).
Cette représentation ou reproduction, par quelque procédé que ce soit,
constituerait donc une contre-façon sanctionnée par les articles 425 et suivants du
Code Pénal.

Titres déjà parus


Initiation au jeu 2D avec Silverlight 4
(ISBN : 978-2-8106-1168-3)
Les structures de données illustrées avec WPF et C#4
(ISBN : 978-2-8106-1387-8)

Éditeur
Books on Demand GmbH,
12/14 rond point des Champs Élysées,
75008 Paris, France
Impression :
Books on Demand GmbH, Norderstedt, Allemagne
ISBN : 978-2-8106-1199-7
Dépôt légal : Juin 2011

Auteur
Patrice REY
85 rue de vincennes
App 23 B
33000 BORDEAUX

e-mail : reypatrice@orange.fr
Table des matières

Avant-propos .......................................................................................... 7

Chapitre 1 - Les bases du graphisme 2D


1. Le système de coordonnées 2D ..................................................... 13
1.1 - Les coordonnées par défaut ..................................................... 13
1.2 - Des coordonnées personnalisées ............................................. 16
1.3 - Zoomer une figure .................................................................... 19
1.4 - Histogramme de données ......................................................... 22
2. Les formes géométriques basiques 2D ............................................ 27
2.1 - La classe Line ............................................................................. 28
2.2 - Les classes Rectangle et Ellipse ................................................. 31
2.3 - La classe Polyline ...................................................................... 33
2.4 - La classe Polygon ...................................................................... 36

Chapitre 2 - Les transformations 2D


1. Mathématiques des matrices et des transformations ..................... 39
1.1 - Les points et les vecteurs ......................................................... 39
1.2 - Mise à l’échelle ........................................................................... 40
1.3 - Rotation ..................................................................................... 42
1.4 - Translation ................................................................................. 43
2. Les coordonnées homogènes ............................................................ 44
2.1 - La translation en coordonnées homogènes .............................. 45
2.2 - La mise à l’échelle en coordonnées homogènes ...................... 46
2.3 - La rotation en coordonnées homogènes .................................. 46
2.4 - Des transformations successives .............................................. 47
4 Table des matières

3. Les vecteurs et les matrices dans WPF ............................................... 49


3.1 - La structure Vector .................................................................... 49
3.2 - La structure Matrix ................................................................... 51
3.3 - Les opérations sur les matrices ................................................ 53
3.4 - Les transformations sur les matrices ........................................ 57
3.5 - Dessiner une ligne perpendiculaire .......................................... 62
4. Les types de transformations 2D ..................................................... 66
4.1 - La classe MatrixTransform ......................................................... 68
4.2 - La classe ScaleTransform ............................................................ 71
4.3 - La classe TranslateTransform ..................................................... 74
4.4 - La classe RotateTransform ......................................................... 76
4.5 - La classe SkewTransform ........................................................... 78
4.6 - La classe TransformGroup ......................................................... 81

Chapitre 3 - Les géométries et les tracés 2D


1. Utilisation des classes Path et Geometry .......................................... 85
1.1 - Les géométries de type ligne, rectangle et ellipse ..................... 86
1.2 - La classe GeometryGroup ......................................................... 88
1.3 - La classe CombinedGeometry .................................................. 90
1.4 - La classe PathGeometry ............................................................ 93
1.5 - La syntaxe xaml pour les tracés ................................................ 100
2. Des figures 2D interactives ................................................................ 102
2.1 - Dessiner des formes basiques ................................................... 102
2.2 - Déplacer une forme ................................................................... 108
2.3 - Combiner deux formes ............................................................. 113
2.4 - Supprimer une forme ................................................................ 116
2.5 - Le test d’atteinte ........................................................................ 118
3. Des figures 2D personnalisées .......................................................... 122
3.1 - Une étoile personnalisée ............................................................ 123
3.2 - Une flèche personnalisée ............................................................ 128
Table des matières 5

Chapitre 4 - Les couleurs et les pinceaux


1. Les couleurs ....................................................................................... 135
1.1 - La représentation des couleurs ................................................. 136
1.2 - Un sélecteur de couleurs ........................................................... 139
2. Les pinceaux ...................................................................................... 145
2.1 - Le pinceau uni SolidColorBrush ................................................. 146
2.2 - Les pinceaux dégradés ................................................................ 149
2.3 - Le pinceau image ........................................................................ 154
2.4 - Le pinceau VisualBrush .............................................................. 157
2.5 - Le pinceau DrawingBrush ......................................................... 162
3. Les effets graphiques ......................................................................... 165
3.1 - L’effet DropShadowEffect ......................................................... 167
3.2 - L’effet BlurEffect ........................................................................ 168
4. Les transformations du pinceau ........................................................ 169

Chapitre 5 - Les animations


1. Principes des animations .................................................................. 173
1.1 - Les types d’animation ................................................................ 173
1.2 - Les animations basiques ............................................................ 174
2. Les classes relatives à l’animation ..................................................... 176
2.1 - Les classes Storyboard et EventTrigger ....................................... 177
2.2 - La classe Timeline ....................................................................... 178
2.3 - Mise en application .................................................................... 181
3. L’animation des transformations ....................................................... 189
3.1 - Animer une translation ............................................................. 189
3.2 - Animer une rotation ................................................................. 192
3.3 - Animer des transformations combinées .................................. 194
4. L’animation sur tracé ......................................................................... 197
5. Les animations d’images clés ............................................................. 205
5.1 - Avec interpolation linéaire ......................................................... 206
6 Table des matières

5.2 - Avec interpolation discrète ........................................................ 209


5.3 - Avec interpolation Spline .......................................................... 212
5.4 - Avec interpolation Easing ......................................................... 217
6. Les animations réalistes ..................................................................... 219
6.1 - La modélisation BounceEase ................................................... 224
6.2 - La modélisation BackEase ....................................................... 225
6.3 - La modélisation CircleEase ..................................................... 227
6.4 - La modélisation CubicEase ..................................................... 228
6.5 - La modélisation ElasticEase .................................................... 230
6.6 - La modélisation ExponentialEase ........................................... 231
6.7 - La modélisation PowerEase .................................................... 233
6.8 - La modélisation QuadraticEase .............................................. 234
6.9 - La modélisation QuarticEase .................................................. 235
6.10 - La modélisation QuinticEase .................................................. 237
6.11 - La modélisation SineEase ....................................................... 238
6.12 - Réaliser une interpolation personnalisée ............................... 239
7. Le rendu image par image ................................................................. 241

Chapitre 6 - Les styles


1. Principe .............................................................................................. 247
1.1 - Définition de valeur de propriétés .............................................. 247
1.2 - Définition d’un style .................................................................. 248
1.3 - Utilisation d’un style .................................................................. 248
1.4 - Le style implicite ....................................................................... 249
1.5 - L’héritage de style ...................................................................... 250
1.6 - Les déclencheurs ....................................................................... 251
2. Mise en application des styles ............................................................ 253

Chapitre 7 - Les modèles de contrôle


1. Principe des modèles .......................................................................... 263
2. Afficher le contenu d’un ControlTemplate ....................................... 264
Table des matières 7

3. Personnalisation d’un ControlTemplate ........................................... 266


4. La gestion des états visuels ................................................................. 270

Annexes
1. Liste des UserControl réalisés ........................................................... 275
2. Vue sur l’organisation du programme ............................................ 277

Index ....................................................................................................... 285


Avant-propos

WPF (Windows Presentation Foundation) est une technologie de développement


d’application riche (Rich Desktop Application) pour la plateforme Windows. La
version actuelle de WPF est la version 4, celle qui correspond à .NET 4.0, qui
est sortie en avril 2010. Depuis 2006, date de la première sortie de WPF, cette
technologie est devenue une technologie majeure du développement pour
Windows en code managé.
Les innovations de WPF, à la fois sur le plan architectural et sur le plan
fonctionnel, sont très importantes dans sa version actuelle. WPF constitue
une synthèse et une unification des différentes branches de l’informatique
graphique (graphismes vectoriels 2D et 3D, saisie, animation, typographie et
multimédia).
A noter que WPF ne cible pas la réalisation des jeux vidéo évolués puisque
Microsoft fournit des technologies évoluées et adaptées qui sont XNA en
code managé et DirectX en code natif.
WPF est un framework managé qui exploite la puissance de Direct3D pour
l’affichage. Cette technologie de développement d’interface utilisateur permet
de moderniser et unifier les techniques de développement visuel, d’exploiter
la puissance graphique des machines modernes, d’intégrer l’infographie dans
le processus de développement, et de simplifier et sécuriser le déploiement
des applications.
En s’appuyant sur Direct3D, WPF exploite toute la puissance du GPU (Graphic
Processing Unit ou le processeur graphique), ce qui permet de libérer le CPU
(Central Processing Unit) et d’envisager des applications graphiques de plus en
plus riches.
WPF sépare dans des couches distinctes la programmation des fonctionnalités
(au moyen d’un langage .NET tel que le C#) de la présentation visuelle par
l’utilisation du langage XAML (eXtensible Application Markup Language).

Les logiciels requis pour le développement

Pour réaliser plus facilement des programmes en C#, il est préférable d’utiliser
un environnement de développement intégré ou IDE (Integrated Development
Environment). Cet IDE permet de développer, compiler et exécuter un
10 Avant-propos
programme dans un langage donné qui sera, dans ce livre, le C# (se prononce
C Sharp). Microsoft propose comme IDE soit Visual Studio 2010 (dans sa
version payante avec 30 jours d’essai gratuit), soit Visual C# 2010 Express
(dans sa version allégée mais gratuite).
Puissante interface IDE garantissant la production d’un code de qualité, Visual
Studio 2010 intègre de nouvelles fonctionnalités qui simplifient le processus
de développement d’application, de la conception au déploiement.
Pour télécharger et installer l’environnement de développement intégré,
ouvrez votre navigateur et allez sur les pages web suivantes au choix:
• pour télécharger Visual C# 2010 Express (figure A1): http://msdn.microsoft.
com/fr-fr/express/aa975050.aspx
• pour télécharger une version d’évaluation Visual Studio 2010 Ultimate
avec une période d’essai gratuite de 30 jours (figure A2): http://www.
microsoft.com/france/visualstudio/download

Figure A1

Figure A2
Avant-propos 11

Le contenu du livre

Les différents chapitres permettent d’apprendre et de mettre en pratique les


notions et concepts essentiels de la programmation graphique 2D de WPF. Les
chapitres abordent les points suivants:
• le chapitre 1 permet d’acquérir les bases du graphisme 2D avec notamment
le fonctionnement des systèmes de coordonnées, et la réalisation des
figures géométriques 2D basiques.
• le chapitre 2 permet d’apprendre les transformations 2D; un rappel est fait
sur les mathématiques des matrices qui est un point essentiel; vous verrez
l’utilisation des vecteurs et des matrices dans WPF ainsi que toutes les
transformations 2D réalisables.
• le chapitre 3 permet d’apprendre à réaliser les géométries et les tracés 2D
avec un grand nombre de classes essentielles dans WPF.
• le chapitre 4 permet de se familiariser aux couleurs et aux pinceaux qui
représentent les fondements de l’expression graphique.
• le chapitre 5 permet d’apprendre ce qu’est l’animation au sein de WPF;
l’animation dans WPF est une partie très importante qui permet de réaliser
des animations du niveau basique au niveau avancé.
• le chapitre 6 permet de se familiariser avec l’utilisation des styles; les styles
sont très utiles dans la réalisation des composants XAML.
• le chapitre 7 permet de se familiariser avec l’utilisation des modèles de
contrôle; ils représentent, par leur puissant mécanisme, une façon de
personnaliser les contrôles tout en gardant leur fonctionnalité.
Toutes les notions abordées dans ce livre font l’objet d’une utilisation directe
et pratique que vous trouverez dans le code source en téléchargement.

Les liens de téléchargement

Tout le code source de cet ouvrage pourra être téléchargé gratuitement à


l’adresse web suivante:
http://www.reypatrice.fr/programmation_2d_wpf.php.
Vous trouverez un dossier pour chacun des chapitres. Chaque dossier contient
tous les éléments nécessaires de programmation qui sont expliqués dans le
livre. De cette façon, vous serez capable de suivre et de programmer assez
12 Avant-propos
rapidement et assez facilement.
C’est une démarche volontaire, dans un but pédagogique, pour progresser
rapidement. Le but de ce livre est d’être capable d’acquérir dans un temps
raisonnable l’acquisition des notions et concepts de la programmation
graphique 2D de WPF.

Bonne lecture à tous.


CHAPITRE 1

Les bases du graphisme 2D

W PF possède une plate-forme graphique puissante qui permet de créer des


interfaces utilisateur innovantes et des objets graphiques performants.
Ce chapitre commence par la description du système de coordonnées utilisé
par WPF. Puis il vous montre différents systèmes de coordonnées que vous
pouvez utiliser pour réaliser rapidement des graphiques. Enfin il vous montre
comment créer des figures 2D basiques.

1 – Le système de coordonnées 2D

Quand vous créez un objet graphique dans WPF, vous devez définir où il
sera visualisé (à l’écran, vers une imprimante, à destination d’un fichier PDF).
Pour cela, vous devez connaître la façon dont WPF affiche les coordonnées
des objets graphiques. Chaque point, dans une fenêtre ou dans une page
WPF, possède une abscisse X et une ordonnée Y. Nous allons voir différents
systèmes de coordonnées.

1.1 - Les coordonnées par défaut

Dans WPF, l’origine du système de coordonnées du repère 2D se trouve en


haut et à gauche de la zone de rendu. Comme le montre la figure 1-1, un
point A d’abscisse 4 et d’ordonnée 5 se situe à droite et en bas par rapport à
l’origine .
Figure 1-1

Origine O(0,0) axe X

Canvas représentant
une zone de rendu

point A(4,5)

axe Y
n x_radbtn_option1
14 La programmation graphique 2D de WPF 4
L’abscisse (axe des X) est horizontale et dirigée vers la droite. L’ordonnée (axe
des Y) est verticale et dirigée vers le bas.
Les coordonnées 2D sont gérées dans WPF par des propriétés de type double
représentant une position (X, Y, Top, Left) ou une dimension (Width, Height).
Dans un système vectoriel, le type double offre un niveau de précision parfait.
La structure Point permet de stocker les coordonnées d’un point qu’elle expose
dans ses propriétés X et Y. La structure Rect permet de définir une surface
rectangulaire et elle expose le coin supérieur gauche dans ses propriétés X et
Y, la largeur dans sa propriété Width et la hauteur dans sa propriété Height.
L’unité de mesure 2D de WPF est le Device Independant Pixel (DIP), c’est-à-
dire un pixel logique indépendant de la résolution du périphérique d’affichage
(écran ou imprimante). Le DIP est une unité de mesure qui accepte les valeurs
décimales.
Chaque périphérique d’affichage, que ce soit un écran ou une imprimante
par exemple, possède une définition et une densité. La définition représente le
nombre de points de la matrice d’affichage (par exemple un écran de 1280
par 1024 comporte 1280 fois 1024 points). La densité représente la densité des
points par unité de mesure. Elle s’exprime en nombre de points par pouce et
peut utiliser trois unités équivalentes : le PPP (Points Par Pouce), le DPI (Dot Per
Inch) ou le PPI (Pixel Per Inch). Rappelons qu’un pouce est égal à 2,54 cm.
Quel que soit la densité du périphérique d’affichage, 1 DIP correspond à
1/96ème de pouce et donc 96 DIP correspondent à 1 pouce. Sur un périphérique
dont la densité est de 96 pixels par pouce, 1 DIP correspond donc à 1 pixel
physique. Sur un périphérique dont la densité est de 120 pixels par pouce, 96
DIP correspondent à 120 pixels physiques, donc 1 DIP correspond à 1,25 pixel
physique.
Dans le dossier chapitre01, le contrôle utilisateur SystemeCoordonneeDefaut.
xaml (figure 1-2) dessine dans un Canvas x_cnv_appli une ligne x_ligne_defaut
de type Line, avec un point de départ de coordonnées X1=50 et Y1=50, et
un point d’arrivée de coordonnées X2=150 et Y2=150. L’objet Line expose la
propriété Stroke pour indiquer la couleur de la ligne dessinée et la propriété
Copyright 2011 Patrice REY

StrokeThickness pour l’épaisseur du trait. Les coordonnées X1, Y1, X2, Y2 et


StrokeThickness sont exprimées en DIP (pixel logique).
La ligne x_ligne_personnalise est par exemple dessinée en utilisant des unités de
mesure différentes : X1=3.5in (in pour pouce, inch en anglais), Y1=1.5cm (cm
pour centimètre), Y2=155pt (pt pour point, 1pt = 96/72 pixel, 1pt = 0.188mm).
A noter que lorsqu’on écrit en xaml une mesure exprimée dans un système
CHAPITRE 1 □ Les bases du graphisme 2D 15
métrique, le concepteur de Visual Studio la convertit en pixel.

Figure 1-2

ml

n1
n2
16 La programmation graphique 2D de WPF 4
<Canvas Height='238' Name='x_cnv_appli' Width='744'
Background='WhiteSmoke' HorizontalAlignment='Left'
VerticalAlignment='Top'>
<Line x:Name='x_ligne_defaut' X1='50' Y1='50' X2='150' Y2='150'
Stroke='Black' StrokeThickness='2'></Line>
<Line x:Name='x_ligne_personnalise' X1='3.5in' Y1='1.5cm' X2='520'
Y2='155pt' Stroke='Blue' StrokeThickness='0.1cm'></Line>
</Canvas>

1.2 - Des coordonnées personnalisées

Avoir l’origine du repère en haut à gauche n’est pas toujours très pratique,
notamment quand on veut dessiner la représentation de l’équation d’une
courbe ou bien un graphique à barres. Sur la figure 1-3, le point A de
coordonnées X=4 et Y=5 se situe à droite et en haut par rapport à l’origine.
L’origine du Canvas a changé d’emplacement, passant du coin haut gauche au
coin bas gauche.
Figure 1-3
axe Y

point A(4,5)
Canvas représentant
une zone de rendu

Origine O(0,0)
axe X

Dans le dossier chapitre01, le contrôle utilisateur CoordonneesPersonnalisees.xaml


(figure 1-4) dessine dans un Canvas x_cnv_appli deux lignes, deux boutons et
deux textes.
Pour changer l’origine de place, il faut effectuer deux transformations qui
Copyright 2011 Patrice REY

sont insérées dans un TransformGroup: une transformation ScaleTransform


met à l’échelle un objet en l’étirant ou le réduisant selon un facteur d’échelle
horizontal ScaleX et vertical ScaleY (ScaleX selon l’axe des X et ScaleY
selon l’axe des Y); une transformation TranslateTransform qui effectue un
déplacement horizontalement X et verticalement Y. Ici, pour déplacer l’origine,
il suffit de faire une mise à l’échelle de -1 selon l’axe Y (pas de réduction ou
CHAPITRE 1 □ Les bases du graphisme 2D 17
d’agrandissement mais inversion du sens) puis une translation selon l’axe Y
d’une valeur égale à la hauteur du Canvas (ici 238).

<Canvas Height='238' Name='x_cnv_appli' Width='744'


Background='WhiteSmoke' HorizontalAlignment='Left'
VerticalAlignment='Top'>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY='-1'></ScaleTransform>
<TranslateTransform Y='238'></TranslateTransform>
</TransformGroup>
</Canvas.RenderTransform>
...
</Canvas>

La figure 1-4 montre la représentation de nos deux lignes x_ligne_defaut et


x_ligne_personnalise dans ce nouveau repère. Cependant si nous ajoutons
un bouton x_btn_avant et un texte x_texte_avant, nous voyons que leur
représentation sur le Canvas est inversée.

<Button Canvas.Top='176' Canvas.Left='343' FontSize='15'


Foreground='Red' Name='x_btn_avant'
Content='Bouton avant transformation' Width='223'></Button>
<TextBlock x:Name='x_text_avant' Canvas.Top='136' Canvas.Left='349'
FontSize='12pt' Foreground='DarkGreen' Width='255'>
<Bold>mon texte avant transformation</Bold></TextBlock>

Il faut leur appliquer aussi une transformation ScaleTransform (transformation


avec un facteur d’échelle égal à -1 selon l’axe Y).

<Button Canvas.Top='121' Canvas.Left='344' FontSize='15'


Foreground='Red' Name='x_btn_apres'
Content='Bouton après transformation' Width='223'>
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY='-1'></ScaleTransform>
</TransformGroup>
</Button.RenderTransform>
</Button>
<TextBlock x:Name='x_text_apres' Canvas.Top='75' Canvas.Left='349'
FontSize='12pt' Foreground='DarkGreen' Width='255'>
18 La programmation graphique 2D de WPF 4
Figure 1-4

ml

n1
n2

Copyright 2011 Patrice REY


CHAPITRE 1 □ Les bases du graphisme 2D 19
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY='-1'></ScaleTransform>
</TransformGroup>
</TextBlock.RenderTransform>
<Bold>mon texte après transformation</Bold>
</TextBlock>

1.3 - Zoomer une figure

Le zoom permet d’agrandir ou de réduire une zone à l’écran pour mieux voir
les détails. Zoomer un ensemble de figures qui sont dessinées dans un Canvas
consiste tout simplement à effectuer une transformation de mise à l’échelle en
appliquant un facteur d’échelle identique dans les deux directions.
Dans le dossier chapitre01, le contrôle utilisateur ZoomerFigure.xaml (figure
1-5) affiche dans un Canvas x_cnv_appli une ligne rouge x_ligne_defaut et un
rectangle x_rectangle rempli par un dégradé de couleurs.

<Line x:Name='x_ligne_defaut' X1='50' Y1='50' X2='200' Y2='150'


Stroke='Red' StrokeThickness='3'
Canvas.Top='38' Canvas.Left='80'>
</Line>
<Rectangle Canvas.Left='449' Canvas.Top='98' Height='64'
Name='x_rectangle' Stroke='Black' Width='143'>
<Rectangle.Fill>
<LinearGradientBrush EndPoint='0.5,1' StartPoint='0.5,0'>
<GradientStop Color='CornflowerBlue' Offset='0' />
<GradientStop Color='CornflowerBlue' Offset='1' />
<GradientStop Color='Cornsilk' Offset='0.484' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

On positionne un contrôle Slider x_slider pour faire varier le zoom d’une valeur
allant de 0 à 3 et avec une valeur de départ fixée à 1. Un TextBlock x_text_zoom
permet d’afficher la valeur du x_slider. Un gestionnaire d’événement est ajouté
au x_slider pour gérer le changement de sa valeur (événement ValueChanged).
A noter qu’il faut positionner le TextBlock avant le Slider en xaml, car dans le
cas contraire, l’événement ValueChanged du x_slider génèrera une erreur lors
de l’appel de la méthode d’initialisation InitializeComponent().
20 La programmation graphique 2D de WPF 4
Figure 1-5

ml

n1
n2

ClipToBounds=
«False»

Copyright 2011 Patrice REY

ClipToBounds=
«True»
CHAPITRE 1 □ Les bases du graphisme 2D 21
<!-- slider de zoom -->
<TextBlock Canvas.Left='659' Canvas.Top='340' Height='23'
Name='x_text_zoom' Text='zoom = 1' FontWeight='Bold'
FontSize='14' TextAlignment='Center' Width='99'></TextBlock>
<Slider Name='x_slider' Minimum='0' Maximum='3' Value='1'
TickPlacement='BottomRight' TickFrequency='0.2'
IsSnapToTickEnabled='True' Canvas.Left='157' Canvas.Top='334'
Width='496' Background='DarkOrange'
Height='33' Cursor='Hand'
ValueChanged='x_slider_ValueChanged'>
</Slider>

//evenement valeur du slider modifiée


private void x_slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
x_text_zoom.Text = «zoom = « + e.NewValue.ToString();
}

Quand la valeur du Slider est fixée au départ, puis est modifiée par la suite, il
faut que le rectangle et la ligne s’agrandissent ou se réduisent en fonction du
facteur d’échelle correspondant à la valeur du zoom.
On commence par ajouter au rectangle et à la ligne, une transformation
ScaleTransform. Puis il faut relier le facteur d’échelle ScaleX et le facteur
d’échelle ScaleY à la valeur en cours du Slider. On utilise donc un objet Binding
(dans l’espace de noms System.Windows.Data) qui fournit un accès de niveau
supérieur à la définition d’une liaison, qui connecte les propriétés d’objets de
cible de liaison (en général, des éléments WPF) et toute source de données (par
exemple, une base de données, un fichier XML ou tout objet qui contient des
données).
La propriété ElementName définit le nom de l’élément à utiliser comme objet
de source de liaison (ici ElementName=x_slider) et la propriété Path affecte au
chemin d’accès la propriété de source de liaison (ici Path=Value puisque Value
est la propriété du Slider qui retourne la valeur en cours du contrôle).
De ce fait, ScaleX et ScaleY sont reliés en permanence à la propriété Value du
x_slider.

<Rectangle Canvas.Left='449' Canvas.Top='98' Height='64'


Name='x_rectangle' Stroke='Black' Width='143'>
...
22 La programmation graphique 2D de WPF 4
<Rectangle.RenderTransform>
<ScaleTransform
ScaleX='{Binding ElementName=x_slider,Path=Value}'
ScaleY='{Binding ElementName=x_slider,Path=Value}' />
</Rectangle.RenderTransform>
</Rectangle>
<Line x:Name='x_ligne_defaut' X1='50' Y1='50' X2='200' Y2='150'
Stroke='Red' StrokeThickness='3'
Canvas.Top='38' Canvas.Left='80'>
<Line.RenderTransform>
<ScaleTransform
ScaleX='{Binding ElementName=x_slider,Path=Value}'
ScaleY='{Binding ElementName=x_slider,Path=Value}' />
</Line.RenderTransform>
</Line>

La figure 1-5 visualise le résultat obtenu quand le zoom passe de la valeur 1


(valeur de départ) à la valeur 2.4. On s’aperçoit que lors de l’agrandissement,
la ligne et le rectangle sortent de la zone du Canvas. La propriété ClipToBounds
du Canvas est par défaut égale à false. Si on la fixe à true, on indique qu’il faut
découper le contenu de cet élément (ou le contenu qui provient d’éléments
enfants de cet élément) pour l’ajuster aux dimensions de l’élément contenant.
De ce fait, tout ce qui déborde du Canvas est coupé lors de l’agrandissement.

1.4 - Histogramme de données

Il arrive fréquemment de devoir afficher un ensemble de données sous la


forme d’un graphique (un graphique à barres, une courbe, etc.). Il faut pouvoir
afficher graphiquement ces données rapidement dans un Canvas et avec un
dimensionnement correct pour une meilleure visibilité.
Comme le schématise le bas de la figure 1-6, une zone de rendu va être créée
avec des dimensions égales à celles du Canvas (avec des dimensions Width et
Height) mais pour afficher des valeurs (x,y) dans une zone limitée par quatre
Copyright 2011 Patrice REY

points aux coordonnées (m_x_min,m_y_min), (m_x_max,m_y_min), (m_x_


max,m_y_max) et (m_x_min,m_y_min). L’origine du repère de la zone de rendu
est positionnée en bas à gauche soit le point (m_x_min,m_y_min).
Dans le dossier chapitre01, le contrôle utilisateur HistogrammeDonnee.xaml
(figure 1-6) va générer aléatoirement une image en 256 niveaux de gris dans
un contrôle Image x_img de 256 par 256 pixels.
CHAPITRE 1 □ Les bases du graphisme 2D 23
Figure 1-6

ml

n1
n2

Canvas d’affichage Zone de rendu


(m_x_min,m_y_max) (m_x_max,m_y_max)
Width = 458

Height = 268 Canvas x_cnv_appli Canvas x_cnv_appli

(m_x_min,m_y_min) (m_x_max,m_y_min)
24 La programmation graphique 2D de WPF 4
Puis nous allons réaliser l’histogramme de cette image c’est-à-dire compter
pour chaque niveau de gris le nombre de pixels trouvés. Enfin, afficher dans
un Canvas x_cnv_appli cet histogramme. Le haut de la figure 1-6 visualise le
résultat obtenu.
Une image en 256 niveaux de gris signifie que chaque pixel de l’image possède
une valeur comprise entre 0 et 255. Le type intégral byte représente un entier
8 bits non signé dont la plage de valeur est comprise entre 0 et 255. Nous
commençons par créer un tableau tab_pixel de 256 lignes et 256 colonnes que
nous remplissons aléatoirement par une couleur coul_gris de type byte.

Random rnd = new Random();


byte[,] tab_pixel = new byte[256, 256];
for (int lig = 0; lig < 256; lig++) {
for (int col = 0; col < 256; col++) {
byte coul_gris = (byte)rnd.Next(0, 256);
tab_pixel[lig, col] = coul_gris;
}
}

Pour visualiser l’image, il faut passer par un objet BitmapSource. Un


BitmapSource est le bloc de construction de base du pipeline de traitement
d’images Windows Presentation Foundation (WPF), représentant de façon
conceptuelle un seul jeu constant de pixels à une certaine taille et à une
certaine résolution. Les données des pixels doivent être passées sous forme
d’un tableau à une dimension. Nous transformons donc le tableau tab_pixel à
deux dimensions (ligne et colonne) en un tableau unidimensionnel data, dont
sa longueur sera de 256 fois 256.

byte[] data = new byte[256 * 256];


int cpt_data = 0;
for (int lig = 0; lig < 256; lig++) {
for (int col = 0; col < 256; col++) {
Copyright 2011 Patrice REY

data[cpt_data] = tab_pixel[lig, col];


cpt_data++;
}
}

La méthode statique BitmapSource.Create(..) permet de générer l’image à


partir d’un tableau de pixels. On lui passe comme paramètres la largeur (256
CHAPITRE 1 □ Les bases du graphisme 2D 25
pixels), la hauteur (256 pixels), le nombre de points par pouce horizontal (72),
le nombre de points par pouce vertical (72), le format 256 niveaux de gris
(PixelFormats.Gray8), la palette de l’image (ici null), les données brutes (tableau
data) et la largeur de numérisation (256 pixels). Le BitmapSource ainsi généré
bitmap_creer est affecté à la propriété Source du contrôle Image x_img.

PixelFormat pf = PixelFormats.Gray8;
BitmapSource bitmap_creer = BitmapSource.Create(256, 256,72, 72, pf,
null, data, 256);
x_img.Source = bitmap_creer;

On stocke dans un tableau histogramme à deux dimensions le nombre de pixels


par niveau de gris. Le TextBlock x_infos affiche le résultat sous forme de texte.

int[,] histogramme = new int[256, 1];


for (int lig = 0; lig < 256; lig++) {
histogramme[lig, 0] = 0;
}
for (int lig = 0; lig < 256; lig++) {
for (int col = 0; col < 256; col++) {
byte niveau_gris = tab_pixel[lig, col];
histogramme[niveau_gris, 0] += 1;
}
}
//affichage dans la zone texte de la qté des niveaux de gris
for (int lig = 0; lig < 256; lig++) {
x_infos.Text +=»niveau de gris= « + lig.ToString() + « -> nombre de
pixel= « + histogramme[lig, 0].ToString() + RC;
}

Pour afficher l’histogramme, nous allons tracer des lignes verticales


représentant le nombre de pixels par niveau de gris. En abscisse, nous aurons
des valeurs x allant de 0 à 255 (ce sont les niveaux de gris). En ordonnée, nous
aurons des valeurs allant de 0 (pas de pixels) à 350 (valeur estimée pour une
séquence aléatoire de 256 fois 256 pixels). Ce qui veut dire que notre zone de
rendu a pour limites m_x_min=0, m_x_max=255, m_y_min=0 et m_y_max=350.
Nous instancions donc des objets Line avec une couleur Colors.Gray et une
épaisseur de 2. Ces objets sont ajoutés à l’arbre visuel du Canvas par la
méthode Add(..).
26 La programmation graphique 2D de WPF 4

m_x_min = 0;
m_x_max = 255;
m_y_min = 0;
m_y_max = 350;
//affichage des lignes rouges sur le canvas
for (int lig = 0; lig < 256; lig++) {
Point pt_1 = new Point(lig, 0);
Point pt_2 = new Point(lig, histogramme[lig, 0]);
Line line = new Line();
line.X1 = XNormalise(pt_1.X);
line.Y1 = YNormalise(pt_1.Y);
line.X2 = XNormalise(pt_2.X);
line.Y2 = YNormalise(pt_2.Y);
line.Stroke = new SolidColorBrush(Colors.DarkGray);
line.StrokeThickness = 2;
x_cnv_appli.Children.Add(line);
}

Les coordonnées de début de ligne (X1,Y1) et de fin de ligne (X2,Y2) doivent


être transformées pour l’affichage dans la zone de rendu. Pour cela on utilise
les méthodes XNormalise(double x) et YNormalise(double y) pour normaliser les
coordonnées (x,y) brutes dans la zone de rendu adaptée.
De plus, pour la coordonnée y, on inverse le sens pour simuler l’origine du
repère en bas à gauche.

//normalisation de la coordonnee x
private double XNormalise(double x) {
double result = (x - m_x_min) * x_cnv_appli.Width / (m_x_max -
m_x_min);
return result;
}
//normalisation de la coordonnee y
private double YNormalise(double y) {
Copyright 2011 Patrice REY

double result = x_cnv_appli.Height - (y - m_y_min) *


x_cnv_appli.Height / (m_y_max - m_y_min);
return result;
}
CHAPITRE 1 □ Les bases du graphisme 2D 27

2 - Les formes géométriques basiques 2D

Dans l’espace de noms System.Windows.Shapes, le dessin des figures


géométriques basiques est confié aux classes héritées de Shape. Ces classes
héritées exploitent les géométries pour déterminer la forme des figures, et
les pinceaux pour déterminer la couleur et le motif du fond et des traits de la
figure. Des classes spécifiques héritées de la classe Brush prennent en charge
les différents types de motif.
Les objets de type Shape peuvent être stockés sous la forme de ressources et
être ainsi réutilisés en xaml au sein de plusieurs éléments.
Un objet Shape est un élément qui affiche une figure géométrique. Sa classe, qui
est une classe abstraite (et donc non instanciable), hérite de FrameworkElement,
qui hérite de UIElement, et par conséquent un objet Shape est interactif (sensible
aux périphériques de saisie) et compatible avec le système de disposition de
WPF.
Les classes dérivées de Shape sont (figure 1-7 visualisant le graphe d’héritage):
• Line qui dessine une ligne droite reliant deux points donnés
• Rectangle qui dessine un rectangle de dimensions données
• Ellipse qui dessine une ellipse inscrite dans un rectangle fictif
• Polygon qui dessine un polygone spécifié par une suite de points
• Polyline qui dessine une suite de lignes droites connectées
• Path qui dessine une suite de segments droits ou courbes connectés
Les classes dérivées de Shape héritent d’un ensemble de caractéristiques
communes qui sont:
• Stroke: propriété qui référence le pinceau utilisé pour le dessin du contour
de la forme (ou de la ligne pour les objets Line, Polyline et Path)
• Fill: propriété qui référence le pinceau utilisé pour le remplissage de la
forme (Rectangle, Ellipse, Polygon et Path)
• StrokeThickness: propriété qui référence l’épaisseur du trait du pinceau
• Stretch: propriété qui référence le type d’étirement appliqué à la figure
lorsqu’elle se trouve dans son conteneur
A noter que les figures s’affichent que si les propriétés Stroke et Fill sont définies.
De plus les propriétés liées au dimensionnement de la figure dépendent de sa
nature.
28 La programmation graphique 2D de WPF 4
Figure 1-7

2.1 - La classe Line

Un objet Line dessine une droite reliant deux points donnés. Ses propriétés de
dépendance sont X1 et Y1 pour spécifier le point d’origine de la ligne, et X2
et Y2 pour spécifier le point de terminaison de la ligne. Ces propriétés sont de
type double. Pour qu’une ligne soit affichée, il faut que ses propriétés Stroke et
StrokeThickness soient définies.
Copyright 2011 Patrice REY

Dans le dossier chapitre01, le contrôle utilisateur DemoClasseLine.xaml (figure


1-8 et figure 1-9) visualise différents types de ligne.
Les propriétés StrokeStartLineCap et StrokeEndLineCap indiquent
respectivement la forme des terminaisons de début et de fin du trait au moyen
d’une énumération PenLineCap. L’énumération PenLineCap.Flat (valeur par
défaut) spécifie aucune terminaison particulière, PenLineCap.Square spécifie
CHAPITRE 1 □ Les bases du graphisme 2D 29
Figure 1-8

ml

n1
n2
30 La programmation graphique 2D de WPF 4
Figure 1-9

ml

n1
n2

un rectangle dont l’épaisseur est la moitié de la largeur du trait, PenLineCap.


Round spécifie un demi-cercle et PenLineCap.Triangle spécifie un triangle
isocèle.
La propriété StrokeDashArray permet de définir un trait en pointillé en lui
affectant une collection de valeurs de type double qui indiquent le motif des
tirets et des espacements qui est utilisé pour esquisser le trait. Par exemple la
collection «5» indique un trait en pointillé dont la longueur du tiret est de 5 et
la longueur de l’espacement est de 5. Une collection de «5,2» indique un tiret
de longueur 5 et un espacement de longueur 2. Si la collection a comme valeur
«5,2,3,1», on aura comme motif répétitif un premier tiret de longueur 5 suivi
Copyright 2011 Patrice REY

d’un espace de longueur 2, suivi d’un deuxième tiret de longueur 3, suivi d’un
espace de longueur 1.
La propriété StrokeDashCap spécifie la manière dont les extrémités d’un tiret
sont dessinées, par l’énumération PenLineCap.
Une ligne Line peut être dessinée par code de programmation. Le bas de la
figure 1-9 montre un exemple de ligne noire en pointillé exprimée par code.
CHAPITRE 1 □ Les bases du graphisme 2D 31
2.2 - Les classes Rectangle et Ellipse

La classe Rectangle permet de spécifier ses dimensions au moyen de ses


propriétés Width (la largeur) et Height (la hauteur). Comme elle n’offre aucune
propriété pour spécifier la position d’un de ses coins, son positionnement se
fera au moyen des propriétés Margin, HorizontalAlignment et VerticalAlignment.
Dans le cas de l’utilisation d’un Canvas comme conteneur hôte, ce seront les
méthodes statiques Canvas.SetTop(..) et Canvas.SetLeft(..) qui permettront le
positionnement du rectangle. Les coins du rectangle peuvent être arrondis en
spécifiant le rayon de l’arrondi dans ses propriétés RadiusX et RadiusY. Quand
la valeur de la propriété Width est égale à la valeur de la propriété Height, on
obtient un carré.
La classe Ellipse fonctionne de façon similaire à la classe Rectangle, en
définissant une ellipse inscrite dans un rectangle fictif. Quand la valeur de
sa propriété Width est égale à la valeur de sa propriété Height, on obtient un
cercle.
Dans le dossier chapitre01, le contrôle utilisateur DemoRectangleEllipse.xaml
(figure 1-10 et figure 1-11) visualise différents types de rectangle et de cercle.
Le trait de contour du rectangle ou du cercle peut être en pointillé en modifiant
les propriétés StrokeDashArray et StrokeDashCap (tout comme on l’a vu pour
les objets Line).
Le rectangle et l’ellipse ont l’aptitude à se redimensionner eux-mêmes pour
remplir un espace vide. Si les propriétés Width et Height ne sont pas spécifiées,
la figure se redimensionne sur la taille de son conteneur. Le bas de la figure
1-11 visualise les différents cas de redimensionnement.
La façon de se redimensionner dépend de la valeur de la propriété Stretch de
la forme. Par défaut, elle est fixée par l’énumération Stretch.Fill pour indiquer
que le contenu est redimensionné pour remplir les dimensions de destination.
Les proportions ne sont pas conservées. L’énumération Stretch.None spécifie
que le contenu conserve sa taille d’origine, Stretch.Uniform spécifie que le
contenu est redimensionné pour s’ajuster aux dimensions de destination
pendant qu’il conserve ses proportions natives, et Stretch.UniformToFill spécifie
que le contenu est redimensionné pour remplir les dimensions de destination
pendant qu’il conserve ses proportions natives. Si les proportions du rectangle
de destination diffèrent de la source, le contenu source est découpé pour
s’ajuster aux dimensions de destination.
32 La programmation graphique 2D de WPF 4
Figure 1-10

ml

n1
n2

Copyright 2011 Patrice REY


CHAPITRE 1 □ Les bases du graphisme 2D 33
Figure 1-11

ml

n1
n2

2.3 - La classe Polyline

La classe Polyline permet de représenter des formes composées d’une série de


droites jointes déterminées par la propriété Points de type PointCollection. Une
collection de points de type PointCollection s’exprime en xaml sous la forme
d’une série de paires de coordonnées de type x1,y1...xn,yn (par exemple «0,75
200,75 200,100 250,50 200,0 200,25 0,25»).
Chaque coin de la figure peut se voir appliquer une forme selon l’énumération
PenLineJoin affectée à la propriété StrokeLineJoin. L’énumération PenLineJoin.
34 La programmation graphique 2D de WPF 4
Miter représente un angle pointu (valeur par défaut), PenLineJoin.Bevel
représente un angle rogné, et PenLineJoin.Round représente un angle arrondi.
Dans le dossier chapitre01, le contrôle utilisateur DemoPolyline.xaml (figure 1-12
et figure 1-13) visualise une forme Polyline représentant une flèche.
La figure 1-12 montre la flèche avec les 3 possibilités de la propriété
Figure 1-12

ml

n1
n2

Copyright 2011 Patrice REY


CHAPITRE 1 □ Les bases du graphisme 2D 35
StrokeLineJoin, et la figure 1-13 montre une flèche dont la bordure est en
trait pointillé ainsi qu’avec des remplissages en couleurs ou en dégradés de
couleurs.
Figure 1-13

ml

n1
n2
36 La programmation graphique 2D de WPF 4

2.4 - La classe Polygon

La classe Polygon dessine un polygone qui est une série connectée de lignes qui
forment une figure fermée. Elle fonctionne de façon identique à la classe Polyline.
Les points des sommets sont spécifiés, avec leurs coordonnées x et y, dans la
propriété Points de type PointCollection. Un segment relie automatiquement le
dernier point au premier point pour fermer la figure. L’intérieur de la figure est
rempli par l’intermédiaire de la couleur affectée à la propriété Fill.
Dans le dossier chapitre01, le contrôle utilisateur DemoPolygone.xaml (figure
1-14) visualise différents polygones.
Un polygone basique est représenté par l’objet x_polygon_1. Les propriétés
Stroke, StrokeThickness, Fill et Points doivent être définies.
Dans le bas de la figure 1-14, on affecte à la propriété Points d’un polygone une
collection de points qui simule une sinusoïde. Cela est possible par code en
calculant tous les points d’une sinusoïde sur un intervalle donné. Puis le dernier
point de cette collection est relié automatiquement au premier point par un
segment de droite. Les méthodes statiques SetLeft(..) et SetTop(..) permettent
de positionner le polygone dans son conteneur parent qui est ici x_cnv_appli.
Puis la méthode Add(..) ajoute le polygone à l’arbre visuel du Canvas par sa
propriété Children.
Les polygones x_polygon_2_gauche et x_polygon_2_droite sont identiques mais
avec une propriété FillRule différente. Le premier a la propriété FillRule égale
à l’énumération FilleRule.EvenOdd, le deuxième avec l’énumération FillRule.
NonZero.
Comme les segments d’un polygone peuvent se croiser pour former une
figure composée de plusieurs zones, la propriété FillRule détermine la façon
dont ces zones sont colorées par le pinceau indiqué dans la propriété Fill.
Le comportement par défaut est FillRule.EvenOdd et spécifie que la zone est
colorée si elle est séparée de l’extérieur de la figure par un nombre impair de
segments. FillRule.NonZero spécifie que la zone est colorée en fonction de la
Copyright 2011 Patrice REY

direction suivie par les points formant les segments. Si le nombre de segments
pour une direction donnée est identique à celui pour la direction opposée, la
zone n’est pas colorée. Si ces nombres sont différents, la zone est colorée. La
figure 1-15 illustre les zones de remplissage selon les 2 cas de l’énumération.
CHAPITRE 1 □ Les bases du graphisme 2D 37
Figure 1-14

ml

n1
n2
38 La programmation graphique 2D de WPF 4
Figure 1-15

1 8 1 10

5 4 6 7

8
6
7 5 4 9

1 2 3 2 3
2

Règle de remplissage si FillRule = NonZero

1 segment trouvé
2 segments trouvés

Règle de remplissage si FillRule = EvenOdd


Copyright 2011 Patrice REY
CHAPITRE 2

Les transformations 2D

La création de figures complexes nécessite de savoir comment réaliser des


transformations sur des objets graphiques. Dans ce chapitre, nous allons
voir les mathématiques de base concernant les vecteurs, les matrices et
les transformations 2D. Dans une application graphique, il est important
de connaitre les matrices et les transformations, nous verrons donc les
transformations linéaires comme la translation, la mise à l’échelle, la rotation
et l’inclinaison.

1 - Mathématiques des matrices et des transformations

Les vecteurs et les matrices jouent un rôle important dans le processus des
transformations. Quand une zone graphique contient plusieurs objets et que
l’on souhaite bouger par exemple un seul de ces objets, il est alors très pratique
d’appliquer une transformation à cet objet, et cela sans toucher aux autres.

1.1 - Les points et les vecteurs

Généralement un vecteur est représenté sous la forme d’un tableau composé


d’une ligne contenant deux coordonnées qui représentent un déplacement
dans l’espace 2D (on parle alors d’un vecteur ligne). Un point est défini par ses
coordonnées x et y, correspondant à une position précise. La différence entre
un vecteur et un point est que le point représente une position fixe tandis que
le vecteur représente une direction et une longueur par rapport à un point
d’origine.
Soit un segment A1B1 composé du point de départ A1(x1,y1) et du point de fin
A2(x2,y2), le vecteur V12 (figure 2-1) représente le vecteur allant de l’origine A1
au point de fin A2 et V12=V2-V1 (V2 est le vecteur allant du point d’origine O
au point A2 et V1 est le vecteur allant du point d’origine O au point A1). En C#,
nous écrirons donc Vector V12 = new Point(x2,y2) - new Point(x1,y1).
Avec WPF, on peut appliquer une matrice de transformation directement sur
un vecteur ou sur un point.
40 La programmation graphique 2D de WPF 4
Figure 2-1

A2 (x2,y2)

A1 (x1,y1) V12
1
2

V1
V2

X
O (0,0)

1.2 - Mise à l’échelle

Une mise à l’échelle est une transformation géométrique qui consiste à


agrandir ou à réduire selon une direction. Pour agrandir ou réduire une figure
dans la direction X, il suffit simplement de multiplier la coordonnées x de
chacun des points de la figure par le facteur d’échelle Sx. On fait de même
pour la direction Y en multipliant la coordonnée y par le facteur d’échelle Sy.
L’équation suivante permet de transformer un point (x,y) en un point (x1,y1)
Copyright 2011 Patrice REY

par une transformation de mise à l’échelle (Sx,Sy):


O (0,0)

Sx 0
( x1 y1 ) = ( x y ) = ( Sxx Syy )
0 Sy
CHAPITRE 2 □ Les transformations 2D 41
La figure 2-2 visualise différentes transformations de mise à l’échelle par des
matrices de transformation (agrandissement et réduction).
Figure 2-2

Y Y

0.5 0
0 0.5

X X
O O
Y Y

0.5 0
0 2

X X
O O

L’effet miroir selon l’axe des X est obtenu par la matrice Sx=-1 et Sy=1. L’effet
miroir selon l’axe des Y est obtenu par la matrice Sx=1 et Sy=-1. La figure 2-3
visualise les effets miroir à axe vertical et horizontal.
On emploie aussi le terme de réflexion, dans le jargon informatique, pour
parler de l’effet miroir.
42 La programmation graphique 2D de WPF 4
Figure 2-3

-1 0
0 1

1 0
X
O 0 -1

1.3 - Rotation

Une rotation est une transformation géométrique qui consiste à faire tourner
à partir d’un point et selon un angle. Un point A1 se trouvant à une distance r
de l’origine O (figure 2-4) a comme coordonnées x1 et y1 :
Copyright 2011 Patrice REY

x1 = r cos(α)
y1 = r sin(α)
Si A2 est un point correspondant au point A1 après une rotation d’un angle α,
alors A2 aura comme coordonnées x2 et y2 :
x2 = r cos(α+β) = r cos(α)cos(β) - r sin(α)sin(β)
y2 = r sin(α+β) = r sin(α)cos(β) - r cos(α)sin(β)
CHAPITRE 2 □ Les transformations 2D 43
En substituant dans cette équation r cos(α) = x1 et r sin(α) = y1, on obtient :
x2 = x1 cos(β) - y1 sin(β)
y2 = x1 sin(β) + y1 cos(β)
Figure 2-4

A2 (x2,y2)
r sin(α+β)

β A1 (x1,y1)
r sin(α)

α
X
O (0,0) r cos(α+β) r cos(α)

Sous forme de matrice, un point (x1,y1) donnera un point (x2,y2) par rotation
d’un angle β en appliquant la matrice de transformation suivante:

cos(β) sin(β)
Rotation (angle β)=
-sin(β) cos(β)

1.4 - Translation

La translation est une transformation géométrique qui consiste à déplacer une


figure d’une distance dx selon l’axe des X et d’une distance dy selon l’axe des
Y (figure 2-5).
44 La programmation graphique 2D de WPF 4
Un point A1(x1,y1) sera déplacé en un point A2(x2,y2) d’une distance dx et dy, et
les coordonnées de A2 seront:
x2=x1+dx
y2=y1+dy
Figure 2-5
Y

A1 (x1,y1)
dx

dy
A2 (x2,y2)

X
O (0,0)

2 - Les coordonnées homogènes

En mathématiques, et plus particulièrement en géométrie projective,


les coordonnées homogènes, introduites par August Ferdinand Möbius,
rendent les calculs possibles dans l’espace projectif comme les coordonnées
Copyright 2011 Patrice REY

cartésiennes le font dans l’espace euclidien.


Les coordonnées homogènes sont largement utilisées en infographie et
plus particulièrement pour la représentation de scènes en trois dimensions
(3D) car elles sont adaptées à la géométrie projective et elles permettent de
caractériser les transformations de l’espace. La notation sous forme matricielle
est plus particulièrement employée dans les bibliothèques de programmation
CHAPITRE 2 □ Les transformations 2D 45
graphique 3D telles que OpenGL et Direct3D.
Un point est représenté par deux coordonnées (x,y). Avec les coordonnées
homogènes, ce même point sera représenté avec trois coordonnées (x,y,w).
Si w est différent de 0, l’écriture des coordonnées de ce point est (x/w,y/w,1).
Les coordonnées x/w et y/w sont appelées les coordonnées du point dans le
système des coordonnées homogènes. Quand w égal 0, on parle de point à
l’infini.
Comme les points ont désormais trois coordonnées, les matrices de
transformation seront de taille 3 x 3.

2.1 - La translation en coordonnées homogènes

L’équation d’une translation en coordonnées homogènes, pour déplacer un


point A1(x1,y1,1) en un point A2(x2,y2,1) s’exprimera de la façon suivante:

1 0 0
( x2 y2 1 ) = ( x1 y1 1 ) 0 1 0
dx dy 1

Cette transformation peut être exprimée par A2=A1.T(dx,dy) où A1 représente


le point (x1,y1), A2 représente le point (x2,y2) et T(dx,dy) représente la matrice
de transformation.
1 0 0
T(dx,dy) = 0 1 0
dx dy 1

Si un point A1 est déplacé par T(dx1,dy1) en un point A2, on aura


A2=A1.T(dx1,dy1) et si ce point A2 est déplacé en un point A3 par T(dx2,dy2),
on aura A3=A2.T(dx2,dy2), ce qui donne

A3=A1.T(dx1,dy1).T(dx2,dy2)

Le produit des matrices T(dx1,dy1).T(dx2,dy2) est:


1 0 0 1 0 0 1 0 0
0 1 0 0 1 0 = 0 1 0
dx1 dy1 1 dx2 dy2 1 dx1+dx2 dy1+dy2 1
46 La programmation graphique 2D de WPF 4
Donc la transformation finale correspond à un déplacement
T(dx1+dx2,dy1+dy2) d’où deux translations sont donc additives.

2.2 - La mise à l’échelle en coordonnées homogènes

L’équation d’une mise à l’échelle en coordonnées homogènes, pour mettre


à l’échelle un point A1(x1,y1,1) en un point A2(x2,y2,1) s’exprimera de la façon
suivante:
Sx 0 0
( x2 y2 1 ) = ( x1 y1 1 ) 0 Sy 0
0 0 1

Cette équation s’écrit aussi A2=A1.S(Sx,Sy)


Si un point A1 est mis à l’échelle par S(Sx1,Sy1) en un point A2, on aura
A2=A1.S(Sx1,Sy1) et si ce point A2 est mis à l’échelle en un point A3 par
S(Sx2,Sy2), on aura A3=A2.S(Sx2,Sy2), ce qui donne

A3=(A1.S(Sx1,Sy1)).S(Sx2,Sy2)

Le produit des matrices est:

Sx1 0 0 Sx2 0 0 Sx1Sx2 0 0


0 Sy1 0 0 Sy2 0 = 0 Sy1Sy2 0
0 0 1 0 0 1 0 0 1

Deux mises à l’échelle successives sont donc multiplicatives.

2.3 - La rotation en coordonnées homogènes

L’équation d’une rotation en coordonnées homogènes, pour faire tourner d’un


angle β un point A1(x1,y1,1) en un point A2(x2,y2,1) s’exprimera de la façon
Copyright 2011 Patrice REY

suivante:
cos(β) sin(β) 0
( x2 y2 1 ) = ( x1 y1 1 ) -sin(β) cos(β) 0
0 0 1
CHAPITRE 2 □ Les transformations 2D 47
Cette équation s’écrit aussi A2=A1.R(β) où R(β) est une matrice de rotation en
coordonnées homogènes.
Si on effectue deux rotations successives à un point A1 pour obtenir un point
A3, on a A2=A1. R(β1) et A3=A2. R(β2) ce qui donne en substituant un terme

A3=(A1. R(β1)). R(β2) = A1.(R(β1). R(β2))

Le produit des matrices est:


cos(β1) sin(β1) 0 cos(β2) sin(β2) 0
-sin(β1) cos(β1) 0 -sin(β2) cos(β2) 0
0 0 1 0 0 1

cos(β1)cos(β2)-sin(β1)sin(β2) cos(β1)sin(β2)+sin(β1)cos(β2) 0
= -sin(β1)cos(β2)-cos(β1)sin(β2) cos(β1)cos(β2)-sin(β1)sin(β2) 0
0 0 1

cos(β1+β2) sin(β1+β2) 0
= -sin(β1+β2) cos(β1+β2) 0
0 0 1

Deux rotations successives sont donc additives.

2.4 - Des transformations successives

Très souvent, des transformations successives seront appliquées à une figure


pour obtenir un résultat demandé. Prenons comme exemple un rectangle
que l’on veut faire tourner d’un certain angle autour d’un point A. La figure
2-6 visualise les étapes nécessaires. On commence par translater le rectangle
à l’origine du repère. Ensuite on fait tourner le rectangle d’un angle donné.
Puis on translate le rectangle vers A. Ainsi, par plusieurs transformations
successives, on a fait tourner le rectangle autour du point A d’un certain angle
donné.
48 La programmation graphique 2D de WPF 4
Figure 2-6

Y Y

A A

X X
O O
au début on a cette on effectue une
configuration translation

Y Y

A A

α
X X
O O

on effectue une on effectue une


translation rotation
Copyright 2011 Patrice REY

Pour ces différentes étapes, on effectue une translation T(-x1,-y1), puis une
rotation d’un angle R(α), enfin une translation inverse de T(x1,y1).

Le produit de ces matrices de transformation est donc:


CHAPITRE 2 □ Les transformations 2D 49

1 0 0 cos(α) sin(α) 0 1 0 0
0 1 0 -sin(α) cos(α) 0 0 1 0
-x1 -y1 1 0 0 1 x1 y1 1

cos(α) -sin(α) 0
= sin(α) cos(α) 0
x1.(1-cos(α)) + y1.sin(α) y1.(1-cos(α)) - x1.sin(α) 1

3. Les vecteurs et les matrices dans WPF

Les vecteurs et les matrices en coordonnées homogènes dans l’espace 2D sont


implémentés dans WPF. Un vecteur ou un point en coordonnées homogènes
possède trois valeurs (x,y,1). Dans WPF, ils sont exprimés avec deux valeurs de
type double puisque la troisième valeur est églale à 1.

3.1 - La structure Vector

Dans WPF, un vecteur est défini par une structure de type Vector. Les structures
et les classes se ressemblent de par leur nature, avec une différence principale
qui fait qu’une structure est un type valeur et une classe est un type référence.
Un type valeur est stocké directement dans la pile alors qu’un type référence
stocke une référence à un objet alloué dynamiquement.
Généralement les structures ont un avantage de performance sur les
classes parce qu’elles sont allouées directement sur la pile et sont retirées
immédiatement de la pile dès qu’elles ne sont plus utilisées. L’inconvénient
d’une structure est qu’elle ne peut pas hériter comme une classe. De
nombreuses fonctions mathématiques sont définies sous forme de structure à
cause de leur avantage de performance.
La classe Vector est définie dans l’espace de noms System.Windows. Un objet
Vector est un tableau de données à une ligne avec trois coordonnées homogènes.
Comme la troisième coordonnée est égale à 1, il est juste nécessaire de fournir
les deux premières coordonnées. Par exemple on instancie un vecteur par
Vector vecteur=new Vector(5,10).
Un vecteur peut être défini aussi en utilisant des points. Les écritures suivantes
sont valides:
50 La programmation graphique 2D de WPF 4
Vector v1 = new Point(5, 10) - new Point(25, 30);
Vector v2 = (Vector)new Point(5, 15);
Point pt1 = (Point)new Vector(5, 15);

Un Vector possède 4 propriétés publiques:


1. X obtient ou définit le composant X du vecteur,
2. Y obtient ou définit le composant Y du vecteur,
3. Length obtient la longueur du vecteur,
4. LengthSquared obtient le carré de la longueur du vecteur.
La structure Vector possède des méthodes publiques et des méthodes statiques
couramment employées. Les méthodes statiques sont les suivantes:
• Add(Vector, Point) convertit le point spécifié par le vecteur spécifié et
retourne le point résultant.
• Add(Vector, Vector) ajoute deux vecteurs et retourne le résultat sous forme
de structure Vector.
• Subtract soustrait le vecteur spécifié d’un autre vecteur spécifié.
• Multiply(Double, Vector) multiplie le scalaire spécifié par le vecteur indiqué
et retourne le Vector résultant.
• Multiply(Vector, Double) multiplie le vecteur spécifié par le scalaire indiqué
et retourne le Vector résultant.
• Multiply(Vector, Matrix) transforme l’espace de coordonnées du vecteur
spécifié à l’aide du Matrix spécifié.
• Multiply(Vector, Vector) calcule le produit scalaire des deux vecteurs spécifiés
et retourne le résultat sous forme de Double.
• Divide divise le vecteur spécifié par le scalaire indiqué et retourne le résultat
sous forme de Vector.
• CrossProduct calcule le produit croisé de deux vecteurs.
• AngleBetween récupère l’angle, exprimé en degrés, entre les deux vecteurs
spécifiés.
Les méthodes publiques sont les suivantes:
Copyright 2011 Patrice REY

• Normalize normalise ce vecteur.


• Equals(Object) détermine si le Object spécifié est une structure Vector et, si
c’est le cas, s’il a les mêmes valeurs X et Y que ce vecteur.
Par exemple si on effectue le code ci-dessous:

Vector v1 = new Vector(5, 10);


AjouterTexte(«vecteur v1 -> « + v1.ToString());
CHAPITRE 2 □ Les transformations 2D 51
Vector v2 = new Vector(10, 25);
AjouterTexte(«vecteur v2 -> « + v2.ToString());
double produit_croise = Vector.CrossProduct(v1, v2);
AjouterTexte(«produit_croise -> « + produit_croise.ToString());
double angle_degre = Vector.AngleBetween(v1, v2);
AjouterTexte(«angle_degre -> « + angle_degre.ToString());
v1.Normalize();
double carre_de_la_longueur = v1.LengthSquared;
AjouterTexte(«carre_de_la_longueur -> « +
carre_de_la_longueur.ToString());

On obtient comme résultat:

3.2 - La structure Matrix

La classe Matrix se trouve dans l’espace de noms System.Windows.Media


(assembly WindowsBase.dll).
Nous avons vu que les matrices de transformation en coordonnées
homogènes avaient toujours comme dernière colonne (0,0,1). WPF définit les
transformations par des matrices de 3 lignes par 2 colonnes, en supposant que
la troisième colonne soit égale à (0,0,1).
Une matrice est un tableau de données qui permet de définir une
transformation de coordonnées. Les matrices 2D de WPF ne permettent que
des transformations affines. La structure Matrix représente une matrice à 3
lignes et 2 colonnes dont les propriétés correspondent aux différentes cellules:

M11 M12
M21 M22
OffsetX OffsetY

La matrice unité, avec une valeur de 1 pour les cellules (1,1) et (2,2), et une
valeur de 0 pour les autres cellules, peut être obtenue au moyen de la propriété
statique Matrix.Identity. La construction d’une structure Matrix est par défaut
52 La programmation graphique 2D de WPF 4
une structure Matrix.Identity.
On peut noter que les membres de la structure Matrix correspondent aux
paramètres des transformations de haut niveau comme vues précédemment:
• M11 est le facteur de mise à l’échelle sur l’axe des X,
• M22 est le facteur de mise à l’échelle sur l’axe des Y,
• M21 est le facteur d’inclinaison sur l’axe des X,
• M12 est le facteur d’inclinaison sur l’axe des Y,
• OffsetX est le déplacement en DIP sur l’axe des X,
• OffsetY est le déplacement en DIP sur l’axe des Y.
Dans les méthodes et les propriétés, la matrice de transformation est
généralement spécifiée comme un vecteur à six membres seulement, comme
suit : (M11, M12, M21, M22, OffsetX, OffsetY).
Un exemple de matrice de type translation matrice_translation qui déplace une
figure de 3 unités sur l’axe X et de 2 unités sur l’axe Y:

double dx = 3;
double dy = 2;
Matrix matrice_translation = new Matrix(1, 0, 0, 1, dx, dy);

Matrix matrice_translation = new Matrix(1, 0, 0, 1, dx, dy)

M12
M11
1 0 0
M21 M22
0 1 0
dx dy 1
OffsetX
OffsetY

Un exemple de matrice de type mise à l’échelle matrice_mise_a_echelle avec un


facteur d’échelle de 0.5 suivant l’axe X et un facteur d’échelle de 1.5 suivant
l’axe Y:
Copyright 2011 Patrice REY

double sx = 0.5;
double sy = 1.5;
Matrix matrice_mise_a_echelle = new Matrix(sx, 0, 0, sy, 0, 0);
CHAPITRE 2 □ Les transformations 2D 53
Matrix matrice_mise_a_echelle = new Matrix(sx, 0, 0, sy, 0, 0)

M12
M11
Sx 0 0
M21 M22
0 Sy 0
0 0 1
OffsetX
OffsetY
Un exemple de matrice de type rotation matrice_rotation qui fait tourner une
figure de 45 degrés par rapport à l’origine du repère:

double theta = Math.PI / 4;


double sin = Math.Sin(theta);
double cos = Math.Cos(theta);
Matrix matrice_rotation = new Matrix(cos, sin, -sin, cos, 0, 0);
OffsetY
Matrix matrice_rotation = new Matrix(cos, sin, -sin, cos, 0, 0)

M12
M11
cos sin 0
M21 M22
-sin cos 0
0 0 1
OffsetX
OffsetY

Un objet Matrix possède en plus quatre propriétés publiques qui sont:


• Determinant obtient le déterminant de cette structure Matrix,
• HasInverse obtient une valeur qui indique si cette structure Matrix est
inversible,
• IsIdentity obtient une valeur qui indique si cette structure Matrix est une
matrice d’identité,
• Identity obtient une identité Matrix (propriété statique).
De nombreuses méthodes publiques sont associées à la structure Matrix,
lesquelles permettent de réaliser des opérations variées sur les matrices.

3.3 - Les opérations sur les matrices

La structure Matrix fournit des méthodes pour accomplir des transformations


54 La programmation graphique 2D de WPF 4
comme des rotations, des mises à l’échelle et des translations. Elle fournit
également différentes méthodes pour exécuter des opérations sur les
matrices. Par exemple, la méthode Invert() qui inverse une matrice inversible.
La méthode statique Matrix.Multiply(..) multiplie une structure Matrix par une
autre structure Matrix, et retourne le résultat sous forme d’une nouvelle Matrix.
Voici les méthodes les plus fréquemment utilisées pour les opérations sur les
matrices:
• Scale ajoute le vecteur d’échelle spécifié à cette structure Matrix.
• ScaleAt met à l’échelle cette Matrix au taux spécifié par rapport au point
spécifié.
• Translate ajoute une traduction des offsets spécifiés à cette structure Matrix.
• Rotate applique une rotation d’un angle spécifié par rapport à l’origine de
cette structure Matrix.
• RotateAt fait pivoter cette matrice par rapport au point spécifié.
• Skew ajoute une inclinaison des degrés spécifiés dans les dimensions x et
y à cette structure Matrix.
• Invert inverse cette structure Matrix.
• Multiply multiplie une structure Matrix par une autre structure Matrix.
• Transform(Point) transforme le point spécifié par la Matrix et renvoie le
résultat.
• Transform(Point[]) transforme les points spécifiés par cette Matrix.
• Transform(Vector) transforme le vecteur spécifié par cette Matrix.
• Transform(Vector[]) transforme les vecteurs spécifiés par cette Matrix.
La multiplication des matrices n’est pas commutative (m1*m2 ne donne pas le
même résultat que m2*m1). Dans une transformation de ce type, l’ordre des
diverses transformations constitue un aspect important.
Par exemple, si vous effectuez d’abord une rotation, puis une mise à l’échelle,
puis une translation, le résultat obtenu n’est pas le même que si vous
commencez par appliquer une translation, puis une rotation, puis une mise à
l’échelle. L’une des raisons qui explique l’importance de l’ordre provient du fait
Copyright 2011 Patrice REY

que les transformations comme la rotation et la mise à l’échelle s’effectuent


par rapport à l’origine du système de coordonnées. La mise à l’échelle d’un
objet centré à l’origine ne produit pas le même résultat que la mise à l’échelle
d’un objet qui a été éloigné de l’origine. De même, la rotation d’un objet
centré à l’origine ne produit pas le même résultat que la rotation d’un objet
qui a été éloigné de l’origine.
Les méthodes citées ci-dessus multiplient la matrice courante par une matrice
CHAPITRE 2 □ Les transformations 2D 55
de transformation dans une opération où la matrice courante occupe la
première position. Ces méthodes disposent d’une méthode équivalente
suffixée par Prepend (par exemple RotatePrepend) dans laquelle la matrice
courante occupe la deuxième position.
Voyons maintenant un exemple de calcul sur les opérations des matrices.
L’UserControl OperationMatrice.xaml dans le dossier chapitre02 visualise des
opérations sur les matrices (figure 2-7).
Figure 2-7

Examinons l’inversion de matrice. Nous avons une matrice (1,2,3,4,0,0). La


méthode HasInverse retourne true pour dire que la matrice est inversible. La
matrice inversée obtenue est (-2,1,1.5,-0.5,0,0).
Le produit d’une matrice A par une matrice inversée B donne la matrice
identité Matrix.Identity : A*B = B*A = (1,0,0,1,0,0).

// inverser une matrice:


Matrix matrice = new Matrix(1, 2, 3, 4, 0, 0);
AjouterTexte(«matrice -> (« + matrice.ToString() + «)»);
//verifier si inversible
if (matrice.HasInverse) {
AjouterTexte(«cette matrice est inversible»);
matrice.Invert();
56 La programmation graphique 2D de WPF 4
AjouterTexte(«matrice inversée -> (« + matrice.ToString() + «)»);
}

Vérifions le calcul en multipliant la matrice A par la matrice B et vérifions que


nous obtenons bien la matrice identité:
1 2 0 -2 1 0
3 4 0 1.5 -0.5 0
0 0 1 0 0 1

-2 + 2*1.5 1 - 2*0.5 0 1 0 0
= -2*3 + 4*1.5 3 - 4*0.5 0 = 0 1 0
0 0 1 0 0 1

Examinons la multiplication des matrices M1 par M2. Nous allons voir que la
multiplication de matrice n’est pas commutative c’est-à-dire que M1*M2 est
différent de M2*M1.

// multiplication de matrices
AjouterTexte(«-----------------------------------------------»);
Matrix m1 = new Matrix(1, 2, 3, 4, 0, 1);
AjouterTexte(«matrice M1 -> (« + m1.ToString() + «)»);
Matrix m2 = new Matrix(0, 1, 2, 1, 0, 1);
AjouterTexte(«matrice M2 -> (« + m2.ToString() + «)»);
Matrix m12 = Matrix.Multiply(m1, m2);
AjouterTexte(«matrice M1*M2 -> (« + m12.ToString() + «)»);
Matrix m21 = Matrix.Multiply(m2, m1);
AjouterTexte(«matrice M2*M1 -> (« + m21.ToString() + «)»);

En effectuant le calcul matriciel, nous obtenons les résultats suivants qui


correspondent bien aux calculs effectués ci-dessus:
Copyright 2011 Patrice REY

1 2 0 0 1 0 4 3 0
M1*M2 3 4 0 2 1 0 = 8 7 0
0 1 1 0 1 1 2 2 1

0 1 0 1 2 0 3 4 0
M2*M1 2 1 0 3 4 0 = 5 8 0
0 1 1 0 1 1 3 5 1
CHAPITRE 2 □ Les transformations 2D 57

3.4 - Les transformations sur les matrices

La structure Matrix dans WPF fournit des méthodes pour la mise à l’échelle, pour la
rotation et pour la translation des matrices. L’UserControl TransformationMatrice.
xaml dans le dossier chapitre02 visualise des transformations effectuées sur les
matrices (figure 2-8).
Figure 2-8

Les méthodes Scale(..) et ScalePrepend(..) permettent d’effectuer des mises à


l’échelle. La méthode Scale(..) ajoute le vecteur d’échelle spécifié à une structure
Matrix. La méthode ScalePrepend(..) ajoute le vecteur d’échelle spécifié au
début d’une structure Matrix.
Nous avons une matrice de départ matrice_depart. On crée une matrice matrice_
echelle qui sera une matrice transformée par la méthode Scale(..) et une matrice
matrice_echelle_prepend qui sera une matrice transformée par la méthode
ScalePrepend(..). Ici, on effectue une mise à l’échelle avec un facteur d’échelle
58 La programmation graphique 2D de WPF 4
de 1 selon l’axe X et un facteur d’échelle de 0.5 selon l’axe Y.

//matrice de départ
Matrix matrice_depart = new Matrix(1, 2, 3, 4, 0, 1);
AjouterTexte(«matrice de départ => (« + matrice_depart.ToString() +
«)»);
//mise a l’échelle
AjouterTexte(«Mise à l’échelle -----------------------------»);
Matrix matrice_echelle = matrice_depart;
matrice_echelle.Scale(1, 0.5);
AjouterTexte(«mise à l’échelle (Scale) => (« +
matrice_echelle.ToString() + «)»);
Matrix matrice_echelle_prepend = matrice_depart;
matrice_echelle_prepend.ScalePrepend(1, 0.5);
AjouterTexte(«mise à l’échelle (ScalePrepend) => (« +
matrice_echelle_prepend.ToString() + «)»);

Le résultat obtenu est une matrice (1,1,3,2,0,0.5) par Scale(..) et une matrice
(1,2,1.5,2,0,1) par ScalePrepend (..). Une vérification par le calcul donne :
matrice de matrice de mise matrice
départ à l’échelle résultante

1 2 1 0 1 1

3 4 0 0.5 = 3 2
0 1 0 0 0 0.5

matrice de mise matrice de matrice


à l’échelle départ résultante
1 0 1 2 1 2
0 0.5 3 4 = 1.5 2
0 0 0 1 0 1

La méthode Translate(..) permet d’effectuer une translation. Elle ajoute


Copyright 2011 Patrice REY

une traduction des offsets spécifiés à une structure Matrix. La méthode


TranslatePrepend(..) ajoute une traduction des offsets spécifiés au début d’une
structure Matrix.
Nous avons une matrice de départ matrice_depart. On crée une matrice matrice_
translation qui sera une matrice transformée par la méthode Translate(..) et une
matrice matrice_translation_prepend qui sera une matrice transformée par la
CHAPITRE 2 □ Les transformations 2D 59
méthode TranslatePrepend(..). Ici, on effectue une translation de 1 unité selon
l’axe X et de 0.5 unité selon l’axe Y.

//translation
AjouterTexte(«Translation ----------------------------------»);
Matrix matrice_translation = matrice_depart;
matrice_translation.Translate(1, 0.5);
AjouterTexte(«translation (Translate) => (« +
matrice_translation.ToString() + «)»);
Matrix matrice_translation_prepend = matrice_depart;
matrice_translation_prepend.TranslatePrepend(1, 0.5);
AjouterTexte(«translation (TranslatePrepend) => (« +
matrice_translation_prepend.ToString() + «)»);

Le résultat obtenu pour la matrice_translation est (1,2,3,4,1,1.5). Si nous vérifions


par le calcul nous obtenons:
matrice de matrice de matrice
départ translation résultante

1 2 1 0 1 2

3 4 0 1 = 3 4
0 1 1 0.5 1 1.5

matrice de matrice de matrice


translation départ résultante
1 0 1 2 1 2
0 1 3 4 = 3 4
1 0.5 0 1 2.5 5

Les méthodes Rotate(..) et RotateAt(..) sont utilisées pour faire tourner une
figure. La méthode Rotate(..) applique une rotation d’un angle spécifié par
rapport à l’origine d’une structure Matrix. Elle prend en paramètre un angle
de type double. La méthode RotateAt(..) fait pivoter une matrice par rapport au
point spécifié. Elle prend en paramètre l’angle de rotation, l’abscisse du centre
de la rotation et l’ordonnée du centre de la rotation (abscisse et ordonnée de
type double).

//rotation
AjouterTexte(«Rotation ----------------------------------»);
60 La programmation graphique 2D de WPF 4
Matrix matrice_rotation = matrice_depart;
matrice_rotation.Rotate(45);
AjouterTexte(«rotation (Rotate) => (« +
ArrondirMatrice(matrice_rotation).ToString() + «)»);
Matrix matrice_rotation_prepend = matrice_depart;
matrice_rotation_prepend.RotatePrepend(45);
AjouterTexte(«rotation (RotatePrepend) => (« +
ArrondirMatrice(matrice_rotation_prepend).ToString() + «)»);

A partir de notre matrice de départ (1,2,3,4,0,1), nous effectuons une rotation


de 45 degrés par matrice_rotation.Rotate(45) et nous obtenons une matrice
matrice_rotation avec les valeurs de (-0.707,2.121,-0.707,4.95,-0.707,0.707).
Dans le cas de la matrice matrice_rotation_prepend, nous obtenons une matrice
avec les valeurs de (2.828,4.243,1.414,1.414,0,1). Nous voyons là encore
l’importance de l’ordre des rotations puisque le résultat est différent d’un cas
à l’autre.
matrice de matrice de matrice
départ rotation résultante

1 2 cos(∏/4) sin(∏/4) -0.707 2.121

3 4 -sin(∏/4) cos(∏/4) = -0.707 4.949


0 1 0 0 -0.707 0.707

matrice de matrice de matrice


rotation départ résultante
cos(∏/4) sin(∏/4) 1 2 2.828 4.243
-sin(∏/4) cos(∏/4) 3 4 1.414 1.414
=
0 0 0 1 0 1

La méthode RotateAt(..) permet d’effectuer une rotation en indiquant un angle


de rotation et un centre de rotation. Nous avons vu précédemment qu’une
rotation (angle de 45 degrés ici) autour d’un point (ici c’est le point x=1 et
Copyright 2011 Patrice REY

y=2) consistait à effectuer 3 transformations successives: une translation pour


amener la figure à l’origine du repère, puis faire une rotation d’un angle donné,
enfin translater au point de départ.
Cela se traduit par un produit de matrices qui est appliqué à la matrice de
départ:
CHAPITRE 2 □ Les transformations 2D 61

matrice d’une rotation de 45 degrés


autour du point (1,2)

matrice de matrice de matrice de matrice


translation rotation translation résultante
1 0 cos(∏/4) sin(∏/4) 1 0 0.707 0.707
0 1 -sin(∏/4) cos(∏/4) 0 1 = -0.707 0.707
-1 -2 0 0 1 2 1.707 -0.121

matrice de matrice matrice


départ résultante résultante
1 2 0.707 0.707 -0.707 2.121
3 4 -0.707 0.707 = -0.707 4.949
0 1 1.707 -0.121 1 0.586

//rotation avec centre de rotation


AjouterTexte(«Rotation avec centre -----------------------»);
Matrix matrice_rotation_centre = matrice_depart;
matrice_rotation_centre.RotateAt(45,1,2);
AjouterTexte(«rotation avec centre (RotateAt) => (« +
ArrondirMatrice(matrice_rotation_centre).ToString() + «)»);
Matrix matrice_rotation_centre_prepend = matrice_depart;
matrice_rotation_centre_prepend.RotateAtPrepend(45,1,2);
AjouterTexte(«rotation avec centre (RotateAtPrepend) => (« +
ArrondirMatrice(matrice_rotation_centre_prepend).ToString() + «)»);

Quand on effectue le calcul, on obtient bien le même résultat, à l’arrondi près


par rapport au calcul effectué à la main.
Maintenant examinons la transformation d’inclinaison par la méthode
Skew(..). La méthode Skew(..) ajoute une inclinaison des degrés spécifiés dans
les dimensions x et y à une structure Matrix. Elle prend deux paramètres de
type double, l’angle dans la dimension x et l’angle dans la dimension y.
La transformation d’inclinaison en coordonnées homogènes d’un point (x1,y1)
en un point (x2,y2) avec une inclinaison angleX suivant l’axe X et angleY suivant
l’axe Y s’exprime par:
62 La programmation graphique 2D de WPF 4
1 tan(angleY) 0
( x2 y2 1 ) = ( x1 y1 1 ) tan(angleX) 1 0
0 0 1

= ( x1 + y1*tan(angleX) x1*tan(angleY) + y1 1)
Nous effectuons ici une inclinaison de 45 degrés suivant X et de 30 degrés
suivant Y. Le calcul des matrices de transformation
( x1 + y1*tan(angleX) donne:
x1*tan(angleY) + y1 1 )

matrice de matrice
départ d’inclinaison
1 tan(30) 1 2 1 0.577
1 2
tan(45) 1 = 3 4 1 1
3 4
0 0 0 1 0 0
0 1
3 2.577
matrice
= 7 5.732
résultante
1 1

Par programme, nous obtenons le même résultat, ce qui est normal (toujours
à l’arrondi près).

//inclinaison
AjouterTexte(«Inclinaison -----------------------»);
Matrix matrice_inclinaison = matrice_depart;
matrice_inclinaison.Skew(45, 30);
AjouterTexte(«inclinaison (Skew) => (« +
ArrondirMatrice(matrice_inclinaison).ToString() + «)»);
Matrix matrice_inclinaison_prepend = matrice_depart;
matrice_inclinaison_prepend.SkewPrepend(45, 30);
AjouterTexte(«inclinaison (SkewPrepend) => (« +
ArrondirMatrice(matrice_inclinaison_prepend).ToString() + «)»);
Copyright 2011 Patrice REY

3.5 - Dessiner une ligne perpendiculaire

Nous avons vu que les matrices de transformation ne sont pas applicables


directement à une figure mais à des points ou des vecteurs. Au travers d’un
exemple simple, nous allons voir comment appliquer ces matrices à des points
et des vecteurs. Pour cela nous allons réaliser une application qui dessine un
CHAPITRE 2 □ Les transformations 2D 63
segment entre deux points, puis qui calcule le segment perpendiculaire au
segment d’origine et passant par le point de terminaison du premier segment.
L’UserControl LignePerpendiculaire.xaml dans le dossier chapitre02 illustre cette
application (figure 2-9).
Figure 2-9

On commence par dessiner une ligne m_ligne_1 de type Line, de couleur bleue
et d’épaisseur 4, entre un point de début pt1 de coordonnées (50,50) et un
point de terminaison pt2 de coordonnées (300,200). Cette ligne est ajoutée
au Canvas x_cnv_appli. Les affichages, au format texte des coordonnées des
points, se font par l’intermédiaire de TextBlock m_texte_pt1 et m_texte_pt2.

Point pt1 = new Point();


Point pt2 = new Point();
64 La programmation graphique 2D de WPF 4
pt1.X = 50;
pt1.Y = 50;
pt2.X = 300;
pt2.Y = 200;
m_ligne_1 = new Line();
m_ligne_1.X1 = pt1.X;
m_ligne_1.Y1 = pt1.Y;
m_ligne_1.X2 = pt2.X;
m_ligne_1.Y2 = pt2.Y;
m_ligne_1.Stroke = Brushes.DarkBlue;
m_ligne_1.StrokeThickness = 4;
x_cnv_appli.Children.Add(m_ligne_1);
m_texte_pt1 = new TextBlock();
m_texte_pt1.FontSize = 14;
Canvas.SetLeft(m_texte_pt1, pt1.X);
Canvas.SetTop(m_texte_pt1, pt1.Y - 20);
m_texte_pt1.Text = «Point1 (« + pt1.X.ToString() + «,» +
pt1.Y.ToString() + «)»;
x_cnv_appli.Children.Add(m_texte_pt1);
m_texte_pt2 = new TextBlock();
m_texte_pt2.FontSize = 14;
Canvas.SetLeft(m_texte_pt2, pt2.X + 10);
Canvas.SetTop(m_texte_pt2, pt2.Y);
m_texte_pt2.Text = «Point2 (« + pt2.X.ToString() + «,» +
pt2.Y.ToString() + «)»;
x_cnv_appli.Children.Add(m_texte_pt2);

Puis on calcule la perpendiculaire au premier segment. Ce sera une ligne m_


ligne_2, de type Line, de couleur rouge, d’épaisseur 4, dessinée en trait pointillé
de longueur 1 et d’espacement 1, avec un point de début pt3 et un point de
terminaison pt4 . On choisit une longueur de 200 unités pour la longueur
du segment perpendiculaire (pt3,pt4) avec pt2 qui soit au milieu du segment
(pt3,pt4). La figure 2-10 visualise la démarche qui va être employée.
On commence par instancier un vecteur v1 allant de pt2 à pt1. On normalise
ce vecteur v1. La normalisation consiste à trouver les propriétés Vector.X et
Copyright 2011 Patrice REY

Vector.Y lorsque la longueur du vecteur Vector.Length est égale à 1. Comme le


segment perpendiculaire doit avoir une longueur de 200 et qu’il soit centré
sur pt2, cela veut dire que la longueur pt2 à pt3 doit être de 100. Donc notre
vecteur v1 normalisé est multiplié par 100, sa longueur est donc maintenant
de v1.Length=100. Nous allons appliquer à ce vecteur v1 une rotation de -90
degrés. Pour cela on crée une matrice m1 (qui est égale par défaut à la matrice
CHAPITRE 2 □ Les transformations 2D 65
identité Matrix.Identity). On effectue une rotation de -90 degrés à cette matrice
par m1.Rotate(-90). L’obtention du vecteur v1 après rotation est v1*m1. Et donc,
le point pt3 est égal au point pt2 plus le vecteur v1.
On procède de la même manière pour obtenir le point pt4, mais cette fois le
vecteur v1 normalisé et de longueur 100 subira une rotation de +90 degrés.
A la fin, le segment (pt3,pt4) a une longueur de 200 et est perpendiculaire au
segment (pt1,pt2) avec pt2 au milieu de celui-ci.
Figure 2-10

v1
O (0,0)
X

pt1 (50,50)

v1 normalisé *100

v1 normalisé

rotation pt2 (300,200)


de -90°

pt3 (249,286)

//calcul de la perpendiculaire
Vector v1 = pt1 - pt2;
Matrix m1 = new Matrix();
Point pt3 = new Point();
Point pt4 = new Point();
m1.Rotate(-90);
66 La programmation graphique 2D de WPF 4
v1.Normalize();
v1 *= 100;
m_ligne_2 = new Line();
m_ligne_2.Stroke = Brushes.Red;
m_ligne_2.StrokeThickness = 4;
m_ligne_2.StrokeDashArray = DoubleCollection.Parse(«1,1»);
pt3 = pt2 + v1 * m1;
m1 = new Matrix();
m1.Rotate(90);
pt4 = pt2 + v1 * m1;
m_ligne_2.X1 = pt3.X;
m_ligne_2.Y1 = pt3.Y;
m_ligne_2.X2 = pt4.X;
m_ligne_2.Y2 = pt4.Y;
x_cnv_appli.Children.Add(m_ligne_2);
pt3.X = Math.Round(pt3.X, 0);
pt3.Y = Math.Round(pt3.Y, 0);
pt4.X = Math.Round(pt4.X, 0);
pt4.Y = Math.Round(pt4.Y, 0);
m_texte_pt3 = new TextBlock();
m_texte_pt3.FontSize = 14;
Canvas.SetLeft(m_texte_pt3, pt3.X);
Canvas.SetTop(m_texte_pt3, pt3.Y );
m_texte_pt3.Text = «Point3 (« + pt3.X.ToString() + «,» +
pt3.Y.ToString() + «)»;
x_cnv_appli.Children.Add(m_texte_pt3);
m_texte_pt4 = new TextBlock();
m_texte_pt4.FontSize = 14;
Canvas.SetLeft(m_texte_pt4, pt4.X);
Canvas.SetTop(m_texte_pt4, pt4.Y);
m_texte_pt4.Text = «Point4 (« + pt4.X.ToString() + «,» +
pt4.Y.ToString() + «)»;
x_cnv_appli.Children.Add(m_texte_pt4);

4. Les types de transformations 2D


Copyright 2011 Patrice REY

WPF intègre un système de transformations qui permet de déplacer, réduire ou


agrandir, faire pivoter ou incliner une figure. De plus, il est possible d’appliquer
une combinaison de ces transformations. Les transformations 2D sont affines
c’est-à-dire qu’elles préservent la structure linéaire et le parallélisme de la
figure originale.
Pour appliquer ces transformations 2D sur des figures ou sur des systèmes
CHAPITRE 2 □ Les transformations 2D 67
de coordonnées, vous devez utiliser les classes dérivées de la classe Transform
(figure 2-11). La classe Transform se trouve dans l’espace de noms System.
Windows.Media (assembly PresentationCore.dll). Elle possède 6 classes dérivées
qui sont:
• la classe MatrixTransform (classe de bas niveau) crée une transformation
affine de matrice arbitraire qui permet de manipuler des objets ou des
systèmes de coordonnées dans un plan 2D.
• la classe TranslateTransform (classe de haut niveau) est une transformation
affine qui déplace les objets d’une certaine quantité suivant l’axe X et l’axe
Y.
• la classe ScaleTransform (classe de haut niveau) est une transformation
affine qui permet la mise à l’échelle par réduction ou agrandissement.
• la classe RotateTransform (classe de haut niveau) permet de faire pivoter
un objet dans le sens des aiguilles d’une montre autour d’un point spécifié
dans un système de coordonnées 2D.
• la classe SkewTransform (classe de haut niveau) permet l’inclinaison dans
un système de coordonnées 2D.
• la classe TransformGroup représente le regroupement d’un ensemble de
transformations de la classe Transform.
Figure 2-11
68 La programmation graphique 2D de WPF 4
Les classes TranslateTransform, ScaleTransform, RotateTransform et SkewTransform
sont des classes de haut niveau permettant d’effectuer une transformation
spécifique. La classe MatrixTransform est une classe de bas niveau qui permet de
réaliser des transformations directement en spécifiant des valeurs à la matrice.

4.1 - La classe MatrixTransform

La classe MatrixTransform permet d’effectuer une transformation arbitraire


à partir d’une matrice. C’est une façon de réaliser une transformation
personnalisée.
L’UserControl ClasseMatrixTransform.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet MatrixTransform sur la propriété RenderTransform d’un
rectangle (figure 2-12).
Une matrice de transformation dans un plan 2D est une matrice de 3 lignes par
3 colonnes. La propriété Matrix de MatrixTransform reçoit une structure Matrix
pour effectuer la transformation. Une structure Matrix expose dans la première
colonne les propriétés M11, M21 et OffsetX, dans la deuxième colonne M12,
M22 et OffsetY, la troisième colonne étant implicitement composée des
valeurs 0, 0 et 1.
Grâce à la propriété MatrixTransform.Matrix, il est possible de définir une
transformation personnalisée mixant les différents facteurs de la matrice.
Nous avons dans le Canvas x_cnv_appli, un rectangle x_rect_modif sur lequel
nous appliquons une matrice personnalisée sur sa propriété RenderTransform.
Ce rectangle qui va être modifié est rempli par une couleur jaune, le rectangle
en trait pointillé représentant le rectangle de départ. On affecte à la propriété
RenderTransform du rectangle une matrice personnalisée x_matrix_transform de
type MatrixTransform.

<Rectangle Name='x_rect_modif' Canvas.Top='70' Canvas.Left='100'


Width='50' Height='100' Fill='Khaki' Opacity='0.5'
Stroke='Black' StrokeThickness='2'>
Copyright 2011 Patrice REY

<Rectangle.RenderTransform>
<MatrixTransform x:Name='x_matrix_transform' />
</Rectangle.RenderTransform>
</Rectangle>

Six contrôles de type Slider permettent de changer les valeurs de la matrice


dans une plage donnée. Ils sont accompagnés d’un TextBlock pour afficher
CHAPITRE 2 □ Les transformations 2D 69
Figure 2-12
70 La programmation graphique 2D de WPF 4
leur valeur en cours d’utilisation. Le gestionnaire d’événement x_slider_
ValueChanged permet de gérer les changements de valeur.

<!-- slider M11 -->


<Slider Canvas.Left='320' Canvas.Top='103' Height='23'
Name='x_slider_m11' Width='321' Minimum='-5' Value='0'
ValueChanged='x_slider_ValueChanged' Cursor='Hand'
TickPlacement='BottomRight' LargeChange='1' Maximum='5'
Background='Cornsilk' SmallChange='1'></Slider>
<TextBlock Canvas.Left='656' Canvas.Top='105' Height='23'
Name='x_text_offset_m11' Text='M11 =' Width='102' />
<!-- slider M12 -->
<Slider Canvas.Left='320' Canvas.Top='142' Height='23'
Name='x_slider_m12' Width='321' Minimum='-5' Value='0'
ValueChanged='x_slider_ValueChanged' Cursor='Hand'
TickPlacement='BottomRight' LargeChange='1' Maximum='5'
Background='Cornsilk' SmallChange='1'></Slider>
<TextBlock Canvas.Left='656' Canvas.Top='144' Height='23'
Name='x_text_offset_m12' Text='M12 =' Width='102' />
<!-- slider M21 -->
<Slider Canvas.Left='320' Canvas.Top='180' Height='23'
Name='x_slider_m21' Width='321' Minimum='-5' Value='0'
ValueChanged='x_slider_ValueChanged' Cursor='Hand'
TickPlacement='BottomRight' LargeChange='1' Maximum='5'
Background='Cornsilk' SmallChange='1'></Slider>
<TextBlock Canvas.Left='656' Canvas.Top='182' Height='23'
Name='x_text_offset_m21' Text='M21 =' Width='102' />
<!-- slider M22 -->
<Slider Canvas.Left='320' Canvas.Top='219' Height='23'
Name='x_slider_m22' Width='321' Minimum='-5' Value='0'
ValueChanged='x_slider_ValueChanged' Cursor='Hand'
TickPlacement='BottomRight' LargeChange='1' Maximum='5'
Background='Cornsilk' SmallChange='1'></Slider>
<TextBlock Canvas.Left='656' Canvas.Top='221' Height='23'
Name='x_text_offset_m22' Text='M22 =' Width='102' />
<!-- slider offsetX -->
Copyright 2011 Patrice REY

<Slider Canvas.Left='320' Canvas.Top='258' Height='23'


Name='x_slider_offset_x' Width='321' Minimum='-200' Value='0'
ValueChanged='x_slider_ValueChanged' Cursor='Hand'
TickPlacement='BottomRight' LargeChange='25' Maximum='200'
Ticks='-200,-150,-100,-50,0,50,100,150,200'
Background='Cornsilk' SmallChange='10'></Slider>
<TextBlock Canvas.Left='656' Canvas.Top='260' Height='23'
Name='x_text_offset_x' Text='OffsetX =' Width='102' />
CHAPITRE 2 □ Les transformations 2D 71
<!-- slider offsetY -->
<Slider Background='Cornsilk' Canvas.Left='320' Canvas.Top='296'
Cursor='Hand' Height='23' LargeChange='25' Maximum='200'
Minimum='-200' Name='x_slider_offset_y' SmallChange='10'
TickPlacement='BottomRight'
Ticks='-200,-150,-100,-50,0,50,100,150,200' Value='0'
Width='321' ValueChanged='x_slider_ValueChanged' />
<TextBlock Canvas.Left='655' Canvas.Top='299' Height='23'
Name='x_text_offset_y' Text='OffsetY =' Width='102' />

La méthode VisualiserRectangle() permet d’instancier une nouvelle matrice


matrice_perso de type Matrix, d’affecter aux propriétés de la matrice les valeurs
relevées dans les différents Slider (x_slider_m11, x_slider_m12, x_slider_m21, x_
slider_m22, x_slider_offset_x et x_slider_offset_y), et d’affecter cette matrice à la
propriété Matrix de x_matrix_transform. De cette façon nous pouvons visualiser
directement l’incidence des changements de valeurs dans la matrice.
Dans le bas de la figure 2-12, nous voyons que si la matrice a les valeurs
M11=1, M12=1, M21=-1 et M22=1, cela correspond à une rotation d’un
angle de 45 degrés. Avec OffsetX=75 et OffsetY=125, on provoque en plus une
translation de 75 unités suivant X et de 125 unités suivant Y.

private void VisualiserRectangle() {


Matrix matrice_perso = new Matrix();
matrice_perso.M11 = x_slider_m11.Value;
matrice_perso.M12 = x_slider_m12.Value;
matrice_perso.M21 = x_slider_m21.Value;
matrice_perso.M22 = x_slider_m22.Value;
matrice_perso.OffsetX = x_slider_offset_x.Value;
matrice_perso.OffsetY = x_slider_offset_y.Value;
x_matrix_transform.Matrix = matrice_perso;
}

4.2 - La classe ScaleTransform

La classe ScaleTransform permet d’effectuer une mise à l’échelle par


agrandissement ou par réduction, sans avoir à indiquer une matrice de
transformation. Tout se fait en fixant les propriétés d’un objet ScaleTransform.
L’UserControl ClasseScaleTransform.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet ScaleTransform sur la propriété RenderTransform d’un
rectangle (figure 2-13).
72 La programmation graphique 2D de WPF 4
Figure 2-13

Copyright 2011 Patrice REY


CHAPITRE 2 □ Les transformations 2D 73
Par l’intermédiaire de deux contrôles de type Slider, x_slider_scale_x et x_
slider_scale_y, on visualise les effets de mise à l’échelle du rectangle. La classe
ScaleTransform expose les propriétés ScaleX, ScaleY, CenterX et CenterY pour
la mise à l’échelle:
• ScaleX définit le facteur d’échelle suivant l’axe X,
• ScaleY définit le facteur d’échelle suivant l’axe Y,
• CenterX définit la coordonnée x du point central de ce ScaleTransform,
• CenterY définit la coordonnée y du point central de ce ScaleTransform.

Pour faire varier la transformation x_scale_transform en fonction des


mouvements effectués sur les Slider, on affecte à la propriété ScaleX la valeur
Slider.Value du x_slider_scale_x, et à la propriété ScaleY la valeur Slider.Value
du x_slider_scale_y.

<Rectangle Name='x_rect_modif' Canvas.Top='120'


Canvas.Left='130' Width='50' Height='100' Fill='Khaki'
Opacity='0.5' Stroke='Black' StrokeThickness='2'>
<Rectangle.RenderTransform>
<ScaleTransform x:Name='x_scale_transform'
ScaleX='{Binding ElementName=x_slider_scale_x,Path=Value}'
ScaleY='{Binding ElementName=x_slider_scale_y,Path=Value}'
CenterX='25' CenterY='50'></ScaleTransform>
</Rectangle.RenderTransform>
</Rectangle>

Le TextBlock x_text_matrice indique en permanence la propriété ScaleTransform.


Matrix (méthode AfficherMatrice() pour visualiser les composantes de la
matrice), représentant la valeur de la matrice de transformation qui est
effectuée. Le bas de la figure 2-13 visualise une transformation dans laquelle
ScaleX=4 et ScaleY=2. Le x_text_matrice affiche la matrice correspondant.

private void AfficherMatrice() {


Matrix matrice = x_scale_transform.Value;
x_text_matrice.Text = «Matrice : « + RC;
x_text_matrice.Text += «M11= « +
Math.Round(matrice.M11, 2).ToString() + RC;
x_text_matrice.Text += «M12= « +
Math.Round(matrice.M12, 2).ToString() + RC;
x_text_matrice.Text += «M21= « +
Math.Round(matrice.M21, 2).ToString() + RC;
74 La programmation graphique 2D de WPF 4
x_text_matrice.Text += «M22= « +
Math.Round(matrice.M22, 2).ToString() + RC;
x_text_matrice.Text += «OffsetX= « +
Math.Round(matrice.OffsetX, 2).ToString() + RC;
x_text_matrice.Text += «OffsetY= « +
Math.Round(matrice.OffsetY, 2).ToString() + RC;
}

4.3 - La classe TranslateTransform

La classe TranslateTransform permet d’effectuer une translation, sans avoir à


indiquer une matrice de transformation. Tout se fait en fixant les propriétés
d’un objet TranslateTransform.
L’UserControl ClasseTranslateTransform.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet TranslateTransform sur la propriété RenderTransform d’un
rectangle (figure 2-14).
Par l’intermédiaire de deux contrôles de type Slider, x_slider_x et x_slider_y, on
visualise les effets de translation sur le rectangle. La classe TranslateTransform
expose les propriétés X et Y pour la translation:
• X définit le décalage horizontal (en DIP) suivant l’axe X,
• Y définit le décalage vertical (en DIP) suivant l’axe Y.
Pour faire varier la transformation x_transform en fonction des mouvements
effectués sur les Slider, on affecte à la propriété X la valeur Slider.Value du
x_slider_x, et à la propriété Y la valeur Slider.Value du x_slider_y.

<Rectangle Name='x_rect_modif' Canvas.Top='120'


Canvas.Left='130' Width='50' Height='100' Fill='Khaki'
Opacity='0.5' Stroke='Black' StrokeThickness='2'>
<Rectangle.RenderTransform>
<TranslateTransform x:Name='x_transform'
X='{Binding ElementName=x_slider_x,Path=Value}'
Y='{Binding ElementName=x_slider_y,Path=Value}'>
Copyright 2011 Patrice REY

</TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>

Le TextBlock x_text_matrice indique en permanence la propriété


TranslateTransform.Matrix (méthode AfficherMatrice() pour visualiser les
composantes de la matrice), représentant la valeur de la matrice de
CHAPITRE 2 □ Les transformations 2D 75
Figure 2-14
76 La programmation graphique 2D de WPF 4
transformation qui est effectuée. Le bas de la figure 2-14 visualise une
transformation dans laquelle X=60 et Y=70. Le x_text_matrice affiche la matrice
correspondant.

private void AfficherMatrice() {


Matrix matrice = x_transform.Value;
x_text_matrice.Text = «Matrice : « + RC;
x_text_matrice.Text += «M11= « +
Math.Round(matrice.M11, 2).ToString() + RC;
x_text_matrice.Text += «M12= « +
Math.Round(matrice.M12, 2).ToString() + RC;
x_text_matrice.Text += «M21= « +
Math.Round(matrice.M21, 2).ToString() + RC;
x_text_matrice.Text += «M22= « +
Math.Round(matrice.M22, 2).ToString() + RC;
x_text_matrice.Text += «OffsetX= « +
Math.Round(matrice.OffsetX, 2).ToString() + RC;
x_text_matrice.Text += «OffsetY= « +
Math.Round(matrice.OffsetY, 2).ToString() + RC;
}

4.4 - La classe RotateTransform

La classe RotateTransform permet d’effectuer une rotation, sans avoir à indiquer


une matrice de transformation. Tout se fait en fixant les propriétés d’un objet
RotateTransform.
L’UserControl ClasseRotateTransform.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet RotateTransform sur la propriété RenderTransform d’un
rectangle (figure 2-15).
Par l’intermédiaire d’un contrôle de type Slider x_slider_angle, on visualise les
effets de rotation, dans le sens des aiguilles d’une montre, sur le rectangle
dont le centre de rotation est le bord haut gauche du rectangle. La classe
RotateTransform expose les propriétés CenterX, CenterY et Angle pour la rotation:
Copyright 2011 Patrice REY

• CenterX et CenterY représentent les coordonnées relative du point d’ancrage


de la figure à partir duquel la transformation s’applique,
• Angle représente l’angle de rotation en degrés, dans le sens des aiguilles
d’une montre.
Pour faire varier la transformation x_rotate en fonction des mouvements
effectués sur le Slider, on affecte à la propriété Angle la valeur Slider.Value du
CHAPITRE 2 □ Les transformations 2D 77
Figure 2-15
78 La programmation graphique 2D de WPF 4
x_slider_angle. La position relative du centre de rotation est en CenterX=0 et
CenterY=0.

<Rectangle Name='x_rect_modif' Canvas.Top='120'


Canvas.Left='130' Width='50' Height='100' Fill='Khaki'
Opacity='0.5' Stroke='Black' StrokeThickness='2'>
<Rectangle.RenderTransform>
<RotateTransform x:Name='x_rotate'
Angle='{Binding ElementName=x_slider_angle,Path=Value}'
CenterX='0' CenterY='0'>
</RotateTransform>
</Rectangle.RenderTransform>
</Rectangle>

Le TextBlock x_text_matrice indique en permanence la propriété RotateTransform.


Matrix (méthode AfficherMatrice() pour visualiser les composantes de la
matrice), représentant la valeur de la matrice de transformation qui est
effectuée. Le bas de la figure 2-15 visualise une transformation dans laquelle
Angle=70. Le x_text_matrice affiche la matrice correspondant.

private void AfficherMatrice() {


Matrix matrice = x_rotate.Value;
x_text_matrice.Text = «Matrice : « + RC;
x_text_matrice.Text += «M11= « +
Math.Round(matrice.M11, 2).ToString() + RC;
x_text_matrice.Text += «M12= « +
Math.Round(matrice.M12, 2).ToString() + RC;
x_text_matrice.Text += «M21= « +
Math.Round(matrice.M21, 2).ToString() + RC;
x_text_matrice.Text += «M22= « +
Math.Round(matrice.M22, 2).ToString() + RC;
x_text_matrice.Text += «OffsetX= « +
Math.Round(matrice.OffsetX, 2).ToString() + RC;
x_text_matrice.Text += «OffsetY= « +
Copyright 2011 Patrice REY

Math.Round(matrice.OffsetY, 2).ToString() + RC;


}

4.5 - La classe SkewTransform

La classe SkewTransform permet d’effectuer une inclinaison, sans avoir à


indiquer une matrice de transformation. Tout se fait en fixant les propriétés
CHAPITRE 2 □ Les transformations 2D 79
d’un objet SkewTransform.
L’UserControl ClasseSkewTransform.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet SkewTransform sur la propriété RenderTransform d’un
rectangle (figure 2-16).
Par l’intermédiaire de deux contrôles de type Slider, x_slider_angle_x et x_
slider_angle_y, on visualise les effets d’inclinaison sur le rectangle dont le
centre est le bord haut gauche du rectangle. La classe SkewTransform expose
les propriétés AngleX, AngleY, CenterX et CenterY pour l’inclinaison:
• AngleX détermine l’angle d’inclinaison sur l’axe des X (en degrés et dans le
sens des aiguilles d’une montre),
• AngleY détermine l’angle d’inclinaison sur l’axe des Y (en degrés et dans le
sens des aiguilles d’une montre),
• CenterX et CenterY représentent les coordonnées relative du point d’ancrage
de la figure à partir duquel la transformation s’applique.
Pour faire varier la transformation x_skew en fonction des mouvements
effectués sur les Slider, on affecte à la propriété AngleX la valeur Slider.Value
du x_slider_angle_x, et à la propriété AngleY la valeur Slider.Value du x_slider_
angle_y. La position relative du point d’ancrage est en CenterX=0 et CenterY=0.

<Rectangle Name='x_rect_modif' Canvas.Top='120'


Canvas.Left='130' Width='50' Height='100' Fill='Khaki'
Opacity='0.5' Stroke='Black' StrokeThickness='2'>
<Rectangle.RenderTransform>
<SkewTransform x:Name='x_skew'
AngleX='{Binding ElementName=x_slider_angle_x,Path=Value}'
AngleY='{Binding ElementName=x_slider_angle_y,Path=Value}'
CenterX='0' CenterY='0'>
</SkewTransform>
</Rectangle.RenderTransform>
</Rectangle>

Le TextBlock x_text_matrice indique en permanence la propriété SkewTransform.


Matrix (méthode AfficherMatrice() pour visualiser les composantes de la
matrice), représentant la valeur de la matrice de transformation qui est
effectuée. Le bas de la figure 2-16 visualise une transformation dans laquelle
AngleX=25 et AngleY=15. Le x_text_matrice affiche la matrice correspondant.
80 La programmation graphique 2D de WPF 4
Figure 2-16

Copyright 2011 Patrice REY


CHAPITRE 2 □ Les transformations 2D 81
private void AfficherMatrice() {
Matrix matrice = x_skew.Value;
x_text_matrice.Text = «Matrice : « + RC;
x_text_matrice.Text += «M11= « +
Math.Round(matrice.M11, 2).ToString() + RC;
x_text_matrice.Text += «M12= « +
Math.Round(matrice.M12, 2).ToString() + RC;
x_text_matrice.Text += «M21= « +
Math.Round(matrice.M21, 2).ToString() + RC;
x_text_matrice.Text += «M22= « +
Math.Round(matrice.M22, 2).ToString() + RC;
x_text_matrice.Text += «OffsetX= « +
Math.Round(matrice.OffsetX, 2).ToString() + RC;
x_text_matrice.Text += «OffsetY= « +
Math.Round(matrice.OffsetY, 2).ToString() + RC;
}

4.6 - La classe TransformGroup

La classe TransformGroup définit une transformation issue du regroupement


de plusieurs transformations. Les transformations sont ajoutées à la propriété
implicite Children du TransformGroup. L’ordre dans lequel elles sont ajoutées,
est important car le rendu généré peut être différent. TransformGroup expose
les propriétés:
• Children qui définit la TransformCollection qui définit ce TransformGroup,
• Value qui obtient la structure Matrix qui décrit la transformation représentée
par ce TransformGroup.
L’UserControl ClasseTransformGroup.xaml dans le dossier chapitre02 illustre
l’utilisation d’un objet TransformGroup sur la propriété RenderTransform d’un
rectangle (figure 2-17).
L’objet TransformGroup x_group contient plusieurs transformations: un
ScaleTransform x_scale dont le centre est positionné au milieu du rectangle, et
un RotateTransform x_rotate dont le centre de rotation est positionné au milieu
du rectangle.

<Rectangle Name='x_rect_modif' Canvas.Top='120'


Canvas.Left='130' Width='50' Height='100' Fill='Khaki'
Opacity='0.5' Stroke='Black' StrokeThickness='2'>
<Rectangle.RenderTransform>
<TransformGroup x:Name='x_group'>
82 La programmation graphique 2D de WPF 4
Figure 2-17

Copyright 2011 Patrice REY


CHAPITRE 2 □ Les transformations 2D 83
<ScaleTransform x:Name='x_scale' CenterX='25'
CenterY='50' />
<RotateTransform x:Name='x_rotate' CenterX='25'
CenterY='50' />
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>

Nous allons animer ce rectangle en changeant les valeurs de mise à l’échelle


et de l’angle de rotation. Pour cela, quand le Canvas est chargé, on déclenche
un Storyboard pour animer des propriétés. En premier, on anime les propriétés
de dépendance ScaleX et ScaleY de la transformation x_scale. En deuxième, on
anime la propriété de dépendance Angle de la transformation x_rotate. Le bas
de la figure 2-17 visualise quelques écrans au cours de l’animation.

<!-- animation -->


<Canvas.Triggers>
<EventTrigger RoutedEvent='Canvas.Loaded'>
<BeginStoryboard>
<Storyboard RepeatBehavior='Forever'
AutoReverse='True'>
<DoubleAnimation Storyboard.TargetName='x_scale'
Storyboard.TargetProperty='ScaleX'
From='0' To='3' Duration='0:0:5' />
<DoubleAnimation Storyboard.TargetName='x_scale'
Storyboard.TargetProperty='ScaleY'
From='0' To='3' Duration='0:0:5' />
<DoubleAnimation
Storyboard.TargetName='x_rotate'
Storyboard.TargetProperty='Angle'
From='0' To='360' Duration='0:0:5' />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
CHAPITRE 3

Les géométries et les tracés 2D

D ans ce chapitre nous allons voir comment créer et manipuler des objets
graphiques 2D complexes par l’utilisation d’une importante et puissante
classe qu’est la classe Path. Nous verrons comment développer des figures 2D
de façon interactive, en utilisant la classe Geometry. Nous aborderons à la fin
de ce chapitre la façon de réaliser des figures personnalisées, réutilisables dans
toute application WPF.

1 - Utilisation des classes Path et Geometry

Au chapitre 1 paragraphe 2, nous avons volontairement laisser de coté la


classe Path, qui est une classe dérivée de Shape. La classe Path se trouve dans
l’espace de noms System.Windows.Shapes (assembly PresentationFramework.dll).
Cette classe permet de dessiner une série de lignes et de courbes connectées.
Ces lignes et courbes sont décrites au moyen d’objet de type Geometry. Pour
utiliser un objet Path, vous devez créer un objet Geometry et l’utiliser pour
l’affecter à la propriété Data de l’objet Path. Vous ne pouvez pas dessiner
directement à l’écran un objet Geometry parce que la classe Geometry est une
classe abstraite. La géométrie définit la forme d’une figure, et le Path en assure
le rendu visuel.
La classe Geometry se trouve dans l’espace de noms System.Windows.Media
(assembly PresentationCore.dll). Elle possède 7 classes dérivées qui sont (figure
3-1):
• LineGeometry qui représente une ligne droite reliant deux points donnés,
• RectangleGeometry qui représente un rectangle de dimensions données,
• EllipseGeometry qui représente une ellipse inscrite dans un rectangle fictif,
• PathGeometry qui représente un ensemble de figures (classe PathFigure)
composées chacune d’une suite de segments droits ou courbes connectés,
• StreamGeometry qui représente une géométrie équivalente à PathGeometry
mais optimisée et en lecture seule (à utiliser de préférence pour définir un
objet Path qui ne sera pas modifié par la suite),
• GeometryGroup qui représente une géométrie issue du regroupement de
86 La programmation graphique 2D de WPF 4
plusieurs géométries,
• CombinedGeometry qui représente une géométrie issue de la combinaison
de deux géométries au moyen d’une opération binaire (union, intersection,
OU exclusif, exclusion).
Figure 3-1

1.1 - Les géométries de type ligne, rectangle et ellipse

Les classes LineGeometry, RectangleGeometry et EllipseGeometry fonctionnent


Copyright 2011 Patrice REY

de la même manière que les classes Line, Rectangle et Ellipse vues dans les
précédents chapitres.
En xaml, une ligne de type Line peut être dessinée par un objet Path dont sa
propriété Data sera affectée d’un objet LineGeometry:

<Line X1='50' Y1='50' X2 ='100' Y2='100'


CHAPITRE 3 □ Les géométries et les tracés 2D 87
Stroke='Blue' StrokeThickness='2'/>

<Path Stroke='Blue' StrokeThickness='2'>


<Path.Data>
<LineGeometry StartPoint='50 50' EndPoint='100 100'/>
</Path.Data>
</Path>

La principale différence est qu’une figure de type Line expose des propriétés
X1, X2, Y1 et Y2 alors qu’une géométrie de type LineGeometry expose des
propriétés StartPoint et EndPoint.
On procède de façon similaire pour un rectangle. Un rectangle de type Rectangle
peut être dessiné par un objet Path dont sa propriété Data sera affectée d’un
objet RectangleGeometry:

<Rectangle Fill= «Gray' Stroke='Blue' StrokeThickness='2'


Width='100' Height='50'/>

<Path Fill='Gray' Stroke='Blue' StrokeThickness='2'>


<Path.Data>
<RectangleGeometry Rect='0,0,100,50'/>
</Path.Data>
</Path>

On voit bien que l’objet Rectangle expose les propriétés Width et Height, alors
que RectangleGeometry expose la propriété Rect (composée de 4 valeurs qui
correspondent, pour les deux premières aux coordonnées du coin supérieur
haut gauche, et pour les deux dernières à la largeur et la hauteur). Il en est de
même pour une ellipse:

<Ellipse Fill='Gray' Stroke='Blue' StrokeThickness='2'


Width='100' Height='50'/>

<Path Fill='Gray' Stroke='Blue' StrokeThickness='2'>


<Path.Data>
<EllipseGeometry RadiuX='50' RadiusY='25'
Center='10,10'/>
</Path.Data>
</Path>
88 La programmation graphique 2D de WPF 4
A remarquer que les valeurs des propriétés RadiusX et RadiusY de EllipseGeometry
sont divisées par deux par rapport aux propriétés Width et Height de Ellipse.
Même si on peut convertir une figure Shape sous forme d’un Path, l’utilisation
pratique de Path permet de réaliser des figures compliquées sous forme de
combinaisons de géométries.

1.2 - La classe GeometryGroup

La façon la plus simple de combiner des géométries est d’utiliser un objet


GeometryGroup. La valeur ajoutée des géométries se trouve dans la réalisation
de figures plus complexes, au sein d’un GeometryGroup regroupant des
géométries basiques. Par exemple, voici le code xaml qui visualise deux
cercles:

<Path Fill='LightGray' Stroke='Blue' StrokeThickness='2'>


<Path.Data>
<GeometryGroup FillRule='Nonzero'>
<EllipseGeometry RadiusX='50' RadiusY='50'
Center='120,120'/>
<EllipseGeometry RadiusX='30' RadiusY='30'
Center='120,120'/>
</GeometryGroup>
</Path.Data>
</Path>

Au lieu d’utiliser deux éléments Path avec dans chacun d’eux, un objet
EllipseGeometry, avec un GeometryGroup, on les regroupe ensemble. Il faut voir
aussi qu’avec ce type de code xaml, on a la possibilité de le réutiliser dans
d’autres parties du programme, et aussi de le mettre comme une ressource
réutilisable.
L’UserControl ClasseGeometryGroup.xaml dans le dossier chapitre03 illustre
l’utilisation d’un objet GeometryGroup (figure 3-2).
Copyright 2011 Patrice REY

Dans cet exemple nous créons deux objets de type GeometryGroup, r_


GeometryNonZero et r_GeometryEvenOdd, qui sont composés de deux objets
EllipseGeometry. Le premier GeometryGroup a la propriété FillRule fixée à Nonzero,
et le second a la propriété FillRule fixée à EvenOdd. Ces objets GeometryGroup
sont ajoutés comme des ressources (propriété UserControl.Resources).
CHAPITRE 3 □ Les géométries et les tracés 2D 89
Figure 3-2

<UserControl.Resources>
...
<GeometryGroup x:Key='r_GeometryNonzero' FillRule='Nonzero'>
<EllipseGeometry RadiusX='50' RadiusY='50' Center='65,60' />
<EllipseGeometry RadiusX='30' RadiusY='30' Center='65,60' />
</GeometryGroup>
<GeometryGroup x:Key='r_GeometryEvenOdd' FillRule='EvenOdd'>
<EllipseGeometry RadiusX='50' RadiusY='50' Center='65,60' />
<EllipseGeometry RadiusX='30' RadiusY='30' Center='65,60' />
</GeometryGroup>
</UserControl.Resources>
90 La programmation graphique 2D de WPF 4
Sur le Canvas x_cnv_appli, on ajoute un objet Path, rempli par la couleur bleue
et avec une géométrie de type r_GeometryNonZero affectée à sa propriété
Data, puis un autre objet Path, rempli par la couleur or et avec une géométrie
de type r_GeometryNonZero affectée à sa propriété Data. Pour utiliser une
ressource, il faut lui faire référence par l’intermédiaire de StaticResource
(on écrit Data=»{StaticResource r_GeometryNonzero}»). On effectue la même
démarche pour créer deux autres objets Path avec une géométrie de type r_
GeometryEvenOdd.

<!-- fillrule nonzero -->


<Path Fill='LightSteelBlue' Stroke='Blue' StrokeThickness='2'
Data='{StaticResource r_GeometryNonzero}'
Canvas.Left='108' Canvas.Top='38' />
<Path Fill='Gold' Stroke='Red' StrokeThickness='2'
Canvas.Left='279'
Data='{StaticResource r_GeometryNonzero}' Canvas.Top='38' />
<!-- fillrule evenodd -->
<Path Fill='LightSteelBlue' Stroke='Blue' StrokeThickness='2'
Data='{StaticResource r_GeometryEvenOdd}'
Canvas.Left='108' Canvas.Top='181' />
<Path Fill='Gold' Stroke='Red' StrokeThickness='2'
Canvas.Left='279'
Data='{StaticResource r_GeometryEvenOdd}' Canvas.Top='181' />

On constate donc l’effet produit par les deux règles FillRule=Nonzero et


FillRule=EvenOdd. La coloration des zones imbriquées suit la règle de
remplissage choisie (Nonzero ou bien EvenOdd).

1.3 - La classe CombinedGeometry

Des figures particulières peuvent être obtenues par la combinaison binaire


de deux géométries au moyen d’un objet CombinedGeometry. La propriété
Geometry1 de CombinedGeometry reçoit la première géométrie et la propriété
Copyright 2011 Patrice REY

Geometry2 reçoit la seconde géométrie. L’opération binaire est définie au


moyen de la propriété GeometryCombineMode qui peut prendre les valeurs du
type énuméré correspondant:
• Union (valeur par défaut) qui consiste en une fusion des deux géométries,
• Xor (c’est le OU exclusif) consiste en une fusion des deux géométries sans
leur partie commune,
CHAPITRE 3 □ Les géométries et les tracés 2D 91
• Intersect consiste en une fusion des deux géométries mais en gardant
uniquement leurs zones communes,
• Exclude consiste à garder la première géométrie à l’exclusion de la zone
commune avec la deuxième.
L’UserControl ClasseCombinedGeometry.xaml dans le dossier chapitre03 illustre
l’utilisation d’un objet CombinedGeometry (figure 3-3).
Figure 3-3

Dans notre exemple, nous combinons deux cercles de type EllipseGeometry,


et selon un mode particulier GeometryCombineMode. La première combinaison
utilise le mode GeometryCombineMode = Union.
92 La programmation graphique 2D de WPF 4
<!-- union -->
<Path Fill='Gold' Stroke='DarkBlue' Canvas.Left='89'
Canvas.Top='54' StrokeThickness='2'>
<Path.Data>
<CombinedGeometry GeometryCombineMode='Union'>
<CombinedGeometry.Geometry1>
<EllipseGeometry Center='50,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center='80,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

La deuxième combinaison utilise le mode GeometryCombineMode = Xor.

<!-- xor -->


<Path Fill='Gold' Stroke='DarkBlue' Canvas.Left='264'
Canvas.Top='54' StrokeThickness='2'>
<Path.Data>
<CombinedGeometry GeometryCombineMode='Xor'>
<CombinedGeometry.Geometry1>
<EllipseGeometry Center='50,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center='80,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Copyright 2011 Patrice REY

La troisième combinaison utilise le mode GeometryCombineMode = Intersect.

<!-- intersect -->


<Path Fill='Gold' Stroke='DarkBlue' Margin='5,0,0,0'
Canvas.Left='418' Canvas.Top='56' StrokeThickness='2'>
<Path.Data>
CHAPITRE 3 □ Les géométries et les tracés 2D 93
<CombinedGeometry GeometryCombineMode='Intersect'>
<CombinedGeometry.Geometry1>
<EllipseGeometry Center='50,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center='80,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

La quatrième combinaison utilise le mode GeometryCombineMode = Exclude.

<!-- exclude -->


<Path Fill='Gold' Stroke='DarkBlue' Margin='10,0,0,0'
Canvas.Left='572' Canvas.Top='54' StrokeThickness='2'>
<Path.Data>
<CombinedGeometry GeometryCombineMode='Exclude'>
<CombinedGeometry.Geometry1>
<EllipseGeometry Center='50,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center='80,50' RadiusX='50'
RadiusY='50' />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

1.4 - La classe PathGeometry

La classe PathGeometry (figure 3-4) permet de constituer des figures évoluées au


moyen de segments de différents types. Un objet PathGeometry représente un
ensemble de figures de type PathFigure. Cet ensemble est affecté à sa propriété
Figures. Les différents types de segments sont assemblés au sein d’un objet
PathFigure. Sa propriété Segments contient une collection de PathSegment.
Autrement dit, un PathGeometry est composé d’une ou de plusieurs illustrations
représentées par la classe PathFigure. Chaque illustration est elle-même
94 La programmation graphique 2D de WPF 4
composée d’un ou de plusieurs segments définis par la classe PathSegment.
Figure 3-4

Copyright 2011 Patrice REY

La classe PathFigure expose quatre propriétés:


• StartPoint détermine le point d’origine du premier segment,
• Segments définit la collection des segments qui définissent la forme de
l’objet PathFigure; chaque segment dispose d’une propriété indiquant son
point destination; à partir du deuxième segment, le point d’origine d’un
CHAPITRE 3 □ Les géométries et les tracés 2D 95
segment correspond au point de destination de son prédécesseur,
• IsClosed indique si un segment relie automatiquement au point d’origine
le point de destination du dernier segment spécifié,
• IsFilled définit si la zone restreinte de ce PathFigure est utilisée pour le test
d’atteinte, le rendu et le découpage.
Un PathSegment représente un segment d’un objet PathFigure. La classe
PathSegment est une classe abstraite. Les classes qui dérivent de PathSegment
représentent des types spécifiques de segments géométriques. Ces classes
spécifiques sont:
• LineSegment qui représente une ligne entre deux points,
• PolyLineSegment qui représente un ensemble de segments de ligne défini
par une collection de type PointCollection, chaque point spécifiant le point
de terminaison d’un segment de ligne,
• ArcSegment qui représente un arc elliptique entre deux points,
• BezierSegment qui représente une courbe de Bézier cubique dessinée entre
deux points,
• PolyBezierSegment qui représente une ou plusieurs courbes de Bézier
cubiques,
• QuadraticBezierSegment qui représente une courbe de Bézier quadratique
entre deux points,
• PolyQuadraticBezierSegment qui représente un jeu de segments Bézier
quadratiques.
L’UserControl ClassePathGeometry.xaml dans le dossier chapitre03 illustre
l’utilisation d’un objet PathGeometry (figure 3-5).
Par exemple, la première figure représente un PathFigure dont le point de
départ est (10,10), qui est composé d’un LineSegment avec la propriété Point
égale à (150,20), puis d’un autre LineSegment avec Point (100,40).

<Path Stroke=' DarkBlue « StrokeThickness='2' >


<Path.Data>
<PathGeometry>
<PathFigure StartPoint='10,10'>
<LineSegment Point='150,20'/>
<LineSegment Point='100,40'/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
96 La programmation graphique 2D de WPF 4
Figure 3-5

Copyright 2011 Patrice REY

Avec l’écriture du mini-langage, notre PathFigure s’exprimera de la façon


suivante (nous verrons plus loin le détail du mini-langage):

<!-- pathgeometry ecrit sous la forme mini langage -->


CHAPITRE 3 □ Les géométries et les tracés 2D 97
<Path Stroke='DarkBlue' Data='M10,10 L150,20 L100,40'
StrokeThickness='2' Canvas.Left='4' Canvas.Top='6'></Path>

Dans le deuxième exemple, un PathFigure est composé d’un PolyLineSegment.


Le PathFigure commence avec StartPoint de (100,120). Puis un PolyLineSegment
est composé d’une collection de points dans sa propriété Points (Points =
«200,120 200,220 100,170»).

<Path Stroke='DarkBlue' StrokeThickness='2' >


<Path.Data>
<PathGeometry>
<PathFigure StartPoint='100,120'>
<PolyLineSegment
Points='200,120 200,220 100,170'/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>

Dans la troisième figure, nous construisons une ellipse avec deux arcs de
cercle. Le premier PathFigure commence à StartPoint (100,50). On lui apporte
un ArcSegment avec la propriété Point égale à (200,50) qui représente le point
de terminaison de l’arc elliptique, Size égale à (50,30) qui représente les rayons
suivant X et Y de l’arc elliptique, enfin SweepDirection égale à Counterclockwise
qui représente le sens contraire des aiguilles d’une montre. Le deuxième
ArcSegment apporté au PathFigure, est dessiné avec le sens des aiguilles d’une
montre (Clockwise).

<Path Stroke='DarkBlue' StrokeThickness='2' Canvas.Left='-75'


Canvas.Top='200'>
<Path.Data>
<PathGeometry>
<PathFigure StartPoint='100,50'>
<ArcSegment Point='200,50' Size='50,30'
SweepDirection='Counterclockwise' />
</PathFigure>
<PathFigure StartPoint='100,50'>
<ArcSegment Point='200,50' Size='50,30'
SweepDirection='Clockwise' />
</PathFigure>
98 La programmation graphique 2D de WPF 4
</PathGeometry>
</Path.Data>
</Path>

Dans la quatrième figure, nous réalisons une courbe de Bézier (courbe


cubique). Comme tous les segments, un objet BezierSegment est constitué
d’un point d’origine (qui est le point de destination du segment précédent ou
bien le StartPoint du PathFigure) et d’un point de destination Point3. De plus il
comporte deux points de contrôle, Point1 qui est associé au point d’origine, et
Point2 qui est associé au point de destination.

<Path Stroke='Black' StrokeThickness='5' Canvas.Left='223'


Canvas.Top='324'>
<Path.Data>
<PathGeometry>
<PathFigure StartPoint='20,20'>
<BezierSegment x:Name='x_bezier_segment'
Point1='150,50' Point2='60,160'
Point3='250,230' />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>

Pour matérialiser les points de contrôle Point2 et Point3, nous réalisons deux
lignes de type LineGeometry, x_ligne_1 et x_ligne_2, ainsi que deux ellipses de
type EllipseGeometry pour matérialiser leur position (x_ellipse_1 et x_ellipse_2).

<Path x:Name='path1' Fill='Red' Stroke='Red' Canvas.Left='224'


Canvas.Top='324'>
<Path.Data>
<GeometryGroup>
<LineGeometry x:Name='x_ligne_1' StartPoint='20,20'
Copyright 2011 Patrice REY

EndPoint='150,50' />
<EllipseGeometry x:Name='x_ellipse_1'
Center='150,50' RadiusX='5' RadiusY='5' />
<LineGeometry x:Name='x_ligne_2' StartPoint='60,160'
EndPoint='250,230' />
<EllipseGeometry x:Name='x_ellipse_2'
Center='60,160' RadiusX='5' RadiusY='5' />
</GeometryGroup>
CHAPITRE 3 □ Les géométries et les tracés 2D 99
</Path.Data>
</Path>

Avec une animation de type Storyboard, nous faisons varier la position des
points de contrôles Point2 et Point3 de x_bezier_segment, en utilisant une
animation de type PointAnimation qui cible une propriété de type double. Nous
voyons ainsi au cours du temps l’influence des points de contrôles sur la forme
de la courbe de Bézier (figure 3-6).
Figure 3-6

déplacement du point de
contrôle horizontalement

déplacement
du point de
contrôle
verticalement

<Canvas.Triggers>
<EventTrigger RoutedEvent='Canvas.Loaded'>
<BeginStoryboard>
<Storyboard RepeatBehavior='Forever'
AutoReverse='True'>
<PointAnimation
Storyboard.TargetName='x_bezier_segment'
Storyboard.TargetProperty='Point1'
From='50 20' To='250 20'
Duration='0:0:5' />
<PointAnimation
Storyboard.TargetName='x_ligne_1'
Storyboard.TargetProperty='EndPoint'
From='50 20' To='250 20'
Duration='0:0:5' />
<PointAnimation
Storyboard.TargetName='x_ellipse_1'
Storyboard.TargetProperty='Center'
100 La programmation graphique 2D de WPF 4
From='50 20' To='250 20'
Duration='0:0:5' />
<PointAnimation
Storyboard.TargetName='x_bezier_segment'
Storyboard.TargetProperty='Point2'
From='60 50' To='60 250'
Duration='0:0:5' />
<PointAnimation
Storyboard.TargetName='x_ligne_2'
Storyboard.TargetProperty='StartPoint'
From='60 50' To='60 250'
Duration='0:0:5' />
<PointAnimation
Storyboard.TargetName='x_ellipse_2'
Storyboard.TargetProperty='Center'
From='60 50' To='60 250'
Duration='0:0:5' />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>

1.5 - La syntaxe xaml pour les tracés

WPF inclut deux classes qui utilisent un mini-langage permettant de décrire


des tracés géométriques qui sont StreamGeometry et PathFigureCollection. Vous
utilisez le mini-langage StreamGeometry lorsque vous définissez une propriété
du type Geometry, telle que la propriété Clip d’un UIElement ou la propriété
Data d’un élément Path. L’exemple suivant utilise la syntaxe d’attribut pour
créer un StreamGeometry:

<Path Stroke='Black' Fill='Gray'


Data='M 10,100 C 10,300 300,-200 300,100' />
Copyright 2011 Patrice REY

Vous utilisez le mini-langage PathFigureCollection lorsque vous définissez la


propriété Figures d’un PathGeometry. L’exemple suivant utilise une syntaxe
d’attribut pour créer un PathFigureCollection pour un PathGeometry:

<Path Stroke='Black' Fill='Gray'>


<Path.Data>
<PathGeometry Figures='M 10,100 C 10,300 300,-200 300,100' />
CHAPITRE 3 □ Les géométries et les tracés 2D 101
</Path.Data>
</Path>

Comme les exemples précédents le montrent, les deux mini-langages sont


très similaires. Dans tous les cas où vous pouvez utiliser un PathGeometry,
vous pourriez également utiliser un StreamGeometry. Utilisez un objet
StreamGeometry lorsque vous n’avez pas besoin de modifier le tracé après
l’avoir créé et un objet PathGeometry dans le cas contraire. La syntaxe du mini-
langage est la suivante:
• pour FillRule: F0 spécifie la règle de remplissage EvenOdd, et F1 spécifie la
règle de remplissage Nonzero.
• Pour le point de départ: M ou m suivi des coordonnées du point.
• Pour une ligne: L ou l suivi des coordonnées d’un point, pour dessiner une
ligne du point précédent à ce point indiqué.
• Pour une ligne horizontale: H ou h suivi de la coordonnée x, pour dessiner
une ligne horizontale du point précédent à la coordonnée x précisée.
• Pour une ligne verticale: V ou v suivi de la coordonnée y, pour dessiner une
ligne verticale du point précédent à la coordonnée y précisée.
• Pour une courbe de Bézier cubique: C ou c suivi de Point1, Point2 et Point3,
pour dessiner une courbe de Bézier du point précédent au point Point3 en
utilisant les points de contrôles Point1 et Point2.
• Pour une courbe de Bézier quadratique: Q ou q suivi de ControlPoint et
de EndPoint, pour dessiner une courbe de Bézier quadratique du point
précédent au point EndPoint en utilisant le point de contrôle ControlPoint.
• Pour une courbe de Bézier cubique lissée: S ou s suivi de ControlPoint2 et
de EndPoint, pour dessiner une courbe de Bézier cubique lissée du point
précédent au point EndPoint en utilisant le point de contrôle ControlPoint2
qui détermine la tangente de fin de la courbe.
• Pour une courbe de Bézier quadratique lissée: T ou t suivi de ControlPoint et
de EndPoint, pour dessiner une courbe de Bézier quadratique lissée du point
précédent au point EndPoint en utilisant le point de contrôle ControlPoint
qui détermine le début de la tangente de la courbe.
• Pour dessiner une arc elliptique: A ou a suivi de Size, RotationAngle,
IsLargeArcFlag, SweepDirectionFlag et EndPoint, pour dessiner un arc
elliptique du point précédent au point EndPoint, avec Size pour les rayons
x et y, RotationAngle pour la rotation de l’ellipse en degrés, IsLargeArcFlag
fixé à 1 si l’angle de l’arc doit être égal ou supérieur à 180 degrés ou bien
102 La programmation graphique 2D de WPF 4
fixé à 0, SweepDirectionFlag fixé à 1 si l’arc est dessiné en direction d’un
angle positif sinon fixé à 0.
• Pour terminer la figure: Z ou z.
Les commandes en majuscule indiquent des coordonnées absolues alors qu’en
minuscule, elles indiquent des coordonnées relatives.

2 - Des figures 2D interactives

Le dessin de figures complexes passe par la manipulation et l’interactivité


comme dans les logiciels de conception assistée par ordinateur (CAO). On
réalise une pièce particulière, puis une autre pièce, et ensuite on les assemble
pour en faire une pièce plus complexe et ainsi de suite. C’est pourquoi
l’interactivité est la phase complémentaire du dessin de base. Nous allons
apprendre et apprécier cette interactivité au travers d’un exemple simple qui
donne les bases à impérativement savoir manipuler.
L’UserControl ApplicationFigure.xaml dans le dossier chapitre03 illustre
l’utilisation de cette interactivité (figure 3-7). Comme vous pouvez le voir
sur la figure, dessiner un bonhomme semble simple à première vue. Pourtant
l’agencement de toutes ces figures de base et leurs combinaisons nécessite un
certain travail et une démarche particulière. Tracer un carré, le déplacer sur
le Canvas, tracer un rectangle, le déplacer à coté du carré, combiner la carré
et le rectangle pour en faire une forme composite unique, etc., ce sont des
pratiques à connaitre et à savoir implémenter.

2.1 - Dessiner des formes basiques

Dans ce programme, nous devons pouvoir dessiner des formes basiques


comme le carré, le rectangle, le cercle et l’ellipse. Nous utilisons un conteneur
de type ToolBar qui permet de grouper des commandes et des contrôles. Dans
ce conteneur x_toolbar, nous ajoutons des contrôles de type RadioButton.
Copyright 2011 Patrice REY

Un RadioButton représente un bouton qui peut être sélectionné mais pas


désélectionné par un utilisateur. Vous pouvez définir la propriété IsChecked
d’une case d’option RadioButton en cliquant sur celle-ci, mais vous ne pouvez
la désélectionner que par programme. Avec un RadioButton représentant un
carré, quand sa propriété IsChecked est à true, on peut alors dessiner un carré.
C’est ce principe que l’on utilise pour choisir la forme basique à dessiner sur
le Canvas. Chaque RadioButton (d_carre, d_rectangle, d_cercle et d_ellipse) reçoit
CHAPITRE 3 □ Les géométries et les tracés 2D 103
Figure 3-7
104 La programmation graphique 2D de WPF 4
un tracé dans sa propriété implicite ContentControl qui représente le tracé à
dessiner.

<ToolBar x:Name='x_toolbar' Canvas.Left='13' Canvas.Top='70'>


<RadioButton x:Name='d_carre' IsChecked='True' ToolTip='carré'>
<Rectangle Width='30' Height='30' Stroke='Black'
Fill='LightSteelBlue' StrokeThickness='2' />
</RadioButton>
<RadioButton x:Name='d_rectangle' IsChecked='False'
ToolTip='rectangle'>
<Rectangle Width='50' Height='20' Stroke='Black'
Fill='LightSteelBlue' StrokeThickness='2' />
</RadioButton>
<RadioButton x:Name='d_cercle' IsChecked='False' ToolTip='cercle'>
<Ellipse Width='30' Height='30' Stroke='Black'
Fill='LightSteelBlue' StrokeThickness='2' />
</RadioButton>
<RadioButton x:Name='d_ellipse' IsChecked='False' ToolTip='ellipse'>
<Ellipse Width='30' Height='20' Stroke='Black'
Fill='LightSteelBlue' StrokeThickness='2' />
</RadioButton>
...
</ToolBar>

Toutes les actions vont se dérouler quand le curseur de la souris est sur le
Canvas x_cnv_appli. En effet, pour dessiner un carré, il faut que le bouton
gauche de la souris soit enfoncé quand il se trouve sur le Canvas. Ce bouton
doit être maintenu enfoncé pour donner une taille à la forme. Enfin il doit être
relâché pour signifier que la forme doit être générée. Il faut donc ajouter au
Canvas les gestionnaires d’événements MouseLeftButtonDown, MouseMove et
MouseLeftButtonUp.
Nous allons voir en premier la démarche pour dessiner un carré. Il en sera de
même pour dessiner les autres formes basiques. On commence par cliquer
une fois sur le RadioButton affichant le carré, il est donc sélectionné. Le bouton
Copyright 2011 Patrice REY

gauche de la souris, enfoncé sur le Canvas, est le point de départ du traçage. Si la


capture de la souris sur le Canvas n’est pas active (x_cnv_appli.IsMouseCaptured
== false), on relève la position de la souris sur le Canvas dans v_point_de_depart
par GetPosition(..). Puis, si un des RadioButton de dessin des formes basiques est
coché (d_carre.IsChecked == true) alors on démarre la capture de la souris sur le
Canvas par x_cnv_appli.CaptureMouse().
CHAPITRE 3 □ Les géométries et les tracés 2D 105
private void x_cnv_appli_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
if (!x_cnv_appli.IsMouseCaptured) {
v_point_depart = e.GetPosition(x_cnv_appli);
if (d_carre.IsChecked == true || d_rectangle.IsChecked == true ||
d_cercle.IsChecked == true || d_ellipse.IsChecked == true) {
x_cnv_appli.CaptureMouse();
}
...
}

Dans le gestionnaire MouseMove du Canvas, si la capture de la souris est active


(x_cn_appli.IsMouseCaptured == true), on relève le point courant v_point_courant
de la souris par GetPosition(..). Pour matérialiser les dimensions de la figure
que l’on souhaite dessiner, on va créer un ruban de couleur Brushes.LightCoral,
d’épaisseur 1, en trait pointillé (StrokeDashArray = «4,2»). Si le ruban m_ruban_
pointille n’existe pas (valeur null), on l’instancie et on l’ajoute au Canvas. Puis
on calcule sa taille (Width et Height) et sa position (Left et Top) pour le fixer sur
le Canvas en fonction de v_point_courant et de v_point_depart.

private void x_cnv_appli_MouseMove(object sender, MouseEventArgs e) {


if (x_cnv_appli.IsMouseCaptured) {
v_point_courant = e.GetPosition(x_cnv_appli);
if (m_ruban_pointille == null) {
m_ruban_pointille = new Rectangle();
m_ruban_pointille.Stroke = Brushes.LightCoral;
m_ruban_pointille.StrokeDashArray =
new DoubleCollection(new double[] { 4, 2 });
if (d_carre.IsChecked == true || d_rectangle.IsChecked == true ||
d_cercle.IsChecked == true || d_ellipse.IsChecked == true) {
x_cnv_appli.Children.Add(m_ruban_pointille);
}
}
double width = Math.Abs(v_point_depart.X - v_point_courant.X);
double height = Math.Abs(v_point_depart.Y - v_point_courant.Y);
double left = Math.Min(v_point_depart.X, v_point_courant.X);
double top = Math.Min(v_point_depart.Y, v_point_courant.Y);
m_ruban_pointille.Width = width;
m_ruban_pointille.Height = height;
Canvas.SetLeft(m_ruban_pointille, left);
Canvas.SetTop(m_ruban_pointille, top);
} }
106 La programmation graphique 2D de WPF 4
Quand on relâche le bouton gauche de la souris, on dessine la forme
sélectionnée par une des méthodes DessinerUnCarre(..), DessinerUnRectangle(..),
DessinerUnCercle(..) ou DessinerUneEllipse(..). Ces méthodes dessinent la forme
choisie en fonction du relevé de deux points (le point haut gauche et le point
bas droit d’un rectangle fictif), et ajoutent cette forme à la liste m_liste_trace
contenant les tracés dessinés. Puis on retire du Canvas le ruban qui a servi de
support pour visualiser le traçage de la forme.

private void x_cnv_appli_MouseLeftButtonUp(object sender,


MouseButtonEventArgs e) {
if (d_carre.IsChecked == true) {
DessinerUnCarre(v_point_depart, v_point_courant);
}
if (d_rectangle.IsChecked == true) {
DessineUnRectangle(v_point_depart, v_point_courant);
}
if (d_cercle.IsChecked == true) {
DessinerUnCercle(v_point_depart, v_point_courant);
}
if (d_ellipse.IsChecked == true) {
DessinerUneEllipse(v_point_depart, v_point_courant);
}
if (m_ruban_pointille != null) {
x_cnv_appli.Children.Remove(m_ruban_pointille);
m_ruban_pointille = null;
x_cnv_appli.ReleaseMouseCapture();
}
...
}

La méthode DessinerUnCarre(..) reçoit en paramètre deux points pt1 et pt2


pour effectuer le traçage d’un carré. Son implémentation est:

private void DessinerUnCarre(Point pt1, Point pt2) {


Copyright 2011 Patrice REY

Path trace = new Path();


trace.Fill = m_coul_remplissage;
trace.Stroke = m_coul_bordure;
RectangleGeometry rg = new RectangleGeometry();
double width = Math.Abs(pt1.X - pt2.X);
double height = Math.Abs(pt1.Y - pt2.Y);
double left = Math.Min(pt1.X, pt2.X);
double top = Math.Min(pt1.Y, pt2.Y);
CHAPITRE 3 □ Les géométries et les tracés 2D 107
double side = width;
if (width > height)
side = height;
rg.Rect = new Rect(left, top, side, side);
trace.Data = rg;
m_liste_trace.Add(trace);
x_cnv_appli.Children.Add(m_liste_trace[m_liste_trace.Count - 1]);
}

La méthode DessinerUnRectangle(..) reçoit en paramètre deux points pt1 et pt2


pour effectuer le traçage d’un rectangle. Son implémentation est:

private void DessineUnRectangle(Point pt1, Point pt2) {


Path trace = new Path();
trace.Fill = m_coul_remplissage;
trace.Stroke = m_coul_bordure;
RectangleGeometry rg = new RectangleGeometry();
double width = Math.Abs(pt1.X - pt2.X);
double height = Math.Abs(pt1.Y - pt2.Y);
double left = Math.Min(pt1.X, pt2.X);
double top = Math.Min(pt1.Y, pt2.Y);
rg.Rect = new Rect(left, top, width, height);
trace.Data = rg;
m_liste_trace.Add(trace);
x_cnv_appli.Children.Add(m_liste_trace[m_liste_trace.Count - 1]);
}

La méthode DessinerUnCercle(..) reçoit en paramètre deux points pt1 et pt2


pour effectuer le traçage d’un cercle. Son implémentation est:

private void DessinerUnCercle(Point pt1, Point pt2) {


Path path = new Path();
path.Fill = m_coul_remplissage;
path.Stroke = m_coul_bordure;
EllipseGeometry eg = new EllipseGeometry();
double width = Math.Abs(pt1.X - pt2.X);
double height = Math.Abs(pt1.Y - pt2.Y);
double left = Math.Min(pt1.X, pt2.X);
double top = Math.Min(pt1.Y, pt2.Y);
double side = width;
if (width > height)
side = height;
108 La programmation graphique 2D de WPF 4
eg.Center = new Point(left + side / 2, top + side / 2);
eg.RadiusX = side / 2;
eg.RadiusY = side / 2;
path.Data = eg;
m_liste_trace.Add(path);
x_cnv_appli.Children.Add(m_liste_trace[m_liste_trace.Count - 1]);
}

La méthode DessinerUneEllipse(..) reçoit en paramètre deux points pt1 et pt2


pour effectuer le traçage d’une ellipse. Son implémentation est:

private void DessinerUneEllipse(Point pt1, Point pt2) {


Path path = new Path();
path.Fill = m_coul_remplissage;
path.Stroke = m_coul_bordure;
EllipseGeometry eg = new EllipseGeometry();
double width = Math.Abs(pt1.X - pt2.X);
double height = Math.Abs(pt1.Y - pt2.Y);
double left = Math.Min(pt1.X, pt2.X);
double top = Math.Min(pt1.Y, pt2.Y);
eg.Center = new Point(left + width / 2, top + height / 2);
eg.RadiusX = width / 2;
eg.RadiusY = height / 2;
path.Data = eg;
m_liste_trace.Add(path);
x_cnv_appli.Children.Add(m_liste_trace[m_liste_trace.Count - 1]);
}

La figure 3-8 visualise les étapes précédentes décrites pour le traçage d’une
forme rectangle.

2.2 - Déplacer une forme

Le déplacement d’une forme consiste à la sélectionner avec le bouton gauche


Copyright 2011 Patrice REY

enfoncé de la souris, puis en maintenant ce bouton gauche enfoncé, de


déplacer la souris avec la forme qui suit le curseur de la souris.
Si la souris n’est pas capturée sur le Canvas, alors on relève la position de la
souris sur le Canvas (v_point_depart). Si le bouton de déplacement d_selectionner
est coché (d_selectionner.IsChecked == true), on démarre la capture de la souris
sur le Canvas par CaptureMouse(). Dans le cas où c’est le Canvas qui est à
l’origine du déclenchement de l’événement, alors on sort du gestionnaire.
CHAPITRE 3 □ Les géométries et les tracés 2D 109
Figure 3-8

un clic dans la ToolBar pour


sélectionner la forme rectangle
puis un MouseLeftButtonDown
sur le Canvas

un MouseMove sur le Canvas

un MouseLeftButtonUp sur le
Canvas

Dans le cas contraire, c’est une forme qui est à l’origine de l’événement, alors
on stocke dans originalElement le tracé de type Path qui a déclenché l’événement.
Le booléen isDown passe à true (pour signifier que l’on a sélectionné une forme).

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
if (!x_cnv_appli.IsMouseCaptured) {
v_point_depart = e.GetPosition(x_cnv_appli);
...
if (d_selectionner.IsChecked == true) {
x_cnv_appli.CaptureMouse();
110 La programmation graphique 2D de WPF 4
if (e.Source == x_cnv_appli) { return; }
isDown = true;
originalElement = (Path)e.Source;
e.Handled = true;
}
...
}
}

Dans le gestionnaire MouseMove, la capture de la souris étant en cours et le


RadioButton d_selectionner étant coché, si le booléen isDown est à true et si le
booléen isDragging est à false, alors on effectue la méthode DragStarting(). Si le
booléen isDown est à true et si le booléen isDragging est à true, alors on effectue
la méthode DragMoving().

private void x_cnv_appli_MouseMove(object sender, MouseEventArgs e) {


...
if (x_cnv_appli.IsMouseCaptured) {
if (d_selectionner.IsChecked == true) {
if (isDown) {
if (!isDragging &&
Math.Abs(v_point_courant.X - v_point_depart.X) >
SystemParameters.MinimumHorizontalDragDistance
&& Math.Abs(v_point_courant.Y - v_point_depart.Y) >
SystemParameters.MinimumVerticalDragDistance) {
DragStarting();
}
if (isDragging) {
DragMoving();
}
}
}
}
}
Copyright 2011 Patrice REY

La méthode DragStarting() consiste à effectuer une copie de la forme à déplacer


(movingElement de type Path), et à l’ajouter au Canvas (c’est un moyen pour
simuler le déplacement de la forme). Le booléen isDragging passe à true pour
signaler que le déplacement commence.

private void DragStarting() {


CHAPITRE 3 □ Les géométries et les tracés 2D 111
isDragging = true;
movingElement = new Path();
movingElement.Data = originalElement.Data;
movingElement.Fill = selectFillColor;
movingElement.Stroke = selectBorderColor;
movingElement.Cursor = Cursors.Hand;
x_cnv_appli.Children.Add(movingElement);
}

La méthode DragMoving() consiste à déplacer la copie de la forme movingElement


par une translation dont les composantes de déplacement suivant X et Y sont
égales à la différence entre le point courant de la souris (v_point_courant) et le
point de départ de la souris (v_point_depart). Cette translation est ajoutée à la
propriété RenderTransform de la copie de la forme movingElement.

private void DragMoving() {


v_point_courant = Mouse.GetPosition(x_cnv_appli);
TranslateTransform tt = new TranslateTransform();
tt.X = v_point_courant.X - v_point_depart.X;
tt.Y = v_point_courant.Y - v_point_depart.Y;
movingElement.RenderTransform = tt;
}

Le déplacement se termine quand le bouton gauche de la souris est relâché.


Dans le gestionnaire MouseLeftButtonUp, isDown étant à true, on effectue la
méthode DragFinishing(..).

private void x_cnv_appli_MouseLeftButtonUp(object sender,


MouseButtonEventArgs e) {
...
if (d_selectionner.IsChecked == true) {
if (isDown) {
DragFinishing(false);
e.Handled = true;
}
}
}

La méthode DragFinishing(..) consiste à conclure le déplacement de la forme.


Elle reçoit en paramètre un booléen cancel fixé à false lors de son appel.
La capture de la souris est arrêtée par la méthode statique Mouse.Capture(..)
112 La programmation graphique 2D de WPF 4
recevant en paramètre la valeur null. Si isDragging est à true et si cancel est
à true, on relève le point courant v_point_courant de la souris. On crée une
géométrie geometry correspondant à la forme déplacée et on l’ajoute au Canvas
à sa position finale par une translation. La forme originalElement est supprimée
du Canvas. La forme movingElement, qui a servi de support pour marquer le
déplacement, est aussi supprimée du Canvas. Les variables movingElement,
isDragging et isDown sont remises à leurs valeurs initiales.

private void DragFinishing(bool cancel) {


Mouse.Capture(null);
if (isDragging) {
if (!cancel) {
v_point_courant = Mouse.GetPosition(x_cnv_appli);
TranslateTransform tt0 = new TranslateTransform();
TranslateTransform tt = new TranslateTransform();
tt.X = v_point_courant.X - v_point_depart.X;
tt.Y = v_point_courant.Y - v_point_depart.Y;
Geometry geometry = (RectangleGeometry)new RectangleGeometry();
string s = originalElement.Data.ToString();
if (s == «System.Windows.Media.EllipseGeometry»)
geometry = (EllipseGeometry)originalElement.Data;
else if (s == «System.Windows.Media.RectangleGeometry»)
geometry = (RectangleGeometry)originalElement.Data;
else if (s == «System.Windows.Media.CombinedGeometry»)
geometry = (CombinedGeometry)originalElement.Data;
if (geometry.Transform.ToString() != «Identity») {
tt0 = (TranslateTransform)geometry.Transform;
tt.X += tt0.X;
tt.Y += tt0.Y;
}
geometry.Transform = tt;
x_cnv_appli.Children.Remove(originalElement);
originalElement = new Path();
originalElement.Fill = m_coul_remplissage;
originalElement.Stroke = m_coul_bordure;
Copyright 2011 Patrice REY

originalElement.Data = geometry;
x_cnv_appli.Children.Add(originalElement);
}
x_cnv_appli.Children.Remove(movingElement);
movingElement = null;
}
isDragging = false;
isDown = false; }
CHAPITRE 3 □ Les géométries et les tracés 2D 113
La figure 3-9 visualise les étapes précédentes décrites pour le déplacement
d’une forme rectangle.
Figure 3-9

un clic dans la ToolBar pour


sélectionner le RadioButton
déplacement puis un
MouseLeftButtonDown sur la
forme

un MouseMove sur la forme

un MouseLeftButtonUp sur la
forme

2.3 - Combiner deux formes

Le but est de combiner deux formes en une seule, selon certaines méthodes de
combinaison (celles que nous avons vues pour les géométries).
Si la souris n’est pas capturée sur le Canvas, alors on relève la position de
la souris sur le Canvas (v_point_depart). Si le bouton de combinaison d_
combiner est coché (d_combiner.IsChecked == true), on effectue la méthode
TraiterCombinerDeuxForme(..).

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
if (!x_cnv_appli.IsMouseCaptured) {
114 La programmation graphique 2D de WPF 4
v_point_depart = e.GetPosition(x_cnv_appli);
...
if (d_combiner.IsChecked == true) {
TraiterCombinerDeuxForme(e);
}
...
}
}

La façon de faire pour combiner deux formes est la suivante: on clique sur
une première forme et elle est sélectionnée; on clique sur une deuxième
forme et elle est sélectionnée; on fait un clic droit sur une de ces deux formes
sélectionnées pour faire apparaitre un menu contextuel et pour choisir la
méthode de combinaison des formes.
La variable path1 de type Path reçoit la première forme sélectionnée et la variable
path2 reçoit la deuxième forme sélectionnée. On ajoute dynamiquement un
menu contextuel cm à cet ensemble. A ce menu contextuel, on lui ajoute des
rubriques men_item de type MenuItem: la propriété Header reçoit le texte de la
rubrique (ici Union, Xor, Intersect et Exclude) et un gestionnaire pour l’événement
Click est associé (Union_Click, Xor_Click, Intersect_Click et Exclude_Click).

private void TraiterCombinerDeuxForme(MouseButtonEventArgs e) {


if (path1.Name != «path1Selected») {
path1 = (Path)e.Source;
path1.Cursor = Cursors.Hand;
path1.Stroke = selectBorderColor;
path1.Name = «path1Selected»;
}
else {
if (path2 != null) {
path2.Stroke = m_coul_bordure;
path2.Cursor = Cursors.Arrow;
}
Copyright 2011 Patrice REY

path2 = (Path)e.Source;
path2.Cursor = Cursors.Hand;
path2.Stroke = selectBorderColor;
ContextMenu cm = new ContextMenu();
path2.ContextMenu = cm;
MenuItem men_item = new MenuItem();
men_item.Header = «Union»;
men_item.Click += new RoutedEventHandler(Union_Click);
CHAPITRE 3 □ Les géométries et les tracés 2D 115
cm.Items.Add(men_item);
men_item = new MenuItem();
men_item.Header = «Xor»;
men_item.Click += new RoutedEventHandler(Xor_Click);
cm.Items.Add(men_item);
men_item = new MenuItem();
men_item.Header = «Intersect»;
men_item.Click += new RoutedEventHandler(Intersect_Click);
cm.Items.Add(men_item);
men_item = new MenuItem();
men_item.Header = «Exclude»;
men_item.Click += new RoutedEventHandler(Exclude_Click);
cm.Items.Add(men_item);
}
}

Les quatre gestionnaires de l’événement Click sur une rubrique du menu


contextuel, vont effectuer la méthode CombinerLesFormes(..) qui reçoit en
paramètre la première forme path1, la deuxième forme path2 et une chaîne de
caractères correspondant à la méthode de combinaison choisie.

private void Union_Click(object sender, RoutedEventArgs e) {


CombinerLesFormes(path1, path2, «Union»);
path1.Name = «»;
}
private void Xor_Click(object sender, RoutedEventArgs e) {
CombinerLesFormes (path1, path2, «Xor»);
path1.Name = «»;
}
private void Intersect_Click(object sender, RoutedEventArgs e) {
CombinerLesFormes (path1, path2, «Intersect»);
path1.Name = «»;
}
private void Exclude_Click(object sender, RoutedEventArgs e) {
CombinerLesFormes (path1, path2, «Exclude»);
path1.Name = «»;
}

La méthode CombinerLesFormes(..) proprement dite consiste à créer une nouvelle


géométrie composite cg de type CombinedGeometry. Cette géométrie cg voit
sa propriété Geometry1 affectée par forme_1.Data et sa propriété Geometry2
affectée par forme_2.Data. Son mode de combinaison GeometryCombineMode
116 La programmation graphique 2D de WPF 4
est fixé par le paramètre passé s_methode. Une nouvelle forme myPath de type
Path est instanciée et sa propriété Data reçoit la nouvelle géométrie cg. Elle est
ajoutée au Canvas et les deux formes initiales sont retirées du Canvas.

private void CombinerLesFormes(Path forme_1, Path forme_2,


string s_methode) {
Path myPath = new Path();
myPath.Fill = m_coul_remplissage;
myPath.Stroke = m_coul_bordure;
CombinedGeometry cg = new CombinedGeometry();
if (s_methode == «Union»)
cg.GeometryCombineMode = GeometryCombineMode.Union;
else if (s_methode == «Xor»)
cg.GeometryCombineMode = GeometryCombineMode.Xor;
else if (s_methode == «Intersect»)
cg.GeometryCombineMode = GeometryCombineMode.Intersect;
else if (s_methode == «Exclude»)
cg.GeometryCombineMode = GeometryCombineMode.Exclude;
cg.Geometry1 = forme_1.Data;
cg.Geometry2 = forme_2.Data;
myPath.Data = cg;
m_liste_trace.Add(myPath);
x_cnv_appli.Children.Add(m_liste_trace[m_liste_trace.Count - 1]);
x_cnv_appli.Children.Remove(forme_1);
x_cnv_appli.Children.Remove(forme_2);
}

La figure 3-10 visualise les étapes précédentes décrites pour la combinaison


d’une forme carré avec une forme rectangle.

2.4 - Supprimer une forme

La suppression d’une forme créée consiste à cocher le RadioButton d_supprimer


de la suppression, à cliquer sur le Canvas et obtenir la forme qui est à l’origine
Copyright 2011 Patrice REY

de l’événement. La méthode SupprimerFigure(..) effectue la suppression de la


figure sélectionnée qui est passée en paramètre.

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
...
CHAPITRE 3 □ Les géométries et les tracés 2D 117
if (d_supprimer.IsChecked == true) {
if (e.Source == x_cnv_appli) { return; }
originalElement = (Path)e.Source;
SupprimerFigure(originalElement);
}
}
}

Figure 3-10

un carré et un rectangle sont dessinés le carré et le rectangle sont sélectionnés

la nouvelle forme composite créée un clic droit pour afficher le menu contextuel
et choisir la combinaison (ici Union par
exemple)

Un message d’alerte demande la confirmation de la suppression (qui est


dans notre cas définitive puisqu’aucune procédure de retour en arrière n’est
implémentée). En cas de réponse positive, la figure sélectionnée est retirée du
118 La programmation graphique 2D de WPF 4
Canvas.

private void SupprimerFigure(Path un_trace) {


un_trace.Stroke = selectBorderColor;
string msg = «voulez-vous vraiment supprimer cette figure?»;
string title = «Supprimer une figure?»;
MessageBoxButton buttons = MessageBoxButton.YesNo;
MessageBoxImage icon = MessageBoxImage.Warning;
MessageBoxResult result = MessageBox.Show(msg, title, buttons,
icon);
if (result == MessageBoxResult.Yes) {
x_cnv_appli.Children.Remove(un_trace);
}
else {
un_trace.Stroke = m_coul_bordure;
return;
}
}

2.5 - Le test d’atteinte

WPF implémente la classe VisualTreeHelper. Cette classe se trouve dans


l’espace de noms System.Windows.Media (assembly PresentationCore.dll). Elle
est composée de méthodes statiques qui sont des méthodes utilitaires qui
exécutent des tâches courantes impliquant des nœuds dans une arborescence
visuelle. Avec en particulier, la méthode statique VisualTreeHelper.HitTest(..)
qui initialise un test de positionnement sur le Visual spécifié.
La classe Visual est la classe de base des objets UIElement, elle assure la prise
en charge du rendu dans WPF, notamment les tests de positionnement, les
transformations de coordonnées et les calculs de zones englobantes.
Chaque élément de WPF susceptible d’être affiché hérite de la classe abstraite
Visual. Chaque objet Visual peut lui-même contenir visuellement d’autres
objets Visual. Cela forme un arbre de composition visuelle, chaque objet
Copyright 2011 Patrice REY

Visual formant un noeud de cet arbre. Une modification d’un élément Visual
est répercutée dans l’arbre, et est automatiquement prise en compte lors du
prochain cycle d’affichage.
L’UserControl TestDatteinte.xaml dans le dossier chapitre03 illustre l’utilisation
de la méthode statique HitTest de la classe VisualTreeHelper (figure 3-11).
CHAPITRE 3 □ Les géométries et les tracés 2D 119
Figure 3-11
120 La programmation graphique 2D de WPF 4
Nous positionnons sur le Canvas x_cnv_appli deux rectangles, une ellipse et
un cercle. La propriété Cursor du Canvas est fixé à Cross de façon à afficher
le curseur de la souris sur le Canvas avec une forme de croix. Le gestionnaire
MouseLeftButtonDown est ajouté au Canvas.

<Rectangle Canvas.Left='61' Canvas.Top='36' Height='100'


Name='x_rectangle_1' Stroke='Black' Width='200'
Fill='#86B0C4DE' StrokeThickness='2' />
<Rectangle Canvas.Left='95' Canvas.Top='81' Fill='#86B0C4DE'
Height='226' Name='x_rectangle_2' Stroke='Black'
Width='122' RadiusX='10' RadiusY='10'
StrokeThickness='2' />
<Ellipse Canvas.Left='129' Canvas.Top='93' Height='157'
Name='x_ellipse_1' Stroke='Black' Width='254'
StrokeThickness='2' Fill='#86B0C4DE' />
<Ellipse Canvas.Left='178' Canvas.Top='211' Fill='#86B0C4DE'
Height='168' Name='x_cercle_1' Stroke='Black'
StrokeThickness='2' Width='168' />

Un pinceau m_coul_remplissage_bleu, de type SolidColorBrush, avec une


couleur bleue et une opacité de 50% est instancié. Un autre pinceau m_coul_
remplissage_rouge, de type SolidColorBrush, avec une couleur rouge et une
opacité de 50% est instancié. Toutes les figures sont remplies avec la couleur
bleue au chargement du contrôle par la méthode RemplissageFigureBleu().

Color bleu_semi_transparent = new Color();


bleu_semi_transparent.R = 176;
bleu_semi_transparent.G = 196;
bleu_semi_transparent.B = 222;
bleu_semi_transparent.A = 134;
m_coul_remplissage_bleu = new SolidColorBrush(bleu_semi_transparent);
Color rouge_semi_transparent = new Color();
rouge_semi_transparent.R = 240;
rouge_semi_transparent.G = 128;
Copyright 2011 Patrice REY

rouge_semi_transparent.B = 128;
rouge_semi_transparent.A = 128;
m_coul_remplissage_rouge =
new SolidColorBrush(rouge_semi_transparent);

Ce que nous voulons réaliser c’est un test d’atteinte dans lequel un clic
gauche sur le Canvas nous informe des objets Visuals qui contiennent le point
CHAPITRE 3 □ Les géométries et les tracés 2D 121
correspondant au clic.
Lors du clic gauche de la souris sur le Canvas, on commence par remplir toutes
les figures en bleu. On relève la position point_souris de la souris sur le Canvas.
On affiche ces coordonnées dans le TextBlock x_infos. La liste m_liste_objet_
atteint, qui contient les objets qui seront trouvés dans l’arbre visuel, est vidée.

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
RemplissageFigureBleu();
//recuperer le clic souris sur le canvas
Point point_souris = e.GetPosition(x_cnv_appli);
x_infos.Text = «souris X=» +
Math.Round(point_souris.X, 2).ToString() +
« Y=» + Math.Round(point_souris.Y, 2).ToString() + RC;
m_liste_objet_atteint.Clear();
...
}

Ensuite on appelle la méthode statique HitTest(..) pour le test d’atteinte. Elle


prend en paramètre:
• l’objet Visual sur lequel est réalisé le test d’atteinte (x_cnv_appli),
• une instance du délégué HitTestResultCallback qui reçoit la méthode qui
gère le résultat du test d’atteinte (TestAtteintePourLePoint),
• une instance de PointHitTestParameters qui spécifie un Point comme
paramètre à utiliser pour le test d’atteinte d’un objet visuel (on passe point_
souris).

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
...
//appeler le test d’atteinte
VisualTreeHelper.HitTest(x_cnv_appli, null,
new HitTestResultCallback(TestAtteintePourLePoint),
new PointHitTestParameters(point_souris));
...
}

La méthode TestAtteintePourLePoint(..) ajoute à la liste m_liste_objet_atteint les


objets visuels qui ont été atteints.
122 La programmation graphique 2D de WPF 4
//retour du test d’atteinte
public HitTestResultBehavior TestAtteintePourLePoint(HitTestResult
resultat) {
// ajouter le resultat du test d’atteinte à la liste
m_liste_objet_atteint.Add(resultat.VisualHit);
//parcourir toutes les profondeurs
return HitTestResultBehavior.Continue;
}

Un parcours de la liste des objets permet de modifier leur couleur de


remplissage en m_coul_remplissage_rouge si l’objet est de type Rectangle ou
Ellipse. Le Textblock x_infos affiche les objets concernés par le test d’atteinte.

private void x_cnv_appli_MouseLeftButtonDown(object sender,


MouseButtonEventArgs e) {
...
//mettre a jour les visuels en fonction du resultat
//du test d’atteinte
foreach (object objet in m_liste_objet_atteint) {
x_infos.Text += objet.ToString() + RC;
if (objet.GetType() == typeof(Rectangle)) {
Rectangle figure = (Rectangle)objet;
figure.Fill = m_coul_remplissage_rouge;
}
else
if (objet.GetType() == typeof(Ellipse)) {
Ellipse figure = (Ellipse)objet;
figure.Fill = m_coul_remplissage_rouge;
}
}
}

3 - Des figures 2D personnalisées Copyright 2011 Patrice REY

Les figures basiques telles que le rectangle et l’ellipse ne suffisent pas toujours
pour réaliser des applications graphiques évoluées. Pour y remédier, vous
pouvez créer des figures personnalisées qui héritent de la classe abstraite
Shape. Dans ce cas, votre figure personnalisée hérite de toutes les propriétés
et de toutes les méthodes de la classe mère Shape, et devient de fait un objet
FrameworkElement. Par la suite vous pouvez réutiliser votre figure personnalisée
comme un simple contrôle dans vos applications WPF.
CHAPITRE 3 □ Les géométries et les tracés 2D 123
Nous allons voir comment créer des figures personnalisées réutilisables, avec
comme exemple une étoile et une flèche. En suivant la façon de faire présentée,
vous pourrez créer vos propres librairies de composants réutilisables.

3.1 - Une étoile personnalisée

Réaliser une figure personnalisée est une chose assez simple. Vous devez
faire hériter votre figure personnalisée par la classe abstraite Shape et redéfinir
la propriété DefiningGeometry qui est en lecture seule (elle retourne un objet
Geometry qui définit la géométrie de votre figure personnalisée). Nous
déclarons une classe ComposantEtoile héritant de la classe Shape. Le fichier
ComposantEtoile.cs se trouve dans le dossier chapitre03.
Notre figure aura comme géométrie un PathGeometry m_geo_etoile, déclaré
protected, qui sera retourné par la propriété redéfinie DefiningGeometry (propriété
de la classe de base). Nous ajoutons deux autres propriétés de dépendance qui
appartiennent à notre figure personnalisée, qui sont DpCentre (de type Point)
et DpRayon (de type double).
La création d’un identifiant de propriété de dépendance (dependency property)
passe par la création d’une instance de la classe DependencyProperty. Cette
instance doit être référencée comme champ static public de la classe supportant
la propriété. Elle doit être aussi enregistrée dans le système de propriété au
moyen de la méthode statique Register de la classe DependencyProperty. Cet
appel inscrit la propriété dans le dictionnaire statique privé PropertyFromName
de la même classe, et lui affecte un identifiant de type disponible dans la
propriété GlobalIndex.
On déclare, par la méthode statique Register(..), une propriété de dépendance
DpCentrePropriete de type DependencyProperty, qui porte le nom DpCentre,
qui est de type Point, dont celui qui l’expose est ComposantEtoile, avec
des coordonnées (50,50), et avec un type de comportement de propriété
AffectsMeasure au niveau de l’infrastructure.
Pour pouvoir accès en lecture et en écriture à DpCentre, on crée un accesseur
DpCentre en utilisant les méthodes SetValue(..) pour fixer la propriété de
dépendance, et GetValue(..) pour obtenir la propriété de dépendance.

//specifier le centre de l’etoile


public static readonly DependencyProperty DpCentrePropriete =
DependencyProperty.Register(«DpCentre», typeof(Point),
124 La programmation graphique 2D de WPF 4
typeof(ComposantEtoile),
new FrameworkPropertyMetadata(new Point(50.0, 50.0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
public Point DpCentre {
set { SetValue(DpCentrePropriete, value); }
get { return (Point)GetValue(DpCentrePropriete); }
}

On procède de même pour la propriété de dépendance DpRayonPropriete qui


elle, est de type double et dont l’accesseur est DpRayon.

//specifier le rayon de l’etoile


public static readonly DependencyProperty DpRayonPropriete =
DependencyProperty.Register(«DpRayon», typeof(double),
typeof(ComposantEtoile),
new FrameworkPropertyMetadata(50.0,
FrameworkPropertyMetadataOptions.AffectsMeasure));
public double DpRayon {
set { SetValue(DpRayonPropriete, value); }
get { return (double)GetValue(DpRayonPropriete); }
}

Comme le montre la figure 3-12, pour dessiner l’étoile, nous utilisons une ligne
composée de segments, de type PolyLineSegment. Cette ligne passe par les
points pt1, pt2, pt3, pt4 et pt5. Tous ces points peuvent être situés par rapport
aux coordonnées du centre DpCentre.X et DpCentre.Y en tenant compte des
angles α, β et de la longueur du rayon DpRayon.
De cette manière on compose une figure en forme d’étoile, centrée sur
DpCentre et inscrite dans un cercle de rayon DpRayon. A noter que l’on utilise
pour la géométrie la règle FillRule.Nonzero, suite aux recoupements des lignes.

protected override Geometry DefiningGeometry {


get {
Copyright 2011 Patrice REY

PolyLineSegment v_polyline = new PolyLineSegment();


PathFigure v_path_figure = new PathFigure();
double r = DpRayon;
double x = DpCentre.X;
double y = DpCentre.Y;
double sinus36 = Math.Sin(36.0 * Math.PI / 180.0);
double sinus72 = Math.Sin(72.0 * Math.PI / 180.0);
double cosinus36 = Math.Cos(36.0 * Math.PI / 180.0);
CHAPITRE 3 □ Les géométries et les tracés 2D 125
Figure 3-12

pt1

pt3 pt4

DpCentre α
X
β
DpRayon

pt2
pt5

double cosinus72 = Math.Cos(72.0 * Math.PI / 180.0);


v_path_figure.StartPoint = new Point(x, y - r);
v_polyline.Points.Add(
new Point(x + r * sinus36, y + r * cosinus36));
v_polyline.Points.Add(
new Point(x - r * sinus72, y - r * cosinus72));
v_polyline.Points.Add(
new Point(x + r * sinus72, y - r * cosinus72));
v_polyline.Points.Add(
new Point(x - r * sinus36, y + r * cosinus36));
v_polyline.Points.Add(new Point(x, y - r));
v_path_figure.Segments.Add(v_polyline);
v_path_figure.IsClosed = true;
m_geo_etoile.Figures.Add(v_path_figure);
m_geo_etoile.FillRule = FillRule.Nonzero;
return m_geo_etoile;
}
}
126 La programmation graphique 2D de WPF 4
L’UserControl EtoilePersonnalisee.xaml montre l’utilisation du contrôle
ComposantEtoile. En ajoutant une référence préfixée par visuel avec l’espace
de noms xmlns:visuel = «clr-namespace:ProgGraphiqueWpf.chapitre03», on peut
directement ajouter une instance x_etoile de type visuel:ComposantEtoile.
Le concepteur de Visual Studio affiche le composant et dans la fenêtre des
propriétés, on a accès aussi aux deux propriétés de dépendance que l’on a
ajoutées avec la possibilité de modifier leurs valeurs (figure 3-13).
Figure 3-13

une instance de
ComposantEtoile

accès aux propriétés


de dépendance

une instance de
ComposantEtoile

Maintenant nous allons animer l’étoile par une animation de mise à l’échelle
et de translation. On ajoute à l’instance du composant un RenderTransform
par visuel:ComposantEtoile.RenderTransform, avec un TransformGroup qui est
Copyright 2011 Patrice REY

composé d’une mise à l’échelle x_etoile_scale et d’une translation x_etoile_


translate.

<visuel:ComposantEtoile x:Name='x_etoile' Fill='DarkOrange'


Stroke='Blue' StrokeThickness='2' Canvas.Left='66'
Canvas.Top='102'>
CHAPITRE 3 □ Les géométries et les tracés 2D 127
<visuel:ComposantEtoile.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name='x_etoile_scale' CenterX='50'
CenterY='50' />
<TranslateTransform x:Name='x_etoile_translate' />
</TransformGroup>
</visuel:ComposantEtoile.RenderTransform>
</visuel:ComposantEtoile>

Quand l’UserControl est chargé, l’animation est déclenchée par la méthode


DemarrerAnimation(). On utilise un objet AnimationTimeline qui définit une
période de temps pendant laquelle les valeurs de sortie sont générées. Ces
valeurs sont ensuite utilisées pour animer une propriété cible. La première
animation at dure 5 secondes (new TimeSpan(0,0,5)) en générant des valeurs
de sortie allant de 0.1 à 1.2. Ces valeurs sont appliquées à la mise à l’échelle x_
etoile_scale sur ses propriétés ScaleX et ScaleY par la méthode BeginAnimation(..).
La deuxième animation dure 5 secondes en générant des valeurs de sortie
allant de 0 à 400. Ces valeurs sont appliquées sur la propriété X de x_etoile_
translate. Ces deux animations s’exécutent de façon simultanée.

private void DemarrerAnimation() {


AnimationTimeline at = new DoubleAnimation(0.1, 1.2,
new Duration(new TimeSpan(0, 0, 5)));
at.RepeatBehavior = RepeatBehavior.Forever;
at.AutoReverse = true;
x_etoile_scale.BeginAnimation(ScaleTransform.ScaleXProperty, at);
x_etoile_scale.BeginAnimation(ScaleTransform.ScaleYProperty, at);
at = new DoubleAnimation(0, 400, new Duration(new TimeSpan(0, 0,
5)));
at.RepeatBehavior = RepeatBehavior.Forever;
at.AutoReverse = true;
x_etoile_translate.BeginAnimation(TranslateTransform.XProperty,
at);
}

La figure 3-14 visualise quelques étapes de l’animation de l’instance du


contrôle ComposantEtoile. L’étoile commence avec une mise à l’échelle de
0.1 quand la translation est de 0 suivant X. Elle se termine avec une mise à
l’échelle de 1.2 quand la translation est de 400 suivant X. Cela est possible car
les deux animations sont simultanées pour une durée identique.
128 La programmation graphique 2D de WPF 4
Figure 3-14

3.2 - Une flèche personnalisée

Nous allons réaliser une flèche personnalisée. La classe ComposantFleche


(fichier ComposantFleche.cs) hérite de la classe Shape. Elle doit redéfinir la
Copyright 2011 Patrice REY

propriété DefiningGeometry qui représente la géométrie de la figure. Nous


utilisons la géométrie visualisée sur la figure 3-15.
Pour cela nous devons ajouter des propriétés de dépendance. La flèche est
contenue dans un rectangle matérialisé par deux points, le point haut gauche et
le point bas droit. Les propriétés seront DpPointHautGauche et DpPointBasDroit.
CHAPITRE 3 □ Les géométries et les tracés 2D 129
Figure 3-15

profondeur de la tête

point haut pt1


gauche
pt4
pt2 pt5

pt3
point bas
droit

//propriete de dependance point haut gauche


public static readonly DependencyProperty PointHautGauchePropriete =
DependencyProperty.Register(«ProprietePointHautGauche»,
typeof(Point),
typeof(ComposantFleche),
new FrameworkPropertyMetadata(new Point(0.0, 0.0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
public Point DpPointHautGauche {
set { SetValue(PointHautGauchePropriete, value); }
get { return (Point)GetValue(PointHautGauchePropriete); }
}
//propriete de dependance point bas droit
public static readonly DependencyProperty PointBasDroitPropriete =
DependencyProperty.Register(«ProprietePointBasDroit»,
typeof(Point),
typeof(ComposantFleche),
new FrameworkPropertyMetadata(new Point(100.0, 50.0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
public Point DpPointBasDroit {
set { SetValue(PointBasDroitPropriete, value); }
get { return (Point)GetValue(PointBasDroitPropriete); }
}
130 La programmation graphique 2D de WPF 4
La tête de la flèche a une profondeur modifiable. La propriété DpProfondeurTete
gère cette profondeur.

//propriete de dependance profondeur de le tete


public static readonly DependencyProperty ProfondeurTetePropriete =
DependencyProperty.Register(«ProprieteProfondeurTete»,
typeof(double),
typeof(ComposantFleche),
new FrameworkPropertyMetadata(50.0,
FrameworkPropertyMetadataOptions. AffectsMeasure));
public double DpProfondeurTete {
set { SetValue(ProfondeurTetePropriete, value); }
get { return (double)GetValue(ProfondeurTetePropriete); }
}

Une énumération ComposantFleche.Tete permet de fixer si la tête de la flèche


est ouverte ou fermée. La propriété DpTypeTete est fixée par défaut à la valeur
Tete.ouverte.

public enum Tete { ouverte, fermee };


//propriete de dependance type de tete
public static readonly DependencyProperty TypeDeTetePropriete =
DependencyProperty.Register(«ProprieteTypeTete», typeof(Tete),
typeof(ComposantFleche),
new FrameworkPropertyMetadata(Tete.ouverte,
FrameworkPropertyMetadataOptions.AffectsMeasure));
public Tete DpTypeTete {
set { SetValue(TypeDeTetePropriete, value); }
get { return (Tete)GetValue(TypeDeTetePropriete); }
}

La géométrie de la figure est composée de deux PathFigure, celui qui contient la


tête de la flèche et celui qui contient la tige de la flèche. Le tracé des segments
se fait par rapport à la position du point haut gauche.
Copyright 2011 Patrice REY

//redefinition de la geometrie
protected override Geometry DefiningGeometry {
get {
double x = DpPointHautGauche.X;
double y = DpPointHautGauche.Y;
double largeur_boite = DpPointBasDroit.X - DpPointHautGauche.X;
CHAPITRE 3 □ Les géométries et les tracés 2D 131
double hauteur_boite = DpPointBasDroit.Y - DpPointHautGauche.Y;
//tete
PathFigure pf_tete = new PathFigure();
pf_tete.StartPoint = new Point(x + DpProfondeurTete, y);
PolyLineSegment pls_tete = new PolyLineSegment();
pls_tete.Points.Add(new Point(x, y + hauteur_boite / 2));
pls_tete.Points.Add(new Point(x + DpProfondeurTete, y + hauteur_
boite));
if (DpTypeTete == Tete.fermee) {
pls_tete.Points.Add(new Point(x + DpProfondeurTete, y));
}
pf_tete.Segments.Add(pls_tete);
m_geo_figure.Figures.Add(pf_tete);
//tige
PathFigure pf_tige = new PathFigure();
pf_tige.StartPoint = new Point(x + DpProfondeurTete, y + hauteur_
boite / 2);
LineSegment line_tige = new LineSegment();
line_tige.Point = new Point(x + largeur_boite, y + hauteur_boite
/ 2);
pf_tige.Segments.Add(line_tige);
m_geo_figure.Figures.Add(pf_tige);
//regle remplissage
m_geo_figure.FillRule = FillRule.Nonzero;
return m_geo_figure;
}

L’UserControl FlechePersonnalisee.xaml, dans le dossier chapitre03, montre


l’utilisation du contrôle ComposantEtoile (figure 3-16). La flèche qui se trouve à
gauche est celle par défaut, et celle qui se trouve à droite est celle dont le type
de la tête est égal à Tete.fermee.
A l’instance de la flèche x_fleche_1, on ajoute une transformation par rotation
x_fleche_1_rotate. Avec une animation de type DoubleAnimation, on anime
l’angle de rotation de la transformation, le faisant passer de 0 à 360 degrés
sur une période de 5 secondes. L’animation a pour cible la transformation
(Storyboard.TargetName= «x_fleche_1_rotate») et a comme propriété cible l’angle
de rotation (Storyboard.TargetProperty= «Angle»).

<visuel:ComposantFleche Canvas.Left='272' Canvas.Top='166'


DpTypeTete='fermee' x:Name='x_fleche_1'>
<visuel:ComposantFleche.RenderTransform>
132 La programmation graphique 2D de WPF 4
Figure 3-16

Copyright 2011 Patrice REY

<RotateTransform x:Name='x_fleche_1_rotate'
CenterX='100' CenterY='25' Angle='0'></RotateTransform>
</visuel:ComposantFleche.RenderTransform>
</visuel:ComposantFleche>
<!-- animation -->
<Canvas.Triggers>
<EventTrigger RoutedEvent='Canvas.Loaded'>
<BeginStoryboard>
CHAPITRE 3 □ Les géométries et les tracés 2D 133
<Storyboard RepeatBehavior='Forever'
AutoReverse='False'>
<DoubleAnimation
Storyboard.TargetName='x_fleche_1_rotate'
Storyboard.TargetProperty='Angle'
From='0' To='360' Duration='0:0:5' />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
CHAPITRE 4

Les couleurs et les pinceaux

T out ce qui est visible à l’écran d’un ordinateur fait appel aux couleurs. Par
exemple, il est courant d’utiliser un pinceau de couleurs pour peindre le fond
d’un bouton, l’arrière-plan d’un texte, le remplissage d’une figure, etc. Jusqu’à
présent nous avons utilisé dans ce livre des couleurs de type SolidColorBrush.
Or tous les objets graphiques peuvent être peints en utilisant des couleurs
simples, des dégradés de couleurs, des motifs, des images, etc.
Ce chapitre nous montre le système de couleurs utilisé par WPF ainsi que les
pinceaux de couleurs, les dégradés, la répétition de motif et les transformations
appliquées aux pinceaux de couleurs.

1 - Les couleurs

Dans WPF, une couleur est représentée par la structure Color qui se trouve
dans l’espace de noms System.Windows.Media (assembly PresentationCore.
dll). La structure Color décrit une couleur en termes de canaux alpha (A pour
l’opacité), rouges (R), verts (G) et bleus (B). Les composantes peuvent être
exprimées dans l’espace de couleur standard sRGB (propriétés A, R, G et B,
en valeurs entières de 0 à 255) ou dans l’espace de couleur ScRGB (propriétés
ScA, ScR, ScG et ScB, en valeurs de type float de 0.0 à 1.0, offrant davantage
de précision).
La classe statique Colors implémente un jeu de couleurs prédéfinies, au nombre
de 141, en fournissant des objets Color pour les couleurs standard (Color.Black,
Color.Red, etc.).
La classe statique SystemColors expose les couleurs système sous la forme
d’objets Color (WindowColor), d’objets pinceaux (WindowBrush), ou de clés de
ressources (WindowColorKey, WindowBrushKey). Cela permet de les utiliser
sous la forme de références statiques ou dynamiques en XAML (par exemple
Fill = «{x:Static SystemColors.WindowBrush}»).
De plus la classe ColorConverter (espace de noms System.Windows.Media.
ColorConverter) convertit les instances d’autres types vers et à partir d’une
instance de Color. Elle contient notamment une méthode statique qui tente de
136 La programmation graphique 2D de WPF 4
convertir une chaîne en Color.

1.1 - La représentation des couleurs

La structure Color dispose de méthodes statiques qui permettent de créer une


couleur par le code, à partir de ses composantes:
• FromRgb crée un objet Color à partir des composantes RGB définies sur 8
bits,
• FromArgb crée un objet Color à partir des composantes alpha et RGB
définies sur 8 bits,
• FromScRgb crée un objet Color à partir des composantes alpha et RGB en
mode ScRGB.
Quelques exemples de création d’objets de type Color par le code:

Color color = new Color();


//creer une couleur avec des valeurs rgb
color = Color.FromRgb(255, 0, 0);
// creer une couleur avec des valeurs argb
color = Color.FromArgb(100, 255, 0, 0);
// creer une couleur avec des valeurs scrgb
color = Color.FromScRgb(0.5f, 1.0f, 0.0f, 0.0f);
// creer une couleur avec une valeur predefinie
color = Colors.Red;
// creer une couleur avec ColorConverter:
color = (Color)ColorConverter.ConvertFromString(«#FFFF0000»);

L’UserControl LeSystemeCouleur.xaml dans le dossier chapitre04 visualise


comment exprimer une couleur en XAML dans le mode sRGB et ScRGB.
Le ListBox affiche les 141 couleurs prédéfinies. Un clic sur une couleur
provoque l’affichage de la couleur dans le rectangle du bas et l’affichage de ses
composantes à droite dans les deux modes. On peut modifier l’opacité pour
obtenir les correspondances (figure 4-1).
Copyright 2011 Patrice REY

Le ListBox x_listbox_couleur est rempli par des chaînes de caractères qui


contiennent le nom des couleurs prédéfinies. On utilise une variable type_
de_couleur, de type Type, qui représente la classe Colors. Dans une boucle
foreach, on parcourt tous les types de couleur en récupérant les propriétés,
de type PropertyInfo, par la méthode GetProperties(). On récupère le nom de
chaque type par la propriété Name, et on l’ajoute au ListBox. Il faut ajouter une
référence using System.Reflection pour pouvoir utiliser les objets PropertyInfo.
CHAPITRE 4 □ Les couleurs et les pinceaux 137
Figure 4-1
138 La programmation graphique 2D de WPF 4
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
this.UpdateLayout();
Type type_de_couleur = typeof(Colors);
foreach (PropertyInfo propriete
in type_de_couleur.GetProperties()) {
x_listbox_couleur.Items.Add(propriete.Name);
v_couleur = Colors.AliceBlue;
x_listbox_couleur.SelectedIndex = 0;
InformationCouleur();
}
}

La méthode InformationCouleur() permet d’afficher les informations relatives


à la couleur sélectionnée dans les 2 modes, sRGB et ScRGB. La variable v_
couleur stocke la couleur sélectionnée sous forme d’un objet Color. Il ne reste
plus qu’à récupérer les différentes composantes et à les afficher. A noter que
la composante hexadécimale du mode sRGB doit être formatée pour son
affichage texte. Pour cela on utilise la méthode statique String.Format qui
remplace chaque élément de mise en forme dans une chaîne spécifiée par
l’équivalent textuel de la valeur d’un objet correspondant.

//affichage des infos de couleur


private void InformationCouleur() {
x_rect_couleur.Fill = new SolidColorBrush(v_couleur);
//sRGB infos
x_srgb_alpha.Text = «Alpha = « + v_couleur.A.ToString();
x_srgb_rouge.Text = «Rouge = « + v_couleur.R.ToString();
x_srgb_vert.Text = «Vert = « + v_couleur.G.ToString();
x_srgb_bleu.Text = «Bleu = « + v_couleur.B.ToString();
string rgbHex = string.Format(«{0:X2}{1:X2}{2:X2}{3:X2}»,
v_couleur.A, v_couleur.R, v_couleur.G, v_couleur.B);
x_hrgb.Text = «ARGB = #» + rgbHex;
// ScRGB info
x_scrgb_alpha.Text = «ScA = « + v_couleur.ScA.ToString();
Copyright 2011 Patrice REY

x_scrgb_rouge.Text = «ScR = « + v_couleur.ScR.ToString();


x_scrgb_vert.Text = «ScG = « + v_couleur.ScG.ToString();
x_scrgb_bleu.Text = «ScB = « + v_couleur.ScB.ToString();
}

Le bas de la figure 4-1 montre le détail des informations obtenues pour la


couleur sélectionnée Colors.BlueViolet, avec une opacité de 1 et de 0.5.
CHAPITRE 4 □ Les couleurs et les pinceaux 139

1.2 - Un sélecteur de couleurs

Un sélecteur de couleurs est bien évidemment indispensable quand l’on


souhaite dessiner des figures. L’UserControl LaSelectionCouleur.xaml, dans
le dossier chapitre04, montre comment dessiner des figures basiques de
différentes couleurs (figure 4-2).
On réalise d’abord le sélecteur de couleurs sous forme d’un UserControl de
type SelecteurCouleur. On ajoute une instance de ce contrôle, nommée x_
selecteur, par l’ajout d’une référence à l’espace de noms xmlns:visuel = «clr-
namespace:ProgGraphiqueWpf.chapitre04» et d’une insertion du contrôle par
<visuel:SelecteurCouleur x:Name = «x_selecteur» Canvas.Left = «486» Canvas.Top
= «71» ></visuel:SelecteurCouleur>.
Le bas de la figure 4-2 visualise la composition du contrôle dans le concepteur
de Visual Studio. Le Slider x_slider_base nous permet de sélectionner une
couleur dans la palette. Le StackPanel x_stack_coul est composé d’un ensemble
d’objets Line qui sont peints avec une couleur uniforme. La méthode
AjouterLigneCouleurAuStackpanel(..) ajoute une ligne horizontale colorée dans
le StackPanel à orientation verticale.

private void AjouterLigneCouleurAuStackpanel(Color couleur) {


Line ligne = new Line();
ligne.X1 = 0;
ligne.Y1 = 0;
ligne.X2 = 50;
ligne.Y2 = 0;
ligne.Width = 50;
ligne.Height = 1;
ligne.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
ligne.VerticalAlignment = System.Windows.VerticalAlignment.Top;
ligne.Stroke = new SolidColorBrush(couleur);
ligne.StrokeThickness = 1;
x_stack_coul.Children.Add(ligne);
}

Nous allons remplir le StackPanel par un ensemble de lignes colorées


représentant un panel de couleurs. Dans le mode sRGB, le noir est représenté
par les canaux R=0, G=0 et B=0, alors que le blanc est représenté par R=255,
G=255 et B=255.
140 La programmation graphique 2D de WPF 4
Figure 4-2

StackPanel x_stack_coul

Rectangle
x_rect_select

TextBlock x_info_rvb_r

TextBlock x_info_rvb_v

TextBlock x_info_rvb_b
Copyright 2011 Patrice REY

TextBlock x_info_rvb_a

Slider x_slider_base Slider x_slider_trans


CHAPITRE 4 □ Les couleurs et les pinceaux 141
La droite qui passe par ces deux couleurs contient donc 256 niveaux de gris.
Quand la composante R est égale à 255 et les autres composantes sont nulles,
on obtient du rouge. Quand la composante G est égale à 255 et les autres
composantes sont nulles, on obtient du vert. Quand la composante B est égale
à 255 et les autres composantes sont nulles, on obtient du bleu.
Le principal attribut utilisé pour désigner une couleur est la teinte (rouge, vert
bleu jaune, etc.) à laquelle s’ajoute l’attribut de luminosité (s’il s’agit d’une
source) ou de clarté (s’il s’agit d’un objet), et l’attribut de pureté également
appelé attribut de saturation. La figure 4-3 visualise le domaine des couleurs
appariables par synthèse additive des couleurs primaires RGB. Si on relie les
points rouge (255,0,0), vert (0,255,0) et bleu (0,0,255) entre eux, on obtient un
triangle composé d’un ensemble de couleurs. Ce triangle, appelé triangle de
Maxwell, relie les primaires rouge, vert et bleu qui ont toutes la même valeur
de luminance.
Figure 4-3

rouge
(255,0,0)

blanc
(255,255,255)
noir
(0,0,0)

vert
(0,255,0)

bleu
(0,0,255)
142 La programmation graphique 2D de WPF 4
Toutes les couleurs situées dans un plan parallèle à ce triangle ont également
toutes la même luminance. Perpendiculairement à ce triangle, la diagonale qui
relie le noir au blanc définit l’axe achromatique.
Le principal problème quand on cherche à analyser une couleur est que celle-
ci dépend de sa chrominance et de sa luminance. Dès lors, deux stimuli de
même chrominance, mais de luminances différentes, peuvent parfaitement
avoir des coordonnées trichromatiques différentes. Toutes les couleurs qui se
trouvent dans le triangle de Maxwell, ont comme particularité que la somme
de leurs composantes est égale à 255.
Pour établir un panel de couleurs pour notre sélecteur, nous allons choisir des
couleurs qui se trouvent sur la droite passant par le point rouge (255,0,0) et le
point bleu (0,0,255), des couleurs qui se trouvent sur la droite passant par le
point bleu (0,0,255) et le point vert (0,255,0), et des couleurs qui se trouvent
sur la droite passant par le point vert (0,255,0) et le point rouge (255,0,0).
Toutes ces couleurs auront la même luminance.
Les couleurs qui se trouvent sur la droite entre le point rouge et le point bleu
sont des couleurs qui ont la composante verte à 0. Il suffit de faire une boucle
en bloquant la composante verte à 0, et en faisant varier la composante rouge
de 255 à 0, et la composante bleue de 0 à 255. Le même principe est appliqué
pour obtenir les couleurs des deux autres droites.
On implémente cela par une boucle de 3 tours qui effectue un parcours xx
allant de 0 à 255, avec à l’intérieur une instruction switch pour fixer le mode de
calcul des composantes. On se limite à un jeu partiel de couleurs en ne prenant
que la couleur pour xx=0 et xx=255, ainsi que les couleurs pour xx%4=0.
L’objet couleur de type Color obtenu est ajouté à une liste m_liste_coul_base de
type List<Color>. Une fois la liste remplie, on récupère chaque objet Color de
la liste et on ajoute une ligne de cette couleur au StackPanel x_stack_coul (on
obtient ainsi un panel de 195 couleurs).

private void UserControl_Loaded(object sender, RoutedEventArgs e) {


double comp_A = 0;
Copyright 2011 Patrice REY

double comp_R = 0;
double comp_G = 0;
double comp_B = 0;
for (int tour = 1; tour <= 3; tour++) {
for (int xx = 0; xx <= 255; xx++) {
switch (tour) {
case 1:
CHAPITRE 4 □ Les couleurs et les pinceaux 143
comp_A = 255;
comp_R = 255 - xx;
comp_G = 0;
comp_B = xx;
break;
case 2:
comp_A = 255;
comp_R = 0;
comp_G = xx;
comp_B = 255 - xx;
break;
case 3:
comp_A = 255;
comp_R = xx;
comp_G = 255 - xx;
comp_B = 0;
break;
}
if (xx == 0 || xx == 255 || xx % 4 == 0) {
Color couleur = new Color();
couleur.A = (byte)comp_A;
couleur.R = (byte)comp_R;
couleur.G = (byte)comp_G;
couleur.B = (byte)comp_B;
m_liste_coul_base.Add(couleur);
}
}
}
for (int xx = m_liste_coul_base.Count - 1; xx >= 0; xx--) {
AjouterLigneCouleurAuStackpanel(m_liste_coul_base[xx]);
}
...
}

Pour sélectionner une couleur, on positionne un Slider x_slider_base collé au


StackPanel, à orientation verticale, avec une valeur initiale à 1, et une plage
de valeurs allant de 1 à 195. La valeur du Slider correspondra à un objet Color
de la liste. Le gestionnaire ValueChanged du x_slider_base permet d’obtenir
la valeur du Slider et donc d’afficher la couleur sélectionnée par la méthode
AfficherCouleurSelectionnee().

<Slider Height='204' Name='x_slider_base' Width='27'


144 La programmation graphique 2D de WPF 4
Orientation='Vertical' SmallChange='1' Maximum='195' Minimum='1'
Canvas.Left='56' Canvas.Top='7'
ValueChanged='x_slider_base_ValueChanged' Cursor='Hand'
TickPlacement='TopLeft' AutoToolTipPlacement='TopLeft'
Value='1' />

private void x_slider_base_ValueChanged(object sender,


RoutedPropertyChangedEventArgs<double> e) {
if (m_liste_coul_base != null) {
AfficherCouleurSelectionnee();
}
}

Un deuxième Slider x_slider_trans permettra de fixer une valeur de transparence


pour la couleur, en modifiant la composante alpha.

<Slider Canvas.Left='100' Canvas.Top='188' Height='23'


Name='x_slider_trans'
Width='139' Value='255' Minimum='0'
Maximum='255' LargeChange='1' SmallChange='1'
AutoToolTipPlacement='BottomRight' Cursor='Hand'
ValueChanged='x_slider_trans_ValueChanged' />

private void x_slider_trans_ValueChanged(object sender,


RoutedPropertyChangedEventArgs<double> e) {
if (m_liste_coul_base != null) {
AfficherCouleurSelectionnee();
}
}

La méthode AfficherCouleurSelectionnee() stocke dans la variable m_couleur_select


l’objet Color de la liste se trouvant à l’index de la valeur du x_slider_base moins
1 (l’index des liste commençant à 0). La composante alpha est modifiée aussi
par m_couleur_select.A = m_valeur_trans. Puis les champs TextBlock affichent au
Copyright 2011 Patrice REY

format texte le détail des composantes de la couleur, et le rectangle x_rect est


rempli avec la couleur sélectionnée.

private void AfficherCouleurSelectionnee() {


m_couleur_select = m_liste_coul_base[(int)(x_slider_base.Value - 1)];
m_valeur_trans = (byte)x_slider_trans.Value;
m_couleur_select.A = m_valeur_trans;
CHAPITRE 4 □ Les couleurs et les pinceaux 145
x_rect_select.Fill = new SolidColorBrush(m_couleur_select);
x_info_rvb_r.Text = «R = « + m_couleur_select.R.ToString();
x_info_rvb_v.Text = «V = « + m_couleur_select.G.ToString();
x_info_rvb_b.Text = «B = « + m_couleur_select.B.ToString();
x_info_rvb_a.Text = «Alpha = « + m_couleur_select.A.ToString();
}

Un accesseur en lecture seule est ajouté pour récupérer la couleur sélectionnée.


Lors de la phase de dessin de figures, dans LaSelectionCouleur.xaml.cs, la couleur
de type Color, sera récupérée par x_selecteur.SelectionCouleur.

//dans SelecteurCouleur.xaml.cs :
public Color SelectionCouleur {
get { return m_couleur_select; }
}

//dans LaSelectionCouleur.xaml.cs (dessiner un rectangle):


private void DessineUnRectangle(Point pt1, Point pt2) {
Path trace = new Path();
trace.Fill = new SolidColorBrush(x_selecteur.SelectionCouleur);
trace.Stroke = m_coul_bordure;
RectangleGeometry rg = new RectangleGeometry();
double width = Math.Abs(pt1.X - pt2.X);
double height = Math.Abs(pt1.Y - pt2.Y);
double left = Math.Min(pt1.X, pt2.X);
double top = Math.Min(pt1.Y, pt2.Y);
rg.Rect = new Rect(left, top, width, height);
trace.Data = rg;
x_cnv_appli.Children.Add(trace);
}

2 - Les pinceaux

La classe Brush (dans System.Windows.Media) définit les objets utilisés pour


peindre des objets graphiques. Les classes qui dérivent de Brush décrivent
comment la zone est peinte. Les pinceaux servent à définir le fond et le
contour d’une figure, sous la forme de motifs plus ou moins évolués, allant de
la couleur à la vidéo, en passant par les dégradés et les images. Les différents
types de motif sont représentés par des classes spécifiques, héritées de la
classe Brush.
146 La programmation graphique 2D de WPF 4
La figure 4-4 visualise l’arbre d’héritage de Brush. Les classes dérivées sont:
• SolidColorBrush qui représente un motif de couleur unie,
• LinearGradientBrush qui représente un motif en dégradé de couleur,
• RadialGradientBrush qui représente un motif en dégradé circulaire de
couleur,
• ImageBrush qui représente un motif basé sur une image,
• VisualBrush qui représente un motif basé sur un objet hérité de Visual,
• DrawingBrush qui représente un motif basé sur un objet hérité de Drawing.
Figure 4-4

L’utilisation des différents pinceaux implémentés dans WPF permet de réaliser


des effets intéressants comme des dégradés, des réflexions et des effets de
lumière.
Copyright 2011 Patrice REY

2.1 - Le pinceau uni (SolidColorBrush)

La classe SolidColorBrush permet de peindre avec un pinceau de couleur unie.


Elle expose principalement comme propriétés:
• Color qui définit la couleur du pinceau.
CHAPITRE 4 □ Les couleurs et les pinceaux 147
• Opacity qui définit le degré d’opacité d’un Brush.
• Transform qui définit la transformation appliquée au pinceau; cette
transformation est appliquée une fois la sortie du pinceau mappée et
positionnée.
• RelativeTransform qui définit la transformation appliquée au pinceau à l’aide
de coordonnées relatives.
L’UserControl ExempleSolidColorBrush.xaml, dans le dossier chapitre04, montre
l’utilisation d’un objet SolidColorBrush (figure 4-5).
Par exemple, on peut peindre avec une couleur de type Brushes. La classe
Brushes implémente un jeu d’objets SolidColorBrush prédéfinis (Brushes.
CornFlowerBlue). Pour peindre avec un objet Colors (classe qui implémente
un jeu de couleurs prédéfinies), on écrira new SolidColorBrush(Colors.Crimson).
Pour peindre avec la structure Color, on utilisera la méthode statique Color.
FromArgb(..) avec des composantes sRGB, et la méthode statique Color.
FromScRgb(..) avec des composantes ScRGB. Pour convertir une couleur
au format hexadécimal en objet Color, on utilisera la méthode statique
ColorConverter.ConvertFromString(..) en lui passant une chaîne hexadécimale.

x_rect_1.Fill = Brushes.CornflowerBlue;
x_text_1.Text = «remplissage avec un Brushes :»;
x_rect_2.Fill = new SolidColorBrush(Colors.Crimson);
x_text_2.Text = «remplissage avec un objet Colors»;
SolidColorBrush brush = new SolidColorBrush(
Color.FromArgb(100, 0, 0, 255));
x_rect_3.Fill = brush;
x_text_3.Text = «remplissage avec des valeurs sRGB d’une structure
Color»;
brush = new SolidColorBrush(Color.FromScRgb(0.5f, 0.7f, 0.0f, 0.5f));
x_rect_4.Fill = brush;
x_text_4.Text = «remplissage avec des valeurs ScRGB d’une structure
Color»;
brush = new SolidColorBrush(
(Color)ColorConverter.ConvertFromString(«#CBFFFFAA»));
x_rect_5.Fill = brush;
x_text_5.Text = «remplissage avec une chaine hexadécimale»;
x_rect_6.Fill = new SolidColorBrush(x_selecteur.SelectionCouleur);
x_text_6.Text = «remplissage avec la couleur du sélecteur»;

La figure 4-5 visualise différentes façons de remplir un rectangle (possibilité de


remplissage aussi avec le sélecteur de couleurs SelecteurCouleur.xaml).
148 La programmation graphique 2D de WPF 4
Figure 4-5

Copyright 2011 Patrice REY


CHAPITRE 4 □ Les couleurs et les pinceaux 149

2.2 - Les pinceaux dégradés

Les classes LinearGradientBrush et RadialGradientBrush définissent un pinceau


avec un dégradé de couleurs respectivement linéaire et circulaire. L’infographie
utilise très souvent ces types de dégradés pour obtenir des effets de reflets.
L’UserControl ExempleGradientBrush.xaml montre les principales caractéristiques
de dégradés linéaires et circulaires obtenus avec LinearGradientBrush et
RadialGradientBrush (figure 4-6).
Le dégradé peut comporter plusieurs couleurs. L’orientation des vagues de
couleur se fait perpendiculairement à un axe défini par des propriétés du
pinceau. Le point culminant de chaque couleur du dégradé occupe une position
sur cet axe. La couleur et sa position relative sur l’axe sont représentées par
un objet GradientStop.
L’axe d’un LinearGradientBrush est défini par ses propriétés StartPoint et
EndPoint. Les coordonnées sont exprimées en relatif (entre 0 et 1) par rapport
à une projection fictive de la figure sur un carré. StartPoint et EndPoint ont les
valeurs par défaut respectivement de (0,0), c’est le coin supérieur gauche, et
(1,1), c’est le coin inférieur droit.
Le bas de la figure 4-6 montre les principaux types de dégradé linéaire. Par
exemple, x_rect_1 est rempli par un dégradé linéaire à orientation horizontale,
et x_rect_2 est rempli par un dégradé linéaire à orientation verticale.

<Rectangle Width='100' Height='75' Canvas.Left='10'


Canvas.Top='6' Name='x_rect_1'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>
<GradientStop Color='CornflowerBlue' Offset='0.182' />
<GradientStop Color='SandyBrown' Offset='1' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width='100' Height='75' Name='x_rect_2'
Canvas.Left='10' Canvas.Top='173'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
<GradientStop Color='SteelBlue' Offset='0.292' />
<GradientStop Color='Yellow' Offset='1' />
</LinearGradientBrush>
</Rectangle.Fill></Rectangle>
150 La programmation graphique 2D de WPF 4
Figure 4-6

dégradé linéaire
vertical

dégradé linéaire
dégradé dégradé dégradé
horizontal
circulaire circulaire circulaire
RadiusX= 1 RadiusX= 1 RadiusX= 0.5
RadiusY= 1 RadiusY= 1 RadiusY= 0.5
dégradé linéaire en
Center= 0.5, 0.5 Center= 0 , 0 Center= 0.5 , 0.5
diagonale avec un
offset de 1
dégradé linéaire en
diagonale avec un
offset de 0.5

dégradé linéaire
dégradé dégradé dégradé
vertical avec
circulaire circulaire circulaire
plusieurs couleurs RadiusX= 0.5 RadiusX= 1 RadiusX= 0.5
dégradé linéaire en RadiusY= 0.5 RadiusY= 0.5 RadiusY= 1
diagonale avec Center= 0 , 0 Center= 0.5 , 0.5 Center= 0.5 , 0.5
plusieurs couleurs

Le rectangle x_rect_3 est rempli par un dégradé linéaire à orientation en


diagonale avec un Offset de 1, et x_rect_4 est rempli par un dégradé linéaire à
Copyright 2011 Patrice REY

orientation en diagonale avec un Offset de 0.5.

<Rectangle Width='100' Height='75' Name='x_rect_3'


Canvas.Left='10' Canvas.Top='354'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,1'>
<GradientStop Color='MediumBlue' Offset='0' />
CHAPITRE 4 □ Les couleurs et les pinceaux 151
<GradientStop Color='White' Offset='1' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width='100' Height='75' Canvas.Left='10'
Canvas.Top='530' Name='x_rect_4'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,1'>
<GradientStop Color='DarkGreen' Offset='0' />
<GradientStop Color='Wheat' Offset='0.5' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

Le rectangle x_rect_5 est rempli par un dégradé linéaire à orientation verticale


avec plusieurs couleurs, et x_rect_6 est rempli par un dégradé linéaire en
diagonale avec plusieurs couleurs.

<Rectangle Width='100' Height='75' Canvas.Left='10'


Canvas.Top='705' Name='x_rect_5'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>
<GradientStop Color='Red' Offset='0.3' />
<GradientStop Color='Green' Offset='0.5' />
<GradientStop Color='Blue' Offset='0.8' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width='100' Height='75' Canvas.Left='10'
Canvas.Top='903' Name='x_rect_6'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,1'>
<GradientStop Color='Red' Offset='0.2' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='Coral' Offset='0.4' />
<GradientStop Color='Blue' Offset='0.5' />
<GradientStop Color='White' Offset='0.6' />
<GradientStop Color='Green' Offset='0.7' />
<GradientStop Color='Purple' Offset='0.8' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
152 La programmation graphique 2D de WPF 4
Le dégradé RadialGradientBrush agit depuis un point central vers la circonférence
d’une ellipse. Le point central est défini par la propriété GradientOrigin, et
correspond à l’emplacement 0 d’un GradientStop. La circonférence correspond
à l’emplacement 1 d’un GradientStop. Le dégradé prend la forme d’une ellipse
dont la position est définie par la propriété Center (0.5 et 0.5 par défaut) et les
dimensions par RadiusX (0.5 par défaut) et RadiusY (0.5 par défaut).
Le bas de la figure 4-6 montre les principaux types de dégradé circulaire. Par
exemple, l’ellipse x_ellipse1 est remplie par un dégradé circulaire de centre
(0.5,0.5) et de RadiusX=1 et RadiusY=1. L’ellipse x_ellipse2 est remplie par un
dégradé circulaire de centre (0,0) et de RadiusX=1 et RadiusY=1.

<Ellipse x:Name='x_ellipse1' Stroke='Black' Width='100'


Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='1174'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0.5,0.5' RadiusX='1' RadiusY='1'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name='x_ellipse2' Stroke='Black' Width='100'
Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='1406'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0,0' RadiusX='1' RadiusY='1'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
Copyright 2011 Patrice REY

</Ellipse>

L’ellipse x_ellipse3 est remplie par un dégradé circulaire de centre (0.5,0.5) et


de RadiusX=0.5 et RadiusY=0.5. L’ellipse x_ellipse4 est remplie par un dégradé
circulaire de centre (0,0) et de RadiusX=0.5 et RadiusY=0.5.

<Ellipse x:Name='x_ellipse3' Stroke='Black' Width='100'


CHAPITRE 4 □ Les couleurs et les pinceaux 153
Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='1640'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0.5,0.5' RadiusX='0.5' RadiusY='0.5'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name='ellipse4' Stroke='Black' Width='100'
Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='1870'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0,0' RadiusX='0.5' RadiusY='0.5'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>

L’ellipse x_ellipse5 est remplie par un dégradé circulaire de centre (0.5,0.5) et


de RadiusX=1 et RadiusY=0.5. L’ellipse x_ellipse6 est remplie par un dégradé
circulaire de centre (0.5,0.5) et de RadiusX=0.5 et RadiusY=1.

<Ellipse x:Name='x_ellipse5' Stroke='Black' Width='100'


Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='2107'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0.5,0.5' RadiusX='1' RadiusY='0.5'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name='x_ellipse6' Stroke='Black' Width='100'
Height='100' Margin='5' Canvas.Left='5'
Canvas.Top='2343'>
154 La programmation graphique 2D de WPF 4
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.5,0.5'
Center='0.5,0.5' RadiusX='0.5' RadiusY='1'>
<GradientStop Color='Red' Offset='0' />
<GradientStop Color='Yellow' Offset='0.3' />
<GradientStop Color='DarkGreen' Offset='0.6' />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>

2.3 - Le pinceau image

La classe ImageBrush définit un pinceau composé d’une image bitmap spécifiée


dans la propriété ImageSource, pour peindre une zone avec une image. La
propriété Stretch indique le mode d’étirement à appliquer (sa valeur par défaut
est Fill). Il est ainsi possible d’appliquer une image sur tout élément WPF
disposant d’une propriété de type Brush.
L’UserControl ExempleImageBrush.xaml, dans le dossier chapitre04, illustre
l’utilisation d’un ImageBrush pour peindre (figure 4-7).
Dans l’exemple, nous avons une image de taille 100 par 100 pixels qui est
la source. Nous allons peindre un rectangle de taille 250 par 200 pixels avec
différents mode d’étirement.
Le rectangle x_rect_1 est rempli par l’image avec le mode d’étirement égal à
None (le contenu ne s’étire pas).

<Rectangle Width='250' Height='200' Canvas.Left='10'


Canvas.Top='158' Name='x_rect_1' Stroke='Black'>
<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/couleur_carre_100x100.jpg'
Stretch='None' TileMode='None' AlignmentX='Left'
AlignmentY='Top' />
Copyright 2011 Patrice REY

</Rectangle.Fill>
</Rectangle>

Le rectangle x_rect_2 est rempli par l’image avec le mode d’étirement égal à
Fill (le contenu est mis à l’échelle pour s’ajuster aux dimensions; toutefois, les
proportions du contenu sont conservées).
CHAPITRE 4 □ Les couleurs et les pinceaux 155
Figure 4-7
156 La programmation graphique 2D de WPF 4
<Rectangle Canvas.Left='287' Canvas.Top='158' Height='200'
Name='x_rect_2' Stroke='Black' Width='250'>
<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/couleur_carre_100x100.jpg'
Stretch='Fill' TileMode='None' />
</Rectangle.Fill>
</Rectangle>

Le rectangle x_rect_3 est rempli par l’image avec le mode d’étirement égal à
Uniform (le contenu est mis à l’échelle afin qu’il remplisse complètement la
zone de sortie mais conserve ses proportions d’origine).

<Rectangle Canvas.Left='10' Canvas.Top='453' Height='200'


Name='x_rect_3' Stroke='Black' Width='250'>
<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/couleur_carre_100x100.jpg'
Stretch='Uniform' TileMode='None' />
</Rectangle.Fill>
</Rectangle>

Le rectangle x_rect_4 est rempli par l’image avec le mode d’étirement égal à
UniformToFill (le contenu est mis à l’échelle pour s’ajuster à la mosaïque; puisque
la hauteur et la largeur du contenu sont mises à l’échelle indépendamment, les
proportions d’origine du contenu ne peuvent pas être conservées; autrement
dit, le contenu peut être déformé pour remplir complètement la mosaïque de
sortie).

<Rectangle Canvas.Left='287' Canvas.Top='453' Height='200'


Name='x_rect_4' Stroke='Black' Width='250'>
Copyright 2011 Patrice REY

<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/couleur_carre_100x100.jpg'
Stretch='UniformToFill' TileMode='None' />
</Rectangle.Fill>
</Rectangle>
CHAPITRE 4 □ Les couleurs et les pinceaux 157
La figure 4-7 montre l’exemple correspondant dans ces quatre modes
d’étirement possibles.

2.4 - Le pinceau VisualBrush

La classe VisualBrush définit un pinceau composé d’un objet Visual, spécifié


dans la propriété Visual. Il est ainsi possible de décorer tout élément WPF
disposant d’une propriété de type Brush avec la représentation visuelle d’un
autre élément WPF, mais sans toutefois bénéficier de son interactivité. Cela
peut être utilisé pour projeter ou agrandir une portion de l’écran vers une zone
donnée, ou encore pour créer des effets de réflexion.
L’UserControl ExempleVisualBrush.xaml, dans le dossier chapitre04, montre
l’utilisation de l’objet VisualBrush (figure 4-8).
Le rectangle x_rect_1 (exemple de gauche sur la figure 4-8) est décoré par la
représentation visuelle d’un TextBlock. Pour ce faire, on remplit le rectangle
en affectant à sa propriété Fill un objet VisualBrush avec une opacité de 0.5,
un mode d’étirement fixé à None et un mode de répétition de motif fixé à
Tile. La propriété Visual du VisualBrush définit le contenu du pinceau (le nœud
VisualBrush.Visual). On affecte à cette propriété un objet TextBlock dont on fixe
son texte et sa couleur d’écriture (TextBlock.Foreground).

<Rectangle Canvas.Left='13' Canvas.Top='70' Height='287' Name='x_


rect_1'
Stroke='Black' Width='338'>
<Rectangle.Fill>
<VisualBrush Opacity='0.5' Viewport='0,0,250,30'
ViewportUnits='Absolute' TileMode='Tile' Stretch='None'>
<VisualBrush.Visual>
<TextBlock FontFamily='Palatino Linotype'
FontSize='20px' Text='Peindre avec ce texte'>
<TextBlock.Foreground>
<LinearGradientBrush StartPoint='0,0.5'
EndPoint='1,0.5'>
<LinearGradientBrush.GradientStops>
<GradientStop Offset='0.0'
Color='Crimson' />
<GradientStop Offset='1.0'
Color='DarkBlue' />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
158 La programmation graphique 2D de WPF 4
Figure 4-8

Copyright 2011 Patrice REY

</TextBlock.Foreground>
</TextBlock>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
CHAPITRE 4 □ Les couleurs et les pinceaux 159
Le StackPanel x_stackpanel_reflet (exemple de droite sur la figure 4-8) contient
un Canvas x_cnv_reflet et un autre Canvas x_cnv_reflechi qui est la représentation
visuelle du x_cnv_reflet, mais avec un effet de réflexion.

<StackPanel Canvas.Left='357' Canvas.Top='70'


Name='x_stackpanel_reflet'>
<Canvas Name='x_cnv_reflet' Height='143' Width='400'>
...
</Canvas>
<Canvas x:Name='x_cnv_reflechi'
...
</Canvas>
</StackPanel>

Le x_cnv_reflet est rempli par un dégradé de couleurs. Il contient un TextBlock


rempli par un texte, et une ellipse x_balle dont l’apparence est celle d’une balle
rouge.

<Canvas Name='x_cnv_reflet' Height='143' Width='400'>


<Canvas.Background>
<LinearGradientBrush StartPoint='0,0.5' EndPoint='1,0.5'>
<LinearGradientBrush.GradientStops>
<GradientStop Offset='0.0' Color='Cornsilk' />
<GradientStop Offset='1.0' Color='Pink' />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.Background>
...
<Ellipse Name='x_balle' Height='50' Width='50' Canvas.Left='6'
Canvas.Top='6'>
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin='0.75,0.25'>
<RadialGradientBrush.GradientStops>
<GradientStop Color='White' Offset='0.0' />
<GradientStop Color='Red' Offset='0.75' />
<GradientStop Color='DarkRed' Offset='1.0' />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<TextBlock Width='390' Canvas.Top='73' Canvas.Left='5'
TextWrapping='Wrap'>
160 La programmation graphique 2D de WPF 4
<Run Text='Lorem ipsum dolor sit amet,
consectetuer adipiscing elit.'></Run>
<Run Text='Lorem ipsum dolor sit amet,
consectetuer adipiscing elit.'></Run>
<Run Text='Lorem ipsum dolor sit amet,
consectetuer adipiscing elit.'></Run>
<Run Text='Lorem ipsum dolor sit amet,
consectetuer adipiscing elit.'></Run></TextBlock>
</Canvas>

Le Canvas x_cnv_reflechi doit avoir la représentation du x_cnv_reflet mais en


sens inversé.
En premier il faut faire une copie du x_cnv_reflet. On affecte à la propriété
Height du x_cnv_reflechi la hauteur du x_cnv_reflet par une liaison de données
Height = «{Binding Path=(Canvas.ActualHeight), ElementName=x_cnv_reflet}». On
fait de même pour la largeur Width = «{Binding Path=(Canvas.ActualWidth),
ElementName=x_cnv_reflet}». Le Canvas x_cnv_reflechi a alors la même taille que
x_cnv_reflet.
En deuxième, on réalise une représentation visuelle de x_cnv_reflet en
remplissant le fond de x_cnv_reflechi par un VisualBrush dont la propriété Visual
est le x_cnv_reflet. Cela se fait par VisualBrush x:Name=»myVisualBrush» Visual
= «{Binding ElementName=x_cnv_reflet}». A ce stade, le x_cnv_reflechi est le reflet
de x_cnv_reflet, mais son reflet n’est pas inversé.
On ajoute alors à la propriété RelativeTransform du VisualBrush, une
transformation pour inverser le x_cnv_reflet par un ScaleTransform (ScaleX=1
et ScaleY=1) ainsi qu’un TranslateTransform (Y=1). Le x_cnv_reflechi est bien le
reflet inversé de x_cnv_reflet.
Et pour faire en sorte que le haut du x_cnv_reflechi soit visible, que le bas du x_
cnv_reflechi soit masqué et qu’entre les deux ce soit une visibilité décroissante,
on ajoute un masque d’opacité dont le pinceau est un dégradé de transparence.

<Canvas x:Name='x_cnv_reflechi'
Copyright 2011 Patrice REY

Height='{Binding Path=(Canvas.ActualHeight),
ElementName=x_cnv_reflet}'
Width='{Binding Path=(Canvas.ActualWidth),
ElementName=x_cnv_reflet}'>
<Canvas.Background>
<VisualBrush x:Name='myVisualBrush' Stretch='None'
AlignmentX='Left' AlignmentY='Top' Opacity='1'
CHAPITRE 4 □ Les couleurs et les pinceaux 161
Visual='{Binding ElementName=x_cnv_reflet}'>
<VisualBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform ScaleX='1' ScaleY='-1' />
<TranslateTransform Y='1' />
</TransformGroup>
</VisualBrush.RelativeTransform>
</VisualBrush>
</Canvas.Background>
<Canvas.OpacityMask>
<LinearGradientBrush StartPoint='0.5,0' EndPoint='0.5,1'>
<LinearGradientBrush.GradientStops>
<GradientStop Color='#FF000000' Offset='0.0' />
<GradientStop Color='#00000000' Offset='0.7' />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Canvas.OpacityMask>
</Canvas>

De façon à mettre en évidence que x_cnv_reflechi soit bien une représentation


visuelle de x_cnv_reflet, on fait bouger la balle x_balle par une animation qui
cible la position d’affichage de la balle sur le Canvas (les propriétés Canvas.Top
et Canvas.Left). Cette animation se déclenche lorsque l’événement Loaded du
x_cnv_reflet se produit. Le bas de la figure 4-8 montre deux copies d’écran où
la balle est en déplacement dans x_cnv_reflet, et l’on voit bien le reflet inversé
de la balle dans x_cnv_reflechi.

<Canvas.Triggers>
<EventTrigger RoutedEvent='Canvas.Loaded'>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName='x_balle'
Storyboard.TargetProperty='(Canvas.Top)'
From='0' To='100' Duration='0:0:3'
RepeatBehavior='Forever'
AutoReverse='True'
AccelerationRatio='1.0' />
<DoubleAnimation Storyboard.TargetName='x_balle'
Storyboard.TargetProperty='(Canvas.Left)'
From='0' To='350' Duration='0:0:20'
RepeatBehavior='Forever'
AutoReverse='True'
162 La programmation graphique 2D de WPF 4
AccelerationRatio='1.0' />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>

2.5 - Le pinceau DrawingBrush

La classe DrawingBrush définit un pinceau composé d’un objet Drawing spécifié


dans la propriété Drawing. Il est ainsi possible d’appliquer une image clipart
sur tout élément WPF disposant d’une propriété de type Brush. L’UserControl
ExempleDrawingBrush.xaml, dans le dossier chapitre04, montre l’utilisation
d’un objet DrawingBrush (figure 4-9).
A gauche de la figure 4-9, le Canvas x_cnv_appli est rempli par un motif en
forme de grille. Cette grille (visible dans le bas de la figure 4-9) est composée
d’une bordure haute horizontale et d’une bordure gauche verticale, de couleur
MediumBlue et d’épaisseur 2. L’intérieur de la grille est composé de lignes
verticales et horizontales dont la couleur est LightCoral et l’épaisseur de 1. Ce
motif de base est répété comme une mosaïque.
On fixe un objet DrawingBrush à la propriété Background du Canvas, dont le
motif de répétition est TileMode = «Tile». La propriété Drawing de DrawingBrush
reçoit un groupe de figures dont les enfants sont dans le nœud Drawing.
Children. Le dessin d’une ligne se fait par un GeometryDrawing qui dessine une
géométrie Geometry à l’aide du Brush et du Pen spécifiés.

<Canvas.Background>
<DrawingBrush Viewport='0,0,50,50' ViewportUnits='Absolute'
TileMode='Tile'>
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Geometry='M0,0 L50,0'>
Copyright 2011 Patrice REY

<GeometryDrawing.Pen>
<Pen Thickness='2' Brush='MediumBlue' />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry='M0,10 L50,10'>
<GeometryDrawing.Pen>
<Pen Thickness='1' Brush='LightCoral' />
</GeometryDrawing.Pen>
CHAPITRE 4 □ Les couleurs et les pinceaux 163
Figure 4-9

bouton dessiner bouton dessiner ellipse


rectangle avec son avec son ToolTip
bouton sélectionner
ToolTip
avec son ToolTip

motif de base de la
grille
164 La programmation graphique 2D de WPF 4
</GeometryDrawing>
...
<GeometryDrawing Geometry='M0,0 L0,50'>
<GeometryDrawing.Pen>
<Pen Thickness='2' Brush='MediumBlue' />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry='M10,0 L10,50'>
<GeometryDrawing.Pen>
<Pen Thickness='1' Brush='LightCoral' />
</GeometryDrawing.Pen>
</GeometryDrawing>
...
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Canvas.Background>

A droite de la figure 4-9, on réalise trois boutons dont le contenu est peint avec
une image dont la source est un DrawingImage. Le bouton x_btn_selection permet
de sélectionner par exemple une figure dans un logiciel de dessin vectoriel. On
réalise son contenu avec une flèche orientée à 45 degrés. La propriété Drawing
du DrawingImage reçoit un GeometryDrawing dont le pinceau de remplissage
est un Brush = «Cornsilk». La propriété Geometry du GeometryDrawing reçoit
une géométrie PathGeometry représentant une flèche orientée à 45 degrés (la
propriété Figures reçoit les points d’une fléche horizontale auquelle on effectue
une rotation). Le GeometryDrawing est dessiné avec un Pen dont le Brush est
DarkBlue et le Thickness est de 3.

<Button Name='x_btn_selection' ToolTip='sélectionner' Canvas.


Left='340'
Canvas.Top='130' Cursor='Hand'>
<Image Width='80' Height='80'>
Copyright 2011 Patrice REY

<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Brush='Cornsilk'>
<GeometryDrawing.Geometry>
<PathGeometry
Figures='M25,75 L 50,0 75,75 60,75 60,100 40,100,40,75Z'>
CHAPITRE 4 □ Les couleurs et les pinceaux 165
<PathGeometry.Transform>
<RotateTransform CenterX='50'
CenterY='50' Angle='45' />
</PathGeometry.Transform>
</PathGeometry>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Brush='DarkBlue' Thickness='3' />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>

Le bas de la figure 4-9 montre les trois boutons affichant leur ToolTip quand le
curseur de la souris survole les boutons en prenant l’apparence Cursor.Hand.

3 - Les effets graphiques

La classe Effect (espace de noms System.Windows.Media.Effects) fournit un effet


bitmap personnalisé. WPF permet d’appliquer à son rendu des effets visuels
évolués, appliqués au niveau de chaque pixel, tout en conservant sa richesse
de composition, ses animations et ses autres fonctionnalités courantes. Ces
effets héritent de la classe Effect et exploitent l’accélération matérielle. La
figure 4-10 visualise l’arbre d’héritage.
Figure 4-10
166 La programmation graphique 2D de WPF 4
Les classes dérivées sont:
• DropShadowEffect qui génère un effet d’ombre portée à la surface affichée.
• BlurEffect qui applique un flou à la surface affichée.
• ShaderEffect qui permet de réaliser des effets personnalisés.
A noter que les propriétés de ces classes permettent de régler les effets, et
peuvent être animées et pilotées au moyen du databinding. La propriété Effect
des objets UIElement permet d’appliquer un effet à un élément visuel et à tous
ses éléments enfants.
L’UserControl ExempleEffect.xaml, dans le dossier chapitre04, visualise ces effets
sur des éléments visuels (figure 4-11).

Figure 4-11

Copyright 2011 Patrice REY


CHAPITRE 4 □ Les couleurs et les pinceaux 167
3.1 - L’effet DropShadowEffect

Le DropShadowEffect est un effet bien adapté aux éléments d’interface


utilisateur. Il ajoute un effet d’ombre, et ses propriétés permettent des
ajustements:
• Color pour la couleur de l’ombre.
• ShadowDepth qui est la distance entre l’élément et l’ombre avec une valeur
comprise entre 0 et 300 pixels (5 pixels par défaut).
• Opacity qui représente le niveau d’opacité de l’ombre avec une valeur
allant de 0 à 1 (1 par défaut).
• BlurRadius qui est l’intensité du flou de l’ombre (valeur de 5 par défaut).
• Direction qui est la direction de l’ombre avec une valeur entre 0 et 360 (315
par défaut); la valeur 0 correspond à la droite de l’élément, et la rotation se
fait dans le sens inverse des aiguilles d’une montre.
• RenderingBias est le comportement de la performance (Quality ou
Performance).
Par exemple, sur la figure 4-11, le bouton x_btn_1 est doté d’un effet
DropShadowEffect, de couleur orange avec une opacité de 0.4.

<Button Canvas.Left='40' Canvas.Top='118' Content='bouton 1' Height='39'


Name='x_btn_1' Width='117' FontSize='14' FontFamily='Verdana'>
<Button.Background>
<LinearGradientBrush EndPoint='0.5,1' StartPoint='0.5,0'>
<GradientStop Color='DodgerBlue' Offset='0' />
<GradientStop Color='DodgerBlue' Offset='1' />
<GradientStop Color='Bisque' Offset='0.5' />
</LinearGradientBrush>
</Button.Background>
<Button.Effect>
<DropShadowEffect ShadowDepth='20' Direction='225' Opacity='0.4'
BlurRadius='15' RenderingBias='Quality'
Color='Orange'>
</DropShadowEffect>
</Button.Effect>
</Button>

Le TextBlock x_text_1 possède aussi une ombre portée, de couleur rouge et


d’opacité 1, qui apporte un style au texte écrit.
168 La programmation graphique 2D de WPF 4
<TextBlock Canvas.Left='226' Canvas.Top='109' Height='54'
Name='x_text_1'
Text='mon texte avec effet' Width='453' TextAlignment='Center'
FontFamily='Verdana' FontSize='40' Foreground='Blue'>
<TextBlock.Effect>
<DropShadowEffect ShadowDepth='20' Direction='225' Opacity='1'
BlurRadius='10' RenderingBias='Quality' Color='Red'>
</DropShadowEffect>
</TextBlock.Effect>
</TextBlock>

3.2 - L’effet BlurEffect

L’effet de flou est assuré par la classe BlurEffect qui expose les propriétés
suivantes:
• Radius: l’intensité du flou est réglable au moyen de la propriété Radius
(une valeur de 5 par défaut), qui indique la profondeur d’échantillonnage
servant à déterminer la couleur d’un pixel à partir de celle de ses voisins.
• KernelType: détermine le type d’algorithme employé, soit Box, soit Gaussian
(valeur par défaut).
• RenderingBias: définit le comportement privilégié, Performance (par défaut)
ou Quality.
Le contrôle image x_img_blur_gaus affiche une image en ressource pour
laquelle un effet BlurEffect lui est appliquée avec une propriété KernelType
fixée à Gaussian. L’image x_img_blur_box affiche la même image mais avec la
propriété KernelType fixée à Box.

<Image Canvas.Left='289' Canvas.Top='233' Height='169'


Name='x_img_blur_gaus'
Source='/ProgGraphiqueWpf;
component/contenu/chap04/rocks.jpg'
Stretch='Fill' Width='180'>
Copyright 2011 Patrice REY

<Image.Effect>
<BlurEffect KernelType='Gaussian' RenderingBias='Performance'
Radius='5' />
</Image.Effect>
</Image>
<Image Canvas.Left='538' Canvas.Top='233' Height='169'
Name='x_img_blur_box'
CHAPITRE 4 □ Les couleurs et les pinceaux 169
Source='/ProgGraphiqueWpf;
component/contenu/chap04/rocks.jpg'
Stretch='Fill' Width='180'>
<Image.Effect>
<BlurEffect KernelType='Box' Radius='5'
RenderingBias='Performance' />
</Image.Effect>
</Image>

4. Les transformations du pinceau

La classe Brush fournit deux propriétés de transformation qui sont Transform


et RelativeTransform. Les propriétés permettent de faire pivoter, d’incliner, de
mettre à l’échelle et de traduire le contenu d’un pinceau. Quand vous appliquez
une transformation à la propriété Transform d’un pinceau, vous devez connaître
la taille de la zone peinte si vous voulez transformer le contenu du pinceau
autour de son centre. Quand vous appliquez une transformation à la propriété
RelativeTransform d’un pinceau, cette transformation est appliquée au pinceau
avant que sa sortie ne soit mappée à la zone peinte.
Voici l’ordre dans lequel le contenu d’un pinceau est traité et transformé:
• Pour GradientBrush, cela signifie la détermination d’une zone de dégradé;
pour TileBrush, Viewbox est mappé au Viewport; cela devient la sortie du
pinceau.
• Projetez la sortie du pinceau sur le rectangle de transformation 1 x 1.
• Appliquez la RelativeTransform du pinceau, le cas échéant.
• Projetez la sortie transformée sur la zone à peindre.
• Appliquez la Transform du pinceau, le cas échéant.
Dans la mesure où RelativeTransform est appliquée alors que la sortie du
pinceau est mappée à un rectangle 1 x 1, les valeurs d’offset et du centre de
transformation apparaissent comme étant relatives.
L’UserControl ExempleBrushTransform.xaml, dans le dossier chapitre04, visualise
les différents cas de transformations appliquées au pinceau (figure 4-12).
Par exemple, le rectangle x_rect_1_rt se voit appliqué une rotation de 45 degrés
sur la propriété RelativeTransform du LinearGradientBrush, et le rectangle x_
rect_1_t se voit appliqué une rotation de 45 degrés sur la propriété Transform
du LinearGradientBrush. Nous voyons bien l’effet donné sur la figure 4-12.
170 La programmation graphique 2D de WPF 4
Figure 4-12

Copyright 2011 Patrice REY

<Rectangle Width='120' Height='60' Margin='5' Grid.Column='2'


Grid.Row='1' Name='x_rect_1_rt'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>
<GradientStop Color='Pink' Offset='0.4' />
<GradientStop Color='LightYellow' Offset='0.5' />
<GradientStop Color='Pink' Offset='0.6' />
<LinearGradientBrush.RelativeTransform>
CHAPITRE 4 □ Les couleurs et les pinceaux 171
<RotateTransform CenterX='0.5' CenterY='0.5'
Angle='45' />
</LinearGradientBrush.RelativeTransform>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width='120' Height='60' Margin='5' Grid.Column='3'
Grid.Row='1' Name='x_rect_1_t'>
<Rectangle.Fill>
<LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>
<GradientStop Color='Pink' Offset='0.4' />
<GradientStop Color='LightYellow' Offset='0.5' />
<GradientStop Color='Pink' Offset='0.6' />
<LinearGradientBrush.Transform>
<RotateTransform CenterX='60' CenterY='30'
Angle='45' />
</LinearGradientBrush.Transform>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

Sur les rectangles x_rect_3_rt et x_rect_3_t, remplis avec un ImageBrush, l’effet


est encore plus saisissant.

<Rectangle Width='120' Height='60' Margin='5' Grid.Column='2'


Grid.Row='3' Name='x_rect_3_rt'>
<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/coucher_soleil.jpg'>
<ImageBrush.RelativeTransform>
<RotateTransform CenterX='0.5' CenterY='0.5'
Angle='45' />
</ImageBrush.RelativeTransform>
</ImageBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width='120' Height='60' Margin='5' Grid.Column='3'
Grid.Row='3' Name='x_rect_3_t'>
<Rectangle.Fill>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/chap04/coucher_soleil.jpg'>
172 La programmation graphique 2D de WPF 4
<ImageBrush.Transform>
<RotateTransform CenterX='60' CenterY='30'
Angle='45' />
</ImageBrush.Transform>
</ImageBrush>
</Rectangle.Fill>
</Rectangle>

Copyright 2011 Patrice REY


CHAPITRE 5

Les animations

Les animations permettent de réaliser des interfaces utilisateur et des


graphiques complexes et spectaculaires. Elles contribuent à rendre vos
applications WPF plus intuitives.
Dans ce chapitre nous allons voir la richesse des classes qui gèrent les
animations et vous montrer comment les utiliser dans les applications WPF.

1 - Principes des animations

WPF permet d’animer les valeurs des propriétés des éléments en configurant
leur progression sur une durée donnée au moyen de différentes stratégies. La
progression peut être linéaire entre une valeur de départ et une valeur cible.
Elle peut suivre une application d’algorithmes d’interpolation prédéfinis entre
une valeur de départ et une valeur cible, dans le but d’augmenter le réalisme.
Elle peut être modifiée parmi une série de valeurs intermédiaires. Elle peut
aussi suivre des valeurs définies dans une géométrie.
Le système d’animation s’applique aux propriétés de dépendance des objets
supportant l’interface IAnimatable. La plupart des objets WPF, y compris ceux
qui sont non visuels, sont concernés. Comme les propriétés de dépendance
ne sont pas toutes du même type, compte tenu que l’animation est définie au
moyen d’un objet qui dépend du type de la propriété de dépendance ciblée,
WPF fournit un grand nombre de classes pour les principaux types susceptibles
d’être animés.

1.1 - Les types d’animation

Généralement le préfixe des classes correspond au type supporté:


DoubleAnimation pour animer les propriétés de type double, ColorAnimation pour
animer les propriétés de type Color, etc. Les types sans correspondance ne
peuvent pas être animés sauf en créant des classes d’animations personnalisées.
Les animations basiques font varier sur une durée donnée (Duration) la valeur
d’une propriété cible entre une valeur de départ (From) et une valeur d’arrivée
174 La programmation graphique 2D de WPF 4
(To), ou bien à partir de la valeur courante en fonction d’un incrément (By).
Leur variation est linéaire.
Les animations d’images clés (KeyFrame) permettent de définir des valeurs
intermédiaires, sous forme d’images clés, et leur mode d’interpolation peut
être varié (interpolation linéaire, discrète, basée sur une courbe de Bézier ou
basée sur un modèle prédéfini). Les noms des classes de ces animations sont
suffixés de UsingKeyFrames: DoubleAnimationUsingKeyFrames pour animer une
propriété de type double avec images clés, etc.
Les animations sur tracé permettent de définir une variation en fonction
d’une géométrie. Les noms des classes de ces animations sont suffixés de
UsingPath: DoubleAnimationUsingPath pour animer une propriété de type double
en fonction d’une géométrie.

1.2 - Les animations basiques

Une animation basique peut définir une valeur de départ (propriété From),
une valeur finale (propriété To) et un incrément (propriété By) avec différentes
combinaisons:
• From et To: de la valeur de départ From à la valeur finale To.
• From et By: de la valeur de départ From à la valeur From incrémentée de By.
• From seul: de la valeur de départ From à la valeur de base de la propriété.
• To seul: de la valeur courante à la valeur finale To.
• By seul: de la valeur courante à la valeur courante incrémentée de By.
• Aucune spécification: retour à la valeur initiale.
L’UserControl AnimationSimple.xaml, dans le dossier chapitre05, montre
l’utilisation d’une animation basique pour animer le déplacement d’un
rectangle (figure 5-1).
Le Canvas x_cnv_appli est rempli par un dégradé de couleurs. Un rectangle
x_rect est positionné en haut du Canvas avec une position (0,0). Copyright 2011 Patrice REY

<Canvas Canvas.Left='225' Canvas.Top='78' Height='200'


Name='x_cnv_appli' Width='300'>
<Canvas.Background>
<LinearGradientBrush EndPoint='1,0.5' StartPoint='0,0.5'>
<GradientStop Color='LightSlateGray' Offset='0' />
<GradientStop Color='Cornsilk' Offset='1' />
</LinearGradientBrush>
</Canvas.Background>
CHAPITRE 5 □ Les animations 175
Figure 5-1

phases successives:

<Rectangle Canvas.Left='0' Canvas.Top='0' Height='50'


Name='x_rect' Stroke='Crimson' Width='100' RadiusX='10' RadiusY='10'>
<Rectangle.Fill>
<LinearGradientBrush EndPoint='0.5,1' StartPoint='0.5,0'>
<GradientStop Color='CornflowerBlue' Offset='0.091' />
<GradientStop Color='CornflowerBlue' Offset='0.877' />
<GradientStop Color='Gold' Offset='0.5' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Canvas>
176 La programmation graphique 2D de WPF 4
L’animation basique consiste à faire varier sa position en X et Y en agissant sur
ses propriétés de dépendance Top et Left. Ces propriétés sont de type double,
donc on utilise une animation double_anim, de type DoubleAnimation, qui fait
varier de façon linéaire des valeurs de type double. La méthode BeginAnimation(..)
lance l’animation double_anim sur la propriété cible Canvas.LeftProperty et la
propriété cible Canvas.TopProperty. L’animation double_anim dure 5 secondes,
et fait varier les cibles de 0 à 150. On ajoute le gestionnaire de l’événement
CurrentTimeInvalidated pour mettre à jour l’affichage des propriétés Left et Top
du rectangle dans les TextBlock x_text_left et x_text_top.

private void UserControl_Loaded(object sender, RoutedEventArgs e) {


DoubleAnimation double_anim = new DoubleAnimation();
double_anim.From = 0;
double_anim.To = 150;
double_anim.Duration = TimeSpan.FromSeconds(5);
double_anim.AutoReverse = true;
double_anim.RepeatBehavior = RepeatBehavior.Forever;
double_anim.CurrentTimeInvalidated +=
new EventHandler(double_anim_CurrentTimeInvalidated);
x_rect.BeginAnimation(Canvas.LeftProperty, double_anim);
x_rect.BeginAnimation(Canvas.TopProperty, double_anim);
}
private void double_anim_CurrentTimeInvalidated(object sender,
EventArgs e) {
x_text_left.Text = «propriété Left du rectangle = « +
Math.Round(Canvas.GetLeft(x_rect), 2).ToString();
x_text_top.Text = «propriété Top du rectangle = « +
Math.Round(Canvas.GetTop(x_rect), 2).ToString();
}

La bas de la figure 5-1 montre le rectangle dans des phases successives de


déplacement.

2 - Les classes relatives à l’animation


Copyright 2011 Patrice REY

Les classes qui permettent de réaliser et de contrôler les animations sont


nombreuses. Le cheminement des paragraphes qui suivent, doit permettre de
mieux cerner cet ensemble.
CHAPITRE 5 □ Les animations 177

2.1 - Les classes Storyboard et EventTrigger

La classe Storyboard définit un objet qui coordonne des animations. Elle


représente une table de montage séquentiel dont son rôle peut être comparé
à celui d’un séquenceur dans le domaine du montage audio. Cette classe
se trouve dans l’espace de noms System.Windows.Media.Animation. Nous
utiliserons le terme anglophone de storyboard à la place du terme de table de
montage séquentiel dans la suite de ce livre car ce terme est plus parlant et
plus adapté.
Un objet Storyboard permet d’assigner à une propriété cible une animation,
au moyen des propriétés attachées Storyboard.TargetProperty et Storyboard.
TargetName. Un storyboard ne peut cibler une propriété que d’un objet
FrameworkElement, FrameworkContentElement ou Freezable.
Un storyboard peut être déclenché lors d’un événement au moyen d’un
déclencheur de type EventTrigger, lors du changement d’une valeur de
propriété au moyen d’un déclencheur de type Trigger (au sein d’un style), lors
de l’activation d’un état VisualState dans un template, ou par code au moyen de
méthodes spécifiques.
La classe EventTrigger, dans l’espace de noms System.Windows, représente un
déclencheur qui applique un ensemble d’actions en réponse à un événement.
Un objet EventTrigger dispose d’une propriété implicite Actions, qui stocke la
collection d’actions à appliquer lorsque l’événement se produit, et permet
de définir des actions de contrôle du storyboard, de type TriggerAction, parmi
lesquelles:
• BeginStoryboard: action de déclencheur qui démarre un Storyboard et
distribue ses animations à leurs propriétés et objets ciblés.
• PauseStoryboard: action de déclencheur qui permet de suspendre les tables
Storyboard.
• ResumeStoryboard: prend en charge une action de déclencheur qui reprend
un Storyboard suspendu.
• StopStoryboard: action de déclencheur qui permet d’arrêter les tables
Storyboard.
La classe FrameworkElement permet de spécifier des objets EventTrigger dans sa
propriété Triggers. Un objet Trigger permet quant à lui de définir des actions de
contrôle dans ses propriétés EnterActions et ExitActions.
Dans le cadre d’une animation, un objet PropertyPath est affecté à la propriété
178 La programmation graphique 2D de WPF 4
attachée Storyboard.TargetProperty pour définir la propriété ciblée. La classe
PropertyPath (espace de noms System.Windows) implémente une structure
de données pour décrire une propriété comme un chemin d’accès sous une
autre propriété ou un type propriétaire. Les chemins de propriété sont utilisés
dans la liaison de données aux objets, ainsi que dans les tables de montage
séquentiel et les chronologies pour les animations. Les objets PropertyPath
sont également utilisés dans le cadre du databinding.
Dans le cas le plus simple, le PropertyPath est simplement constitué du nom de
la propriété ciblée: Storyboard.TargetProperty = «Height».
Le chemin peut désigner le nom d’une propriété B d’un objet exposé dans une
propriété A par l’objet source, et alors, la syntaxe sera A.B: Ellipse.Fill.
La syntaxe type.propriété sera employée pour une propriété définie par un
chemin XAML, une propriété attachée ou une propriété statique: (Canvas.
Left).
Pour référencer un élément de collection, d’indexeur ou de dictionnaire, l’indice
ou la clé est passé entre crochets. Les différentes syntaxe peuvent être utilisées
conjointement pour cibler la propriété comme dans l’exemple suivant où la
propriété Color d’un GradientStop est ciblée: (Ellipse.Fill) . (RadialGradientBrush.
GradientStops)[2] . (GradientStop.Color).

2.2 - La classe Timeline

La classe Timeline (espace de noms System.Windows.Media.Animation) qui


représente une séquence, une chronologie, est la classe de base de toutes les
animations ainsi que de Storyboard. Un objet Timeline est un segment de temps
correspondant à l’exécution d’un processus. Il expose des propriétés permettant
de définir sa durée, son moment relatif au démarrage, s’il doit être répété, sa
réversibilité, son facteur d’accélération, sa vitesse et son comportement en fin
de séquence. Les classes qui héritent de la classe Timeline proposent d’autres
fonctionnalités, telles que la lecture de médias et d’animations. Différents
types de chronologies spécialisées parmi ceux disponibles sont donnés en
Copyright 2011 Patrice REY

exemple ci-dessous:
• AnimationTimeline: définit les paramètres d’une animation; un
AnimationTimeline est un type de chronologie qui produit des valeurs de
sortie; lorsque vous associez une animation à une propriété, l’animation
met à jour la valeur de la propriété lors de sa lecture, contribuant ainsi à
l’animer.
CHAPITRE 5 □ Les animations 179
• TimelineGroup: conteneur qui permet d’organiser de façon hiérarchique des
objets Timeline; cette classe abstraite est l’ancêtre de la classe Storyboard
(qui définit une propriété cible) et de la classe ParallelTimeline (qui permet
de regrouper des animations au sein d’un storyboard).
• MediaTimeline: un MediaTimeline est un type de chronologie qui contrôle la
lecture d’un fichier multimédia.
• ParallelTimeline: un ParallelTimeline est un type de chronologie qui regroupe
d’autres chronologies.
• Storyboard: un Storyboard est un type spécial de ParallelTimeline qui propose
des informations de ciblage sur les objets et les propriétés destinées aux
chronologies qu’il contient.
La figure 5-2 visualise l’arbre d’héritage de la classe Animatable. On voit bien
où se situe Storyboard, Timeline, et toutes les animations de propriétés (comme
DoubleAnimation, etc.).

Figure 5-2
180 La programmation graphique 2D de WPF 4
Le storyboard est un conteneur d’objets Timeline, il est donc possible d’imbriquer
plusieurs storyboards au sein d’un Timeline. De ce fait, cette imbrication
sous forme d’organisation hiérarchique permet d’élaborer des scénarios de
séquencement évolués. Car la plupart des propriétés d’un objet Timeline ont
un effet relatif au conteneur dans lequel il évolue.
Un objet Timeline a une durée de base déterminée par la propriété Duration.
En XAML une durée de 4 secondes s’exprimera par Duration = «0:0:4». Par
le code, on utilise un objet TimeSpan (structure représentant un intervalle de
temps) qui est passé au constructeur par l’intermédiaire de la méthode statique
TimeSpan.FromSeconds. Une durée de 4 secondes s’exprimera par Duration
duree = new Duration(TimeSpan.FromSeconds(4)). A noter que la durée d’une
animation est toujours précisée en unités de temps finies. Pour cette raison,
des valeurs spéciales peuvent être utilisées pour Storyboard et ParallelTime
(Duration.Automatic qui est la valeur par défaut indiquant que la chronologie
s’arrête avec la dernière animation contenue, et Duration.Forever indiquant une
durée infinie).
La propriété BeginTime, de type TimeSpan, indique le temps à partir duquel
l’exécution commence. Ce temps est relatif à l’objet Timeline conteneur. Pour
un storyboard, on exprimera en XAML <Storyboard BeginTime = «0:0:4»>.
La propriété RepeatBehavior permet de configurer le comportement à
répétition d’une chronologie c’est-à-dire si l’exécution d’un objet Timeline doit
être répétée et comment. La propriété RepeatBehavior définit soit le nombre
d’exécutions de l’objet Timeline (propriété Count), soit la durée d’exécution
(propriété Duration). Une itération Count spécifie le nombre de répétitions de
la chronologie, une valeur TimeSpan spécifie la durée totale de la période active
de cette chronologie ou la valeur spéciale Forever qui spécifie la répétition à
l’infini de la chronologie. La valeur par défaut est un RepeatBehavior ayant une
valeur Count de 1, ce qui indique que la chronologie s’accomplit une seule
fois. En XAML on exprimera cela par <Storyboard RepeatBehavior = «Forever»>.
La propriété booléenne AutoReverse permet d’inverser l’exécution d’un objet
Timeline après une première exécution normale. Elle indique si la chronologie
Copyright 2011 Patrice REY

repart en arrière après avoir accompli une itération en avant. En XAML on


exprimera cela par <Storyboard AutoReverse = «true»>.
L’interpolation des valeurs d’une exécution est linéaire par défaut. Les
propriétés AccelerationRatio et DecelerationRatio permettent respectivement
de démarrer et d’arrêter progressivement l’exécution d’un objet Timeline.
Elles spécifient le pourcentage du Duration de la chronologie nécessaire à
CHAPITRE 5 □ Les animations 181
l’accélération temporelle ou à la décélération temporelle, pour passer de zéro
à son taux maximal. La valeur est comprise entre 0 et 1.
La propriété SpeedRatio permet de faire varier la vitesse d’exécution d’un objet
Timeline par un facteur correspondant à sa valeur (1.0 par défaut) et relatif au
conteneur. Les facteurs des différents niveaux se multiplient entre eux (un
conteneur avec un SpeedRatio de 2.0 et un Timeline imbriqué avec un facteur
de 3.0 donne un facteur résultant de 6.0).
La propriété énumérée FillBehavior définit une valeur qui spécifie le
comportement du Timeline une fois qu’il a atteint la fin de sa période active:
• la valeur HoldEnd (par défaut): une fois la fin de sa période active atteinte,
la chronologie maintient sa progression jusqu’à la fin de la période active
et de mise en attente de son parent.
• la valeur Stop: la chronologie s’arrête si elle se trouve hors de sa période
active alors que son parent se trouve dans sa période active.

2.3 - Mise en application

L’UserControl MiseEnApplicationAnimation.xaml, dans le dossier chapitre05,


montre différents exemples d’animations mettant en application les concepts
des classes Storyboard, EventTrigger et Timeline (figure 5-3 et 5-4).
Le survol du bouton x_btn1 (figure 5-4) déclenche une animation qui consiste à
modifier la taille du bouton en l’agrandissant. Un storyboard peut être déclenché
lors d’un événement au moyen d’un déclencheur de type EventTrigger. La
classe FrameworkElement permet de spécifier des objets EventTrigger dans sa
propriété Triggers. Comme la classe Button est une classe dérivée dans l’arbre
d’héritage de FrameworkElement, elle hérite de la propriété Triggers.
On peut donc placer les déclencheurs EventTrigger dans sa propriété <Button.
Triggers>. On positionne un EventTrigger dont l’événement correspond à l’entrée
de la souris sur le bouton (<EventTrigger RoutedEvent = «Mouse.MouseEnter»>).
Dans la propriété Actions de EventTrigger, on ajoute un storyboard qui anime
l’agrandissement du bouton en largeur et hauteur (le storyboard k_story_btn_
mouse_enter_taille_1 sous forme d’une ressource statique). On procède de la
même manière pour la sortie de la souris de sur le bouton pour permettre
au bouton de revenir à sa taille initiale (<EventTrigger RoutedEvent = «Mouse.
MouseLeave»> et un storyboard Storyboard = «{StaticResource k_story_btn_mouse_
leave_taille_1}»).
182 La programmation graphique 2D de WPF 4
Figure 5-3

Copyright 2011 Patrice REY


CHAPITRE 5 □ Les animations 183
Figure 5-4
184 La programmation graphique 2D de WPF 4
<Button Name='x_btn1' Width='126' Height='29' Canvas.Left='23'
Canvas.Top='109' Content='bouton 1' Cursor='Hand'
FontFamily='Verdana' FontSize='14'>
<Button.Triggers>
<EventTrigger RoutedEvent='Mouse.MouseEnter'>
<EventTrigger.Actions>
<BeginStoryboard
Storyboard='{StaticResource k_story_btn_mouse_enter_taille_1}' />
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent='Mouse.MouseLeave'>
<EventTrigger.Actions>
<BeginStoryboard
Storyboard=
'{StaticResource k_story_btn_mouse_leave_taille_1}' />
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>

Dans les ressources de l’UserControl (<UserControl.Resources>), on ajoute les


deux Storyboard qui ciblent le bouton x_btn1.

<UserControl.Resources>
<Storyboard x:Key='k_story_btn_mouse_enter_taille_1'>
<DoubleAnimation Storyboard.TargetName='x_btn1'
Storyboard.TargetProperty='Width' Duration='0:0:0.2'
To='200' />
<DoubleAnimation Storyboard.TargetName='x_btn1'
Storyboard.TargetProperty='Height' Duration='0:0:0.2'
To='45' />
</Storyboard>
</UserControl.Resources>

Le survol du bouton x_btn2 (figure 5-4) déclenche une animation qui consiste
Copyright 2011 Patrice REY

à modifier la taille du bouton en l’agrandissant. Or le bouton applique un style


s_btn_anim lors de son rendu. Ce style est une ressource statique (StaticResource
s_btn_anime). Dans ce cas le changement de valeur d’une propriété se fait au
moyen d’un déclencheur de type Trigger.


CHAPITRE 5 □ Les animations 185
<Button Canvas.Left='23' Canvas.Top='195' Content='bouton 2'
Cursor='Hand' Height='29' Name='x_btn2' Width='126'
Style='{StaticResource s_btn_anime}' FontSize='14'
FontFamily='Verdana'>
</Button>

Le style s_btn_anim (référencé par x:Key = «s_btn_anime») cible le type Button


par sa propriété TargetType. Sa propriété Triggers reçoit les Trigger qui agiront
sur la propriété Property des Trigger. Le Trigger correspondant au survol de la
souris sur le bouton (<Trigger Property = «Button.IsMouseOver» Value = «True»>)
déclenche les storyboards dans sa propriété EnterActions et ExitActions. Dans
Trigger.EnterActions, on ajoute un Storyboard (k_story_btn_mouse_enter_taille_2)
qui provoque l’agrandissement du bouton, et dans Trigger.ExitActions, on ajoute
un Storyboard (k_story_btn_mouse_leave_taille_2) qui provoque l’affichage initial
du bouton.

<Style x:Key='s_btn_anime' TargetType='Button'>


<Style.Triggers>
<Trigger Property='Button.IsMouseOver' Value='True'>
<Trigger.EnterActions>
<BeginStoryboard
Storyboard='{StaticResource k_story_btn_mouse_enter_taille_2}' />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard
Storyboard='{StaticResource k_story_btn_mouse_leave_taille_2}' />
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>

Le bouton x_btn3 (figure 5-4) montre comment contrôler une animation par
code. On réalise un storyboard qui fait varier l’arrière-plan d’un bouton en
utilisant différentes couleurs. On ajoute une couleur unie x_btn3_couleur, de
type SolidColorBrush, à la propriété Background du bouton.

<Button Content=»bouton 3» Width=»126» Height=»37» Canvas.Left=»23»


Canvas.Top=»281» FontFamily=»Verdana» FontSize=»14»
Name=»x_btn3» Cursor=»Hand»>
<Button.Background>
186 La programmation graphique 2D de WPF 4
<SolidColorBrush x:Name=»x_btn3_couleur»></SolidColorBrush>
</Button.Background>
</Button>

Par code, on instancie un nouvel objet Storyboard story1. Pour faire varier
une couleur de type Color, on utilise une animation color_anim1 de type
ColorAnimation. On fait varier les couleurs de Colors.Blue à Colors.Yellow sur
une durée de 5 secondes (TimeSpan(0,0,5) comme intervalle de temps). Par la
méthode statique Storyboard.SetTargetName, on cible l’animation color_anim1
au bouton x_btn3. Puis par Storyboard.SetTargetProperty, on cible l’animation
à la propriété Color du bouton avec un objet PropertyPath (SolidColorBrush.
ColorProperty). On ajoute à l’arbre visuel du storyboard story1 l’animation color_
anim1 par la propriété Children. Et on démarre le storyboard par la méthode
Begin(..).

Storyboard story1 = new Storyboard();


ColorAnimation color_anim1 = new ColorAnimation( Colors.Blue,
Colors.Yellow, new Duration(new TimeSpan(0, 0, 5)));
color_anim1.RepeatBehavior = RepeatBehavior.Forever;
color_anim1.AutoReverse = true;
Storyboard.SetTargetName(color_anim1, «x_btn3_couleur»);
Storyboard.SetTargetProperty(color_anim1,
new PropertyPath(SolidColorBrush.ColorProperty));
story1.Children.Add(color_anim1);
story1.Begin(this);

Le bouton x_btn4 (figure 5-4), lors de son événement Click, déclenche une
animation qui consiste à modifier la taille en largeur de 6 rectangles identiques,
avec différentes propriétés du storyboard. Cela permet de mettre en évidence
l’impact des propriétés SpeedRatio, AccelerationRatio et DecelerationRatio.

<Button Canvas.Left='23' Canvas.Top='375'


Copyright 2011 Patrice REY

Content='lancer les animations des rectangles' Height='32'


Width='281' FontFamily='Verdana' FontSize='14' Name='x_btn4'
Cursor='Hand'>
<Button.Triggers>
<EventTrigger RoutedEvent='Button.Click'>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
CHAPITRE 5 □ Les animations 187
<DoubleAnimation Storyboard.TargetName='x_rect1'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10' />
<DoubleAnimation Storyboard.TargetName='x_rect2'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10'
SpeedRatio='1.5' />
<DoubleAnimation Storyboard.TargetName='x_rect3'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10'
SpeedRatio='0.5' />
<DoubleAnimation Storyboard.TargetName='x_rect4'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10'
AccelerationRatio='0.5' />
<DoubleAnimation Storyboard.TargetName='x_rect5'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10'
DecelerationRatio='0.5' />
<DoubleAnimation Storyboard.TargetName='x_rect6'
Storyboard.TargetProperty='Width'
From='20' To='400' Duration='0:0:10'
AccelerationRatio='0.5'
DecelerationRatio='0.5' />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>

L’animation concernant l’ellipse x_ellipse_anim montre comment contrôler une


animation par des boutons. L’ellipse est remplie par un dégradé de 8 couleurs.
On ajoute un déclencheur EventTrigger pour l’événement Click sur le bouton
x_btn_demarrer. Ce déclencheur modifie par des Storyboard les propriétés
ciblées RadiusX, RadiusY et GradientStops[2].Color. Cette animation x_story_
ellipse_anim est lancée par BeginStoryboard.

<EventTrigger RoutedEvent='Button.Click'
SourceName='x_btn_demarrer'>
<EventTrigger.Actions>
<BeginStoryboard Name='x_story_ellipse_anim'>
188 La programmation graphique 2D de WPF 4
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_ellipse_anim'
Storyboard.TargetProperty='Fill.RadiusX'
From='0' To='1' Duration='0:0:2'
RepeatBehavior='5x' />
<DoubleAnimation
Storyboard.TargetName='x_ellipse_anim'
Storyboard.TargetProperty='Fill.RadiusY'
From='0' To='1' Duration='0:0:2'
RepeatBehavior='5x' />
<ColorAnimation
Storyboard.TargetName='x_ellipse_anim'
Storyboard.TargetProperty='Fill.GradientStops[2].Color'
To='Black' Duration='0:0:2'
RepeatBehavior='5x' />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>

Un clic sur le bouton x_btn_pause mettra en pause l’exécution du Storyboard


x_story_ellipse_anim par PauseStoryboard. Un clic sur le bouton x_btn_reprendre
reprendra l’exécution du Storyboard x_story_ellipse_anim par ResumeStoryboard.
Et un clic sur le bouton x_btn_arret arrêtera l’exécution du Storyboard x_story_
ellipse_anim par StopStoryboard en remettant la tête de lecture au début.

<EventTrigger RoutedEvent='Button.Click'
SourceName='x_btn_pause'>
<PauseStoryboard BeginStoryboardName='x_story_ellipse_anim' />
</EventTrigger>
<EventTrigger RoutedEvent='Button.Click'
SourceName='x_btn_reprendre'>
<ResumeStoryboard BeginStoryboardName='x_story_ellipse_anim' />
</EventTrigger>
Copyright 2011 Patrice REY

<EventTrigger RoutedEvent='Button.Click'
SourceName='x_btn_arret'>
<StopStoryboard BeginStoryboardName='x_story_ellipse_anim' />
</EventTrigger>
CHAPITRE 5 □ Les animations 189

3 - L’animation des transformations

La transformation constitue une puissante approche dans le domaine de la


personnalisation des figures. Quand vous appliquez une transformation, vous
ne vous contentez pas uniquement de modifier les dimensions de l’objet,
mais vous ajoutez aussi des translations, des inclinaisons, des rotations, des
déformations et des combinaisons de transformations. Par exemple, animer
un bouton pourrait consister à le faire tourner avec son contenu.
L’UserControl AnimerTransformation.xaml, dans le dossier chapitre05, montre 3
animations représentatives des transformations animées (figure 5-5 et 5-6).

3.1 - Animer une translation

L’animation n°1 (figure 5-5) permet d’animer la translation de quatre rectangles


dès que le curseur de la souris se positionne sur les rectangles. La figure 5-6
montre le résultat obtenu.
Quatre rectangles sont positionnés sur le Canvas x_cnv_ex1 et on leur applique
un style s_style_rect sous forme d’une ressource statique (Style = «{StaticResource
s_style_rect}»). Ce style, appliqué aux rectangles, est ajouté aux ressources de
l’UserControl (<UserControl.Resources>). Il cible les objets Rectangle (propriété
TargetType) et est référencé par la clé s_style_rect (propriété x:Key).
Les balises <Setter> permettent de fixer des propriétés. On fixe pour chaque
rectangle, la propriété Cursor à Hand (<Setter Property = «Cursor» Value =
«Hand»></Setter>).
On fixe une transformation TranslateTransform avec les décalages X=0 et Y=0.
On fixe les Triggers qui contiennent, dans leur collection, les EventTrigger.
Pour l’événement MouseEnter sur un Rectangle, on démarre un Storyboard qui
cible la propriété RenderTransform.X, en lui fixant des valeurs de 0 à 70 sur une
durée de une seconde.
Pour l’événement MouseLeave sur un Rectangle, on démarre un Storyboard
d’une durée de 0.5 seconde, qui ramène le rectangle à sa taille initiale.

<Style TargetType='{x:Type Rectangle}' x:Key='s_style_rect'>


<Setter Property='Cursor' Value='Hand'></Setter>
<Setter Property='RenderTransform'>
<Setter.Value>
<TranslateTransform X='0' Y='0'></TranslateTransform>
190 La programmation graphique 2D de WPF 4
Figure 5-5

Copyright 2011 Patrice REY


CHAPITRE 5 □ Les animations 191
Figure 5-6
192 La programmation graphique 2D de WPF 4
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent='Rectangle.MouseEnter'>
<EventTrigger.Actions>
<BeginStoryboard Name='s_story_translate'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty='RenderTransform.X'
From='0' To='70' Duration='0:0:1'
RepeatBehavior='1x' />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent='Rectangle.MouseLeave'>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty='RenderTransform.X'
Duration='0:0:0.5' />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>

3.2 - Animer une rotation

L’animation n°2 (figure 5-5) permet d’animer la rotation de quatre balles


représentées par des cercles colorées. Le bouton démarrer l’animation exécute
les animations selon leur configuration. La figure 5-6 montre le résultat obtenu.
Les quatre balles sont représentées par des cercles colorées, de type Ellipse. On
Copyright 2011 Patrice REY

leur ajoute une transformation de rotation dont le centre de rotation se trouve


au milieu du cercle.

<Ellipse x:Name='ellipse1' Width='50' Height='50' Stroke='Black'


Canvas.Top='53' Canvas.Left='20'>
<Ellipse.Fill>
<LinearGradientBrush>
CHAPITRE 5 □ Les animations 193
<GradientStop Color='Blue' Offset='0.5' />
<GradientStop Color='LightBlue' Offset='0.5' />
</LinearGradientBrush>
</Ellipse.Fill>
<Ellipse.RenderTransform>
<RotateTransform x:Name='ellipse1Rotate' CenterX='25'
CenterY='25' Angle='0' />
</Ellipse.RenderTransform>
</Ellipse>

L’animation est réalisée ici par code. Le déplacement des balles se fait sur
une longueur de 450 pixels. On instancie une animation double_anim de type
DoubleAnimation, qui fait varier de 0 à 450 pour une durée de 5 secondes. On
cible la propriété Canvas.LeftProperty que l’on anime avec double_anim. De plus
on veut que la balle tourne d’un angle au fur et à mesure de son déplacement.
On agit donc sur la rotation ellipse1Rotate dont on fait varier l’angle de rotation
de 0 à nbre_rotation. La propriété ciblée est RotateTransform.AngleProperty. Le
booléen AutoReverse des animations est à true de façon à ce que les animations
reviennent en sens inverse.

m_double_anim = new DoubleAnimation(0, 450, TimeSpan.FromSeconds(5));


m_double_anim.RepeatBehavior = RepeatBehavior.Forever;
m_double_anim.AutoReverse = true;
ellipse1.BeginAnimation(Canvas.LeftProperty, m_double_anim);
m_double_anim = new DoubleAnimation(0, nbre_rotation,
TimeSpan.FromSeconds(5));
m_double_anim.RepeatBehavior = RepeatBehavior.Forever;
m_double_anim.AutoReverse = true;
ellipse1Rotate.BeginAnimation(RotateTransform.AngleProperty,
m_double_anim);

La première balle se déplace avec des valeurs par défaut pour les animations,
donc à vitesse constante. Dans le cas de la deuxième balle, on fixe une vitesse
d’accélération égale à 0.4 (m_double_anim.AccelerationRatio = 0.4), ce qui
modifie le déplacement. Dans le cas de la troisième balle, on fixe une vitesse
de décélération égale à 0.6 (m_double_anim.DecelerationRatio = 0.6), ce qui
modifie le déplacement. Dans le cas de la quatrième balle, on fixe une vitesse
d’accélération et une vitesse de décélération (m_double_anim.DecelerationRatio =
0.6 et m_double_anim.AccelerationRatio = 0.4), ce qui modifie le déplacement.
La figure 5-6 montre bien que pour un temps donné, les balles ne sont pas
194 La programmation graphique 2D de WPF 4
forcément aux mêmes emplacements compte tenu des accélérations et des
décélérations.

3.3 - Animer des transformations combinées

L’animation n°3 (figure 5-5) permet d’animer un bouton lors du survol par le
curseur de la souris. Cette animation consiste à effectuer des transformations
combinées simultanées. La figure 5-6 montre le bouton quand il tourne.
Le bouton x_btn_ex3 a une taille de 180 de long par 30 de haut. On lui affecte
la valeur Hand à sa propriété Cursor.

<Button Canvas.Left='287' Canvas.Top='85'


Content='mon bouton tournant' Height='30'
Name='x_btn_ex3' Width='180' FontSize='14'
FontFamily='Verdana' FontWeight='Bold' Cursor='Hand'>
...
</Button>

On ajoute à sa propriété RenderTransform un groupe de transformations


composé d’une mise à l’échelle ScaleTransform (ScaleX=1 et ScaleY=1),
d’une inclinaison SkewTransform (AngleX=0 et AngleY=0) et d’une rotation
RotateTransform (Angle=0). Toutes ces transformations ont un ordre et un
centre fixé au milieu du bouton.

<Button.RenderTransform>
<TransformGroup>
<ScaleTransform CenterX='90' CenterY='15' ScaleX='1'
ScaleY='1'>
</ScaleTransform>
<SkewTransform CenterX='90' CenterY='15' AngleX='0'
AngleY='0'>
</SkewTransform>
<RotateTransform CenterX='90' CenterY='15' Angle='0'>
Copyright 2011 Patrice REY

</RotateTransform>
</TransformGroup>
</Button.RenderTransform>

On ajoute au bouton un EventTrigger pour l’événement MouseEnter sur le


contrôle. Dans la propriété Actions de EventTrigger, on ajoute un storyboard
bouton_story pour effectuer l’animation complète.
CHAPITRE 5 □ Les animations 195
Pour ScaleTransform, on fait varier ScaleX de 1 à 1.5. Le PropertyPath
correspondant au ciblage de cette propriété est RenderTransform.Children[0].
ScaleX ([0] car la transformation est le premier enfant du TransformGroup). On
fait de même pour ScaleY.
Pour SkewTransform, on fait varier les angles d’inclinaison de 0 à 30 (AngleX et
AngleY). Le PropertyPath correspondant est RenderTransform.Children[1].AngleX
et RenderTransform.Children[1].AngleY ([1] car la transformation est le deuxième
enfant du TransformGroup).
Pour RotateTransform, on fait varier l’angle de rotation de 0 à 360 (Angle). Le
PropertyPath correspondant est RenderTransform.Children[2].Angle ([2] car la
transformation est le troisième enfant du TransformGroup).

<Button.Triggers>
<EventTrigger RoutedEvent='Button.MouseEnter'>
<EventTrigger.Actions>
<BeginStoryboard Name='bouton_story'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[0].ScaleX'
To='1.5' Duration='0:0:1'
RepeatBehavior='1x'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[0].ScaleY'
To='1.5' Duration='0:0:1'
RepeatBehavior='1x'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[1].AngleX'
To='30' Duration='0:0:1'
RepeatBehavior='1x'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[1].AngleY'
To='30' Duration='0:0:1'
RepeatBehavior='1x'>
</DoubleAnimation>
<DoubleAnimation
196 La programmation graphique 2D de WPF 4
Storyboard.TargetProperty=
«RenderTransform.Children[2].Angle'
To='360' Duration='0:0:1'
RepeatBehavior='1x'>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
...
</Button.Triggers>

On ajoute au bouton un EventTrigger pour l’événement MouseLeave sur le


contrôle. Dans ce storyboard correspondant, on fait revenir les propriétés
ciblées à leurs valeurs initiales.

<Button.Triggers>
...
<EventTrigger RoutedEvent='Rectangle.MouseLeave'>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[0].ScaleX'
Duration='0:0:0.5'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[0].ScaleY'
Duration='0:0:0.5'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[1].AngleX'
Copyright 2011 Patrice REY

Duration='0:0:0.5'>
</DoubleAnimation>
<DoubleAnimation
Storyboard.TargetProperty=
«RenderTransform.Children[1].AngleY'
Duration='0:0:0.5'>
</DoubleAnimation>
<DoubleAnimation
CHAPITRE 5 □ Les animations 197
Storyboard.TargetProperty=
«RenderTransform.Children[2].Angle'
Duration='0:0:0.5'>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>

La figure 5-6 montre des copies d’écran quand le bouton est en cours de
transformation au cours du temps.

4 - L’animation sur tracé

Une animation sur tracé est un type d’animation qui utilise un tracé pour
obtenir les valeurs qui servent au ciblage des propriétés. Elle définit la valeur
de la propriété cible en fonction des coordonnées d’un objet PathGeometry
spécifiées dans la propriété de même nom. Les types de propriété animables par
cette technique sont Point, Double et Matrix. La classe PointAnimationUsingPath
permet de déplacer un point le long d’un chemin défini par les points de la
géométrie. L’animation d’une valeur de type double peut se faire au moyen
d’une DoubleAnimationUsingPath, dont la valeur de sortie dépend de la valeur
affectée à sa propriété Source:
• X retourne la coordonnée X du point courant de la géométrie.
• Y retourne la coordonnée Y du point courant de la géométrie.
• Angle retourne l’angle de la tangente au point courant de la géométrie.
La classe MatrixAnimationUsingPath intègre toutes les informations au sein
d’une matrice. Elle s’avère particulièrement efficace pour animer la propriété
Matrix d’une transformation MatrixTransform appliquée à un objet. La propriété
DoesRotateWithTangent indique si une rotation est appliquée en fonction de
l’angle de la tangente. L’UserControl ExPathAnimation.xaml, dans le dossier
chapitre05, montre l’utilisation des tracés pour l’animation (figure 5-7).
La première animation consiste à faire déplacer une balle jaune, avec son
centre matérialisé en rouge, en suivant un tracé en forme de huit. On réalise un
tracé de couleur MediumBlue et d’épaisseur 2, composé d’une géométrie path1
représentant une forme de huit (on utilise le mini-langage pour le traçage de
la géométrie).
198 La programmation graphique 2D de WPF 4
Figure 5-7

Copyright 2011 Patrice REY


CHAPITRE 5 □ Les animations 199
<Path Stroke=»MediumBlue» Canvas.Left=»100» Canvas.Top=»20»
StrokeThickness=»2»>
<Path.Data>
<PathGeometry x:Name=»path1» Figures=»M50,120 C75,20 175,20 200,120
220,220 325,220 350,120 325,20 220,20 200,120
175,220 75,220 50,120» />
</Path.Data>
</Path>

La balle jaune est représentée par un objet Path, lequel est rempli avec un
dégradé à base de jaune et un centre en rouge, et qui est composé d’une
géométrie x_cercle1 de type EllipseGeometry (un centre aux coordonnées 10,10
et des rayons RadiusX=10 et RadiusY=10).

<Path Stroke=»DarkGoldenrod» Canvas.Left=»100» Canvas.Top=»20»>


<Path.Fill>
<RadialGradientBrush>
<GradientStop Color=»Red» Offset=»0» />
<GradientStop Color=»Gold» Offset=»0.416» />
</RadialGradientBrush>
</Path.Fill>
<Path.Data>
<EllipseGeometry x:Name=»x_cercle1» Center=»10,10»
RadiusX=»10» RadiusY=»10» />
</Path.Data>
</Path>

En récupérant tous les points qui composent le tracé path1 et en remplaçant


les coordonnées du centre de x_cercle1 par les points de path1, la balle
va donc suivre le tracé. Par code, on instancie une animation pa de type
PointAnimationUsingPath. On affecte à sa propriété PathGeometry la géométrie
path1. On fixe la durée de la chronologie et l’énumération de la répétition.
Puis on applique la méthode BeginAnimation(..) au x_cercle1 en faisant varier la
propriété Center de x_cercle1 (EllipseGeometry.CenterProperty).

private void UserControl_Loaded(object sender, RoutedEventArgs e) {


//animation 1
path1.Freeze(); //performance
PointAnimationUsingPath pa = new PointAnimationUsingPath();
pa.PathGeometry = path1;
pa.Duration = TimeSpan.FromSeconds(5);
200 La programmation graphique 2D de WPF 4
pa.RepeatBehavior = RepeatBehavior.Forever;
x_cercle1.BeginAnimation(EllipseGeometry.CenterProperty, pa);
this.UpdateLayout();
}

Le bas de la figure 5-7 montre des copies d’écran où la balle jaune suit la forme
du huit. La vitesse est constante dans la mesure où rien n’a été précisé.
La deuxième animation consiste à faire suivre une boule bleue avec un centre
rouge sur une ligne brisée de couleur rouge. La ligne brisée représente une
route, commençant par un plat, puis une descente et terminant par un plat.
Pour augmenter le réalisme, la boule accélèrera puis décélèrera en fin de
parcours. Quatre boutons permettront de lancer l’animation, de la mettre en
pause, de la reprendre et de l’arrêter. Cette fois c’est une ellipse de type Ellipse
qui va servir de boule pour le déplacement. Nous allons donc utiliser une
autre démarche pour effectuer cette animation, et cela tout en XAML.
Il faut commencer par déterminer le tracé de la route qui va servir de support
pour les valeurs à fournir à l’animation. On réalise un PathGeometry que l’on
ajoute aux ressources de l’UserControl et que l’on référence par x:Key = «k_
geometrie». Cette géométrie est composée de 3 lignes brisées, qui sont affectées
à sa propriété Figures, et qui sont visualisées sur la figure 5-8.
Figure 5-8 (0,0) (96,0)

(320,100) (475,100)

<UserControl.Resources>
<PathGeometry x:Key=»k_geometrie»
Copyright 2011 Patrice REY

Figures=»M0,0 96,0 320,100 475,100»>


</PathGeometry>
</UserControl.Resources>

On positionne sur un Canvas x_cnv_anim2 la boule représentée par une ellipse


de taille 50 par 50. On change l’origine en la passant de (0,0) à (25,25) qui
correspond au centre du cercle, par la propriété RenderTransformOrigin. On
CHAPITRE 5 □ Les animations 201
ajoute une transformation x_cercle_translate, de type TranslateTransform. On
remplit le cercle par un dégradé dont le centre est un point rouge.

<Canvas x:Name=»x_cnv_anim2» Canvas.Left=»85» Canvas.Top=»39»


Background=»WhiteSmoke» Width=»536» Height=»233»>
...
<!-- balle -->
<Ellipse Name=»x_cercle2» Stroke=»Black» Width=»50»
Height=»50» Canvas.Left=»1» Canvas.Top=»75»>
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color=»LightSteelBlue»
Offset=»0.299» />
<GradientStop Color=»Red» Offset=»0.058» />
</RadialGradientBrush>
</Ellipse.Fill>
<Ellipse.RenderTransformOrigin>
<Point X=»25» Y=»25»></Point>
</Ellipse.RenderTransformOrigin>
<Ellipse.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name=»x_cercle_translate»
X=»0» Y=»0»></TranslateTransform>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
</Canvas>

On réalise un tracé de type Path, identique au tracé de la route, pour matérialiser


le déplacement de la boule. On lui applique une translation pour que le début
du tracé coïncide avec le centre de la boule.

<Canvas x:Name=»x_cnv_anim2» Canvas.Left=»85» Canvas.Top=»39»


Background=»WhiteSmoke» Width=»536» Height=»233»>
<!-- trace du chemin pour voir -->
<Path Stroke=»Crimson» StrokeThickness=»4» Name=»x_trace»
Canvas.Left=»0» Canvas.Top=»0»>
<Path.Data>
<PathGeometry Figures=»M0,0 96,0 320,100 475,100»></PathGeometry>
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X=»25» Y=»100»></TranslateTransform>
202 La programmation graphique 2D de WPF 4
</Path.RenderTransform>
</Path>
...
</Canvas>

Dans le Canvas où sont positionnés les boutons, on ajoute à la propriété


Triggers les événements EventTrigger qui seront déclenchés par les boutons.
Pour le bouton x_btn_demarrer2, on ajoute un Storyboard x_story qui gère:
• un DoubleAnimationUsingPath dont la cible TargetName est x_cercle_translate,
la cible TargetProperty est X (le décalage horizontal de la translation), le
PathGeometry qui fournit les valeurs est StaticResource k_geometrie, la Source
dans ce PathGeometry est X, la durée Duration est de 5 secondes, la vitesse
d’accélération est de 0.6 et la vitesse de décélération est de 0.4.
• un DoubleAnimationUsingPath dont la cible TargetName est x_cercle_translate,
la cible TargetProperty est Y (le décalage vertical de la translation), le
PathGeometry qui fournit les valeurs est StaticResource k_geometrie, la Source
dans ce PathGeometry est Y, la durée Duration est de 5 secondes, la vitesse
d’accélération est de 0.6 et la vitesse de décélération est de 0.4.

<Button Canvas.Left=»627» Canvas.Top=»39» Content=»Démarrer»


Height=»23» Name=»x_btn_demarrer2» Width=»111»
Cursor=»Hand»></Button>
...
<Canvas.Triggers>
<EventTrigger RoutedEvent=»Button.Click»
SourceName=»x_btn_demarrer2»>
<EventTrigger.Actions>
<BeginStoryboard x:Name=»x_story»>
<Storyboard>
<DoubleAnimationUsingPath
Storyboard.TargetName=»x_cercle_translate»
Storyboard.TargetProperty=»X»
Source=»X» Duration=»0:0:5»
Copyright 2011 Patrice REY

RepeatBehavior=»Forever»
AutoReverse=»True»
PathGeometry=»{StaticResource k_geometrie}»
AccelerationRatio=»0.6»
DecelerationRatio=»0.4»>
</DoubleAnimationUsingPath>
<DoubleAnimationUsingPath
Storyboard.TargetName=»x_cercle_translate»
CHAPITRE 5 □ Les animations 203
Storyboard.TargetProperty=»Y»
Source=»Y» Duration=»0:0:5»
RepeatBehavior=»Forever»
AutoReverse=»True»
PathGeometry=»{StaticResource k_geometrie}»
AccelerationRatio=»0.6»
DecelerationRatio=»0.4»>
</DoubleAnimationUsingPath>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
...
</Canvas.Triggers>

On ajoute les autres EventTrigger pour le clic sur les boutons de pause, de
reprise et d’arrêt (x_btn_pause2, x_btn_reprendre2 et x_btn_arreter2).

<Button Canvas.Left=»627» Canvas.Top=»68» Content=»Pause»


Cursor=»Hand» Height=»23» Name=»x_btn_pause2»
Width=»111»></Button>
<Button Canvas.Left=»627» Canvas.Top=»97» Content=»Reprendre»
Cursor=»Hand» Height=»23» Name=»x_btn_reprendre2»
Width=»111»></Button>
<Button Canvas.Left=»627» Canvas.Top=»126» Content=»Stopper»
Cursor=»Hand» Height=»23» Name=»x_btn_arreter2»
Width=»111»></Button>
...
<Canvas.Triggers>
...
<EventTrigger RoutedEvent=»Button.Click»
SourceName=»x_btn_pause2»>
<EventTrigger.Actions>
<PauseStoryboard BeginStoryboardName=»x_story»>
</PauseStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent=»Button.Click»
SourceName=»x_btn_reprendre2»>
<EventTrigger.Actions>
<ResumeStoryboard BeginStoryboardName=»x_story»>
</ResumeStoryboard>
</EventTrigger.Actions>
204 La programmation graphique 2D de WPF 4
</EventTrigger>
<EventTrigger RoutedEvent=»Button.Click»
SourceName=»x_btn_arreter2»>
<EventTrigger.Actions>
<StopStoryboard BeginStoryboardName=»x_story»>
</StopStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>

Le bas de la figure 5-7 montre des copies d’écran de la boule en déplacement


sur le tracé. A noter que sur 60% du trajet, la boule accélère et sur 40% du
trajet la boule décélère. Quand rien n’est précisé, la vitesse est constante. Si
un AccelerationRatio et un DecelerationRatio sont précisés, leur somme doit être
inférieure ou égale à 1. Dans notre cas, leur somme étant égale à 1, il n’y a
pas de zone où la vitesse est constante. La figure 5-9 montre les différents cas
concernant les propriétés AccelerationRatio et DecelerationRatio.
Figure 5-9

début fin

vitesse constante (rien de péciser)

AccelerationRatio = 1

AccelerationRatio = 0.33

AccelerationRatio = 0.33
DecelerationRatio = 0.33

AccelerationRatio = 0.60
Copyright 2011 Patrice REY

DecelerationRatio = 0.40

vitesse constante AccelerationRatio DecelerationRatio


CHAPITRE 5 □ Les animations 205

5 - Les animations d’images clés

Une animation d’images clés, dite aussi KeyFrame, permet d’augmenter le


nombre de valeurs cibles avec la première valeur correspondant à la valeur
de début, avec la dernière valeur correspondant à la valeur de fin, et avec
les autres valeurs correspondant aux valeurs intermédiaires. Chaque valeur
est associée à un instant donné. L’animation définit une série de transitions
successives (les frames) entre chacune de ces valeurs aux instants indiqués.
Les noms des classes des animations d’images clés sont suffixés de
UsingKeyFrames: par exemple DoubleAnimationUsingKeyFrames pour animer
une valeur de type double avec des images clés, ColorAnimationUsingKeyFrames
pour le type Color, etc.
Les valeurs sont définies au moyen d’objets KeyFrame qui déterminent
également le mode d’interpolation avec la valeur précédente. Leur nom de
classe s’exprime de la façon suivante: interpolation.type.KeyFrame. On trouve
donc par exemple:
• LinearDoubleKeyFrame pour une interpolation linéaire pour des valeurs de
type double avec images clés.
• DiscreteBooleanKeyFrame pour une interpolation discrète (par palier) pour
des valeurs de type booléenne avec images clés.
• SplineColorKeyFrame pour une interpolation Spline (par courbe de Bézier)
pour des valeurs de type Color avec images clés.
• EasingVectorKeyFrame pour une interpolation Easing (fonction réaliste)
pour des valeurs de type Vector avec images clés.
Un objet KeyFrame définit sa valeur au moyen de la propriété Value, et l’instant
auquel la valeur doit être atteinte dans sa propriété KeyTime. La propriété
KeyTime s’exprime:
• soit sous la forme d’un objet TimeSpan (propriété du même nom ou par la
méthode statique KeyFrame.FromTimeSpan).
• soit sous la forme d’un pourcentage (propriété PerCent ou par la méthode
statique KeyFrame.FromPercent).
• soit à partir de la propriété statique KeyTime.Paced qui détermine un taux
constant d’interpolation entre les objets KeyFrame.
• soit à partir de la propriété statique KeyTime.Uniform qui détermine une
répartition uniforme du temps entre les objets KeyFrame.
206 La programmation graphique 2D de WPF 4
Les objets KeyFrame sont regroupés au sein de l’animation dans sa propriété
KeyFrames. Normalement il faut mettre le premier objet sur le temps zéro. Si
ce n’est pas le cas, une transition est automatiquement effectuée depuis la
valeur courante.
L’UserControl ExAnimationKeyFrame.xaml, dans le dossier chapitre05, montre
au travers de plusieurs exemples l’utilisation des animations d’images clés
avec l’interpolation linéaire, l’interpolation discrète, l’interpolation Spline et
l’interpolation Easing (figure 5-10 et figure 5-11).

5.1 - Avec interpolation linéaire

La première animation (figure 5-10) concerne l’interpolation linéaire. On


anime la couleur de fond d’un rectangle en utilisant l’interpolation linéaire.
Lors de l’exécution de l’animation, la couleur varie en permanence, à taux
constant, en commençant par le rouge au temps zéro, en passant par toutes
les couleurs intermédiaires se trouvant entre le rouge et le jaune, en passant
par le jaune au temps 2 secondes, etc., pour terminer par le bleu au temps 6
secondes.
Pour animer une propriété de type Color par images clés, on utilise une
animation de type ColorAnimationUsingKeyFrames. Cette animation cible la
couleur de fond du rectangle par Storyboard.TargetProperty = « (Rectangle.Fill).
(SolidColorBrush.Color) ». L’interpolation linéaire d’une valeur Color par images
clés nécessite un objet LinearColorKeyFrame, avec sa propriété KeyTime (pour
indiquer le temps) et sa propriété Value (pour indiquer la valeur de la cible).

<Rectangle Name='x_anim1_rect1' Width='170' Height='42'


Fill='Red' Canvas.Left='304' Canvas.Top='34'>
<Rectangle.Triggers>
<EventTrigger RoutedEvent='Loaded'>
<EventTrigger.Actions>
<BeginStoryboard>
Copyright 2011 Patrice REY

<Storyboard>
<ColorAnimationUsingKeyFrames
RepeatBehavior='Forever'
AutoReverse='True'
Storyboard.TargetProperty=
«(Rectangle.Fill).(SolidColorBrush.Color)'>
<LinearColorKeyFrame KeyTime='0:0:0'
Value='Red' />
CHAPITRE 5 □ Les animations 207
Figure 5-10
208 La programmation graphique 2D de WPF 4
Figure 5-11

<LinearColorKeyFrame KeyTime='0:0:2'
Value='Yellow' />
<LinearColorKeyFrame KeyTime='0:0:4'
Value='Cyan' />
<LinearColorKeyFrame KeyTime='0:0:6'
Copyright 2011 Patrice REY

Value='Blue' />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
CHAPITRE 5 □ Les animations 209

5.2 - Avec interpolation discrète

L’interpolation discrète signifie une utilisation par palier. Ce n’est pas vraiment
une méthode d’interpolation proprement dit, puisque son évolution se fait par
des paliers successifs. Elle définit une évolution de valeur par palier à chaque
KeyFrame. La valeur ne subit aucune interpolation sinon elle résulterait d’un
calcul particulier. Elle ne subit qu’une réaffectation de valeur en fonction de la
propriété Value du KeyFrame.
La deuxième animation (figure 5-10) anime la couleur de fond de deux
rectangles. Pour le rectangle de gauche, on fait varier la couleur de fond en
utilisant les mêmes couleurs que dans la première animation, mais avec une
interpolation discrète. Pour le rectangle de droite, on alterne la visibilité du
rectangle (un palier où le rectangle est visible, un palier où le rectangle n’est
plus visible).
La chronologie, pour le rectangle de gauche, dure 8 secondes. La figure 5-12
visualise les différents paliers avec les temps. Durant le temps qui s’écoule
pour un palier, le fond du rectangle reste de la même couleur.
Y
Figure 5-12
le fond est Blue

le fond est Cyan

le fond est Yellow

le fond est Red

t=0s t=2s t=4s t=6s t=8s

Pour une interpolation discrète pour des valeurs de type Color, on utilise des
images clés au travers d’objets DiscreteColorKeyFrame. La propriété KeyTime
détermine l’instant donné et la propriété Value reçoit le nom d’une couleur.
210 La programmation graphique 2D de WPF 4
<Rectangle Width='170' Height='42' Fill='Red' Canvas.Left='179'
Canvas.Top='43'>
<Rectangle.Triggers>
<EventTrigger RoutedEvent='Loaded'>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetProperty=
«(Rectangle.Fill).(SolidColorBrush.Color)'
Duration='0:0:8'
RepeatBehavior='Forever'>
<DiscreteColorKeyFrame
KeyTime='0:0:0' Value='Red' />
<DiscreteColorKeyFrame
KeyTime='0:0:2'
Value='Yellow' />
<DiscreteColorKeyFrame
KeyTime='0:0:4' Value='Cyan' />
<DiscreteColorKeyFrame
KeyTime='0:0:6' Value='Blue' />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

La chronologie, pour le rectangle de droite, dure 2 secondes. La figure 5-13


visualise les différents paliers avec les temps. Durant le temps qui s’écoule
pour un palier, le fond du rectangle reste dans le même état de visibilité.
Pour un rectangle, la visibilité s’exprime par Visiblity.Visible ou Visibility.
Collapsed, ce qui correspond à une énumération. Dans ce cas, il s’agit de valeur
de type object. L’animation sera donc une ObjectAnimationUsingKeyFrames dont
Copyright 2011 Patrice REY

la propriété TargetProperty cible Visibility. Pour une interpolation discrète pour


des valeurs de type énuméré (type object), on utilise des images clés au travers
d’objets DiscreteObjectKeyFrame. La propriété KeyTime détermine l’instant
donné et la propriété Value reçoit le terme à affecter à la cible. Comme Value
reçoit un objet on écrira <DiscreteObjectKeyFrame.Value> <Visibility>Visible</
Visibility> </DiscreteObjectKeyFrame.Value>.
CHAPITRE 5 □ Les animations 211
Figure 5-13 Y

le fond est invisible

le fond est visible

t=0s t=1s t=2s

<Rectangle Width='170' Height='42' Fill='Red' Canvas.Left='425'


Canvas.Top='43'>
<Rectangle.Triggers>
<EventTrigger RoutedEvent='Loaded'>
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty='Visibility'
Duration='0:0:2'
RepeatBehavior='Forever'>
<DiscreteObjectKeyFrame
KeyTime='0:0:0'>
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame
KeyTime='0:0:1'>
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
212 La programmation graphique 2D de WPF 4
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>

Le scénario d’animation des variations de propriétés non modifiables


progressivement, est DiscreteObjectKeyFrame pour les propriétés
énumérées, DiscreteBooleanKeyFrame pour les propriétés booléennes, et
DiscreteStringKeyFrame pour les propriétés de type string.

5.3 - Avec interpolation Spline

La méthode d’interpolation Spline utilise l’équation d’une courbe de Bézier


cubique pour déterminer ses valeurs instantanées de transition. Cela permet
de définir soi-même la courbe d’interpolation de l’animation. Il est possible
ainsi d’obtenir des effets d’accélération et de décélération réalistes.
Comme on l’a vu dans un précédent chapitre, une courbe de Bézier est
composée d’un point initial et d’un point final, chacun associé à un point de
contrôle. Le point de contrôle définit une tangente fictive et a la particularité
de déterminer la courbure au niveau du point d’extrémité. L’amplitude de la
courbure est d’autant plus importante que ces deux points sont écartés.
La courbe d’interpolation de type Spline se définit au moyen d’un objet de
type KeySpline spécifié par la propriété de même nom. Un objet KeySpline
définit une courbe de Bézier fictive avec un point initial de coordonnées (0,0)
et un point final de coordonnées (1,1), et dont les points de contrôle sont
paramétrables au moyen des propriétés ControlPoint1 et ControlPoint2 de type
Point.
La forme de la courbe ainsi obtenue définit le taux d’accroissement de la
valeur instantanée. L’abscisse représente la proportion du temps et l’ordonnée
Copyright 2011 Patrice REY

représente la proportion de la valeur. Plus la pente est forte et plus la valeur


s’accroît rapidement. Un plateau représente au contraire un ralentissement de
cet accroissement.
La troisième animation (figure 5-10) montre la comparaison de 3 interpolations
de type Spline pour l’animation un déplacement d’une image représentant
un missile. Le bouton x_btn_demarrer3 lance les 3 animations simultanément
et l’on peut comparer les effets de la vitesse, de l’accélération, et de la
CHAPITRE 5 □ Les animations 213
décélération. L’animation consiste donc à animer la propriété Canvas.Left
de l’image, la faisant passer de la valeur 184 à 584 lors d’une chronologie
de 5 secondes de durée. Pour animer une valeur de type double, on utilise
une animation DoubleAnimationUsingKeyFrames. La propriété TargetName du
Storyboard est x_img et la propriété TargetProperty est Canvas.Left.
La figure 5-14 visualise la courbe d’interpolation Spline employée pour
l’animation du déplacement de l’image x_img31. Les points de contrôle de la
courbe de Bézier sont (0,0) pour le premier point et (1,1) pour le deuxième
point. La courbe se trouve alors avec une forme de droite, ce qui implique que
la vitesse sera constante
Y
le temps de la chronologie.
Figure 5-14
(0,1) point de
contrôle 2 en
(1,1)

point de
contrôle 1
en (0,0)
(0,0) (1,0)

On utilise un SplineDoubleKeyFrame avec une KeyTime indiquant le temps


donné et un KeySpline indiquant la position des deux points de contrôle
(KeySpline=»0,0 1,1»).

<Button Canvas.Left='35' Canvas.Top='33' Content='Démarrer'


Height='23' Name='x_btn_demarrer3' Width='118'>
<Button.Triggers>
<EventTrigger RoutedEvent='Button.Click'>
<EventTrigger.Actions>
<BeginStoryboard Name='begin_story_missile'>
<Storyboard>
<!-- pour x_img31 -->
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName='x_img31'
Storyboard.TargetProperty='(Canvas.Left)'
Duration='0:0:5'
214 La programmation graphique 2D de WPF 4
AutoReverse='False'
RepeatBehavior='1x'>
<SplineDoubleKeyFrame
KeyTime='0:0:0'
KeySpline='0,0 1,1'
Value='184'>
</SplineDoubleKeyFrame>
<SplineDoubleKeyFrame
KeyTime='0:0:5'
KeySpline='0,0 1,1'
Value='584'>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
...
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>

La figure 5-15 visualise la courbe d’interpolation Spline employée pour


l’animation du déplacement de l’image x_img32. Les points de contrôle de la
courbe de Bézier sont (0,1) pour le premier point et (1,0) pour le deuxième
point. La courbe croît très rapidement au début du fait de la position éloignée
du premier point de contrôle, ce qui indique une vitesse importante. Puis
une phase de croissance faible qui est le signe d’une décélération. Enfin, de
nouveau une phase de croissance rapide due à l’éloignement du deuxième
point de contrôle, qui est le signe d’une accélération.
On utilise un SplineDoubleKeyFrame avec une KeyTime indiquant le temps
donné et un KeySpline indiquant la position des deux points de contrôle
(KeySpline=»0,1 1,0»). Copyright 2011 Patrice REY

<Button Canvas.Left='35' Canvas.Top='33' Content='Démarrer'


Height='23' Name='x_btn_demarrer3' Width='118'>
<Button.Triggers>
<EventTrigger RoutedEvent='Button.Click'>
<EventTrigger.Actions>
<BeginStoryboard Name='begin_story_missile'>
<Storyboard>
...
Y CHAPITRE 5 □ Les animations 215
Figure 5-15
(0,1)

point de contrôle 1
en (0,1)

point de contrôle 2
en (1,0)
(0,0)
(1,0)

zone
d’accélération

<!-- pour x_img32 -->


<DoubleAnimationUsingKeyFrames
Storyboard.TargetName='x_img32'
Storyboard.TargetProperty='(Canvas.Left)'
Duration='0:0:5'
AutoReverse='False'
RepeatBehavior='1x'>
<SplineDoubleKeyFrame
KeyTime='0:0:0'
KeySpline='0,1 1,0'
Value='184'>
</SplineDoubleKeyFrame>
<SplineDoubleKeyFrame
KeyTime='0:0:5'
KeySpline='0,1 1,0'
Value='584'>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
...
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
216 La programmation graphique 2D de WPF 4
La figure 5-16 visualise la courbe d’interpolation Spline employée pour
l’animation du déplacement de l’image x_img33. Les points de contrôle de la
courbe de Bézier sont (1,0) pour le premier point et (0,1) pour le deuxième
point. La courbe croît très lentement au début du fait de la position éloignée
du premier point de contrôle, ce qui indique une vitesse faible. Puis une phase
de croissance importante qui est le signe d’une accélération. Enfin, de nouveau
une phase de croissance faible due à l’éloignement du deuxième point de
contrôle, qui est le signe d’une décélération.
Figure 5-16 Y

(0,1)

point de contrôle 2
en (0,1)

point de contrôle 1
en (1,0)

(0,0) (1,0)

zone d’accélération

On utilise un SplineDoubleKeyFrame avec une KeyTime indiquant le temps


donné et un KeySpline indiquant la position des deux points de contrôle
(KeySpline=»1,0 0,1»).

<Button Canvas.Left='35' Canvas.Top='33' Content='Démarrer'


Height='23' Name='x_btn_demarrer3' Width='118'>
<Button.Triggers>
Copyright 2011 Patrice REY

<EventTrigger RoutedEvent='Button.Click'>
<EventTrigger.Actions>
<BeginStoryboard Name='begin_story_missile'>
<Storyboard>
...
<!-- pour x_img33 -->
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName='x_img33'
CHAPITRE 5 □ Les animations 217
Storyboard.TargetProperty='(Canvas.Left)'
Duration='0:0:5'
AutoReverse='False'
RepeatBehavior='1x'>
<SplineDoubleKeyFrame
KeyTime='0:0:0'
KeySpline='1,0 0,1'
Value='184'>
</SplineDoubleKeyFrame>
<SplineDoubleKeyFrame
KeyTime='0:0:5'
KeySpline='1,0 0,1'
Value='584'>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>

5.4 - Avec interpolation Easing

Les classes d’images clés dont le nom est préfixé par Easing (comme
EasingDoubleKeyFrame) peuvent utiliser une interpolation évoluée réaliste
définie au moyen de leur propriété EasingFunction par un objet supportant
l’interface IEasingFunction.
La quatrième animation consiste à animer le déplacement de l’image x_img41
en utilisant la fonction EasingFunction modélisant un rebond (BounceEase). La
figure 5-17 visualise cette fonction en indiquant les valeurs prises en fonction
du temps.
Les images clés sont représentées par des objets EasingDoubleKeyFrame. La
propriété EasingFunction reçoit la modélisation BounceEase. Nous verrons
d’autres modélisations un peu plus loin dans ce chapitre.

<Button Canvas.Left='35' Canvas.Top='33' Content='Démarrer'


Height='23' Name='x_btn_demarrer4' Width='118'>
<Button.Triggers>
<EventTrigger RoutedEvent='ButtonBase.Click'>
<BeginStoryboard Name='beginStoryboard1'>
218 La programmation
Y
graphique 2D de WPF 4
Figure 5-17 Ecart valeurs

Temps

<BeginStoryboard.Storyboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
AutoReverse='False'
Duration='0:0:5'
RepeatBehavior='1x'
Storyboard.TargetName='x_img41'
Storyboard.TargetProperty='(Canvas.Left)'>
<EasingDoubleKeyFrame
KeyTime='0:0:0' Value='184'>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase></BounceEase>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame
KeyTime='0:0:5' Value='584'>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase></BounceEase>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
Copyright 2011 Patrice REY

</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard.Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
CHAPITRE 5 □ Les animations 219

6 - Les animations réalistes

Les animations réalistes sont des animations supportant un mécanisme


d’atténuation ou d’accélération progressive, nommé ease (ou fonction
d’accélération). Elles permettent de simuler des phénomènes physiques tels
que des rebonds ou la chute d’une feuille. Ce mécanisme permet de configurer
la courbe d’interpolation d’une animation.
Les animations basiques et les animations d’images clés exposent une
propriété EasingFunction de type IEasingFunction, interface exposant la méthode
Ease(). L’implémentation de cette méthode définit la fonction d’interpolation
de l’animation, c’est-à-dire la façon dont la valeur animée évolue durant
l’animation.
L’interface IEasingFunction est implémentée par la classe abstraite
EasingFunctionBase dont héritent des classes qui implémentent des
interpolations. Certaines de ces interpolations correspondent à des modèles
physiques de type amortissement. La figure 5-18 visualise l’arbre d’héritage
de la classe EasingFunctionBase avec ses 11 classes dérivées.
Le système est extensible puisqu’il est possible de modéliser ses propres
interpolations personnalisées par une classe dérivant de EasingFunctionBase.
L’utilisation de ces classes évite d’avoir à développer soi-même des modèles
physiques au moyen d’interpolations Spline utilisant des courbes de Bézier
qui sont difficiles à mettre au point.
L’UserControl ExAnimationRealiste.xaml, dans le dossier chapitre05, montre
l’utilisation de ces 11 classes de modélisation plus une classe de modélisation
personnalisée (figure 5-19, 5-20 et 5-21). Chaque animation consiste à animer
le déplacement d’une balle orange en utilisant les valeurs de la modélisation
choisie en fonction du temps. On peut aisément comparer ces modélisations.
Dans chacune de ces animations, la progression de la modélisation est dessinée
sur un Canvas avec ses valeurs. La méthode DessinerCourbeDeModelisation(..)
dessine le tracé de la modélisation dans un PathGeometry. Elle reçoit en
paramètre une fonction de modélisation EasingFunctionBase.

private void DessinerCourbeDeModelisation(EasingFunctionBase ease) {


PathGeometry geometry = new PathGeometry();
m_chemin_courbe.Data = geometry;
PathFigure figure = new PathFigure();
figure.StartPoint = new Point(0.0, 100.0);
220 La programmation graphique 2D de WPF 4
Figure 5-18

Copyright 2011 Patrice REY


CHAPITRE 5 □ Les animations 221
Figure 5-19
222 La programmation graphique 2D de WPF 4
Figure 5-20

Copyright 2011 Patrice REY


CHAPITRE 5 □ Les animations 223
Figure 5-21

geometry.Figures.Add(figure);
double resolution = 2000;
for (int i = 0; i <= resolution; i++) {
double d = ease.Ease(i / resolution);
LineSegment segment = new LineSegment();
segment.Point =
new Point(i * 100 / resolution, 100.0 - (d * 100.0));
figure.Segments.Add(segment);
}
}

La méthode AjouterDessinModelisationAnimation(..) ajoute le tracé de la


modélisation à un Canvas passé en paramètre.
224 La programmation graphique 2D de WPF 4
private void AjouterDessinModelisationAnimation(Canvas x_cnv_graph,
EasingFunctionBase modelisation) {
m_chemin_courbe = new Path();
m_chemin_courbe.Stroke = new SolidColorBrush(Colors.MediumBlue);
m_chemin_courbe.StrokeThickness = 3;
DessinerCourbeDeModelisation(modelisation);
x_cnv_graph.Children.Add(m_chemin_courbe);
}

6.1 - La modélisation BounceEase

La classe BounceEase représente une fonction d’accélération qui crée un effet


rebondissant animé. L’animation n°2 (figure 5-19) utilise cette modélisation
pour animer le déplacement de la balle x_balle2.
Un objet BounceEase expose principalement les propriétés:
• Bounces qui définit le nombre de rebonds par une valeur supérieure ou
égale à 0 (3 par défaut).
• Bounciness qui définit une valeur qui indique l’amplitude de l’animation de
rebond; des valeurs faibles pour cette propriété entraînent des rebonds avec
une perte de hauteur limitée entre les rebonds (amplitude élevée) tandis
que des valeurs élevées se traduisent par des rebonds réduits (amplitude
faible); sa valeur doit être positive (3 par défaut).
• EasingMode qui définit une valeur énumérée qui indique la manière dont
l’animation est interpolée; les valeurs sont EasingMode.EaseIn, EasingMode.
EaseOut (par défaut) et EasingMode.EaseInOut; EasingMode.EaseIn signifie
que l’interpolation suit la formule mathématique associée à la fonction
d’accélération; EasingMode.EaseOut signifie que l’interpolation suit
100% d’interpolation moins la sortie de la formule associée à la fonction
d’accélération; EasingMode.EaseInOut signifie que l’interpolation utilise
EaseIn pour la première moitié de l’animation et EaseOut pour la seconde;
la figure 5-22 visualise les différents modes.
Copyright 2011 Patrice REY

Figure 5-22
CHAPITRE 5 □ Les animations 225
On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer2.
L’événement lance le Storyboard story_2 dont la TargetName est x_balle2,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.BounceEase (figure 5-23).
Figure 5-23
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer2

AjouterDessinModelisationAnimation(x_cnv_ex2_graph, new BounceEase()


{ Bounces = 3, Bounciness = 2, EasingMode = EasingMode.EaseOut })
<EventTrigger SourceName='x_btn_demarrer2'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_2'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle2'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<BounceEase Bounces='3' Bounciness='2'
EasingMode='EaseOut'>
</BounceEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.2 - La modélisation BackEase

La classe BackEase représente une fonction d’accélération qui rétracte


légèrement le mouvement d’une animation avant qu’elle ne commence à
s’animer dans le chemin d’accès spécifié.
226 La programmation graphique 2D de WPF 4
Un objet BackEase expose principalement les propriétés:
• Amplitude définit l’amplitude de rétractation associée à une animation
BackEase; sa valeur doit être supérieure ou égale à zéro (1 par défaut).
• EasingMode qui définit une valeur énumérée qui indique la manière dont
l’animation est interpolée.
La figure 5-24 visualise plusieurs valeurs de la propriété Amplitude. Plus
l’amplitude est grande, plus la rétractation est importante avant et/ou après
l’animation.
Figure 5-24

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer3.


L’événement lance le Storyboard story_3 dont la TargetName est x_balle3,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.BackEase (figure 5-25).

Figure 5-25
déplacement de Left de 220 à 675 pixels

Copyright 2011 Patrice REY

Button x_btn_demarrer3

AjouterDessinModelisationAnimation(x_cnv_ex3_graph, new BackEase()


{ Amplitude = 1, EasingMode = EasingMode.EaseOut })
CHAPITRE 5 □ Les animations 227
<EventTrigger SourceName='x_btn_demarrer3'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_3'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle3'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<BackEase Amplitude='1' EasingMode='EaseOut'>
</BackEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.3 - La modélisation CircleEase

La classe CircleEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant une fonction circulaire.
La figure 5-26 visualise la modélisation ainsi que la formule employée. Les
valeurs valides pour t sont comprises entre -1 et +1 (bornes comprises).
Les valeurs supérieures à 1 sont évaluées comme étant égales à 1 et les
valeurs inférieures à -1 comme étant égales à -1. Cela signifie que pour les
valeurs à l’extérieur de cet intervalle, l’animation continue, mais la fonction
d’accélération est suspendue à l’entrée dans le domaine non valide, et reprend
à la sortie du domaine non valide.
Figure 5-26

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer4.


L’événement lance le Storyboard story_4 dont la TargetName est x_balle4,
228 La programmation graphique 2D de WPF 4
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.CircleEase (figure 5-27).
Figure 5-27

déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer4

AjouterDessinModelisationAnimation(x_cnv_ex4_graph,
new CircleEase() { EasingMode = EasingMode.EaseOut })

<EventTrigger SourceName='x_btn_demarrer4'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_4'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle4'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode='EaseOut'></CircleEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
Copyright 2011 Patrice REY

6.4 - La modélisation CubicEase

La classe CubicEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant la formule f(t) =t3. La figure
5-28 visualise la modélisation de la fonction CubicEase.
CHAPITRE 5 □ Les animations 229
Figure 5-28

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer5.


L’événement lance le Storyboard story_5 dont la TargetName est x_balle5,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.CubicEase (figure 5-29).
Figure 5-29
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer5

AjouterDessinModelisationAnimation(x_cnv_ex5_graph,
new CubicEase() { EasingMode = EasingMode.EaseOut })
<EventTrigger SourceName='x_btn_demarrer4'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_4'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle4'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode='EaseOut'></CircleEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
230 La programmation graphique 2D de WPF 4

6.5 - La modélisation ElasticEase

La classe ElasticEase représente une fonction d’accélération qui crée une


animation qui ressemble à un ressort qui oscille jusqu’à ce qu’il s’immobilise.
Un objet ElasticEase expose principalement:
• Oscillations qui définit le nombre de fois où la cible se déplace sur la
destination d’animation (une valeur supérieure ou égale à 0, 3 par défaut).
• Springiness qui définit la résistance du ressort; plus la valeur d’effet de
ressort est réduite, plus le ressort est raide et plus vite l’élasticité perd de
son intensité à chaque oscillation (une valeur positive, 3 par défaut).
La figure 5-30 visualise la modélisation de la fonction ElasticEase ainsi que les
graphiques de plusieurs valeurs Springiness. Plus la valeur de Springiness est
petite, plus le ressort est raide et le plus l’élasticité diminue rapidement en
intensité à chaque oscillation.
Figure 5-30

Copyright 2011 Patrice REY

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer6.


L’événement lance le Storyboard story_6 dont la TargetName est x_balle6,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
CHAPITRE 5 □ Les animations 231
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.ElasticEase (figure 5-31).
Figure 5-31
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer6

AjouterDessinModelisationAnimation(x_cnv_ex6_graph, new ElasticEase()


{ Oscillations = 3, Springiness = 3, EasingMode = EasingMode.EaseOut })

<EventTrigger SourceName='x_btn_demarrer6'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_6'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle6'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<ElasticEase Oscillations='3' Springiness='3'
EasingMode='EaseOut'></ElasticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.6 - La modélisation ExponentialEase

La classe ExponentialEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant une formule exponentielle.
Un objet ExponentialEase expose principalement la propriété Exponent, qui
définit l’exposant utilisé pour déterminer l’interpolation de l’animation (valeur
2 par défaut).
La figure 5-32 visualise la modélisation de la fonction ExponentialEase ainsi
232 La programmation graphique 2D de WPF 4
qu’un graphique illustrant l’effet de plusieurs valeurs différentes pour la
propriété Exponent.
Figure 5-32

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer7.


L’événement lance le Storyboard story_7 dont la TargetName est x_balle7,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.ExponentialEase (figure 5-33).
Figure 5-33 déplacement de Left de 220 à 675 pixels

Copyright 2011 Patrice REY

Button x_btn_demarrer7

AjouterDessinModelisationAnimation(x_cnv_ex7_graph,
new ExponentialEase() { Exponent = 2, EasingMode = EasingMode.EaseOut })
CHAPITRE 5 □ Les animations 233
<EventTrigger SourceName='x_btn_demarrer7'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_7'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle7'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode='EaseOut'
Exponent='2'></ExponentialEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.7 - La modélisation PowerEase

La classe PowerEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant la formule f(t) = tn où n est
égal à la propriété Power.
Un objet PowerEase expose principalement la propriété Power qui définit la
puissance exponentielle de l’interpolation d’animation. Par exemple, une
valeur de 7 créera une courbe de l’interpolation de l’animation qui suit la
formule f(t) = t7. La figure 5-34 montre la modélisation PowerEase.
Figure 5-34

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer8.


L’événement lance le Storyboard story_8 dont la TargetName est x_balle8,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.PowerEase (figure 5-35).
234 La programmation graphique 2D de WPF 4
Figure 5-35 déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer8

AjouterDessinModelisationAnimation(x_cnv_ex8_graph,
new PowerEase() { Power = 2, EasingMode = EasingMode.EaseOut })
<EventTrigger SourceName='x_btn_demarrer8'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_8'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle8'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<PowerEase Power='2'
EasingMode='EaseOut'></PowerEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.8 - La modélisation QuadraticEase

La classe QuadraticEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant la formule f(t) =t2. La figure
Copyright 2011 Patrice REY

5-36 visualise la modélisation QuadraticEase.


Figure 5-36
CHAPITRE 5 □ Les animations 235
On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer9.
L’événement lance le Storyboard story_9 dont la TargetName est x_balle9,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.QuadraticEase (figure 5-37).
Figure 5-37
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer9

AjouterDessinModelisationAnimation(x_cnv_ex9_graph,
new QuadraticEase() { EasingMode = EasingMode.EaseOut })

<EventTrigger SourceName='x_btn_demarrer9'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_9'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle9'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode='EaseOut'></QuadraticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.9 - La modélisation QuarticEase

La classe QuarticEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant la formule f(t) =t4. La figure
5-38 visualise la modélisation QuarticEase.
236 La programmation graphique 2D de WPF 4
Figure 5-38

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer10.


L’événement lance le Storyboard story_10 dont la TargetName est x_balle10,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.QuarticEase (figure 5-39).
Figure 5-39 déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer10

AjouterDessinModelisationAnimation(x_cnv_ex10_graph,
new QuarticEase() { EasingMode = EasingMode.EaseOut })
<EventTrigger SourceName='x_btn_demarrer10'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_10'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle10'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
Copyright 2011 Patrice REY

<DoubleAnimation.EasingFunction>
<QuarticEase EasingMode='EaseOut'></QuarticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
CHAPITRE 5 □ Les animations 237

6.10 - La modélisation QuinticEase

La classe QuinticEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant la formule f(t) =t5. La figure
5-40 visualise la modélisation QuinticEase.
Figure 5-40

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer11.


L’événement lance le Storyboard story_11 dont la TargetName est x_balle11,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.QuinticEase (figure 5-41).
Figure 5-41
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer11

AjouterDessinModelisationAnimation(x_cnv_ex11_graph,
new QuinticEase() { EasingMode = EasingMode.EaseOut })

<EventTrigger SourceName='x_btn_demarrer11'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_11'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle11'
238 La programmation graphique 2D de WPF 4
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode='EaseOut'></QuinticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

6.11 - La modélisation SineEase

La classe SineEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou décélère en utilisant une formule de sinus. La
figure 5-42 visualise la modélisation SineEase.
Figure 5-42

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer12.


L’événement lance le Storyboard story_12 dont la TargetName est x_balle12,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.SineEase (figure 5-43).

<EventTrigger SourceName='x_btn_demarrer12'
Copyright 2011 Patrice REY

RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_12'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle12'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
CHAPITRE 5 □ Les animations 239
<DoubleAnimation.EasingFunction>
<SineEase EasingMode='EaseOut'></SineEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
Figure 5-43
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer12

AjouterDessinModelisationAnimation(x_cnv_ex12_graph,
new SineEase() { EasingMode = EasingMode.EaseOut })

6.12 - Réaliser une interpolation personnalisée

Une classe d’interpolation personnalisée peut être créée en dérivant la classe


EasingFunctionBase et en redéfinissant les méthodes CreateInstanceCore() et
EaseInCore(..). La méthode CreateInstanceCore() est définie comme abstraite
au niveau de la classe Freezable. Elle doit renvoyer une instance de la classe
concrète. La méthode EaseInCore(..) reçoit en paramètre le temps, sous une
forme personnalisée (valeur entre 0 et 1, l’étendue représentant la durée
de l’animation). Elle doit renvoyer une valeur normalisée entre 0 et 1, qui
s’applique à la valeur de la propriété animée, en tenant compte des propriétés
From, To et By. Seul le mode EaseIn doit être implémenté au moyen de cette
fonction, les modes EaseOut et EaseInOut sont déterminés automatiquement
par la méthode Ease() de la classe de base EasingFunctionBase.
L’animation n°1 (figure 5-19) représente une modélisation linéaire. Pour cela
on crée une classe ModelisationLineaireEase qui dérive de EasingFunctionBase.
On redéfinit les méthodes CreateInstanceCore() et EaseInCore(..) par l’instruction
override.
240 La programmation graphique 2D de WPF 4
La première méthode retourne une instance de la classe (return new
ModelisationLineaireEase()). La seconde méthode retourne le calcul de la valeur
résultant d’un calcul avec une fonction. Ici nous voulons une interpolation
linéaire (f(t)=t) donc on retourne normalizedTime.

public class ModelisationLineaireEase : EasingFunctionBase {


//
protected override double EaseInCore(double normalizedTime) {
return normalizedTime;
}
//
protected override Freezable CreateInstanceCore() {
return new ModelisationLineaireEase();
}
}

On ajoute un EventTrigger qui gère le clic sur le bouton x_btn_demarrer1.


L’événement lance le Storyboard story_1 dont la TargetName est x_balle1,
la TargetProperty est Canvas.Left, pour une plage de valeurs allant de 220 à
675, pour une durée de 4 secondes, en utilisant la méthode d’interpolation
EasingFunction.ModelisationLineaireEase (figure 5-44).
Figure 5-44
déplacement de Left de 220 à 675 pixels

Button x_btn_demarrer1
Copyright 2011 Patrice REY

AjouterDessinModelisationAnimation(x_cnv_ex1_graph,
new ModelisationLineaireEase())

Une référence à un espace de noms doit être ajoutée et on choisit le préfixe


modelisation ce qui donne xmlns:modelisation = «clr-namespace:ProgGraphiqueWpf.
chapitre05».
CHAPITRE 5 □ Les animations 241
<EventTrigger SourceName='x_btn_demarrer1'
RoutedEvent='Button.Click'>
<BeginStoryboard Name='story_1'>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName='x_balle1'
Storyboard.TargetProperty='(Canvas.Left)'
From='220' To='675' Duration='0:0:4'>
<DoubleAnimation.EasingFunction>
<modelisation:ModelisationLineaireEase
EasingMode='EaseInOut'>
</modelisation:ModelisationLineaireEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>

7 - Le rendu image par image

La classe CompositionTarget représente la surface d’affichage de votre


application. Le moteur d’animation WPF propose de nombreuses
fonctionnalités permettant de créer une animation par trame. Il existe
toutefois des scénarios d’application dans lesquels vous devez exercer un
contrôle sur le rendu image par image. L’objet CompositionTarget permet de
créer des animations personnalisées sur la base d’un rappel image par image.
Cette classe expose les propriétés suivantes:
• RootVisual définit le visuel racine du CompositionTarget.
• TransformFromDevice obtient une matrice qui peut être utilisée pour
transformer les coordonnées du périphérique de rendu de destination
dans cette cible.
• TransformToDevice obtient une matrice qui peut être utilisée pour
transformer les coordonnées de cette cible dans le périphérique de rendu
de destination.
L’événement Rendering se produit juste avant le rendu des objets dans
l’arborescence de composition. Il est routé vers le gestionnaire d’événements
spécifié une fois l’animation et la disposition appliquées à l’arborescence de
composition. Pour inscrire un délégué EventHandler, il faut utiliser la méthode
statique CompositionTarget.Rendering.
242 La programmation graphique 2D de WPF 4
L’UserControl AnimationParrendu.xaml, dans le dossier chapitre05, montre
l’utilisation de la classe CompositionTarget (figure 5-45) avec deux animations.
Figure 5-45

La première animation est composée de deux ellipses sur lesquelles on fait


tourner deux balles. Le dessin des balles se fait par l’intermédiaire de deux
géométries de type EllipseGeometry dont on va animer le déplacement de leur
Copyright 2011 Patrice REY

centre (par leur propriété Center).

<Path Fill=»DarkBlue» Canvas.Left=»9» Canvas.Top=»5»>


<Path.Data>
<EllipseGeometry x:Name=»x_balle1» Center=»30,180»
RadiusX=»5» RadiusY=»5» />
</Path.Data>
CHAPITRE 5 □ Les animations 243
</Path>
<Path Fill=»Red» Canvas.Left=»9» Canvas.Top=»6»>
<Path.Data>
<EllipseGeometry x:Name=»x_balle2» Center=»180,30»
RadiusX=»5» RadiusY=»5» />
</Path.Data>
</Path>
<Path Stroke=»CornflowerBlue» Canvas.Left=»9» Canvas.Top=»6»>
<Path.Data>
<EllipseGeometry Center=»180,180» RadiusX=»150»
RadiusY=»75» />
</Path.Data>
</Path>
<Path Stroke=»LightCoral» Canvas.Left=»9» Canvas.Top=»6»>
<Path.Data>
<EllipseGeometry Center=»180,180» RadiusX=»75»
RadiusY=»150» />
</Path.Data>
</Path>

Quand le contrôle est chargé, on ajoute un délégué EventHandler pour un rendu


par image par la méthode statique CompositionTarget.Rendering. La méthode
CompositionTarget_Rendering(..) va contenir tout ce qui doit être mis à jour pour
le rendu d’une image.

//usercontrol chargé
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
this.UpdateLayout();
v_dernier_rendu = TimeSpan.FromTicks(DateTime.Now.Ticks);
CompositionTarget.Rendering +=
new EventHandler(CompositionTarget_Rendering);
}

Il suffit de mettre à jour la position des centres des objets x_balle1 et x_balle2
en fonction du temps écoulé entre deux rendus d’image.

private void CompositionTarget_Rendering(object sender, EventArgs e) {


//animation 1
RenderingEventArgs renderArgs = (RenderingEventArgs)e;
v_temps = (renderArgs.RenderingTime - v_dernier_rendu).TotalSeconds;
v_dernier_rendu = renderArgs.RenderingTime;
double x = 180 + 150 * Math.Cos(2 * time);
244 La programmation graphique 2D de WPF 4
double y = 180 + 75 * Math.Sin(2 * time);
x_balle1.Center = new Point(x, y);
x = 180 + 75 * Math.Cos(0.5 * time);
y = 180 + 150 * Math.Sin(0.5 * time);
x_balle2.Center = new Point(x, y);
time += v_temps;
...
}

La deuxième animation consiste à déterminer le temps écoulé depuis le départ


du rendu image par image, à déterminer le nombre d’images qui ont été
calculées, et à déterminer le taux moyen de FPS (frame per second ou nombre
d’image par seconde). Pour cela, on utilise un objet Stopwatch.
La classe Stopwatch (espace de noms System.Diagnostics dans l’assembly System.
dll) fournit un jeu de méthodes et de propriétés que vous pouvez utiliser pour
mesurer le temps écoulé précisément. Elle expose les propriétés suivantes:
• Elapsed qui permet d’obtenir le temps total écoulé, mesuré par l’instance
actuelle.
• ElapsedMilliseconds qui permet d’obtenir le temps total écoulé, mesuré par
l’instance actuelle, en millisecondes.
• ElapsedTicks qui permet d’obtenir le temps total écoulé, mesuré par
l’instance actuelle, en graduations de minuterie.
• IsRunning qui permet d’obtenir une valeur indiquant si la minuterie
Stopwatch s’exécute.
Une instance de Stopwatch peut mesurer le temps écoulé pour un intervalle ou
le total du temps écoulé sur plusieurs intervalles. Dans un scénario Stopwatch
typique, vous appelez la méthode Start, puis la méthode Stop, et, enfin, vous
vérifiez le temps écoulé à l’aide de la propriété Elapsed.
Une instance de Stopwatch s’exécute ou est arrêtée. Utilisez IsRunning pour
déterminer l’état actuel de Stopwatch. Utilisez Start pour commencer à mesurer
le temps écoulé. Utilisez Stop pour arrêter de mesurer le temps écoulé.
Copyright 2011 Patrice REY

Demandez la valeur de temps écoulé par l’intermédiaire des propriétés Elapsed,


ElapsedMilliseconds ou ElapsedTicks. Vous pouvez interroger les propriétés de
temps écoulé pendant que l’instance s’exécute ou est arrêtée. Les propriétés de
temps écoulé augmentent régulièrement pendant que le Stopwatch s’exécute.
Ils restent constants lorsque l’instance s’arrête.
Par défaut, la valeur de temps écoulé d’une instance de Stopwatch équivaut
au total de tous les intervalles de temps mesurés. Chaque appel à Start
CHAPITRE 5 □ Les animations 245
commence à compter au temps total passé. Chaque appel de Stop termine la
mesure d’intervalle en cours et gèle la valeur du temps total passé. Utilisez la
méthode Reset pour effacer le temps total passé dans une instance de Stopwatch
existante. Stopwatch mesure le temps écoulé en comptant les graduations
de la minuterie dans le mécanisme de minuterie sous-jacent. Si le matériel
et le système d’exploitation installés prennent en charge un compteur de
performance haute résolution, la classe Stopwatch utilise ce compteur pour
mesurer le temps écoulé. Sinon, la classe Stopwatch utilise l’horloge système
pour mesurer le temps écoulé. Utilisez les champs Frequency et IsHighResolution
pour déterminer la précision et la résolution de l’implémentation du minutage
Stopwatch.

private void CompositionTarget_Rendering(object sender, EventArgs e) {


...
//animation 2
if (v_compteur_image++ == 0) {
// Starting timing.
m_montre.Start();
}
//determiner le fps
long taux_image = (long)(v_compteur_image /
this.m_montre.Elapsed.TotalSeconds);
if (taux_image > 0) {
//remplir les champs texte
x_label_ecoule.Content = m_montre.Elapsed.ToString();
x_label_compteur_image.Content = v_compteur_image.ToString();
x_label_fps.Content = taux_image.ToString();
}
}
CHAPITRE 6

Les styles

La séparation du fonctionnel et du visuel est un des atouts principaux de WPF.


En effet, WPF sépare la fonction d’un contrôle de sa représentation visuelle.
Cela facilite grandement la répartition du travail entre un développeur et un
infographiste. Les styles et les templates, associés à la notion de ressource,
sont les mécanismes fondamentaux de cette séparation.
Ce chapitre vous explique les styles et vous montre comment les utiliser en
les appliquant sur des contrôles.

1 - Principe

Un style WPF permet d’appliquer automatiquement des valeurs de propriétés


à un ensemble d’objets WPF cibles, au moyen d’objets de type Setter. Il permet
également d’appliquer des gestionnaires d’événements au moyen d’objets de
type EventSetter. Cette centralisation améliore la maintenabilité du code. Un
style est généralement centralisé dans les ressources. Il peut être référencé au
moyen de la propriété Style.
L’utilisation d’un style défini en ressource facilite grandement la création et la
maintenance d’interface utilisateur, ne serait-ce que par la simple centralisation
des couleurs ou des polices. Comme pour le langage CSS (cascading style
sheets), les styles peuvent êtres hérités. Mais la notion de style est encore
plus importante que cela car il est possible de définir des comportements
dynamiques au moyen de déclencheurs (triggers). L’exemple type du style,
avec un comportement dynamique, est le style dans lequel le survol d’un
élément par la souris provoque le déclenchement d’une animation, le tout de
façon déclarative au moyen de XAML.

1.1 - Définition de valeur de propriétés

Dans un style, le nom et la valeur de la propriété cible se définissent au moyen


des propriétés Property et Value d’un objet Setter. La propriété TargetName du
Setter permet de cibler un objet particulier par le nom.
248 La programmation graphique 2D de WPF 4
La classe Setter (espace de noms System.Windows dans l’assembly
PrensationFramework.dll) représente un accesseur qui applique une valeur de
propriété. Elle expose principalement comme propriétés:
• Property qui définit la propriété à laquelle la propriété Value sera appliquée.
• Value qui définit la valeur à appliquer à la propriété spécifiée par ce Setter.
• TargetName qui définit le nom de l’objet pour qui ce Setter est prévu.
Par exemple, pour définir une largeur Width de 100, on écrira <Setter Property
= «Width» Value = «100» ></Setter>. Pour cibler des objets particuliers comme
une ellipse nommée x_balle1, on écrira <Setter TargetName = «x_balle1» Property
= «Width» ></Setter>.

1.2 - Définition d’un style

Un style est typiquement stocké dans un dictionnaire de ressources dans


lequel il est identifié par une clé. La propriété Setters d’un style (qui est une
propriété implicite en XAML) permet de définir une collection d’objets Setter
et par conséquent, de valeurs de propriétés pour l’objet cible. Un style peut
aussi cibler un type particulier spécifié par la propriété TargetType.

<Style x:Key='style_rectangle' TargetType='{x:Type Rectangle}'>


<Setter Property='Width' Value='150'></Setter>
<Setter Property='Height' Value='200'></Setter>
</Style>

A noter que la syntaxe simplifiée est supportée par la propriété TargetType


(TargetType = «Rectangle» est identique à TargetType = «{x:Type Rectangle}»). Si la
propriété TargetType est omise, les noms de propriété des objets Setter doivent
être préfixés du type ciblé.

<Style x:Key='style_rectangle'>
<Setter Property='Rectangle.Width' Value='150'></Setter>
Copyright 2011 Patrice REY

<Setter Property='Rectangle.Height' Value='200'></Setter>


</Style>

1.3 - Utilisation d’un style

Un objet FrameworkElement ou FrameworkContentElement peut indiquer le style


qu’il souhaite utiliser au moyen de sa propriété Style qui référence un objet de
CHAPITRE 6 □ Les styles 249
ce type. Une fois appliqué à un objet, un objet Style ne peut plus être modifié.
Sa propriété IsSealed reflète cet état.
Par exemple l’application du style style_rectangle à un objet Rectangle se fera
par:

<UserControl.Ressources>
<Style x:Key='style_rectangle'>
<Setter Property='Rectangle.Width' Value='150'></Setter>
<Setter Property='Rectangle.Height' Value='200'></Setter>
</Style>
</UserControl.Ressources>
...
<Canvas>
<Rectangle x:Name='x_rect' Style='{StaticRessource style_rectangle}'>
</Rectangle>
</Canvas>

Par code de programmation, on écrira, par exemple, pour appliquer un style


à un ListBox:

Style style = new Style(typeof(ListBoxItem));


style.Setters.Add(
new Setter(ListBoxItem.HorizontalContentAlignmentProperty,
HorizontalAlignment.Stretch));
ListBox lb = new ListBox();
lb.ItemContainerStyle = style;
ListBoxItem lbi1 = new ListBoxItem();
Button btn = new Button();
btn.Content = «Button as styled list box item.»;
lbi1.Content = (btn);
lb.Items.Add(lbi1);

1.4 - Le style implicite

Chaque objet FrameworkElement et FrameworkContentElement peut spécifier


son style par sa propriété Style. Si dix objets de type Button ont la même
apparence, soit on remplit dix fois leurs propriétés pour leur apparence, soit
on crée un style une seule fois et on applique ce style à chaque bouton en y
faisant référence dans leur propriété Style. Ce qui pouvait s’avérer fastidieux,
finalement s’avère facile par la possibilité d’appliquer automatiquement un
250 La programmation graphique 2D de WPF 4
style à tous les objets d’un type donné (sauf pour ceux ayant explicitement
renseigné leur propriété Style).
WPF tire partie du fait que la clé d’un élément du dictionnaire n’est pas
forcément une chaîne de caractères mais peut être de n’importe quel type. En
l’occurrence, si la clé d’un style dans un dictionnaire de ressources est un objet
Type, le style s’applique automatiquement à tous les objets de ce type. Si le
style dispose d’une propriété TargetType et ne spécifie pas de clé, sa clé prend
automatiquement la valeur indiquée dans la propriété TargetType. Ce qui veut
dire qu’un style sans clé s’applique automatiquement à tous les objets du type
défini dans TargetType.
Affecter à la propriété TargetType le type TextBlock sans définir de x:Key affecte
implicitement à x:Key la valeur {x:Type TextBlock}. Cela signifie également que
si vous donnez une x:Key autre que {x:Type TextBlock} au Style précité, le Style
n’est pas appliqué automatiquement à tous les éléments TextBlock. Vous devez
alors appliquer explicitement le style aux éléments TextBlock.

<Style TargetType='{x:Type TextBlock}'>


<Setter Property='FontFamily' Value='Segoe Black' />
<Setter Property='HorizontalAlignment' Value='Center' />
<Setter Property='FontSize' Value='12pt' />
<Setter Property='Foreground' Value='#777777' />
</Style>

De nombreux contrôles WPF se composent d’une combinaison d’autres


contrôles WPF. La création d’un style qui s’applique à tous les contrôles d’un
type peut donc avoir un large impact. Par exemple, si vous créez un style
qui cible les contrôles TextBox d’un Canvas, le style est appliqué à tous les
contrôles TextBox de la zone de dessin, même si le TextBox fait partie d’un autre
contrôle, tel qu’un ComboBox.

1.5 - L’héritage de style


Copyright 2011 Patrice REY

La propriété BasedOn de l’objet Style, permet de faire de l’héritage de style,


et de bénéficier dans un style hérité des objets Setter définis dans les styles
ancêtres. Une fois référencé comme ancêtre d’un autre style, un objet Style ne
peut plus être modifié. Sa propriété IsSealed reflète cet état.
Dans l’exemple suivant, Style2 hérite de la valeur Control.Background fixée à
Yellow et ajoute une valeur Control.Foreground fixée à Blue.
CHAPITRE 6 □ Les styles 251
<Style x:Key='Style1'>
<Setter Property='Control.Background' Value='Yellow'/>
</Style>
<Style x:Key='Style2' BasedOn='{StaticResource Style1}'>
<Setter Property='Control.Foreground' Value='Blue'/>
</Style>

1.6 - Les déclencheurs

Un style permet d’appliquer des gestionnaires d’événements au moyen


d’objets de type EventSetter. La propriété Event indique le nom de l’événement,
qui doit être un événement routé. La propriété Handler indique le gestionnaire
d’événements défini en code .NET.
Un déclencheur est un objet de type Trigger qui peut assigner des propriétés
ou déclencher des actions quand une propriété prend une valeur donnée.
La propriété surveillée est définie dans Property, et sa valeur dans Value.
Les propriétés à assigner sont définies par des objets Setter spécifiés dans
la propriété Setters du trigger (qui est une propriété implicite en XAML). Les
déclencheurs sont essentiellement des objets qui vous permettent d’appliquer
des modifications lorsque certaines conditions sont remplies (par exemple,
une propriété spécifiée prend la valeur true quand un événement précis se
produit).
L’exemple suivant montre un Style nommé style_btn_presser, disponible pour
les contrôles Button. Le Style définit un élément Trigger qui modifie la propriété
Foreground d’un bouton lorsque la propriété IsPressed a la valeur true.

<Style x:Key='style_btn_presser' TargetType='Button'>


<Style.Triggers>
<Trigger Property='IsPressed' Value='true'>
<Setter Property = «Foreground' Value='Green'/>
</Trigger>
</Style.Triggers>
</Style>

Les objets hérités de FrameworkElement exposent leur état au moyen de


propriétés booléennes en lecture seule, telles que IsMouseOver, dont l’objectif
est notamment d’être exploitées au moyen de déclencheurs. Par exemple,
le trigger suivant cible x_balle de type Ellipse et lui change sa couleur de
remplissage au survol de la souris sur l’ellipse.
252 La programmation graphique 2D de WPF 4
<Trigger Property='IsMouseOver' Value='true'>
<Setter TargetName='x_balle' Property = «Fill'
Value='{StaticRessource survol_ellipse}'></Setter>
</Trigger>

Il existe d’autres types de déclencheurs. MultiTrigger vous permet d’appliquer


des modifications selon l’état de plusieurs propriétés. EventTrigger vous permet
d’appliquer des modifications lorsqu’un événement se produit. DataTrigger et
MultiTrigger sont destinées aux propriétés liées aux données.
La classe MultiTrigger définit un déclencheur qui répond à plusieurs conditions,
représentées par des objets Condition spécifiés dans la propriété Conditions. Les
propriétés EnterActions et ExitActions du Trigger peuvent être spécifiées pour
des actions relatives aux animations (comme nous l’avons vu dans le chapitre
précédent) et à la lecture de flux multimédia.
L’exemple suivant contient deux MultiTrigger. Le premier MultiTrigger définit
la valeur de la propriété MinWidth lorsque la propriété HasItems a la valeur
false et que la propriété Width a la valeur Auto. Le deuxième MultiTrigger est
semblable mais est destiné à la propriété MinHeight.

<Style.Triggers>
<Trigger Property='IsEnabled' Value='false'>
<Setter Property='Background' Value='#EEEEEE' />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property='HasItems' Value='false' />
<Condition Property='Width' Value='Auto' />
</MultiTrigger.Conditions>
<Setter Property='MinWidth' Value='120'/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property='HasItems' Value='false' />
Copyright 2011 Patrice REY

<Condition Property='Height' Value='Auto' />


</MultiTrigger.Conditions>
<Setter Property='MinHeight' Value='95'/>
</MultiTrigger>
</Style.Triggers>
CHAPITRE 6 □ Les styles 253

2 - Mise en application des styles

L’UserControl ExStyle.xaml, dans le dossier chapitre06, montre une mise en


application des styles au travers de cinq exemples de référence (figure 6-1).
La première animation permet de mettre en pratique le concept d’application
des styles en général. Sur un Canvas x_cnv_appli1, on positionne cinq boutons,
de gauche à droite, nommés x_btn1_ordi, x_btn1_1, x_btn1_2, x_btn1_3 et x_
btn1_4.
Un bouton dit «ordinaire», c’est-à-dire sans application d’un style particulier,
aura sa propriété Style définie par une valeur par défaut qui est null. En XAML,
soit on omet la balise Style, soit on la met en écrivant Style=»{x:Null}».

<Button Canvas.Left='138' Canvas.Top='27'


Content='Bouton ordinaire' Height='23'
Name='x_btn1_ordi' Width='112' Style='{x:Null}'
Cursor='Hand' />

Dans le dictionnaire défini localement, des ressources du Canvas, on positionne


deux styles différents nommés s_style_btn_orange et s_style_btn_vert.
Le style s_style_btn_orange défini un style pour des objets Button. On lui attribut
une référence x:Key = «s_style_btn_orange» sans rien préciser d’autre. Comme
nous voulons appliquer ce style uniquement à des boutons et que l’on n’a pas
préciser de TargetType, les objets Setter devront avoir leur propriété Property
qui indique une attribution à des objets Button (en préfixant donc par Button
les propriétés en notation pointée). Pour fixer une taille de police à 22, on
écrira Property = «Button.FontSize». Quand la valeur d’une propriété est un objet
complexe, on utilise la notation pointée <Setter.Value> et on affecte l’objet
correspondant. Ici pour la Property Button.RenderTransform, on affecte une
valeur qui est un objet RotateTransform avec une propriété Angle de 10.
L’application de ce style aux boutons x_btn1_1 et x_btn1_2, se fera par l’écriture
de Style = «{StaticResource s_style_btn_orange}» (animation n°1 dans figure 6-1).

<Canvas.Resources>
<Style x:Key='s_style_btn_orange'>
<Setter Property='Button.FontSize' Value='22' />
<Setter Property='Button.Background' Value='Orange' />
<Setter Property='Button.Foreground' Value='Black' />
254 La programmation graphique 2D de WPF 4
Figure 6-1

Copyright 2011 Patrice REY


CHAPITRE 6 □ Les styles 255
<Setter Property='Button.Height' Value='50' />
<Setter Property='Button.Width' Value='50' />
<Setter Property='Button.RenderTransformOrigin'
Value='0.5,0.5' />
<Setter Property='Button.RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
<Setter Property='Button.Cursor' Value='Hand'></Setter>
</Style>
...
</Canvas.Resources>
...
<Button Style='{StaticResource s_style_btn_orange}'
Canvas.Left='265' Canvas.Top='14' Name='x_btn1_1'>1</Button>
<Button Style='{StaticResource s_style_btn_orange}'
Canvas.Left='336' Canvas.Top='14' Name='x_btn1_2'>2</Button>

Le style s_style_btn_vert est, quant à lui, référencé par x:Key = «s_style_btn_


vert» et par TargetType fixé à {x:Type Button}. Cela veut dire que ce style cible
uniquement les objets Button. Dans ce cas, on se contentera d’exprimer la
propriété Property des Setter par le nom de la propriété ciblée.
L’application de ce style aux boutons x_btn1_3 et x_btn1_4, se fera par l’écriture
de Style = «{StaticResource s_style_btn_vert}» (animation n°1 dans figure 6-1).

<-- ressources -->


<Canvas.Resources>
...
<Style x:Key='s_style_btn_vert' TargetType='{x:Type Button}'>
<Setter Property='FontSize' Value='22' />
<Setter Property='Background' Value='LightGreen' />
<Setter Property='Foreground' Value='Black' />
<Setter Property='Height' Value='50' />
<Setter Property='Width' Value='50' />
<Setter Property='RenderTransformOrigin' Value='0.5,0.5' />
<Setter Property='RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
<Setter Property='Cursor' Value='Hand'></Setter>
256 La programmation graphique 2D de WPF 4
</Style>
</Canvas.Resources>

La deuxième animation permet de mettre en pratique le concept d’héritage


des styles. Sur un Canvas x_cnv_appli2, on positionne deux boutons, de gauche
à droite, nommés x_btn2_1 et x_btn2_2.
Dans le dictionnaire des ressources du Canvas x_cnv_appli2, on défini un
style s_style_btn_bleu (pour réaliser un bouton bleu avec une inclinaison et
une écriture normale) et un style s_style_btn_bleu_gras (pour réaliser le même
bouton bleu mais avec une écriture en gras).

<Canvas.Resources>
<Style x:Key='s_style_btn_bleu' TargetType='{x:Type Button}'>
<Setter Property='FontSize' Value='18' />
<Setter Property='Background' Value='LightSkyBlue' />
<Setter Property='Foreground' Value='Black' />
<Setter Property='Height' Value='50' />
<Setter Property='Width' Value='50' />
<Setter Property='RenderTransform'>
<Setter.Value>
<SkewTransform CenterX='0' CenterY='0'
AngleX='20' AngleY='0'></SkewTransform>
</Setter.Value>
</Setter>
<Setter Property='Cursor' Value='Hand'></Setter>
</Style>
...
</Canvas.Resources>

Pour mettre en pratique l’héritage d’un style, on crée un nouveau style


référencé par s_style_btn_bleu_gras. On lui signifie que sa cible est de type
Button par un TargetType = «{x:Type Button}». Et pour hériter du style s_style_
btn_bleu, on fixe la propriété BasedOn en faisant référence au style hérité, soit
Copyright 2011 Patrice REY

BasedOn = «{StaticResource s_style_btn_bleu}».

<Canvas.Resources>
...
<Style x:Key='s_style_btn_bleu_gras'
TargetType='{x:Type Button}'
BasedOn='{StaticResource s_style_btn_bleu}'>
CHAPITRE 6 □ Les styles 257
<Setter Property='Button.FontWeight' Value='Bold' />
</Style>
</Canvas.Resources>

L’application du style s_style_btn_bleu au bouton x_btn2_1 se fera par l’écriture


de Style = «{StaticResource s_style_btn_bleu}» et au bouton x_btn2_2 par l’écriture
de Style = «{StaticResource s_style_btn_bleu_gras}» (animation n°2 dans figure
6-1).
La troisième animation permet de montrer comment appliquer un même style
à différents types de contrôles. Dans le dictionnaire des ressources du Canvas
x_cnv_appli3, on définit un style référencé par s_style_control. Pour signifier que
ce style puisse être appliqué à des contrôles en général, la propriété Property
des objets Setter sera préfixée en notation pointée par Control (par exemple
Property = «Control.Background»).

<Canvas.Resources>
<Style x:Key='s_style_control'>
<Setter Property='Control.FontSize' Value='22' />
<Setter Property='Control.Background' Value='LightCoral' />
<Setter Property='Control.Foreground' Value='Yellow' />
<Setter Property='Control.Height' Value='50' />
<Setter Property='Control.Width' Value='50' />
<Setter Property='Control.RenderTransformOrigin'
Value='0.5,0.5' />
<Setter Property='Control.RenderTransform'>
<Setter.Value>
<RotateTransform Angle='-10' />
</Setter.Value>
</Setter>
</Style>
</Canvas.Resources>

Les différents contrôles auront donc leur propriété Style fixée par Style =
«{StaticResource s_style_control}» (animation n°3 dans figure 6-1).

<Button Canvas.Left='138' Canvas.Top='14' Content='01'


Cursor='Hand' Style='{StaticResource s_style_control}' />
<ComboBox Style='{StaticResource s_style_control}'
Canvas.Left='211' Canvas.Top='14'>
<ComboBox.Items>2</ComboBox.Items>
258 La programmation graphique 2D de WPF 4
</ComboBox>
<Expander Style='{StaticResource s_style_control}' Content='3'
Canvas.Left='286' Canvas.Top='14' />
<TabControl Style='{StaticResource s_style_control}'
Canvas.Left='360' Canvas.Top='14'>
<TabControl.Items>4</TabControl.Items>
</TabControl>
<ToolBar Style='{StaticResource s_style_control}'
Canvas.Left='438' Canvas.Top='14'>
<ToolBar.Items>5</ToolBar.Items>
</ToolBar>
<InkCanvas Style='{StaticResource s_style_control}'
Canvas.Left='511' Canvas.Top='14' />
<TextBox Style='{StaticResource s_style_control}' Text='7'
Canvas.Left='587' Canvas.Top='14' />

Pour le bouton x_btn3_8, on lui applique le style s_style_control qui contient


un Setter pour la propriété Background. Mais comme on fixe une nouvelle
propriété Background explicitement à Red, c’est cette dernière qui est prise en
compte (animation n°3 dans figure 6-1).

<Button Canvas.Left='660' Canvas.Top='14' Content='01'


Cursor='Hand' Style='{StaticResource s_style_control}'
Background='Red' Name='x_btn3_8' />

La quatrième animation permet de mettre en pratique le fait qu’avec la


présence d’un style non référencé mais ciblant des objets particuliers, tous les
objets définis localement et du même type se verront appliquer ce style (sauf
si leurs propriétés sont redéfinies explicitement, et/ou s’ils ont une propriété
Style fixée explicitement).
Dans le dictionnaire des ressources du Canvas x_cnv_appli4, on définit un style
non référencé, ciblant les objets Button par TargetType = «{x:Type Button}».
Copyright 2011 Patrice REY

<Canvas.Resources>
<Style TargetType='{x:Type Button}'>
<Setter Property='FontSize' Value='22' />
<Setter Property='Background' Value='MediumBlue' />
<Setter Property='Foreground' Value='White' />
<Setter Property='Height' Value='50' />
<Setter Property='Width' Value='50' />
<Setter Property='RenderTransformOrigin' Value='0.5,0.5' />
CHAPITRE 6 □ Les styles 259
<Setter Property='RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
<Setter Property='Cursor' Value='Hand'></Setter>
</Style>
</Canvas.Resources>

Sur le Canvas, nous avons trois boutons nommés, de gauche à droite x_btn4_1,
x_btn4_2 et x_btn4_3. Les trois boutons n’ont pas de propriétés Style fixées
explicitement, donc le style appliqué à tous ces boutons sera celui qui est
défini localement (ici le style qui cible les boutons et qui est inscrit dans le
dictionnaire des ressources du Canvas). Pour le bouton x_btn4_3, on fixe
explicitement une propriété Background à YellowGreen, donc cette couleur sera
substituée à celle du style (animation n°4 dans figure 6-1).

<Button Canvas.Left='138' Canvas.Top='14' Content='01'


Cursor='Hand' Name='x_btn4_1' />
<Button Canvas.Left='204' Canvas.Top='14' Content='02'
Cursor='Hand' Name='x_btn4_2' />
<Button Canvas.Left='276' Canvas.Top='14' Content='03'
Cursor='Hand' Background='YellowGreen' Name='x_btn4_3' />

La cinquième animation permet de mettre en pratique les déclencheurs


(Trigger et MultiTrigger).
Dans le dictionnaire des ressources du Canvas x_cnv_appli5, on définit un style
s_style_bouton qui cible les contrôles Button, et pour lequel on veut que, lors du
survol de la souris sur le contrôle, celui-ci effectue une rotation d’un angle de
10 degrés centrée sur son milieu. On commence par ajouter un Setter qui cible
RenderTransformOrigin en le fixant au point (0.5,0.5). Ensuite, dans la propriété
Triggers du Style, on ajoute un objet Trigger qui cible le survol de la souris par
Property = «IsMouseOver» avec une valeur Value à true pour signifier qu’elle se
produit. Et, dans ce Trigger, on ajoute les objets Setter qui correspondent à ce
qui doit se passer lors du survol de la souris (un Setter qui cible RenderTransform
et qui lui fixe un RotateTransform avec un Angle).

<Canvas.Resources>
<Style x:Key='s_style_bouton' TargetType='{x:Type Button}'>
260 La programmation graphique 2D de WPF 4
<Style.Triggers>
<Trigger Property='IsMouseOver' Value='True'>
<Setter Property='RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
<Setter Property='Foreground' Value='Black' />
</Trigger>
</Style.Triggers>
<Setter Property='FontSize' Value='22' />
<Setter Property='Background' Value='Red' />
<Setter Property='Foreground' Value='YellowGreen' />
<Setter Property='Height' Value='50' />
<Setter Property='Width' Value='50' />
<Setter Property='RenderTransformOrigin' Value='0.5,0.5' />
<Setter Property='Cursor' Value='Hand'></Setter>
</Style>
...
</Canvas.Resources>

En appliquant ce style au bouton x_btn5_1, le survol de la souris sur celui-ci


provoque une rotation du bouton de 10 degrés (animation n°5 dans la figure
6-1).

<Button Canvas.Left='138' Canvas.Top='14' Content='01'


Name='x_btn5_1' Style='{StaticResource s_style_bouton}' />

Le style s_style_bouton2 applique le même Trigger que le s_style_bouton1 pour


le survol de la souris, mais avec en plus, un autre Trigger pour l’événement
IsFocused (qui provoque une rotation du bouton si celui-ci a le focus).

<Canvas.Resources>
...
Copyright 2011 Patrice REY

<Style x:Key='s_style_bouton2' TargetType='{x:Type Button}'>


<Style.Triggers>
<Trigger Property='IsMouseOver' Value='True'>
<Setter Property='RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
CHAPITRE 6 □ Les styles 261
<Setter Property='Foreground' Value='Black' />
</Trigger>
<Trigger Property='IsFocused' Value='True'>
<Setter Property='RenderTransform'>
<Setter.Value>
<RotateTransform Angle='10' />
</Setter.Value>
</Setter>
<Setter Property='Foreground' Value='Black' />
</Trigger>
</Style.Triggers>
<Setter Property='FontSize' Value='22' />
<Setter Property='Background' Value='MediumBlue' />
<Setter Property='Foreground' Value='White' />
<Setter Property='Height' Value='50' />
<Setter Property='Width' Value='50' />
<Setter Property='RenderTransformOrigin' Value='0.5,0.5' />
<Setter Property='Cursor' Value='Hand'></Setter>
</Style>
</Canvas.Resources>

En appliquant ce style au bouton x_btn5_2, le survol de la souris sur celui-ci


provoque une rotation du bouton de 10 degrés, ainsi que si le bouton a le
focus (animation n°5 dans la figure 6-1). Dès que le bouton perd le focus, il
reprend son état de base.

<Button Canvas.Left='200' Canvas.Top='14' Content='02'


Name='x_btn5_2' Style='{StaticResource s_style_bouton2}' />
CHAPITRE 7

Les modèles de contrôle

L’architecture des contrôles WPF permet la réalisation de contrôles


composites dont la représentation visuelle est configurable à volonté grâce au
mécanisme des templates. Les contrôles exposent de nombreuses propriétés qui
permettent de personnaliser leur apparence (comme les propriétés Background
et Foreground d’un Button). Un modèle de contrôle (un control template) vous
permet de remplacer complètement l’arbre visuel d’un contrôle tout en
gardant les fonctionnalités du contrôle.
Dans ce chapitre, nous allons voir comment est organisé le modèle d’un
contrôle, et comment appliquer un ControlTemplate à un contrôle.

1 - Principe des modèles

Une caractéristique importante des contrôles WPF est qu’ils ne gèrent pas eux-
mêmes leur représentation visuelle. En effet, celle-ci est confiée à un objet de
type ControlTemplate, qui contient une hiérarchie d’objets Visual. Un template
signifie un modèle et le terme de template est couramment employé comme
terme général pour désigner un ControlTemplate. De ce fait, si le contrôle n’a
pas de template, il n’y a pas d’affichage du contrôle.
Cette notion de template fait apparaître la distinction entre l’arbre logique et
l’arbre visuel des éléments WPF affichés. L’arbre logique est défini par le code
XAML, alors que l’arbre visuel prend en compte la composition des templates
des éléments affichés.
Bien entendu, tous les contrôles ont un template par défaut qui est fourni. En
changeant de template au moyen de la propriété Template, il est possible de
personnaliser complètement l’apparence visuelle d’un contrôle.
La classe du contrôle ne gère donc pas son visuel mais définit sa fonction. La
représentation du contrôle est définie au moyen d’un template, qui exploite les
propriétés définies dans la classe du contrôle.
La classe ControlTemplate (espace de noms System.Windows.Controls dans
l’assembly PresentationFramework.dll) spécifie la structure visuelle et les aspects
comportementaux d’un Control qui peuvent être partagés par plusieurs
264 La programmation graphique 2D de WPF 4
instances du contrôle. Elle expose principalement les propriétés:
• Template qui définit une référence à l’objet qui enregistre ou lit les nœuds
XAML pour le modèle lorsque le modèle est défini.
• Triggers qui obtient une collection d’objets TriggerBase qui appliquent des
modifications de propriété ou effectuent des actions selon des conditions
spécifiées.
• VisualTree qui définit le nœud racine du modèle.
• TargetType qui définit le type auquel le ControlTemplate est destiné.
• Ressources qui définit la collection des ressources qui peuvent être utilisées
dans la portée de ce modèle.
Le ControlTemplate vous permet de spécifier la structure visuelle d’un contrôle.
L’auteur du contrôle peut définir le modèle ControlTemplate par défaut, tandis
que l’auteur de l’application peut substituer ce modèle ControlTemplate afin
de reconstruire la structure visuelle du contrôle. Un ControlTemplate est conçu
comme une unité indépendante de détail d’implémentation, invisible aux
utilisateurs extérieurs et aux objets, y compris les styles. La seule façon de
manipuler le contenu du modèle de contrôle est de le faire à partir du même
modèle de contrôle.
L’UserControl ExTemplate.xaml, dans le dossier chapitre07, visualise la
personnalisation des modèles de contrôle au travers d’exemples (figure 7-1).

2 - Afficher le contenu d’un ControlTemplate

L’animation n°1 permet de visualiser le contenu d’un ControlTemplate, de façon


à mieux comprendre l’organisation des modèles de contrôle.
Le bouton x_btn1_template est un bouton classique utilisant le modèle par
défaut. Nous allons ajouter la méthode AfficherDetailTemplate(..) qui va afficher
le contenu du ControlTemplate du bouton dans la zone de texte x_infos1 de
type TextBlock.
Il faut ajouter les références using System.Xml, using System.Windows.Markup et
Copyright 2011 Patrice REY

using System.IO, qui sont nécessaires au traitement de la méthode. On instancie


xml_ecrit, de type XmlWriter, par la méthode statique XmlWriter.Create(..), qui
crée une chaîne XML stockée dans sb (de type StringBuilder) avec des réglages
reglages_xml (de type XmlWriterSettings). La méthode statique XamlWriter.
Save(..) est utilisée pour une sérialisation XAML limitée, d’objets d’exécution
fournis en balises XAML. La propriété Template de Button fournit le contenu
XAML.
CHAPITRE 7 □ Les modèles de contrôle 265
Figure 7-1
266 La programmation graphique 2D de WPF 4
private void AfficherDetailTemplate(Button btn,
TextBlock champ_texte) {
XmlWriterSettings reglages_xml = new XmlWriterSettings();
reglages_xml.Indent = true;
reglages_xml.IndentChars = « «;
reglages_xml.NewLineOnAttributes = true;
StringBuilder sb = new StringBuilder();
XmlWriter xml_ecrit = XmlWriter.Create(sb, reglages_xml);
XamlWriter.Save(btn.Template, xml_ecrit);
champ_texte.Text = sb.ToString();
}

La figure 7-2 visualise le contenu du ControlTemplate du bouton x_btn1_template,


affiché dans la zone texte.
Figure 7-2

Dans l’arbre visuel du bouton, on voit qu’il y a une balise ContentPresenter.


Le ContentPresenter a pour rôle d’afficher le contenu défini dans la propriété
Content du bouton. Les propriétés des objets du template sont associées à celles
de la classe Button, ou d’une classe ancêtre, au moyen d’une extension markup
TemplateBinding. Le ContentPresenter joue ainsi son rôle de présentation du
contenu du contrôle.

3 - Personnalisation d’un ControlTemplate


Copyright 2011 Patrice REY

Dans l’animation n°2, on choisit de modifier le ControlTemplate du bouton x_


btn_21. On ajoute dans le dictionnaire des ressources du Canvas x_cnv_appli2,
un modèle de contrôle référencé par x:Key = «t_btn21_template». Ce modèle est
composé de deux ellipses remplies par des dégradés de couleurs.

<Canvas.Resources>
<ControlTemplate x:Key='t_btn21_template'>
CHAPITRE 7 □ Les modèles de contrôle 267
<Grid>
<Ellipse Width='100' Height='100'>
<Ellipse.Fill>
<LinearGradientBrush StartPoint='0,0'
EndPoint='0,1'>
<GradientStop Offset='0' Color='Blue' />
<GradientStop Offset='1' Color='Red' />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width='80' Height='80'>
<Ellipse.Fill>
<LinearGradientBrush StartPoint='0,0'
EndPoint='0,1'>
<GradientStop Offset='0' Color='White' />
<GradientStop Offset='1' Color='Transparent' />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
</ControlTemplate>
</Canvas.Resources>

Pour pouvoir afficher ce modèle par un bouton, il suffit de modifier la propriété


Template de celui-ci en la faisant pointer vers la ressource t_btn21_template par
Template = «{StaticResource t_btn21_template}». Quand on survole le bouton, le
curseur prend la forme d’une main, et en cliquant dessus, le détail du Template
du bouton s’affiche dans la zone de texte x_infos2. Ainsi, on a modifié le
ControlTemplate mais on a gardé les fonctionnalités du bouton comme le clic
(figure 7-1).

<Button Template='{StaticResource t_btn21_template}'


Content='Bouton 01' Canvas.Left='138' Canvas.Top='6'
Cursor='Hand' Name='x_btn_21' Click='x_btn_anim2_Click' />

On ajoute un deuxième modèle de contrôle t_btn22_template pour l’appliquer


au bouton x_btn_22. Ce modèle va faire en sorte de faire changer l’apparence
du bouton lors de son survol ainsi que lors de l’événement clic. Les événements
sont gérés par des déclencheurs Triggers (comme on l’a déjà vu). La propriété
Triggers du ControlTemplate contient la collection des Trigger qui doivent être
gérés.
268 La programmation graphique 2D de WPF 4
Pour le survol de la souris sur le contrôle, on ajoute un Trigger dont la propriété
Property cible l’événement Button.IsMouseOver et la propriété Value fixée à
true. A ce Trigger, on ajoute un Setter dont la cible TargetName cible l’ellipse
outerCircle en lui faisant changer la couleur de remplissage (Property = «Fill»,
Value = «Orange»).
Pour le clic de la souris sur le contrôle, on ajoute un Trigger avec la propriété
Property fixée à Button.IsPressed et la propriété Value à true. On ajoute un
Setter qui cible la propriété RenderTransform et lui affecte une transformation
ScaleTransform avec ScaleX=0.9 et ScaleY=0.9, ainsi qu’un changement
d’origine qui passe au centre du bouton (RenderTransformOrigin avec 0.5,0.5).

<Canvas.Resources>
<ControlTemplate x:Key='t_btn22_template'>
<Grid>
<Ellipse x:Name='outerCircle' Width='100'
Height='100'>
<Ellipse.Fill>
<LinearGradientBrush StartPoint='0,0'
EndPoint='0,1'>
<GradientStop Offset='0' Color='Blue' />
<GradientStop Offset='1' Color='Red' />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width='80' Height='80'>
<Ellipse.Fill>
<LinearGradientBrush StartPoint='0,0'
EndPoint='0,1'>
<GradientStop Offset='0' Color='White' />
<GradientStop Offset='1' Color='Transparent' />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
Copyright 2011 Patrice REY

<ControlTemplate.Triggers>
<Trigger Property='Button.IsMouseOver' Value='True'>
<Setter TargetName='outerCircle' Property='Fill'
Value='Orange' />
</Trigger>
<Trigger Property='Button.IsPressed' Value='True'>
<Setter Property='RenderTransform'>
<Setter.Value>
CHAPITRE 7 □ Les modèles de contrôle 269
<ScaleTransform ScaleX='0.9' ScaleY='0.9' />
</Setter.Value>
</Setter>
<Setter Property='RenderTransformOrigin'
Value='0.5,0.5' />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Canvas.Resources>

Il suffit alors de modifier la propriété Template du bouton x_btn_22 en la


faisant pointer vers la ressource t_btn22_template par Template = «{StaticResource
t_btn22_template}». De ce fait, lors du survol du bouton, celui-ci change sa
couleur de remplissage par de l’orange, et quand on clique dessus, le bouton
se réduit en taille de 10% (figure 7-1).

<Button Template='{StaticResource t_btn22_template}'


Content='Bouton 01' Canvas.Left='246' Canvas.Top='6'
Cursor='Hand' Name='x_btn_22' Click='x_btn_anim22_Click' />

Dans les deux boutons précedents, nous avons modifié leur propriété Template.
Or, dans l’instanciation du bouton, nous avons rempli la propriété Content par
Bouton 01, et pourtant, ce contenu n’était pas affiché.
Le ControlTemplate t_btn23_template permet d’afficher la propriété Content
du bouton. En reprenant à l’identique le template t_btn22_template, il suffit
d’ajouter en premier une propriété TargetType qui cible le contrôle Button par
TargetType = «{x:Type Button}». En deuxième, il faut ajouter, dans le Grid qui
contient la définition visuelle du bouton, un objet Viewbox avec à l’intérieur un
ContentPresenter dont la propriété Content fait référence à la propriété Content
du type Button. Cette référence se fait au moyen d’une extension markup
TemplateBinding qui pointe sur la propriété Content du Button.

<ControlTemplate x:Key='t_btn23_template'
TargetType='{x:Type Button}'>
<Grid>
...
<Viewbox>
<ContentPresenter Margin='20'
Content='{TemplateBinding Content}' />
</Viewbox>
270 La programmation graphique 2D de WPF 4
</Grid>
...
</ControlTemplate>

Le bouton x_btn_23, doté du modèle t_btn23_template, affiche désormais le


contenu Bouton 01 tout en appliquant le modèle réalisé (figure 7-1).

<Button Template='{StaticResource t_btn23_template}'


Content='Bouton 01' Canvas.Left='355' Canvas.Top='6'
Cursor='Hand' Name='x_btn_23' Click='x_btn_anim23_Click'/>

4 - La gestion des états visuels

Depuis WPF 4, le visuel des états est géré par la classe VisualStateManager. Nous
avons vu des états gérés par des Trigger dans le ControlTemplate. Maintenant
nous allons voir des états qui sont gérés par VisualStateManager.
La documentation MSDN de Microsoft, à l’adresse suivante http://msdn.
microsoft.com/fr-fr/library/aa970773.aspx, donne pour chaque contrôle les
catégories des états visuels qui sont disponibles. Pour un contrôle de type
Button, on a les états de base (par défaut, survol de la souris, activé ou désactivé),
les états concernant le focus (avec focus, sans focus), et les états de validation.
Le modèle objet du VisualStateManager représente ces trois catégories d’états
sous la forme d’objets de classe VisualStateGroup, chacun contenant des objets
VisualState pour représenter les états (définis par leur propriété Name).
Pour la classe Button, les groupes d’états sont les suivants (d’après la
documentation):
• le groupe CommonStates contient les états Normal, MouseOver, Pressed et
Disabled.
• le groupe FocusStates contient les états Focused et Unfocused.
• le groupe ValidationStates contient les états Valid, InvalidFocused et
InvalidUnfocused.
Copyright 2011 Patrice REY

Les groupes d’états applicables à un template doivent être assignés au niveau


du conteneur racine de la structure visuelle, au moyen de la propriété attachée
VisualStateManager.VisualStateGroups. Chaque VisualStateGroup contient dans
sa propriété States une collection d’objets VisualState. Cette propriété est
implicite en XAML. Le visuel associé à un objet VisualState est décrit au moyen
d’une animation dans sa propriété Storyboard. Le type d’animation doit bien
CHAPITRE 7 □ Les modèles de contrôle 271
entendu correspondre à celui de la propriété ciblée. Si on ne souhaite pas
animer le visuel de l’état, la durée de l’animation doit être initialisée à zéro.
La classe VisualTransition permet de définir une transition entre un état désigné
par la propriété From et un autre état désigné par la propriété To. La durée
de la transition est définie par la propriété GeneratedDuration. La propriété
Storyboard permet de définir une animation pendant la transition. La transition
et son animation s’exécutent avant que l’état de destination soit atteint. Une
transition se définit au sein de la collection Transitions d’un groupe d’états
VisualStateGroup. Elle peut evidemment s’appliquer à tous les états du groupe.
L’animation n°3 (figure 7-1) visualise un bouton pour lequel on réalise un
effet de verre par la modification de son ControlTemplate et en gérant les états
visuels par le VisualStateManager. Nous réalisons un style référencé s_btn_
effet_glass que nous appliquons au bouton x_btn_31 par sa propriété Style. Ce
style contient une modification de son ControlTemplate pour gérer les états
visuels. Le bas de la figure 7-1 montre l’effet de survol par la souris et l’effet
réalisé quand le bouton a le focus.

<Button Canvas.Left='208' Canvas.Top='12'


Content='bouton effet glass' Height='47' Name='x_btn_31'
Width='180' Style='{StaticResource s_btn_effet_glass}'
FontSize='14' FontFamily='Verdana' Cursor='Hand'
Click='x_btn_31_Click' />

On commence par créer dans le dictionnaire des ressources du Canvas


x_cnv_appli3, un style référencé x:Key = «s_btn_effet_glass» qui cible les
contrôles de type Button (TargetType = «Button»). On ajoute différents Setter
pour les propriétés de base comme Margin, Padding, Foreground et Background.
La modification du ControlTemplate se fait par un Setter dont la propriété
Property est Template et dont la propriété Value est un ControlTemplate (donc
<Setter.Value> <ControlTemplate> ... </Setter.Value> </ControlTemplate>). Ce
ControlTemplate cible les Button. Un Grid sert de conteneur pour le bouton. La
configuration est la suivante:

<Style x:Key='s_btn_effet_glass' TargetType='Button'>


<Setter Property='Margin' Value='2' />
<Setter Property='Padding' Value='10' />
<Setter Property='Foreground' Value='White' />
<Setter Property='Background'
272 La programmation graphique 2D de WPF 4
Value='{StaticResource BlueBrush1}' />
<Setter Property='Template'>
<Setter.Value>
<ControlTemplate TargetType='Button'>
<Grid>
...
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Les divers rectangles qui permettent de simuler le fond et l’effet du verre


sont positionnés les uns sur les autres. Le ContentPresenter est ajouté avec les
références au moyen des extensions markup TemplateBinding pour récupérer
les propriétés nécessaires.

<Style x:Key='s_btn_effet_glass' TargetType='Button'>


...
<Grid>
...
<Rectangle Name='backgroundRectangle'
Style='{StaticResource RectangleStyle}'
Fill='{TemplateBinding Background}' />
<Rectangle
Style='{StaticResource RectangleStyle}'
Stroke='{TemplateBinding BorderBrush}'
StrokeThickness='{TemplateBinding BorderThickness}'
Fill='White'
OpacityMask='{StaticResource RefractionBrush}' />
<Rectangle Fill='White'
OpacityMask='{StaticResource ReflectionBrush}'
Height='8' RadiusX='5'
RadiusY='5' Margin='2'
VerticalAlignment='Top' />
Copyright 2011 Patrice REY

<ContentPresenter
Content='{TemplateBinding Content}'
ContentTemplate='{TemplateBinding ContentTemplate}'
VerticalAlignment=
'{TemplateBinding VerticalContentAlignment}'
HorizontalAlignment=
'{TemplateBinding HorizontalContentAlignment}'
Margin='{TemplateBinding Padding}' />
CHAPITRE 7 □ Les modèles de contrôle 273
<Rectangle Name='focusRectangle'
Style='{StaticResource RectangleStyle}'
Visibility='Collapsed'
Stroke='{StaticResource BlueBrush4}' />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Il ne reste plus qu’à ajouter les effets visuels avec le VisualStateManager par
sa propriété VisualStateGroups qui est une collection composée des objets
VisualStateGroup. Nous avons les VisualStateGroup CommonStates et FocusStates.
Ils sont composés, pour CommonStates par les VisualState Normal, MouseOver et
Pressed, et pour FocusStates par les VisualState Focused et Unfocused.

<Style x:Key='s_btn_effet_glass' TargetType='Button'>


...
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name='CommonStates'>
<VisualStateGroup.Transitions>
<VisualTransition
GeneratedDuration='0:0:0.2'
To='MouseOver' />
<VisualTransition
GeneratedDuration='0:0:0.2'
To='Pressed' />
</VisualStateGroup.Transitions>
<VisualState Name='Normal' />
<VisualState Name='MouseOver'>
<Storyboard>
<ColorAnimation
Storyboard.TargetName='backgroundRectangle'
Storyboard.TargetProperty=
«(Shape.Fill).(SolidColorBrush.Color)'
Duration='0'
To='{StaticResource BlueColor2}' />
</Storyboard>
</VisualState>
<VisualState Name='Pressed'>
<Storyboard>
274 La programmation graphique 2D de WPF 4
<ColorAnimation
Storyboard.TargetName='backgroundRectangle'
Storyboard.TargetProperty=
«(Shape.Fill).(SolidColorBrush.Color)'
Duration='0'
To='{StaticResource BlueColor3}' />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name='FocusStates'>
<VisualState Name='Focused'>
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName='focusRectangle'
Storyboard.TargetProperty='Visibility'
Duration='0'>
<DiscreteObjectKeyFrame
KeyTime='0'>
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name='Unfocused' />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Le bas de la figure 7-1 visualise les effets sur le bouton quand il est survolé par
Copyright 2011 Patrice REY

la souris et quand il a le focus.


Annexes

1. Liste des UserControl réalisés

Chaque chapitre contient un ensemble de contrôle, chacun d’eux traite les


notions abordées.
Dans le chapitre 1 on a les contrôles suivants:

Dans le chapitre 2 on a les contrôles suivants :


276 Annexes
Dans le chapitre 3 on a les contrôles suivants:

Dans le chapitre 4 on a les contrôles suivants:

Dans le chapitre 5 on a les contrôles suivants:


Annexes 277
Dans le chapitre 6 on a les contrôles suivants:

Dans le chapitre 7 on a les contrôles suivants:

2. Vue sur l’organisation du programme

L’arborescence des fichiers est visualisée sur la figure N-1. Le point d’entrée
du programme est la fenêtre MainWidow.xaml avec son code MainWindow.cs.
La fenêtre est composée par un Grid qui est divisé en lignes et colonnes pour
le placement des contrôles.

<Grid x:Name='x_grid_root'>
<Grid.RowDefinitions>
<RowDefinition Height='25'></RowDefinition>
<RowDefinition Height='*'></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='200'></ColumnDefinition>
<ColumnDefinition Width='*'></ColumnDefinition>
</Grid.ColumnDefinitions>
...
</Grid>

Un menu est ajouté à la fenêtre avec la rubrique Fichier et l’item Quitter. un


gestionnaire d’événement est positionné sur la sélection de l’item Quitter.

<!-- menus -->


<Menu x:Name='x_menu_fen' Grid.Row='0' Grid.Column='0'
Grid.ColumnSpan='2'>
<Menu.Background>
278 Annexes
Figure N-1
Annexes 279
<LinearGradientBrush EndPoint='0.5,1' StartPoint='0.5,0'>
<GradientStop Color='DarkOrange' Offset='0' />
<GradientStop Color='LightGoldenrodYellow' Offset='0.359' />
<GradientStop Color='LightGoldenrodYellow' Offset='0.641' />
<GradientStop Color='DarkOrange' Offset='1' />
</LinearGradientBrush>
</Menu.Background>
<MenuItem x:Name='x_men_fichier' Header='Fichier'>
<MenuItem x:Name='x_men_fichier_it_quitter' Header='Quitter'
Click='x_men_fichier_it_quitter_Click'></MenuItem>
</MenuItem>
</Menu>

Les onglets des chapitres sont matérialisés par des StackPanel à l’intérieur
desquels se trouve un Canvas contenant un TextBlock, muni d’un gestionnaire
d’événements.

<!-- index des chapitres -->


<StackPanel x:Name='x_stackpanel_chapitre' Grid.Row='1' Width='200'
HorizontalAlignment='Left'>
<StackPanel.Background>
<ImageBrush
ImageSource='/ProgGraphiqueWpf;
component/contenu/image/fondBleu.jpg'
Stretch='Fill' TileMode='Tile' Viewport='0,0,266,122'
ViewportUnits='Absolute' />
</StackPanel.Background>
<!-- stack vide -->
<StackPanel x:Name='x_stack_chap_vide_1' Width='200' Height='100'
Background='Transparent'></StackPanel>
<!-- sommaire -->
<Canvas x:Name='x_cnv_chap_00' Width='200' Height='35'
Background='Crimson' Tag='00'
MouseLeftButtonDown='x_stack_chap_MouseLeftButtonDown'>
<TextBlock Name='x_text_chap_00' Text='Sommaire'
Foreground='White' FontFamily='Verdana' FontSize='16'
VerticalAlignment='Center' Canvas.Top='5'
Canvas.Left='4' Height='25' Width='190' Cursor='Hand' />
</Canvas>
<!-- stack vide -->
<StackPanel x:Name='x_stack_chap_vide_2' Width='200' Height='5'
Background='Transparent'></StackPanel>
<!-- chapitre 1 -->
280 Annexes
<Canvas x:Name='x_cnv_chap_01' Width='200' Height='35'
Background='DarkBlue' Tag='01'
MouseLeftButtonDown='x_stack_chap_MouseLeftButtonDown'>
<TextBlock Name='x_text_chap_01' Text='Chapitre 1'
Foreground='White' FontFamily='Verdana' FontSize='16'
VerticalAlignment='Center' Canvas.Top='5'
Canvas.Left='4' Height='25' Width='190' Cursor='Hand' />
</Canvas>
...

Un ScrollViewer reçoit les UserControl PageIndex.xaml de chacun des chapitres.


Chaque dossier de chapitre contient une page nommée PageIndex.xaml.

<!-- scroll contenant la page a afficher -->


<ScrollViewer Grid.Column='1' Grid.Row='1' Name='x_scroll_contenu'
Background='Crimson' CanContentScroll='True'>
<visuel:PageIndex></visuel:PageIndex>
</ScrollViewer>

Le gestionnaire de chargement des pages est assuré par la méthode x_stack_


chap_MouseLeftButtonDown dont le détail est le suivant:

//clic sur les index des chapitres


private void x_stack_chap_MouseLeftButtonDown(object sender, Mouse-
ButtonEventArgs e) {
Canvas cnv = (Canvas)sender;
string recup_tag = cnv.Tag.ToString();
switch (recup_tag) {
case «00»:
chapitre00.PageIndex page_index_00 = new chapitre00.PageIndex();
x_scroll_contenu.Content = page_index_00;
break;
case «01»:
chapitre01.PageIndex page_index_01 = new chapitre01.PageIndex();
page_index_01.FenetreParent = this;
x_scroll_contenu.Content = page_index_01;
break;
case «02»:
chapitre02.PageIndex page_index_02 = new chapitre02.PageIndex();
page_index_02.FenetreParent = this;
x_scroll_contenu.Content = page_index_02;
break;
Annexes 281
case «03»:
chapitre03.PageIndex page_index_03 = new chapitre03.PageIndex();
page_index_03.FenetreParent = this;
x_scroll_contenu.Content = page_index_03;
break;
case «04»:
chapitre04.PageIndex page_index_04 = new chapitre04.PageIndex();
page_index_04.FenetreParent = this;
x_scroll_contenu.Content = page_index_04;
break;
case «05»:
chapitre05.PageIndex page_index_05 = new chapitre05.PageIndex();
page_index_05.FenetreParent = this;
x_scroll_contenu.Content = page_index_05;
break;
case «06»:
chapitre06.PageIndex page_index_06 = new chapitre06.PageIndex();
page_index_06.FenetreParent = this;
x_scroll_contenu.Content = page_index_06;
break;
case «07»:
chapitre07.PageIndex page_index_07 = new chapitre07.PageIndex();
page_index_07.FenetreParent = this;
x_scroll_contenu.Content = page_index_07;
break;
}
x_scroll_contenu.ScrollToTop();
cnv.Background = new SolidColorBrush(Colors.Crimson);
for (int xx = 0; xx < x_stackpanel_chapitre.Children.Count; xx++) {
FrameworkElement frw =
(FrameworkElement)x_stackpanel_chapitre.Children[xx];
if (frw.Name.Contains(«x_cnv_chap_») ==
true && frw.Name.Contains(recup_tag) == false) {
Canvas recup_cnv = (Canvas)frw;
recup_cnv.Background = new SolidColorBrush(Colors.DarkBlue);
}
}
}
INDEX
Index

A
285
Bounces 224
Bounciness 224
Box 168
Brush 27, 145
Button.IsMouseOver 268
A 135 Button.IsPressed 268
accélération matérielle 165 By 174
AccelerationRatio 180
Add 50

C
AffectsMeasure 123
agrandissement 41
Amplitude 226
Angle 76
AngleBetween 50
AngleProperty 193
Canvas.SetLeft(..) 31
AngleX 79
Canvas.SetTop(..) 31
AngleY 79
Canvas.TopProperty 176
animation 176
CaptureMouse 108
animation des transformations 189
CenterProperty 199
animations basiques 173
CenterX 73, 76, 79
animations d’images clés 205
CenterY 73, 76, 79
animations réalistes 219
Children 81
animation sur tracé 197
chrominance 142
AnimationTimeline 127, 178
CircleEase 227
ArcSegment 95, 97
clarté 141
AutoReverse 180
ClipToBounds 22
Color 135, 146

B
ColorAnimation 173
ColorAnimationUsingKeyFrames 206
ColorConverter 135
Colors 135
CombinedGeometry 86
CommonStates 270
B 135 CompositionTarget 241
BackEase 225 CompositionTarget.Rendering 241
BasedOn 250 Condition 252
BeginAnimation 127 ContentPresenter 266
BeginStoryboard 177 Control.Foreground 250
BeginTime 180 ControlPoint 101
BezierSegment 95 ControlTemplate 263
Binding 21 coordonnées homogènes 44
BitmapSource 24 coordonnées par défaut 13
BlurEffect 166 Count 180
BlurRadius 167 CrossProduct 50
BounceEase 224 CubicEase 228
D
286 Index
EasingVectorKeyFrame 205
Effect 165
effet miroir 41
Elapsed 244
ElapsedMilliseconds 244
Data 90 ElapsedTicks 244
databinding 178 ElasticEase 230
DecelerationRatio 180 ElementName 21
déclencheur 251 Ellipse 27
DefiningGeometry 128 EllipseGeometry 85
définition 14 EndPoint 87, 149
dégradés de couleurs 135 EnterActions 177
densité 14 Equals 50
DependencyProperty 123 espace projectif 44
Determinant 53 états visuels 270
DIP 14 EventHandler 241
Direction 167 EventSetter 247
Disabled 270 EventTrigger 177
DiscreteBooleanKeyFrame 205 Exclude 91
DiscreteColorKeyFrame 209 ExitActions 177
Divide 50 ExponentialEase 231
DoesRotateWithTangent 197 extension markup 266
DoubleAnimation 173
DoubleAnimationUsingKeyFrames 174

F
DoubleAnimationUsingPath 174, 197
DragFinishing 111
DragMoving 111
DragStarting 110
Drawing 146
DrawingBrush 146, 162
facteur d’échelle 40
Drawing.Children 162
Figures 93
DropShadowEffect 166
Fill 36
Duration 173
FillBehavior 181
FilleRule.EvenOdd 36

E
FillRule 36
FillRule.NonZero 36
Copyright 2011 Patrice REY

Focused 270
FocusStates 270
Forever 180
FrameworkContentElement 177
Easing 217 FrameworkElement 27, 177
EasingDoubleKeyFrame 217 Freezable 177, 239
EasingFunction 217 From 173
EasingFunctionBase 219 FromArgb 136
EasingMode 224 FromRgb 136
Index 287
FromScRgb 136 ImageBrush 146, 154
ImageSource 154

G
interpolation discrète 209
interpolation Easing 217
interpolation personnalisée 239
interpolation Spline 212
Intersect 91
InvalidFocused 270
G 135 InvalidUnfocused 270
Gaussian 168 Invert 54
géométrie projective 44 IsChecked 102
Geometry 85 IsClosed 95
Geometry1 90 IsFilled 95
Geometry2 90 IsIdentity 53
GeometryCombineMode 90 IsLargeArcFlag 101
GeometryDrawing 162 IsMouseCaptured 105
GeometryEvenOdd 90 IsMouseOver 251
GeometryGroup 85 IsRunning 244
GlobalIndex 123 IsSealed 250
GradientStop 149

K
Gray8 25

H
HasInverse 53
KernelType 168
KeyFrame 174, 205
KeyFrame.FromPercent 205
KeyFrame.FromTimeSpan 205
Height 14
Histogramme 22 KeySpline 212
HitTest 118 KeyTime.Paced 205
HitTestResultCallback 121 KeyTime.Uniform 205
HoldEnd 181

I L
L’abscisse 14
IAnimatable 173 La modélisation BackEase 225
Identity 53 La modélisation BounceEase 224
IEasingFunction 217 La modélisation CircleEase 227
288 Index
La modélisation CubicEase 228 MouseOver 270
La modélisation ElasticEase 230 Multiply 50, 54
La modélisation ExponentialEase 231 MultiTrigger 252
La modélisation PowerEase 233

N
La modélisation QuadraticEase 234
La modélisation QuarticEase 235
La modélisation QuinticEase 237
La modélisation SineEase 238
Length 50
LengthSquared 50
Line 14, 27 Normal 270
LinearColorKeyFrame 206 Normalize 50
LinearDoubleKeyFrame 205
LinearGradientBrush 146

O
LineGeometry 85
LineSegment 95
ListBox 136
Loaded 161
L’ordonnée 14
luminance 142 Offset 150
luminosité 141 OffsetX 52
OffsetY 52

M
Opacity 147
Oscillations 230

M11 52
M12 52
M21 52
M22 52
markup 266
P
ParallelTimeline 179
Path 21, 27, 85
matrice 41 PathFigure 85, 94
matrices 39 PathGeometry 85
Matrix 51 PathSegment 93
Copyright 2011 Patrice REY

MatrixAnimationUsingPath 197 PauseStoryboard 177


Matrix.Identity 51 PenLineCap 28
MatrixTransform 67 PenLineCap.Flat 28
MatrixTransform.Matrix 68 PenLineCap.Square 28
Maxwell 141 PenLineCap.Triangle 30
MediaTimeline 179 PenLineJoin 33
MenuItem 114 PenLineJoin.Bevel 34
mise à l’échelle 40 PenLineJoin.Round 34
motifs 135
Index 289
PixelFormats 25 Rect 14
Point 14 Rectangle 27
PointAnimation 99 RectangleGeometry 85
PointAnimationUsingPath 197 réduction 41
PointCollection 33 réflexion 41
PointHitTestParameters 121 Register 123
points 39 RelativeTransform 147
PolyBezierSegment 95 RenderingBias 167
Polygon 27 RenderTransform 68
Polyline 27 rendu image par image 241
PolyLineSegment 95 RepeatBehavior 180
PolyQuadraticBezierSegment 95 répétition de motif 135
Power 233 Ressources 264
PowerEase 233 ResumeStoryboard 177
Prepend 55 RootVisual 241
Pressed 270 Rotate 54
Property 247 RotateAt 54
PropertyInfo 136 RotatePrepend 55
PropertyPath 177 RotateTransform 67
propriété cible 173 rotation 42
pureté 141 RotationAngle 101

Q
QuadraticBezierSegment 95
S
ScA 135
QuadraticEase 234 Scale 54
QuarticEase 235 ScaleAt 54
QuinticEase 237 ScalePrepend 57
ScaleTransform 16, 67, 71

R
ScaleX 21, 73
ScaleY 21, 73
ScB 135
ScG 135
ScR 135
ScRGB 135
R 135 segment 39
RadialGradientBrush 146, 152 Segments 94
RadioButton 102 Setter 247
RadiusX 152 Setters 251
RadiusY 152 ShaderEffect 166
ShadowDepth 167
290 Index
Shape 27 System.Windows.Media.Effects 165
SineEase 238 System.Windows.Shapes 27
Skew 54

T
SkewTransform 67, 78
Slider 19
Slider.Value 74
SolidColorBrush 135, 146
Spline 212
SplineColorKeyFrame 205
SplineDoubleKeyFrame 213 TargetName 202, 247
Springiness 230 TargetType 264
sRGB 135 teinte 141
StartPoint 87, 149 template 263
StaticResource 90 TemplateBinding 266
Stop 181 TextBlock 19
StopStoryboard 177 TileMode 162
Stopwatch 244 Timeline 178
Storyboard 83, 177 TimelineGroup 179
Storyboard.TargetProperty 178 TimeSpan 180
StreamGeometry 85, 100 To 174
Stretch.Fill 31 ToolTip 165
Stretch.Uniform 31 Transform 54, 147
Stretch.UniformToFill 31 transformation géométrique 40
String.Format 138 transformations 39
StrokeDashArray 30 transformations successives 47
StrokeEndLineCap 28 TransformCollection 81
StrokeLineJoin 33 TransformFromDevice 241
StrokeStartLineCap 28 TransformGroup 16, 67, 81
StrokeThickness 14 TransformToDevice 241
structure Matrix 51 Transitions 271
style 247 Translate 54
Style 250 TranslatePrepend 59
style implicite 249 TranslateTransform 16, 67
SweepDirectionFlag 102 TranslateTransform.Matrix 74
SystemColors 135 translation 43
System.Diagnostics 244 triangle de Maxwell 141
système 13
Copyright 2011 Patrice REY

Trigger 177
système de coordonnées 13
système de coordonnées 2D 13

U
System.Reflection 136
System.Windows 49, 178
System.Windows.Data 21
System.Windows.Media 51, 145
System.Windows.Media.Animation 178
System.Windows.Media.ColorConverter 135
X
UIElement 27
Unfocused 270
Union 90
UserControl.Resources 88

x:Key 250

V
Xor 90
x:Type 250

Valid 270
ValidationStates 270
Value 81, 247
ValueChanged 143
vecteur 39
Z
Zoomer 19
vecteurs 39
Vector 39, 49
Viewbox 169, 269
Visual 118, 146, 157
VisualBrush 146, 157
VisualBrush.Visual 157
VisualState 177, 270
VisualStateGroup 270
VisualStateManager 270
VisualTransition 271
VisualTree 264
VisualTreeHelper 118

W
Width 14
WindowBrush 135
WindowBrushKey 135
WindowColor 135
WindowColorKey 135
Guide
de
programmation
Initiation au jeu 2D avec Silverlight 4
(ISBN 978-2-8106-1168-3)

chapitre 1 : la vectorisation
chapitre 2 : création du projet
chapitre 3 : boucle de jeu et rendu
chapitre 4 : le déplacement des images
chapitre 5 : la détection des collisions
chapitre 6 : le score et la fin de partie
chapitre 7 : un écran d’accueil
chapitre 8 : la pause et l’arrêt de jeu
chapitre 9 : sonoriser le jeu
chapitre 10 : téléchargement du jeu

En vente sur la boutique en ligne de www.decitre.fr


à l’adresse suivante:

http://www.decitre.fr/livres/Initiation-au-jeu-avec-silverlight-4.aspx/9782810611683
Les structures de données
illustrées avec WPF et C#4
(ISBN 978-2-8106-1387-8)

chapitre 1 : la récursivité
chapitre 2 : les piles
chapitre 3 : les files
chapitre 4 : les listes chaînées
chapitre 5 : les arbres
chapitre 6 : les méthodes de tri
chapitre 7 : les recherches
chapitre 8 : les tables

En vente sur la boutique en ligne de www.decitre.fr


à l’adresse suivante:

http://www.decitre.fr/livres/Les-structures-de-donnees.aspx/9782810613878
La programmation graphique 2D de WPF 4

Windows Presentation Foundation (WPF) est une technologie de


développement d’application riche (Rich Desktop Application)
pour la plateforme Windows. Sa version actuelle est la version 4,
celle qui correspond au Framework .NET 4.0.
WPF est devenue une technologie majeure du développement
pour Windows en code managé. Ses innovations sont très
importantes, à la fois sur le plan architectural et sur le plan
fonctionnel. Elle constitue une synthèse et une unification des
différentes branches de l’informatique graphique (graphismes
Patrice REY est vectoriels 2D et 3D, saisie, animation, typographie et
informaticien et formateur multimédia).
indépendant, diplômé en
WPF est un framework managé qui exploite la puissance de
informatique et certifié
Microsoft MCTS (Client Direct3D pour l’affichage. Cette technologie de développement
Application Development). d’interface utilisateur permet de moderniser et d’unifier les
Passionné par les techniques de développement visuel, d’exploiter la puissance
technologies Microsoft
graphique des machines modernes, d’intégrer l’infographie dans
Silverlight et WPF, adepte
du langage C#, au travers le processus de développement, et de simplifier et sécuriser le
de cet ouvrage, il vous déploiement des applications.
fait profiter de son WPF sépare dans des couches distinctes la programmation des
expertise et vous fait
fonctionnalités (au moyen d’un langage .NET tel que le C#) de la
partager sa passion pour
le développement présentation visuelle par l’utilisation du langage XAML
d’applications. (eXtensible Application Markup Language).
Les différents chapitres permettent d’apprendre et de mettre en
pratique les notions essentielles de la programmation graphique
2D de WPF 4 avec l’utilisation de l’environnement intégré de
Visual Studio 2010 (dans sa version Express ou Ultimate).
Vous apprendrez notamment à programmer les figures
géométriques 2D dans les différents systèmes de coordonnées,
les transformations 2D avec les vecteurs et les matrices, les
géométries complexes et les tracés 2D, l’utilisation des couleurs
et des pinceaux pour l’expression graphique, les animations du
niveau basique au niveau avancé, et l’utilisation des styles et
des modèles de contrôles (template).
Tout le code source du livre est en téléchargement gratuit sur le
Patrice
Pa
Pattri
trice
r ic
ceeR
REY
REEY site http://www.reypatrice.fr

Edition
Edit
di ion numé
numériqu
numérique
n érique
www.reypatrice.fr

ISBN : 978-2-8106-1199-7
Éditeur :
Books on Demand GmbH, 41 €
12/14 rond point des
Champs Élysées,
75008 Paris,
France

Vous aimerez peut-être aussi