précédentsommairesuivant

5. L'arbre visuel et logique

Jusqu'à maintenant, nous avons construit des interfaces sans réellement nous préoccuper de l'architecture ou de l'imbrication des composants. Dans ce chapitre, nous listerons les familles de contrôles graphiques, de la simple forme primitive jusqu'au composant de données élaboré, en affinant notre analyse au fur et à mesure de notre avancée. Nous aurons ainsi une idée plus précise de leur rôle et de leurs capacités. Puis, nous utiliserons le projet précédent, SiteAgencePortfolio, pour apprendre à y manipuler, ajouter et modifier les enfants d'un composant conteneur. Nous aborderons les méthodes et les moyens techniques offerts par Silverlight pour concevoir l'imbrication, l'architecture et le design visuel de manière complémentaire. À cette fin, nous étudierons chaque cas aussi bien du point de vue d'un designer interactif que de celui d'un développeur C#.

5-1. Composants visuels

Au sein de Silverlight, tous les objets visuels sont des composants et font donc référence à une classe. La moindre primitive, qui semble pourtant anodine au premier abord, est un composant aux comportements étendus. Connaître les familles de composants est une chose naturelle pour un développeur, car cette notion est directement liée à l'architecture de l'application qu'il développe. Pour un designer, cette démarche est moins intuitive, car il est moins soumis à la technique qu'au résultat visuel final. Toutefois, un designer interactif doit se faire force de proposition sur l'expérience utilisateur et sur un ensemble de notions directement liées à l'architecture de l'application. Il est également très souvent conduit à modifier des composants existants pour arriver à ses fins. Il lui est donc nécessaire de connaître au moins partiellement les familles et types de contrôles visuels dont il peut profiter. Tous les composants que nous allons maintenant étudier peuvent et feront sans doute partie de l'arbre visuel d'une de vos applications Silverlight.

5-1-1. Familles de composants

De manière générale, on classe les composants par genre. Voici une classification possible des familles du point de vue d'un graphiste ou d'un ergonome. Toutefois, nous verrons que ces catégories ne sont pas si évidentes.

  • Les primitives vectorielles. Les formes de type Rectangle, Path, Line ou Ellipse sont considérées comme des composants, car elles font bien plus qu'afficher une forme ou un tracé. Elles sont interactives et possèdent des propriétés complexes que nous allons étudier.
  • Les conteneurs. Il s'agit de composants à enfants uniques ou multiples. Ils sont à la base de tout visuel. Par exemple, le composant racine UserControl est considéré comme le conteneur principal de l'application. Nous verrons qu'il n'est pas forcément facile de les répertorier, car de nombreux composants inattendus sont des conteneurs à enfants uniques.
  • Les contrôles de gestion et d'affichage de texte. Ils vous permettront d'afficher du texte de différentes manières.
  • Les composants de liste de données. Cette catégorie regroupe les listes (ListBox), les listes déroulantes (ComboBox) et les grilles de données (DataGrid). Le composant AutoCompleteBox en fait également partie, il est à mi-chemin entre le champ de saisie et la liste déroulante.
  • Les gestionnaires de médias. Silverlight est une plate-forme plurimédia avant tout : elle peut diffuser du son, des images et de la vidéo dans divers formats, grâce à trois composants essentiels : Image et MediaElement pour le son et la vidéo et MultiScale-Image basé sur la technologie DeepZoom.
  • Les composants d'interfaces utilisateur. C'est la catégorie fourre-tout dans laquelle vous trouverez les boutons (Button), les barres de défilement (ScrollBar), les cases à cocher (CheckBox) ou encore les barres de progression (ProgressBar)… Pour résumer, c'est tout ce qui n'est pas défini dans les catégories citées précédemment.

Répertorier n'est toutefois pas si simple. Pour le démontrer, nous allons essayer de catégoriser certains des composants que nous avons utilisés dans le projet SiteAgencePortfolio. Prenons, par exemple, le cas de la grille (Grid). Elle peut-être classée dans le genre conteneur. Cela est d'autant plus visible qu'elle peut contenir plus d'un objet enfant. Le WrapPanel ou le StackPanel sont eux aussi dans cette catégorie. La seule différence entre ces contrôles concerne les contraintes d'agencement subies par leurs objets enfants. Le bouton semble être facile à classer lui aussi, car c'est avant tout un objet assurant la gestion de comportement utilisateur. Il se retrouve donc dans la dernière catégorie des composants d'interface utilisateur. Toutefois, les boutons, comme de nombreux autres composants, possèdent la faculté de contenir un unique objet enfant. Deux des boutons que nous avons instanciés sur la scène ont été utilisés de cette manière et possèdent un enfant. C'est grâce à la propriété Content que nous avons pu imbriquer un enfant en leur sein. Cela fait-il d'eux des conteneurs ? Dans une certaine mesure, oui. Grâce à diverses techniques de programmation orientée objet, dont la composition et l'héritage, ces boutons possèdent naturellement cette capacité. Toutefois, leur rôle premier n'est pas d'agencer des objets. Ils n'ont pas la vocation des conteneurs spécialisés. De la même manière, le pied de page de notre site est constitué de menus représentés par des TextBlock. Comme tous les objets visuels, ces TextBlock sont cliquables et rien ne nous empêche de les utiliser comme des boutons sans interactions visuelles. De plus, chaque TextBlock contient du texte, soit via sa propriété Text, soit en imbriquant des balises de type Run. L'utilité principale du TextBlock est d'afficher du texte, nous le classons donc dans la catégorie des gestionnaires de texte. La nature même du XAML génère ce type d'ambiguïté de par les relations familiales propres aux arborescences XML, mais facilite la construction d'interfaces riches et non rigides. Dans la section suivante de ce chapitre, nous allons essayer de comprendre pourquoi cette classification est simpliste en étudiant l'arbre d'héritage des objets d'affichage.

5-1-2. Arbre d'héritage des classes graphiques

5-1-2-1. De Object à FrameWorkElement

La notion d'héritage remonte aux débuts de la programmation orientée objet. Lorsqu'une classe hérite d'une autre classe, elle récupère les méthodes, les champs et les propriétés de celle-ci. Ainsi, le code créé pour la classe mère, et échu aux classes filles, n'est écrit qu'une seule fois par le développeur. L'héritage fait partie des nombreuses techniques de réutilisation de code existantes en POO. Comme de nombreuses autres plates-formes, Silverlight est basé sur ce concept et les ingénieurs l'ayant conçu ont privilégié cette méthodologie. En tant que designer interactif ou développeur, la connaissance de la bibliothèque vous permettra de concevoir vos propres composants de manière optimisée et perspicace en héritant de la classe adéquate, qui n'aura ni trop ni pas assez de fonctionnalités. La classe mère de toutes les autres est Object. Elle possède plusieurs méthodes dont toutes les autres classes héritent et qu'elles utilisent. En voici une liste non exhaustive :

  • Equals : cette méthode permet de comparer les valeurs contenues par deux références d'objet ;
  • GetType : renvoie le type de l'instance qui l'invoque, ce qui est très pratique pour étudier et énumérer à l'avance un type inconnu ;
  • MemberwiseClone : renvoie une nouvelle référence clonée à partir de l'instance qui l'invoque ;
  • ReferenceEquals : permet de comparer deux références pour savoir si elles possèdent la même allocation mémoire ;
  • ToString : lors de son appel, elle renvoie la valeur de l'instance sous forme de chaîne de caractères ; il peut être pratique d'outrepasser la méthode fournie par défaut pour générer une représentation personnalisée si celle implémentée ou héritée par défaut ne correspond pas à votre besoin.

Nous allons maintenant détailler les classes graphiques qui héritent de Object. Dans le schéma de la Figure 5.1, en dessous de Object, vous trouvez DependencyObject. Cette classe est abstraite, cela signifie qu'elle ne peut pas être directement instanciée. Elle fournit les mécanismes de base propres aux objets graphiques de la plate-forme Silverlight, dont le principe de DependencyProperty, aux classes en héritant. L'animation, l'imbrication et la notion de liaison (Binding) prennent appui sur ce principe. C'est là le cœur de la puissance et de la souplesse de Silverlight. Héritant de DependencyObject, la classe abstraite UIElement définit les mécanismes d'affichage propres aux objets graphiques. Elle possède les méthodes et propriétés permettant de calculer la taille et le positionnement de son instance dans l'espace, elle pose donc les bases du système d'agencement graphique. C'est dans cette classe que les propriétés d'opacité, de visibilité et de masque de découpe sont, par exemple, pourvues. Elle définit également les événements propres aux interactions utilisateurs : MouseEnter, Mouse-Leave, MouseLeftButtonUp, KeyUp, etc. La classe abstraite FrameworkElement hérite à son tour de UIElement et y ajoute le système de disposition visuel de Silverlight : le SLS pour Silverlight Layout System. Le SLS - ou système d'agencement de Silverlight - repose sur les méthodes Arrange-Override, MeasureOverride, sur les propriétés Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, ActualWidth, ActualHeight et enfin, sur l'événement Size-Changed. La classe FrameworkElement définit également Loaded, diffusé lorsqu'une nouvelle instance est initialisée au sein de l'arbre visuel et logique de l'application. Pour finir, bien que les bases de la liaison de données soient définies en majeure partie dans la classe Dependency-Object, c'est la classe FrameworkElement qui résout l'accès aux membres de classe en fournissant la propriété DataContext nécessaire à la liaison de données. Finalement, FrameworkElement donne naissance à la majeure partie des classes concrètes, donc des composants visuels instanciables dans Silverlight.

Image non disponible
Figure 5.1 - Arbre d'héritage des classes d'objets graphiques

5-1-2-2. Formes primitives

Commençons par les primitives. Elles héritent toutes de la classe abstraite Shape, qui fournit les propriétés nécessaires pour affecter le remplissage du fond et le contour d'une forme primitive grâce aux propriétés Stroke et Fill. Shape permet également de personnaliser la forme du contour, ses extrémités et ses sommets. Toutes les classes héritant de Shape sont verrouillées, il est donc impossible de les étendre via l'héritage. La classe Path est particulière, car le tracé vectoriel est défini par la propriété Data. Toutefois, les propriétés Width et Height sont tout de même utilisées. Il faudra modifier la propriété Stretch de l'objet Path pour déterminer de quelle manière le tracé remplit les dimensions imposées en largeur et en hauteur. Par défaut, la propriété Strech a pour valeur Fill. Cela signifie que le tracé s'étirera pour remplir complètement les dimensions du composant Path en largeur et en hauteur.

5-1-2-3. Conteneurs primitifs

Cette catégorie concerne une dizaine de classes qui, pour la majorité, sont fermées à l'extension et héritent en droite ligne de FrameworkElement. Elles ont pour but d'afficher ou de mettre en valeur un contenu simple et spécifique. Ci-dessous, les composants que vous pouvez y trouver.

  • TextBlock : affiche du texte.
  • Glyph : composant de gestion de texte bas niveau. Il permet d'afficher et de mettre en forme n'importe quel caractère via ses propriétés UnicodeString et Indices. Il offre plus d'options et plus de capacités que TextBlock, mais il est plus complexe à mettre en œuvre.
  • Image : affiche et charge des images aux formats JPEG et PNG. Le format PNG 32 bits est supporté, ce qui vous permettra d'afficher des icônes transparentes, par exemple.
  • MediaElement : contrôle qui permet de lire de la vidéo et du son aux formats WMV, WMA, MP3, ainsi que les fichiers encodés en H.264, ce qui étend ses capacités au format MP4 depuis la version 3 de Silverlight. Le logiciel Expression Encoder possède la capacité d'encoder la vidéo dans tous ces formats.
  • MultiScaleImage: communément appelé composant DeepZoom, il prend en paramètre un ensemble d'images qu'il chargera par la suite dynamiquement afin de proposer une expérience utilisateur spécifique. Pour vous familiariser avec ce comportement utilisateur, vous pouvez vous rendre sur le sitehttp://www.laguna-coupe.com/.
  • Popup et Border : conteneurs à enfant unique. Ils possèdent tous les deux la propriété Child qui leur permet de contenir n'importe quel UIElement. Border peut être considéré comme un rectangle amélioré, car il possède des propriétés de remplissage et d'affichage avantageuses. Popup permet, quant à lui, d'afficher une information au-dessus des autres composants constituant l'arbre visuel et logique. Il ne s'agit pourtant pas d'une fenêtre modale.
  • ItemsPresenter : ce composant est assez particulier. Il est intégré au sein des composants de type ItemsControl et des classes en héritant. Il permet d'établir la position et l'espace alloués aux objets (ListBoxItem) d'une ListBox par exemple. Il est donc étroitement lié à la propriété ItemsSource d'une ListBox.
  • ContentPresenter : gère le comportement de glisser-déposer et l'imbrication d'enfants uniques propres aux composants de type ContentControl. Il fait donc partie intégrante de leur structure.

5-1-2-4. Composants personnalisables

Parmi les contrôles que nous avons évoqués, seuls quelques-uns implémentent la capacité d'être personnalisable tant au niveau de leur style que dans leur forme visuelle. Si l'on prend le cas du composant Image, celui-ci ne peut être directement paramétré pour posséder une bordure ou un fond. Pour cela, vous devrez utiliser la technique de composition propre à la programmation orientée objet. Vous devrez donc créer un contrôle, qui s'appellera, par exemple, Visionneuse et qui en interne possédera un Border ainsi qu'un composant Image. Si vous souhaitez que votre contrôle Visionneuse soit paramétrable par un designer interactif, vous devrez le faire hériter de la classe Control. Celle-ci hérite de Framework-Element et apporte, en plus, tous les mécanismes inhérents à la personnalisation du visuel d'un composant. Tous les contrôles que nous allons à présent évoquer héritent de cette classe et sont donc personnalisables par un graphiste, un designer ou un intégrateur. Nous ne traiterons pas de tous les composants, car ils sont nombreux, donc en voici une liste non exhaustive.

  • AutoCompleteBox est un composant à mi-chemin entre un TextBox et une liste déroulante (ComboBox). De ce fait, il hérite directement de Control et non de la classe ItemsControls ou Selector. Il s'agit en fait d'un champ de saisie qui propose une liste de choix au fur et à mesure de la saisie utilisateur.
  • RangeBase est une classe abstraite apportant les mécanismes, méthodes et propriétés de base propres aux objets de défilement ou de progression tels que Slider ou Progress-Bar. Si vous souhaitez faire un Slider à double curseur, vous devrez étendre cette classe.
  • ItemsControl constitue le fondement des composants exposant une liste d'objets. Ceux-ci sont, la plupart du temps, reliés à un contexte de données ou directement fournis par le développeur à partir d'un fichier XML, d'une base de données ou d'un service Web.
  • TextBox et PasswordBox sont deux composants permettant la saisie de données utilisateur. Alors que TextBlock n'est représenté visuellement que par le texte qu'il affiche, ceux-ci possèdent en leur sein une bordure extérieure ainsi qu'un fond et de nombreuses propriétés leur permettant d'être personnalisés.
  • ContentControl est une classe abstraite apportant avec elle la capacité de contenir n'importe quel objet enfant. Cette capacité fait que, par défaut, tous les objets en héritant possèdent en interne un composant de type ContentPresenter et une propriété Content. Cette dernière est typée Object. Lorsque vous glissez un tracé vectoriel dans un bouton, vous affectez sa propriété Content, elle-même liée au ContentPresenter situé au sein du bouton. Cette capacité en fait une classe très puissante et très souple d'utilisation. De très nombreux composants héritent de cette classe du fait de cette capacité. Il arrivera sans doute que vous ayez besoin d'étendre cette classe pour vos propres besoins.
  • ButtonBase hérite de ContentControl et apporte une gestion simplifiée et directe du clic gauche de la souris. Les classes en découlant, comme Button, RadioButton ou CheckBox, constituent la majeure partie des contrôles utilisateur au sein d'une interface. Il est à noter qu'il n'y a pas de différence fonctionnelle entre un ToggleButton et une CheckBox. La seule différence entre ces deux composants est purement visuelle.
  • ToolTip est un peu spécifique. Cette classe permet d'afficher une bulle d'information lorsque la souris survole un objet graphique. Toutefois, il est impossible d'instancier cette classe directement, vous devrez passer par la classe statique ToolTipService et ses méthodes SetToolTip et GetToolTip. ToolTip hérite de ContentControl, elle peut donc afficher n'importe quel enfant de type UIElement ou héritant de UIElement. Pour ajouter une bulle d'information à un objet graphique, il vous suffira d'affecter la propriété ToolTip (du même nom que la classe, attention donc aux confusions) de cet objet. Cette propriété est attribuée par défaut aux objets de l'arbre visuel par la classe ToolTipService. ToolTip est une propriété dite attachée, elle n'est pas héritée mais attribuée dynamiquement. Nous reviendrons sur cette notion tout au long de ce chapitre.

5-1-2-5. Conteneurs de mise en forme

Comme nous l'avons vu lors des chapitres précédents, les conteneurs à enfants multiples facilitent la mise en page et la création d'applications redimensionnables. Ce chapitre leur est dédié en grande partie, car ils constituent le fondement de toute application Silverlight. Ces conteneurs particuliers héritent de la classe abstraite Panel. De nombreuses notions et propriétés propres à ces composants sont basées sur les mêmes concepts existant au sein du langage HTML. Lorsque vous imbriquez un contrôle au sein d'un conteneur, l'objet imbriqué reçoit automatiquement des propriétés dites attachées. Par exemple, lorsque nous avons utilisé la grille d'alignement au Chapitre 3 Hello World, les objets imbriqués en son sein ont automatiquement reçu des propriétés attribuées par la grille (voir Figure 5.2).

Image non disponible
Figure 5.2 - Propriétés héritées dynamiquement d'un conteneur Grid

Voici la liste des conteneurs de type Panel les plus courants.

  • Canvas : c'est le conteneur le moins contraignant et le plus simple. Il faut considérer le conteneur Canvas comme un point dans l'espace à partir duquel les objets qui y sont contenus sont positionné. Les enfants de ce conteneur peuvent donc avoir des coordonnées positives ou négatives. Il peuvent donc être en dehors du Canvas, mais sont affichés dans tous les cas. Le redimensionnement d'un Canvas via ses propriétés Width et Height n'affecte en rien la position ou l'affichage de ses enfants (voir Figure 5.3).
    Les objets contenus au sein d'un Canvas récupèrent automatiquement les propriétés Top et Left attribuées par celui-ci. Lorsqu'une propriété est de type attachée, cela est visible dans le XAML. Les propriétés Top et Left sont préfixées de Canvas, comme le montre le code XAML ci-dessous :

     
    Sélectionnez
    <Canvas Margin="75,110,161,80">
          <Button Height="27" Width="72" Content="Button" Canvas.Left="45" 
                                Canvas.Top="29"/>
    </Canvas>
    Image non disponible
    Figure 5.3 - Composants imbriqués au sein d'un Canvas
  • Image non disponible
    DockPanel permet de docker des objets en leur attribuant un espace dans une des quatre directions. Les composants qu'il contient pourront être dockés à gauche, en haut, à droite ou en bas du DockPanel. L'ordre dans lequel ceux-ci sont imbriqués, ainsi que les valeurs Width et Height de ces composants, déterminent l'espace qui leur est alloué et celui restant pour les autres composants du DockPanel. À cette fin DockPanel bénéficie de la propriété booléenne LastChildFill, qui permet de spécifier si le dernier élément imbriqué remplit ou non la totalité de l'espace restant. Par défaut cette propriété est à true, ce qui n'est pas le cas pour le DockPanel de la Figure 5.4 puisqu'il reste une place libre.

  • Grid est un composant d'agencement très puissant et souple d'utilisation. Il est assez complexe et permet de résoudre la majorité des problématiques d'agencement. C'est pour cette raison que ce composant est directement placé au sein du UserControl racine lors de la création d'un projet Silverlight. Ce composant propose des options de mise en page très semblables à ce que vous pouvez réaliser en HTML et CSS avec les balises de type DIV. Il s'agit en réalité d'une grille qui peut contenir un nombre indéfini de colonnes et de lignes. Comme vous avez pu le voir dans les chapitres précédents, il contraint automatiquement les objets qui sont imbriqués en son sein.

  • StackPanel permet d'empiler les objets les uns après les autres dans la direction horizontale ou verticale. Comme vous l'avez constaté, les options de marges influencent directement le positionnement des objets empilés les uns par rapport aux autres.

  • WrapPanel possède le même comportement que StackPanel, mais renvoie à la ligne automatiquement (voir Chapitre 4 Un site plein écran en deux minutes).

Ces composants répondent à la grande majorité des besoins en matière d'agencement. Toutefois, vous devrez étendre la classe Panel pour créer vos propres conteneurs personnalisés. Cette classe possède à cette fin deux méthodes virtuelles, MeasureOverride et ArrangeOverride. Vous devrez donc les surcharger lorsque vous créerez vos propres conteneurs. Tous les composants héritant de Panel partagent la propriété Children. Cette dernière est un ensemble de contrôles UIElement donnant accès aux enfants du conteneur, qui seront forcément accessibles, car ce sont des objets graphiques. Ils sont donc au moins de type UIElement.

Ce qu'on appelle l'arbre visuel et logique d'une application est en fait l'ensemble des objets affichés ainsi que leurs relations enfant / parent. Trois propriétés permettent ainsi d'imbriquer les éléments : Content pour les contrôles de type ContentControl, Children pour les conteneurs héritant de Panel et Child pour tous les autres conteneurs spécifiques, tels que ViewBox par exemple. Nous allons maintenant nous intéresser à la propriété Children et apprendre à gérer la liste d'affichage d'un conteneur de type Panel.

5-2. Principe d'imbrication

5-2-1. Ordre d'imbrication

La propriété Children de la classe Panels étant une collection, il est possible d'accéder à ses enfants par un index numérique correspondant à son ordre d'imbrication. L'ordre d'imbrication influe sur deux pôles : le design pur et la conception.

5-2-1-1. Du point de vue design

Le design est influencé, car l'ordre d'imbrication impacte directement l'ordre de superposition des objets les uns par rapport aux autres, de la même manière que les calques sous Photoshop ou Expression Design (voir Figure 5.5).

Voici un exemple d'agencement au sein d'une grille. Comme vous le remarquez, rectangle0 est en dessous des autres d'un point de vue visuel. Il est situé à l'index 0 de la liste d'affichage, toutefois au sein de Blend il est représenté en haut de la liste d'affichage dans l'arbre visuel. Blend affiche par défaut l'arbre visuel en respectant l'ordre de création XAML. Le code XAML ci-dessous correspond à l'arbre visuel affiché à la Figure 5.5 :

 
Sélectionnez
<Grid x:Name="LayoutRoot" Background="#FFFFFFFF">
      <Rectangle x:Name="rectangle0" Fill="#FF4B7145" Stroke="#FF000000" 
            Height="108" HorizontalAlignment="Left" 
            Margin="137,89,0,0" VerticalAlignment="Top" Width="118"/>
      <Rectangle x:Name="rectangle1" Fill="#FF5F9657" Stroke="#FF000000" 
            Height="89" HorizontalAlignment="Left" 
            Margin="198,146,0,0" VerticalAlignment="Top" Width="122"/>
      <Rectangle x:Name="rectangle2" Fill="#FF8CAC95" Stroke="#FF000000" 
            Margin="255,201,249,191"/>
      <Rectangle x:Name="rectangle3" Fill="#FFB0BF9D" Stroke="#FF000000" 
            Margin="306,239,218,167"/>
      <Rectangle x:Name="rectangle4" Fill="#FF8DDB86" Stroke="#FF000000" 
            Height="67" HorizontalAlignment="Right" 
            Margin="0,0,180,136" VerticalAlignment="Bottom" 
            Width="95"/>
</Grid>

L'ordre proposé par défaut est donc assez logique d'un point de vue architecture. L'arbre visuel peut au départ vous induire en erreur si vous êtes habitué à d'autres logiciels, car l'objet le plus haut dans l'arbre est celui situé le plus à l'arrière-plan par rapport aux autres. Vous pouvez à tout moment changer ce mode de présentation en cliquant sur le bouton situé en bas à gauche de l'arbre visuel (). Toutefois, vous vous habituerez très vite à cette représentation, car elle est cohérente avec l'ordre des balises XAML générées.

Image non disponible
Figure 5.- Ordre d'imbrication et index d'affichage

5-2-1-2. Du point de vue conception

D'un point de vue conception, l'ordre d'imbrication est déterminant, car il impose souvent le positionnement des objets au sein des conteneurs. L'agencement des enfants au sein d'un contrôle StackPanel ou WrapPanel, par exemple, en est une conséquence directe comme vous avez pu le constater au Chapitre 4 Un site plein écran en deux minutes (voir Figure 5.4 et 5.6).

Image non disponible
Figure 5.6 - Influance de l'index de l'objet enfant au sein d'un WrapPanel

Le bouton le plus à droite au sein du WrapPanel est ContactBtn. Il s'agit en fait du dernier bouton imbriqué et du seul qui est nommé. Il possède l'index 4. L'index influence donc également l'architecture même de l'application. Voici le code XAML représenté à la Figure 5.6 :

 
Sélectionnez
<controls:WrapPanel Height="Auto" HorizontalAlignment="Stretch" 
            VerticalAlignment="Top" Width="Auto" Margin="30,10,90,0" 
            d:LayoutOverrides="Width">
      <Button Height="Auto" Width="Auto" Content="Nouveautés" 
            Margin="0,0,20,0" FontSize="14" FontFamily="Trebuchet MS" 
            Visibility="Visible" />
      <Button Height="Auto" Width="Auto" Content="Portfolio" 
            Margin="0,0,20,0" FontSize="14" FontFamily="Trebuchet MS" 
            Visibility="Visible"/>
      <Button Height="Auto" Width="Auto" Content="Médias" 
            Margin="0,0,20,0" 
            FontSize="14" FontFamily="Trebuchet MS" 
            Visibility="Visible"/>
      <Button Height="Auto" Width="Auto" Content="Savoir faire" 
            VerticalAlignment="Stretch" Margin="0,0,20,0" 
            FontSize="14" FontFamily="Trebuchet MS" 
            Visibility="Visible"/>
      <Button x:Name="ContactBtn" Height="Auto" Width="Auto" 
            Content="Contact" Margin="0,0,0,0" FontSize="14" 
            FontFamily="Trebuchet MS" Visibility="Visible"/>
</controls:WrapPanel>

Nommer les objets est d'une aide précieuse dans de nombreux cas. Même si ce n'est pas toujours obligatoire d'un point de vue technique, cela permet aux développeurs et aux designers interactifs de communiquer simplement et d'éviter les malentendus. Ces deux profils sont concernés par le nommage. Donner un nom à un objet consiste à définir sa fonctionnalité et par conséquent une partie de l'architecture. Le développeur est en général très concerné par cette phase, il doit donc veiller à ce que les intégrateurs ne perdent pas de temps sur la compréhension de l'arbre visuel.

Parfois, le développeur voudra manipuler un objet non nommé ou réorganiser une interface en arrangeant la liste d'affichage. Le graphiste, quant à lui, souhaitera tout de même gérer la superposition des éléments les uns entre les autres sans s'occuper directement de l'architecture ou de la conception technique. Nous allons aborder les méthodes et techniques qui répondent à ces problématiques en nous efforçant de garder un flux de production souple et en préservant les rôles de chacun.

5-2-2. Accéder aux objets de l'arbre visuel

5-2-2-1. Accéder à un composant nommé

Un objet nommé est facile à cibler, car lorsqu'un objet est nommé, il devient un champ privé de la classe de l'application. Il faut en effet garder à l'esprit que tout ce que nous produisons au sein du fichier XAML est en réalité créé au sein de la classe partielle. Un développeur n'a qu'à écrire le nom d'un objet déclaré dans le XAML suivi d'un point pour accéder à tous ses membres. Nous pourrions de cette manière facilement accéder au bouton ContactBtn (voir Figure 5.7).

Image non disponible
Figure 5.7 - Accéder aux objets nommés

Toutefois, vous n'aurez pas toujours la possibilité de connaître le nom d'un objet à l'avance ou encore de nommer tous les objets dans Blend. Nous allons donc apprendre à accéder à un objet anonyme.

5-2-2-2. Accéder à un composant anonyme

Ouvrez le projet SiteAgencePortfolio créé lors du précédent chapitre. Pour atteindre un objet présent au sein de la liste d'affichage d'un conteneur, il suffit de cibler son index. Nous allons changer le texte du bouton anonyme représentant les nouveautés par le texte Accueil en procédant par étape. Étudions l'arbre visuel et logique de notre projet (voir Figure 5.8).

Le bouton des nouveautés est situé à l'index 0 du WrapPanel. Pour l'atteindre, nous devrons utiliser la propriété Children du WrapPanel. Cependant, le WrapPanel n'est lui-même pas nommé. Nous devrons donc également utiliser l'index enfant de celui-ci au sein du conteneur nommé LayoutRoot.

Image non disponible
Figure 5.8 - Arbre visuel du projet SiteAgencePortfolio

Voici comment accéder à l'index 0 via la propriété Children d'un composant de type Panel :

 
Sélectionnez
MonConteneur.Children[0]

Pour accéder à l'objet WrapPanel, qui est le premier enfant de LayoutRoot, on écrira donc :

 
Sélectionnez
LayoutRoot.Children[0];

Pour des raisons de conception, Children est une collection d'UIElement. Les enfants d'un conteneur peuvent être de type différent les uns en les autres, mais ils héritent tous de la classe UIElement, mère de toutes les classes d'affichage. Elle semble donc idéale pour représenter le type général des objets stockés dans la propriété Children. Nous n'aurons donc pas accès dans un premier temps aux membres de la classe WrapPanel mais plutôt aux membres de la classe UIElement. Nous devrons transtyper la référence récupérée en WrapPanel, puis stocker sa valeur au sein d'une référence de même type :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;

Vous pouvez à tous moments récupérer le type d'un objet via la méthode héritée de Object, GetType(). Voici comment faire :

 
Sélectionnez
LayoutRoot.Children[0].GetType();

La méthode GetType() renvoie un objet Type qui fournit toutes les informations que vous souhaitez obtenir sur la classe réelle de l'objet. Vous pouvez récupérer le nom de la classe via sa propriété Name. Il suffit ensuite de l'afficher dans le champ texte au centre du site, de cette manière :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;
if (menuHaut != null)
{
   string nomType = menuHaut.GetType().Name;
   TitrePage.Text = nomType;
}

Lorsque vous compilez, vous pouvez constater qu'il s'agit bien d'un WrapPanel. Si vous souhaitez vérifier qu'il contient les menus, il faut regarder le nombre de ses enfants grâce à la propriété Count de Children :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;
if (menuHaut != null)
{   string nomType = menuHaut.GetType().Name;
   TitrePage.Text = nomType+" - Nb enfants : "+ menuHaut.Children.Count;
}

Pour modifier le champ texte du premier bouton, nous devons le cibler via Children, accéder à sa propriété Content et affecter celle-ci :

 
Sélectionnez
Button un = menuHaut.Children[0] as Button;
un.Content = "Accueil";

5-2-2-3. Gérer les erreurs d'accès

Que se passe-t-il lorsque nous essayons d'accéder à un index indéfini de la liste d'affichage ? Pour le savoir, il vous suffit d'essayer d'accéder à l'index 5 de la liste d'enfants du WrapPanel. Celui-ci ne possédant que cinq enfants, le dernier index utilisé est 4. Pour générer cette erreur, il faut donc écrire :

 
Sélectionnez
Button Un = menuHaut.Children[5] as Button;

Compilez via le raccourci F5. Aucune exception n'est levée en apparence, mais si vous regardez en bas à gauche du navigateur Internet Explorer, vous apercevrez une icône jaune . Celle-ci vous indique une erreur au sein de l'application Silverlight. Double-cliquez dessus pour faire apparaître le détail de l'erreur rencontrée (voir Figure 5.9).

En réalité, Blend ne possède pas de vrai débogueur comme Visual Studio. Les erreurs sont levées, mais n'interrompent pas l'exécution de l'application. Il n'est donc pas possible de poser des points d'arrêt, d'afficher les valeurs des variables durant l'exécution ou d'afficher directement la ligne de code fautive ou l'exception levée. Le designer qui ne remarquerait pas l'icône d'avertissement aurait la fausse impression qu'il s'agit d'un échec en silence. De plus, ainsi que vous pouvez le constater à la Figure 5.9, le numéro de ligne spécifié dans le panneau d'erreur dans le navigateur n'est pas vraiment le bon. Cependant, un designer interactif trouvera cela déjà très utile, car l'erreur renvoyée est la bonne :

  • l'argument spécifié n'était pas dans les limites de la plage de valeurs valide.nom du paramètre : index.
Image non disponible
Figure 5.9 - Erreur levée au sein du navigateur

Visual Studio offre un environnement plus confortable pour déboguer une application. Nous allons compiler la solution dans ce logiciel. Clic droit sur la solution dans le panneau Projet et sélectionnez Ouvrir dans Visual Studio. Une fois le projet ouvert sous Visual Studio, recompilez-le via le raccourci F5. Dès le chargement de l'application, le débogueur lève une exception et affiche la ligne de code posant problème (voir Figure 5.10).

Image non disponible
Figure 5.10 - Erreur levée dans le débogueur

Il est ainsi plus rapide et pratique de trouver d'où vient notre erreur, car si le message est le même, la ligne de code fautive est correctement localisée.

L'un des avantages majeurs du débogueur est de pouvoir récupérer les valeurs des références durant l'exécution de l'application. Il suffit pour cela de positionner votre souris au-dessus d'une variable, lorsqu'une erreur est levée ou lorsque vous avez posé un point d'arrêt. Pour poser un point d'arrêt, cliquez à gauche de la ligne sur laquelle vous le souhaitez dans l'éditeur de code. Vous pouvez également faire un clic droit sur la ligne où vous désirez que l'exécution soit stoppée, puis sélectionner Breakpoint (Point d'arrêt) et Insert Breakpoint (Ajouter un point d'arrêt) - voir Figure 5.11.

Image non disponible
Figure 5.11 - insérer un point d'arrêt

Examinons maintenant la valeur de l'instance du bouton Un à l'exécution. Pour cela, positionnez votre souris au-dessus de sa référence (voir Figure 5.12).

Image non disponible
Figure .12 - Valeur de la référence Un de type Button

La valeur du bouton Un est null. L'expression à droite du signe égal ne renvoie donc aucune valeur. Positionnez votre souris au-dessus de la propriété Children pour explorer son contenu (voir Figure 5.13).

Image non disponible
Figure 5.13 - Exploration de la propriété Children d'un conteneur grâce au débogueur

Nous nous apercevons que cette propriété ne possède que cinq éléments, indexés de 0 à 4. Cibler l'index 5 revient à cibler un sixième élément indéfini dans cette collection. Nous mettons donc facilement en évidence l'erreur levée.

Parfois, pour diverses raisons, par exemple pour montrer l'application à une tierce personne, vous ne souhaiterez pas lancer le débogueur de Visual Studio. Il suffit dans ce cas d'utiliser le raccourci Ctrl+F5. Celui-ci permet une compilation équivalente à celle fournie par Blend en ne lançant pas le débogueur.

5-2-3. Parcourir la liste des enfants

Nous allons maintenant apprendre à parcourir la liste des enfants grâce aux boucles. Il peut être très pratique de parcourir la collection d'enfants Children pour affecter ou récupérer les propriétés de ceux-ci. Nous allons procéder par étapes. Tout d'abord, nous allons créer une méthode de parcours dans la classe MainPageMainPage. Celle-ci aura pour but de créer une représentation de l'arbre visuel sous forme de chaîne de caractères. Pour visualiser la chaîne de caractères, il vous faudra créer un composant TextBlock à qui nous affecterons la propriété Text. La méthode doit recevoir comme paramètre le conteneur de type Panel que nous souhaitons parcourir ainsi que la valeur de tabulation. En voici une ébauche :

 
Sélectionnez
private void ParcoursArbreVisuel ( Panel container, string tab ) 
{
   
}

Voyons comment l'écrire avec une boucle for :

 
Sélectionnez
private void ParcoursArbreVisuel ( Panel container, string tab ) 
{
   // Les deux écritures suivantes sont utilisables
   //int Lng = container.Children.Count;
   var Lng = container.Children.Count;

   for (var i=0; i < Lng; i++)
   {
      
   }
}

Créez maintenant le TextBlock n'importe où dans la grille principale LayoutRoot et nommez-le ArbreTxt. Ce champ texte va nous permettre d'afficher notre arbre visuel. Chaque fois que nous trouverons un enfant, nous concaténerons le nom de sa classe suivi de son nom d'instance si celle-ci est nommée :

 
Sélectionnez
private void ParcoursArbreVisuel ( Panel container, string tab ) 
{
   // Les deux écritures suivantes sont utilisables
   //int Lng = container.Children.Count;
   var Lng = container.Children.Count;


   for (var i=0; i < Lng; i++)
   {
      //on récupère le nom de la classe de chaque enfant
      ArbreTxt.Text += tab + container.Children[i].GetType().Name;

      // On récupère le nom de l'instance récupérée
      string nom = (container.Children[i] as FrameworkElement ).Name;

      // si l'instance est nommée
      if (!String.IsNullOrEmpty(nom))
      {
         ArbreTxt.Text += " - " + nom;
      }

      //On va à la ligne grâce au caractère d'échappement n
      ArbreTxt.Text += "\n";
      
   }
}

L'opérateur + au début de la boucle permet de concaténer des chaînes de caractères. Concaténer une chaîne revient à générer une chaîne en juxtaposant plusieurs autres chaîne de caractères les unes aux autres. La propriété Name est utilisée deux fois dans la boucle : la première fois pour récupérer le nom de l'objet Type retourné, la seconde fois pour retrouver le nom de l'instance. Toutefois, la propriété Name n'est pas héritée de UI-Element, mais de FrameworkElement. Nous sommes donc obligés de transtyper (convertir le type) UIElement en FrameworkElement. Le mot-clé as permet de considérer un type en lieu et place d'un autre type. Pour finir, grâce au caractère d'échappement \, le caractère n est considéré comme un retour à la ligne. Cette méthode nous permet de lister tous les objets situés dans LayoutRoot. Il nous faut maintenant l'appeler au sein de la méthode MainPage_Loaded :

 
Sélectionnez
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   …
   ArbreTxt.Text = LayoutRoot.GetType().Name+" - "+LayoutRoot.Name+"\n";
   //appel de la méthode
   ParcoursArbreVisuel( LayoutRoot, "\t" );
}

Lors de cet appel, nous listons le premier niveau d'imbrication, soit tous les objets présents dans LayoutRoot. Pour cette raison, nous passons la chaîne de caractères "\t" correspondant à une seule tabulation (voir Figure 5.14).

Image non disponible
Figure .14 - Affichage des enfants de LayoutRoot

Nous n'avons affiché que le premier niveau d'imbrication, mais il serait intéressant de parcourir l'arbre visuel dans sa totalité. Pour cela, nous pouvons transformer notre méthode en méthode récursive. Une méthode récursive est une fonction capable de s'appeler elle-même tant qu'une condition est réalisée. Notre condition est simple : à chaque fois que nous listons un enfant, nous pouvons vérifier s'il est de type Panel. Dans ce cas il peut donc contenir des objets enfants. Vérifier le type Panel est pratique : que l'enfant soit un WrapPanel, un StackPanel ou un DockPanel importe peu, car notre méthode sera réinvoquée dans tous ces cas. Voici la méthode de parcours améliorée :

 
Sélectionnez
private void ParcoursArbreVisuel ( Panel container, string tab ) 
{
   // Les deux écritures suivantes sont utilisables
   //int Lng = container.Children.Count;
   var Lng = container.Children.Count;

   for (var i=0; i < Lng; i++)
   {
      //on récupère le nom de la classe de chaque enfant
      ArbreTxt.Text += tab + container.Children[i].GetType().Name;

      // On récupère le nom de l'instance récupérée
      string nom = (container.Children[i] as FrameworkElement ).Name;

      // si l'instance est nommée
      if (!String.IsNullOrEmpty(nom))
      {
         ArbreTxt.Text += " - " + nom;
      }

      // On saute une ligne grâce au caractère d'échappement n
      ArbreTxt.Text += "\n";
      
      // Si l'enfant que l'on vient de lister est aussi un Panel 
      // alors on appelle à nouveau la méthode de parcours

      Panel panel = container.Children[i] as Panel;
      if (panel != null)
      {
         ParcoursArbreVisuel( panel,"\t\t");
      }
   }
}

Vous devriez obtenir un résultat proche de la Figure 5.15.

Image non disponible
Figure 5.15 - Affichage récursif

La boucle for n'est pas la plus adéquate, car Children de type UIElementCollection implémente l'interface IEnumerable. Celle-ci décrit les méthodes permettant le parcours de la collection avec une boucle foreach. Le code est bien plus simple à écrire dans ce cas, le voici mis à jour :

 
Sélectionnez
private void ParcoursArbreVisuel ( Panel container, string tab ) 
{
   foreach (UIElement enfant in container.Children)
   {
      ArbreTxt.Text +=  tab + enfant.GetType().Name;
      
      string nom = (enfant as FrameworkElement ).Name;
      
      if (!String.IsNullOrEmpty(nom))
      {
         ArbreTxt.Text += " - " + nom;
      }
      
      ArbreTxt.Text += "\n";
      
      Panel panel = enfant as Panel;
      if (panel != null)
      {
         ParcoursArbreVisuel( panel,"\t\t");
      }
   }
}

Nous allons en rester là pour le parcours de l'arbre visuel. Toutefois, trois propriétés permettent l'imbrication, comme nous l'avons dit au début de ce chapitre : Children, Child et Content. Pour parcourir la totalité de l'arbre visuel de notre application, nous devrions également tester si l'enfant est de type ContentControl ou si celui-ci possède la propriété Child. Derrière ces propriétés peut se cacher une imbrication élaborée et complexe. Vous pouvez télécharger l'exercice pleinEcran_parcours.zip dans le dossier chap5 des exemples.

5-3. Ajouter des enfants à la liste d'affichage

Ajouter des enfants en cours d'exécution est une opération courante. Cela nous permet de faire vivre et évoluer l'application. Les applications dynamiques apportent une expérience utilisateur très différente, car elles évoluent lors de leur utilisation, dans des directions quelquefois surprenantes. Cela ajoute une profondeur aux applications et les rend plus attractives, donnant envie d'en explorer tous les recoins. Nous allons maintenant apprendre à réaliser ces opérations en ajoutant des objets à la volée, puis en reconstruisant la totalité de notre menu de manière dynamique.

5-3-1. Mécanisme d'instanciation d'objets graphiques

Cela peut paraître étrange, mais lorsque le graphiste crée des objets graphiques au sein d'Expression Blend, il accomplit en réalité deux choses différentes. Il commence par instancier l'objet en le décrivant au sein d'une balise :

 
Sélectionnez
<Button Height="Auto" Width="Auto" Content="Portfolio" Margin="0,0,20,0" 
         FontSize="14" FontFamily="Trebuchet MS" Visibility="Visible" />

Toutefois, cela ne suffit pas, le bouton ne s'afficherait pas s'il ne faisait pas partie de l'arbre visuel. Il doit donc appartenir à l'un des objets de cet arbre. Un conteneur de type Grid fait l'affaire.

La deuxième étape réalisée sans réellement y prêter attention est donc la création du lien de parenté avec son conteneur :

 
Sélectionnez
<Grid x:Name="LayoutRoot" Background="#FFFFFFFF" Margin="0,0,0,0" >
   <Button Height="Auto" Width="Auto" Content="Portfolio" />
</Grid>

Au sein de Blend, cette étape est naturelle puisque l'on ne peut créer des objets qu'au sein de l'arbre visuel (voir Figure 5.16).

Image non disponible
Figure 5.16 - Mécanismes d'ajout d'objets graphiques à l'arbre visuel

L'instance du plug-in Silverlight possède la propriété RootVisual héritée de la classe Application. Celle-ci renvoie une instance du UserControl racine du fichier MainPage.xaml. Tout objet appartenant directement ou indirectement à la propriété RootVisual fait partie de l'arbre visuel de l'application. Lors de l'initialisation de l'application, cette propriété est en fait affectée au sein du fichier App.xaml.cs, dans la classe App :

 
Sélectionnez
private void OnStartup(object sender, StartupEventArgs e) 
{
   // Chargement de l'arborescence contenue dans MainPage.xaml
   this.RootVisual = new MainPage();
}

Cette propriété est également accessible en C# à partir de n'importe quel fichier XAML. Vous pouvez l'atteindre en ciblant la propriété statique Current de la classe App :

  • App.Current.RootVisual

La propriété statique Current de la classe App renvoie l'instance actuelle de l'application.

5-3-2. Ajouter et insérer des objets dans l'arbre

Ajoutons un nouveau menu dans la barre. Nous allons réaliser la première étape consistant à instancier un bouton via C# :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;
// on instancie le bouton
Button clientMenu = new Button();
// on lui spécifie de nouvelles marges
clientMenu.Margin = new Thickness (20,0,0,0);
// on lui affecte une nouvelle police
clientMenu.FontFamily = new FontFamily("Trebuchet MS");
// on définit une taille de police
clientMenu.FontSize = 14;
// on affecte sa propriété Content pour afficher une chaîne de caractères
clientMenu.Content = "Nos Clients";

Dans tous les cas nous utiliserons le mot-clé new. Cela semble étrange, mais la propriété Margin est de type Thickness. C'est une manière d'économiser le poids du lecteur Silverlight, car la valeur est structurée de la même manière qu'un objet d'épaisseur, de type Thickness. Le premier chiffre indique la marge à gauche, puis le deuxième la marge haute et ainsi de suite dans le sens des aiguilles d'une montre. Comme nous pouvons le voir, affecter une police est assez simple. La police Trebuchet MS possède l'avantage d'être embarquée par défaut dans le lecteur Silverlight. Il est donc facile de l'affecter au bouton. Vous remarquez que nous n'avons spécifié aucune largeur ou hauteur. Lorsque vous ne précisez pas ces valeurs, celles-ci sont par défaut en mode Auto. Pour finir, il suffit d'afficher un texte au sein de notre bouton. La seconde phase est plus facile, nous allons utiliser la méthode Add :

 
Sélectionnez
// on ajoute ce bouton à la liste des enfants du WrapPanel
menuHaut.Children.Add(ClientMenu);

La méthode Add n'ajoute pas les objets au hasard, en fait elle ajoute l'objet à la fin de la liste d'enfants, donc à un nouvel index. Notre menu contenait cinq éléments indexés de 0 à 4. À la fin du chargement de l'application, le menu contient désormais six éléments. Le sixième élément ajouté dynamiquement est situé au dernier et nouvel index, soit 5. Cela est particulièrement facile à constater dans un conteneur de type WrapPanel, car l'index affecte directement l'ordre des objets imbriqués (voir Figure 5.17).

Image non disponible
Figure 5.17 - Ajout d'une rubrique Button avec la méthode Add.

La méthode Add n'appartient pas à l'objet lui-même mais à sa propriété de type UI-ElementCollection. Lorsque vous voudrez manipuler les enfants ou la liste elle-même, il faudra utiliser les membres (propriétés et méthodes) de celle-ci. Cette opération est finalement assez facile, toutefois, notre nouveau menu n'est pas vraiment à sa place. Il serait plus judicieux de le placer entre les menus Savoir faire et Contact. Cela est facile à réaliser grâce à la méthode Insert :

 
Sélectionnez
menuHaut.Children.Insert(4,ClientMenu);

Il faut commencer par préciser l'index auquel l'objet sera positionné, puis le composant graphique à insérer. Modifiez, si besoin est, la propriété Margin pour une mise en forme adaptée (voir Figure 5.18).

Image non disponible
Figure 5.18 - Insertion d'une rubrique Button avec la méthode Insert.

Lorsque vous accomplissez une telle opération, vous décalez de 1 chaque objet imbriqué suivant l'index d'insertion au sein de la liste d'enfants.

5-3-3. Erreurs levées

Plusieurs erreurs peuvent être levées lors de l'appel de ces méthodes. La première arrive lorsque vous passez un index aberrant à la méthode Insert. Lorsque vous spécifiez un index dépassant le nombre d'enfants, soit Children.Count, le compilateur lève une erreur correspondant à celle levée lors d'une tentative d'accès à un index indéfini (voir section Gérer les erreurs d'accès Gérer les erreurs d'accès, plus haut dans ce chapitre). Il est en effet illogique d'insérer un objet à un index supérieur au nombre d'enfants. Si vous souhaitez connaître l'index du dernier enfant de la collection, il suffit de retrancher 1 à Children.Count. Insérer un enfant à l'index Children.Count revient à utiliser la méthode Add.

La deuxième erreur est levée si vous essayez de déplacer un objet graphique déjà présent dans la liste d'enfants d'un autre conteneur. Ce cas est plus pernicieux. L'une des règles d'affichage est qu'une référence d'objet graphique ne peut être située en même temps dans deux conteneurs différents. Lorsque vous attribuez un objet présent dans l'arbre visuel à un autre conteneur, une exception est levée. Elle vous indique que l'objet est déjà présent ailleurs (voir Figure 5.19). Pour vous en convaincre, il suffit d'ajouter au StackPanel, notre pied de page, un élément présent dans le WrapPanel, notre menu du haut de page :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;

StackPanel footer = LayoutRoot.Children[1] as StackPanel;

Footer.Children.Add( menuHaut.Children[0] );
Image non disponible
Figure 5.19 - Exception levée lors de l'imbrication élément appartenant déjà à un autre conteneur

Pour résoudre cette problématique, vous devrez supprimer l'enfant de la liste de son ancien conteneur, puis invoquer la méthode Add ou Insert. Pour savoir si un enfant est contenu au sein d'une liste de type UIElementCollection (Children est de ce type), il suffit d'utiliser les méthode Contains ou IndexOf :

 
Sélectionnez
menuHaut.Children.Contains(Toto); 
// renvoie false, car Toto n'existe pas

menuHaut.Children.Contains(ContactBtn); 
// renvoie true, car Contact est bien un enfant de menuHaut

menuHaut.Children.IndexOf(Titi); 
// renvoie -1, car Titi n'est pas présent dans la liste des enfants

menuHaut.Children.IndexOf (ContactBtn); 
// renvoie 4, car ContactBtn est le 5e élément contenu

Pour de plus amples informations sur la suppression d'objets graphiques, vous pouvez consulter la section 5.4 Supprimer des objets de l'arbre visuel.

5-3-4. Créer un menu dynamique

Nous allons maintenant recréer le menu principal de toutes pièces. Pour cela, depuis la fenêtre de design, supprimons tous les boutons de menu du WrapPanel. Dans la méthode MainPage_Loaded, il nous faut également supprimer toutes les références vers ces menus, sauf la ligne permettant de récupérer une référence du WrapPanel :

 
Sélectionnez
WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;

Pour créer un menu à l'exécution, il est plus pratique de créer un tableau de chaîne de caractères. Ensuite, nous pourrons le parcourir. À chaque fois que nous trouverons un élément contenu dans le tableau, nous instancierons dynamiquement un nouveau bouton dans le WrapPanel. Le tableau doit être un champ privé de la classe MainPage, ainsi il sera accessible et modifiable à partir de n'importe quelle méthode de la classe. Voici comment le déclarer :

 
Sélectionnez
public partial class MainPage : UserControl
{
   string [] tabMenus = new string[]{"Accueil", "Portfolio", "Medias", 
                          "Savoir faire","Contact"};
   
   public MainPage()
   {
      InitializeComponent();
      Loaded +=new RoutedEventHandler(MainPage_Loaded);
   }

   private void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      PleinEcranBT.Cursor = Cursors.Hand;
      WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;
   }
}

Ensuite, nous pouvons définir la nouvelle méthode CreateMenus sur la classe MainPage et l'appeler lors du chargement de l'application. La variable MenuHaut est une variable locale à la méthode MainPage_Loaded. Cela signifie que la variable n'existe et n'est accessible que dans cette méthode. Pourtant, nous en avons besoin au sein de la méthode Create-Menus pour ajouter des éléments graphiques à sa liste d'enfants. À ce stade, vous avez trois choix : soit vous créez un champ MenuHaut dans votre classe, puis vous l'affectez au sein de la méthode MainPage_Loaded, soit vous passez MenuHaut comme paramètre de la méthode CreateMenus ou alors vous faites les deux. Il peut être pratique de créer les boutons de menu dans un autre conteneur, si besoin est. La méthode CreateMenus mérite donc un paramètre. Toutefois, nous pourrions également vouloir supprimer tous les objets contenus au sein du WrapPanel dans une autre fonction. Cela peut donc être utile d'en stocker une référence en tant que membre de la classe MainPage. Voici comment procéder :

 
Sélectionnez
public partial class MainPage : UserControl
{
   string [] tabMenus = new string[5]{"Accueil", "Portfolio", "Medias", 
                        "Savoir faire","Contact"};
   
   // on déclare un champ de type WrapPanel
   WrapPanel menuHaut;
   
   public MainPage()
   {
      InitializeComponent();
      Loaded +=new RoutedEventHandler(MainPage_Loaded);
   }

   private void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      
      PleinEcranBT.Cursor = Cursors.Hand;
      
      // on affecte le champ de type WrapPanel
      menuHaut = LayoutRoot.Children[0] as WrapPanel;
      if (menuHaut!=null)
      {
         // on passe le conteneur menuHaut comme paramètre de CreateMenus
         CreateMenus(menuHaut);
      }
   }

   // cette méthode pourra créer des menus quel que soit le conteneur ;)
   private void CreateMenus(Panel conteneur)
   {}
}

Typer le paramètre en tant que Panel est pratique, car nous pourrons spécifier n'importe quel conteneur héritant de ce type, pour centraliser les menus. La méthode Insert n'est pas idéale dans notre cas, car il faudrait que des composants soient déjà présents dans notre WrapPanel. La méthode Add est la plus adéquate, car elle ajoute les menus au fur et à mesure. L'ordre de notre tableau sera donc respecté. Voici la définition de la méthode CreateMenus :

 
Sélectionnez
private void CreateMenus(Panel conteneur)
{
   foreach(string titre in TabMenus)
   {
   
      // on instancie le bouton
      Button monMenu = new Button();
      // on lui spécifie de nouvelles marges
      monMenu.Margin = new Thickness (0,0,20,0);
      // on lui affecte une nouvelle police
      monMenu.FontFamily = new FontFamily("Trebuchet MS");
      // on définit une taille de police
      monMenu.FontSize = 14;
      // on affecte sa propriété Content pour afficher 
      // une chaîne de caractères
      monMenu.Content = titre;
      // on ajoute ce bouton à la liste des enfants du WrapPanel
      conteneur.Children.Add(monMenu);
   
   }
}

Comme vous pouvez le voir, mis à part spécifier les marges de chaque bouton, nous n'avons rien à coder concernant le placement des menus. C'est le conteneur WrapPanel qui s'occupe de cette partie.

Dans cet exercice, nous aurions pu utiliser une classe Rubrique et créer un tableau d'instances de rubrique. L'avantage est non seulement de pouvoir décrire et afficher plus qu'une simple chaîne de caractères, mais aussi de définir des événements et méthodes spécifiques. Ce genre d'instance est appelée Value Object. Ce sont des instances dont le rôle majeur est de contenir les enregistrements récupérés depuis une base de données tout en les adaptant au modèle objet. C'est généralement la méthodologie à suivre lorsque le site est véritablement mis à jour depuis l'extérieur.

Vous trouverez le projet finalisé dans l'archive pleinEcran_dynamique.zip du dossier chap5.

5-3-5. Affecter les propriétés Child et Content

Nous allons évoquer rapidement les propriétés Child et Content. Elles occupent une place importante en matière d'imbrication. Il suffit de voir le nombre d'objets héritant de ContentControl ou assurant une logique d'imbrication spécifique pour s'en convaincre. Ces propriétés pourraient en effet stocker un conteneur à enfants multiples, qui lui-même contiendrait de nombreux objets. Il n'y a pas de limites en la matière. La Figure 5.20 illustre un exemple d'imbrication réalisé dans Blend par un graphiste, avec les trois propriétés Children, Child et Content.

Image non disponible
Figure 5.20 - Exemple d'arbre visuel utilisant les trois propriétés d'imbrication

Dans l'exemple de la Figure 5.20, le composant Border permet de gérer l'affichage et le graphisme de la barre de lecture. Sa propriété Child est affectée d'un StackPanel empilant horizontalement des boutons personnalisés via sa propriété Children. Le bouton stop utilise sa propriété Content pour être affecté d'un simple rectangle. Vous remarquez que le bouton playPause est un bouton de type ToggleButton. Pour rappel, la classe CheckBox hérite de ToggleButton et ne se différencie que par son visuel, fait pour simplifier la vie au graphiste. Il possède deux états, l'un représentant une icône de lecture, l'autre une icône pause. Pour ajouter un enfant via les propriétés Child et Content en C#, il suffit de les affecter :

 
Sélectionnez
MonBorder.Child = new StackPanel();
MonBoutonStop.Content = new Rectangle();

N'oubliez pas cependant que si vous réaffectez ces propriétés à l'exécution, le nouvel objet affecté remplacera l'ancien :

 
Sélectionnez
MonBorder.Child = new StackPanel();
MonBorder.Child = new grid();

Le StackPanel ne sera jamais utilisé dans le code précédent, car il est directement remplacé par une grille. Vous n'aurez pas d'erreur levée : le nouvel enfant écrase l'ancien.

5-3-6. Événement diffusé

Lorsque vous ajoutez une instance de type FrameworkElement à l'arbre visuel, elle diffuse l'événement Loaded. Nous allons écouter l'événement Click sur le bouton ContactBtn. Lorsqu'il sera diffusé, nous ajouterons le bouton ContactBtn dans le WrapPanel  :

 
Sélectionnez
public partial class MainPage : UserControl
{
   private Button clientMenu;

   public MainPage()
   {
      InitializeComponent();
      Loaded +=new RoutedEventHandler(MainPage_Loaded);
   }

   private void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      // on instancie le bouton
      clientMenu = new Button();// nous écoutons l'événement Loaded sur le bouton ClientMenu
      clientMenu.Loaded +=new RoutedEventHandler(clientMenu_Loaded);
      ContactBtn.Click +=new RoutedEventHandler(ContactBtn_Click);

   }

   private void ContactBtn_Click(object sender, RoutedEventArgs e)
   {
      WrapPanel menuHaut = LayoutRoot.Children[0] as WrapPanel;
      if (menuHaut!=null)
      {
         // on ajoute ce bouton à la liste des enfants du WrapPanel
         menuHaut.Children.Insert(4,ClientMenu);
      }
   }

   private void clientMenu_Loaded(object sender, RoutedEventArgs e)
   {
      // Lorsque le bouton est ajouté à l'arbre visuel, 
      // l'événement Loaded est diffusé pour le montrer 
      // on affecte le champ texte central
      TitrePage.Text="Le bouton a été ajouté et initialisé.";
   }
}

Toutefois, il faut faire attention au fait que nous parlons bien d'arbre visuel et non de la propriété Children. Si jamais la propriété Children, à laquelle vous ajoutez un enfant, n'appartient pas à un objet de l'arbre visuel, alors l'événement Loaded ne sera pas diffusé :

 
Sélectionnez
private void ContactBtn_Click(object sender, RoutedEventArgs e)
{
   StackPanel unConteneur = new StackPanel();
   // on ajoute ce bouton à la liste des enfants du StackPanel
   menuHaut.Children.Insert(4,clientMenu);
}

private void clientMenu_Loaded(object sender, RoutedEventArgs e)
{
   // Lorsque le bouton est ajouté au StackPanel 
   // l'événement Loaded n'est pas diffusé, car 
   // le StackPanel ne fait pas partie de l'arbre visuel
   TitrePage.Text="Ce message ne doit pas être tracé.";
}

Ainsi, le même bouton ajouté à un conteneur absent de l'arbre visuel et logique ne diffusera pas d'événement Loaded.

5-4. Supprimer des objets de l'arbre visuel

Nous allons maintenant apprendre à supprimer les enfants d'un conteneur. Comme nous allons le voir, les méthodologies sont très proches de celles que nous avons utilisées pour ajouter des enfants.

5-4-1. Les différentes méthodes

5-4-1-1. Supprimer des enfants de conteneur Panel

Trois méthodes sont accessibles pour supprimer un objet de l'arbre visuel et logique : Remove, RemoveAt et Clear. Ces méthodes sont invoquées par la propriété Children d'un conteneur de type Panel. Voici leur syntaxe :

 
Sélectionnez
// 1 - La méthode Remove reçoit la référence à l'élément contenu
menuHaut.Children.Remove( ContactBtn );

// 2 - La méthode RemoveAt doit recevoir l'index
menuHaut.Children.RemoveAt( 2 );

// 3 - Clear n'a besoin d'aucun argument, la collection 
// de UIElement est simplement vidée de son contenu
menuHaut.Children.Clear( );

(ContactBtn.Parent as Panel).Children.Remove(ContactBtn);

La dernière ligne permet à un objet d'appeler la méthode Remove de son conteneur. Tous les objets de type FrameworkElement possèdent la propriété Parent. Celle-ci renvoie la référence du conteneur parent de l'objet. Dans certains cas, il peut arriver que vous ne connaissiez pas la référence du conteneur à l'avance. Cette méthode répond à cette problématique, car chaque objet de l'arbre visuel possède une propriété parent non nulle.

5-4-1-2. Supprimer des enfants uniques

Supprimer l'enfant d'un conteneur tel que Border ou ContentControl est très simple. Pour ajouter un enfant au sein d'un conteneur à enfant unique, il nous a suffi d'affecter une instance d'UI-Element aux propriétés Content ou Child. Cette fois, il nous suffira d'affecter la valeur null :

 
Sélectionnez
MonBorder.Child = null;
MonBouton.Content = null;

Lorsque vous exécutez les méthodes de suppression de la liste d'enfants, vous ne supprimez que la valeur des propriétés Children, Child ou Content. Les références des enfants qui étaient imbriqués existent toujours en mémoire si celles-ci étaient des membres de classe. Nous verrons comment désactiver des objets en mémoire à la section Désactiver des objets Désactiver des objets.

5-4-2. L'effondrement de la liste d'enfants

5-4-2-1. Principe

Comme vous avez pu le remarquer lorsque vous avez compilé votre application, la suppression d'un objet au sein du StackPanel a décalé chaque index suivant de -1. Si, par exemple, vous avez supprimé le bouton à l'index 2, les objets présents à l'index 3 et 4 ont vu leur index passer respectivement à 2 et 3 (voir Figure 5.21).

Image non disponible
Figure 5.21 - Principe d'effondrement de la liste d'enfants

Lorsque la liste d'enfants est modifiée, l'agencement du composant se met automatiquement à jour, ce qui peut être pratique pour mettre en forme des contenus dynamiques.

5-4-2-2. Trier une liste d'enfants

Vous serez donc souvent tenté d'utiliser l'effondrement des index pour mettre en forme ou dynamiser une application. Nous allons voir les limites et les contraintes de l'effondrement des index. Vous allez utiliser ce comportement pour supprimer des champs texte contenus dans un Stack-Panel. Ceux dont la propriété Text ne possède pas une chaîne de caractères précise seront effacés de la liste. Le StackPanel nous servira à cette occasion de liste de tri. Au sein d'un nouveau projet nommé TriChildren, instancions le tableau suivant comme champ de la classe MainPage, dans le code logique :

 
Sélectionnez
string[] moisAnnee = new string[12] { "janvier", "février" , "mars", 
   "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", 
   "novembre","décembre"};

Positionnons un nouveau StackPanel dans LayoutRoot. Nous pouvons soit l'instancier dynamiquement, via C#, soit le créer via l'interface visuelle de Blend. Appelez-le MonStackPanel. Il serait pratique de l'imbriquer au sein d'un Border afin de définir un contour visuel. Nous allons parcourir le tableau et créer une occurrence de TextBlock pour chaque élément du tableau lu par la boucle :

 
Sélectionnez
string[] moisAnnee = new string[12] { "janvier", "février" , "mars", 
   "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", 
   "novembre","décembre"};

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{

   foreach (string mois in moisAnnee)
   {
      TextBlock moisTexte = new TextBlock();
      moisTexte.Foreground = new SolidColorBrush(Colors.Gray);
      moisTexte.FontSize = 16;
      moisTexte.FontFamily = new FontFamily("Trebuchet MS");
      moisTexte.Text = mois;
      monStackPanel.Children.Add(moisTexte);    
   }
}

Plaçons maintenant un composant de champ texte de saisie, TextBox, au-dessus du StackPanel dans l'interface visuelle de Blend. Nous pouvons là aussi créer ces éléments par code. Appelons le champ de saisie FiltreMois (voir Figure 5.22).

Image non disponible
Figure 5.22 - Arbre visuel et interface de TriChildren

Ajoutez maintenant la logique nécessaire pour trier les mois affichés dans le Stack-Panel. Pour cela, vous pouvez ajouter une méthode dans le panneau des événements du champ de saisie. Les champs de saisie TextBox possèdent l'événement TextChanged. Il vous suffit d'écrire le nom de la méthode, à droite de TextChanged. Elle pourrait se nommer OnChangedFiltre. Blend ajoute directement le code correspondant dans la classe MainPage :

 
Sélectionnez
private void OnChangedFiltre(object sender, TextChangedEventArgs e)
{
   // TODO: Add event handler implementation here.
}

Lorsque le champ de saisie sera modifié par l'utilisateur, supprimez tous les champs texte du StackPanel ne contenant pas la chaîne de caractères entrée par l'utilisateur. Toutefois, vous n'effectuerez le tri qu'à partir d'un minimum de trois caractères saisis :

 
Sélectionnez
private void OnChangedFiltre(object sender, TextChangedEventArgs e)
{
   // on vérifie qu'il y a au minimum 3 caractères entrés
   if ( FiltreMois.Text.Length>2 )
   {
      //on parcourt les enfants
      foreach (TextBlock moisTextBlock in MonStackPanel.Children)
      {
         // on récupère chaque chaîne de caractères
         string mt = moisTextBlock.Text as string;
         // si l'un des TextBlock du StackPanel ne contient pas la chaîne
         if ( !mt.Contains( FiltreMois.Text ) )
         {
            // on le supprime
            MonStackPanel.Children.Remove( moisTextBlock );
         }
      }
   }
}

Vous remarquez le typage TextBlock dans la définition de la boucle foreach. Après tout, TextBlock est une sous-classe du type UIElement. Il faudra juste faire attention à ne positionner aucun autre type d'objet dans le StackPanel sous peine de lever une erreur. Compilez et testez votre application. Vous le constatez assez rapidement, le comportement du filtre est étrange. Une erreur est même levée (voir Figure 5.23).

Image non disponible
Figure 5.23 - erreur levée lorsque le type attendu ne correspond pas à l'enfant

Le message est clair, vous parcourez la liste et vous la modifiez en même temps pour la mettre en forme. Toutefois, vous ne réalisez pas une modification anodine dans le cas présent, vous supprimez définitivement un enfant de la liste. La boucle ne peut donc pas être lue correctement et vous génère des erreurs d'accès. Dans ce cas précis, la méthode que nous employons n'est pas la bonne, car n'oubliez pas que supprimer des objets modifie la mise en forme certes, mais affecte surtout l'arbre visuel de l'application et donc son architecture. Nous allons étudier une méthode plus appropriée à ce cas de figure dans la section suivante.

5-4-2-3. Suppression vs. Collapsed

Toute sous-classe de UIElement possède la propriété Visibility, qui doit être affectée d'une valeur de l'énumération Visibility. Attention à ne pas confondre la propriété d'objet et l'énumération (une liste de valeurs, voir Chapitre 3 Hello World). L'énumération possède les valeurs Visible et Collapsed. Lorsque vous écrivez :

 
Sélectionnez
MonUIElement.Visibility = Visibility.Collapsed;

vous pratiquez un pseudo effondrement. Dans ce cas précis, le système d'agencement de Silverlight (Silverlight Layout System) ne prend pas en compte l'objet. Ses marges, sa largeur, sa hauteur et ses enfants sont simplement ignorés, l'objet n'est pas affiché et n'influence pas les autres. Pourtant celui-ci reste un élément de la collection Children, vous n'affectez donc en rien l'architecture de votre application. Il faut privilégier cette propriété aux méthodes de suppression lorsque vous souhaitez accomplir des opérations liées à la mise en forme. Cela est simplement plus intuitif, plus pertinent et plus simple. Voici notre code pour la liste de tri, mis à jour en utilisant cette propriété :

 
Sélectionnez
private void OnChangedFiltre(object sender, TextChangedEventArgs e)
{
   // on parcourt le tableau
   foreach (TextBlock MoisTextBlock in MonStackPanel.Children)
   {
      // on récupère chaque chaîne de caractères
      string Mt = MoisTextBlock.Text as string;
      // si l'un des TextBlock du StackPanel ne contient pas la chaîne
      if ( !Mt.Contains( FiltreMois.Text ) )
         MoisTextBlock.Visibility = Visibility.Collapsed;    
      else 
         MoisTextBlock.Visibility = Visibility.Visible;
   }
}

Le code est beaucoup plus simple dans ce cas. Avec les méthodes Remove et RemoveAt, nous aurions dû stocker les enfants supprimés dans un tableau temporaire pour obtenir un résultat équivalent et ne pas perdre leur référence.

5-4-3. Désactiver des objets

Nous pourrions nous demander l'intérêt que présente la désactivation des objets. Il faut admettre que plus une application est performante, plus elle est conviviale. D'un point de vue utilisateur, rien n'est plus énervant qu'effectuer des actions et voir leur résultat apparaître avec un temps de latence. Cela peut parfaitement se comprendre pour des accès distants aux bases de données mais c'est une problématique que l'on sait traiter et que l'utilisateur comprend. Pour des actions plus simples, comme naviguer dans un site ou déplier un menu par exemple, il est anormal d'avoir des temps de latence et cela révèle un développement un peu trop hasardeux. Il faut optimiser au maximum les performances de votre application. Pour cette raison, alléger les allocations mémoire est une tâche réellement importante. Sans cela, l'application pourrait utiliser de plus en plus de mémoire vive et ralentir l'exécution.

Désactiver des objets est le processus inverse de celui consistant à ajouter des objets à l'arbre visuel. Pour ajouter un objet à la liste d'affichage, nous avons d'abord instancié cet objet, puis nous l'avons affecté comme enfant d'un conteneur Panel grâce aux méthodes Add et Insert. Les méthodes de suppression Remove et RemoveAt ne sont donc pas suffisantes pour désactiver complètement un objet en mémoire. Il faut faire exactement l'opposé de l'instanciation, toutefois une méthode aussi directe n'existe pas.

Les allocations mémoire sont gérées par le ramasse-miettes (ou Garbage Collector). C'est lui qui surveille et décide si un objet est désactivé ou non de la mémoire. Nous devons juste permettre au ramasse-miettes de jouer son rôle. Pour cela, la première étape consiste à supprimer de l'arbre visuel l'instance que vous souhaitez désactiver. Tant que celle-ci est présente dans l'arbre visuel, elle ne peut être désactivée puisqu'elle est référencée par une collection de composants UIElement. Nous invoquons donc en premier la méthode Remove ou RemoveAt. La seconde étape est de passer la valeur de la référence à null :

 
Sélectionnez
MonConteneur.Children.Remove(UnUIElement);
UnUIElement = null;

Cette méthode est saine, toutefois vous ne saurez pas quand l'objet sera supprimé de la mémoire. Le langage C# est managé, cela signifie que les allocations mémoire ne sont pas gérées par le développeur C#, mais par le ramasse-miettes. Celui-ci gère la désactivation des instances de classes en fonction de leur occupation mémoire. Moins une référence prend de mémoire, moins elle est prioritaire. Une mauvaise pratique consiste à utiliser la méthode Collect du ramasse-miettes, pour forcer son passage. Cela est très coûteux en performances, car le ramasse-miettes doit parcourir toutes les références et décider de les supprimer ou non :

 
Sélectionnez
MonConteneur.Children.Remove(UnUIElement);
UnUIElement = null;
GC.Collect();

Lorsque vous concevrez vos propres composants, vous devrez faire attention à ce qu'ils suppriment bien tous les écouteurs internes d'événements propres à l'application ou à des références externes. Sans cela, vos instances de composants personnalisés pourraient ne pas être collectées par le ramasse-miettes (voir Chapitre 11 Composants personnalisés).

5-4-4. Événement diffusé

Nous avons vu qu'à chaque fois qu'un objet est ajouté à l'arbre visuel, soit à l'objet RootVisual, celui-ci diffuse l'événement Loaded. Il vous suffira donc d'écouter cet événement pour savoir si l'objet est ajouté à l'arbre visuel. Un événement semblable n'est pas diffusé lorsqu'un objet est supprimé de l'arbre visuel. Vous pouvez à la place utiliser l'événement LayoutUpdate. Toutefois, cet événement est diffusé lors de chaque modification d'agencement. Il est donc diffusé de nombreuses fois, même lorsqu'un objet est ajouté à l'arbre visuel. Voici une méthode pour vérifier qu'un enfant vient d'être supprimé de l'arbre visuel :

 
Sélectionnez
private void MainPage_Loaded(object sender, e)
{
   MonStackPanel.LayoutUpdated += MonStackPanel_LayoutUpdated;
}

private void MonStackPanel_LayoutUpdated(object sender, EventArgs e)
{
   if (MonStackPanel.Children.Contains(MonBouton)
      { /* objet non supprimé */ }
   else 
      { /* objet supprimé */ }
}

Dans tous les cas, il vous faudra tester si l'objet a été supprimé. Ce n'est pas l'idéal d'un point de vue performance, car l'événement LayoutUpdate est diffusé très souvent.

5-5. Échanges d'index

Nous avons abordé de nombreux mécanismes de l'arbre, mais nous n'avons pas pratiqué d'échanges d'index de la liste d'enfants. En fait, Children ne possède pas une méthode spécifique assurant directement cette opération. Nous allons étudier les différentes manières de procéder dans cette section.

5-5-1. Échange d'index du point de vue conception

Comme aucune méthode n'existe, nous allons coder notre échange d'index en prenant soin d'éviter les erreurs d'accès, d'ajout ou même de suppression que nous connaissons bien maintenant. Voici le code permettant d'effectuer cette opération délicate :

 
Sélectionnez
public partial class MainPage : UserControl
{
   public Button ClientMenu;
   public WrapPanel MenuHaut;
 
   public MainPage()
   {
      InitializeComponent();
      Loaded +=new RoutedEventHandler(MainPage_Loaded);
   }

   private void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      menuHaut = LayoutRoot.Children[0] as WrapPanel;

      ContactBtn.Click +=new RoutedEventHandler(ContactBtn_Click);
      
   }

   private void ContactBtn_Click(object sender, RoutedEventArgs e)
   {
      // on récupère l'un des enfants dont on veut échanger l'index
      Button Enfant1 = menuHaut.Children[1] as Button;
      
      // on stocke les index dont nous aurons besoin par la suite
      int IndexContact = menuHaut.Children.IndexOf( ContactBtn );
      
      // Ensuite on supprime les enfants de la liste
      menuHaut.Children.Remove( Enfant1 );
      menuHaut.Children.Remove( ContactBtn );
      
      // Puis on les réinsère en respectant un ordre précis
      // toujours commencer par réinsérer l'élément 
      // que l'on souhaite positionner à l'index le plus bas
      menuHaut.Children.Insert(1,ContactBtn);
      
      // pour finir, on ajoute à la liste d'enfants l'élément 
      // qui a l'index le plus haut
      menuHaut.Children.Insert(IndexContact,Enfant1);

   }
}

Comme vous pouvez le voir, nous suivons un ordre vraiment très précis pour éviter au maximum les erreurs d'accès à la liste. Échanger l'ordre des enfants modifie l'arbre visuel, mais ne lui retire pas d'enfants. Nous aurons donc le même nombre d'enfants au départ de notre code qu'à l'arrivée. Il nous faut dans un premier temps stocker les index que nous souhaiterons échanger par la suite. Si nous le faisions après avoir supprimé un enfant de la liste, ceux-ci ne correspondraient pas au bon emplacement dans la liste.

La deuxième étape consiste à supprimer de la liste les enfants à échanger. L'ordre importe peu. Pour finir, on les réinsère, mais pas n'importe comment. Dans notre cas, nous avons échangé le dernier enfant de la liste avec un enfant situé en plein milieu. Si nous commençons par réinsérer l'élément le plus haut dans la liste, nous courons le risque de spécifier un index hors de la portée maximale qui correspond à notre nombre d'enfants, Children.Count. Le code suivant renvoie une erreur, car nous essayons d'insérer Enfant1 à un index qui n'est pas encore accessible :

 
Sélectionnez
private void ContactBtn_Click(object sender, RoutedEventArgs e)
{
   // on récupère l'un des enfants dont on veut échanger l'index
   Button Enfant1 = menuHaut.Children[1] as Button;
   
   // on stocke les index dont nous aurons besoin par la suite
   int IndexContact = menuHaut.Children.IndexOf( ContactBtn );
   
   // Ensuite on supprime les enfants de la liste
   menuHaut.Children.Remove( Enfant1 );
   menuHaut.Children.Remove( ContactBtn );
   
   menuHaut.Children.Insert(IndexContact,Enfant1);
   menuHaut.Children.Insert(1,ContactBtn);

}

Cette méthode est assez fastidieuse. Réaliser un échange d'index sans connaître à l'avance les objets à échanger nous forcerait également à prévoir toutes les erreurs possibles. Avoir une méthode de UIElementCollection qui accomplirait directement cette opération serait un plus. C'est ce que nous allons aborder dès maintenant.

5-5-2. Une méthode d'extension pour UIElementCollection

Les méthodes d'extension sont apparues avec C# 3. C'est un nouveau pas en avant pour rendre C# souple et faciliter l'ajout de méthodes personnalisées aux classes normalement fermées à l'extension. Voici la signature de la classe UIElementCollection :

 
Sélectionnez
public sealed class UIElementCollection :
                    PresentationFrameworkCollection<UIElement>

Comme vous pouvez le constater, cette classe empêche tout héritage. Pourtant, les méthodes d'extension nous permettent de lui ajouter des fonctionnalités sans pour autant générer du code spaghetti. Cliquez droit sur votre projet et sélectionnez Ajouter un nouvel élément. Dans la fenêtre qui vient de s'ouvrir, choisissez Class. Nommez-la UtilsMethod.cs, vous pouvez également la glisser dans un dossier spécifique pour éviter de mélanger les genres (voir Figure 5.24). La création de répertoires est accessible par un simple clic droit sur le projet ou un autre répertoire.

Image non disponible
Figure 5.24 - Arborescence de dossier pour le fichier Utils.cs

Comme vous pourriez avoir besoin de notre méthode plus tard, il vaut mieux inclure la future classe qui contiendra notre méthode dans un espace de nom spécifique. Voici le code contenant la définition de notre méthode d'extension :

 
Sélectionnez
namespace Org.Tweened.Utils
{
   public static class UtilsMethod
   {
      public static void SwapChildren(this UIElementCollection E, 
                    UIElement child1, UIElement child2 )
      {
           
         if ( E.Contains(child1) &&  E.Contains(child2) )
         {
            int Index1 = E.IndexOf(child1);
            int Index2 = E.IndexOf(child2);
            E.Remove(child1);
            E.Remove(child2);

            if (Index1>Index2)
            {
               E.Insert(Index2,child1);
               E.Insert(Index1,child2);
            }
            else
            {
               E.Insert(Index1,child2);
               E.Insert(Index2,child1);
            }
         }
         else 
            throw new NotImplementedException("Au moins l'un des deux 
            enfants n'est pas contenu dans la liste d'enfants.");
      }
   }
}

Créer une méthode d'extension est relativement simple. L'objectif de ces méthodes est la décoration. Pas au sens artistique, bien sûr, mais d'un point de vue technique. Décorer signifie ajouter de nouvelles fonctionnalités et propriétés à un objet. La décoration est un concept permettant de résoudre de nombreuses problématiques de conception objet. Le mot-clé static est obligatoire aussi bien pour la classe que pour la méthode. Grâce à ce mot-clé, le simple fait de référencer notre espace de noms permet à la méthode d'extension d'être utilisée. La signature d'une méthode d'extension contient toujours au moins un premier argument commençant par this, celui-ci est suivi du type puis du nom du paramètre. Le mot-clé this indique au compilateur que le type qui le suit pourra utiliser la méthode. Les paramètres qui suivent sont, quant à eux, utilisés lors de l'appel. À ce moment, n'oubliez pas de référencer l'espace de noms via le mot-clé using :

 
Sélectionnez
MenuHaut.Children.SwapChildren(Enfant1, ContactBtn);

Vous n'aurez sans doute pas d'IntelliSense pour les méthodes d'extension au sein d'Expression Blend, mais vous en aurez au sein de Visual Studio. Compilez et testez votre application. Vous constatez que les boutons échangent bien leur place, toutefois contrairement à notre première version, la logique est complètement réutilisable au sein d'autres projets.

5-5-3. Échange d'index du point de vue design

Comme nous l'avons vu à la section 5.2 Principe d'imbrication, l'ordre d'imbrication est directement lié à l'ordre de superposition. Toutefois cela n'est pas forcément pratique, car l'ordre d'imbrication est également lié aux contraintes de positionnement au sein d'un conteneur. Nous allons le démontrer à travers un petit exemple et trouver des solutions adaptées aux designers et aux développeurs.

5-5-3-1. Créer le projet

Créez un nouveau projet de type application et nommez-le SuperpositionOrder. Dans le conteneur principal, créez un StackPanel avec une orientation horizontale. Celui-ci doit s'adapter à son contenu, vous devrez donc lui affecter une largeur et une hauteur en mode Auto. Faites en sorte qu'il soit centré horizontalement au sein de la grille principale. Ensuite, imbriquez à l'intérieur du StackPanel cinq objets Rectangle de différentes couleurs. Chacun doit faire 100 pixels de largeur par 100 pixels de hauteur. Pour finir, espacez-les en leur spécifiant une marge à droite (voir Figure 5.25).

Il faudrait agrandir un rectangle afin de voir comment ceux-ci se superposent, par exemple celui du milieu. Ceci n'est pas aussi facile à faire qu'il n'y paraît.

Image non disponible
Figure 5.25 - Exercice pratique de superposition

5-5-3-2. Introduction aux RenderTransform

Notre problème est simple : agrandir la largeur décalera les autres rectangles les uns par rapport aux autres, car la largeur et la hauteur sont liées au comportement d'empilement du StackPanel. C'est une impasse si nous nous contentons de ces propriétés. Heureusement un autre type de transformation existe, les transformations vectorielles. Nous aborderons en profondeur ces transformations au Chapitre 5 L'arbre visuel et logique. Pour l'instant, nous nous contentons de les utiliser. Sélectionnez le rectangle du milieu, puis dans le panneau des propriétés, ouvrez l'onglet Transform (voir Figure 5.26).

Image non disponible
Figure 5.26 - L'onglet des transformations

Cet onglet vous propose deux types de transformation : les transformations vectorielles de type RenderTransform et les transformations de type Projection qui permettent de gérer l'affichage d'objets en pseudo 3D (voir Chapitre 8 Les bases de la projection 3D). Nous allons utiliser les transformations vectorielles pour nous libérer partiellement des contraintes d'agencement propres au StackPanel. Cliquez sur l'icône de transformation d'échelle (Image non disponible), puis saisissez la valeur 1,4 dans les deux champs de saisie (voir Figure 5.27).

Image non disponible
Figure 5.27 - L'onglet des transformations d'échelle

Les transformations de type RenderTransform sont totalement indépendantes de l'agencement imposé par le contexte conteneur. Elles permettent donc aux graphistes de s'affranchir des contraintes liées à la conception et à l'architecture. Notre Rectangle mesure désormais 40 % de largeur et de hauteur de plus et il ne décale plus les autres (voir Figure 5. 28).

Image non disponible
Figure 5.28 - Rectangle avec changement d'échelle

5-5-3-3. La propriété ZIndex

Comme vous pouvez le constater, notre Rectangle étant en troisième position dans l'ordre d'imbrication XAML, il apparaît au-dessus du rectangle à sa gauche et sous le rectangle à sa droite. Nous allons l'afficher au-dessus des autres rectangles. Sur Internet, vous trouverez de nombreux exemples dans lesquels un menu se superpose aux autres lors du survol de la souris. Pour réaliser ce type de menu, il faudrait nous affranchir, une nouvelle fois, des contraintes liées à l'imbrication.

Tous les conteneurs pouvant contenir plus d'un enfant possèdent la propriété attachée ZIndex,
fournie par la classe Canvas. Par défaut, la propriété Zindex contient la valeur 0. Comme tous les enfants d'un conteneur possèdent ZIndex à 0, c'est l'ordre d'imbrication XAML qui prime dans un premier temps. Il existe ainsi une compétition constante entre l'ordre d'imbrication, soit l'index de l'enfant dans la collection Children,
et la propriété ZIndex. Lorsque deux enfants possèdent le même ZIndex, l'ordre de superposition est déterminé par l'index enfant de chacun. Au contraire, s'ils possèdent un ZIndex différent, l'ordre de superposition est défini par le ZIndex. La valeur ZIndex la plus haute représente l'objet affiché au-dessus des autres. Toutefois, cette propriété n'exerce son influence qu'au sein d'un même conteneur et il nous faut la modifier pour le rectangle central, pour cela, il suffit d'ouvrir le panneau des propriétés, dans les options d'agencement, puis de passer sa valeur à 1 (voir Figure 5.29).

Image non disponible
Figure 5.29 - Rectangle avec changement d'échelle et modification de la propriété ZIndex

Pour mieux comprendre la compétition entre l'index enfant et le ZIndex, vous pouvez sélectionner tous les rectangles et passer leur ZIndex à 1. Notre rectangle du milieu passe à nouveau en dessous du rectangle de droite. Les ZIndex étant tous égaux, c'est l'ordre d'imbrication XAML qui prime à nouveau.

Vous êtes maintenant familiarisé avec les différents composants visuels proposés par le framework Silverlight. Dans le prochain chapitre, nous étudierons les mécanismes liés à l'animation et propres à la plate-forme Silverlight. Nous verrons en quoi Silverlight se révèle être un puissant moteur d'animation vectorielle. Nous aborderons donc la création d'animations dans Expression Blend ou avec C#.

Animations

Dans ce chapitre, nous étudierons en profondeur les mécanismes d'interpolations vectorielles propres à Silverlight. Pour cela, nous allons apprendre les principes fondamentaux de l'animation. Nous utiliserons notre projet de site plein écran comme point de départ pratique pour acquérir les concepts de base, ainsi qu'une aisance technique. Nous aborderons tout d'abord l'animation du point de vue d'un designer interactif via Blend. Dans un second temps, nous verrons que C# facilite et dynamise la création d'animations, mais qu'il simplifie et optimise également le flux de production sans délaisser le travail des designers ou des animateurs. Nous apprendrons en quoi les transformations relatives sont efficaces et incontournables en matière d'animation et comment les générer à l'exécution. Pour finir, nous aborderons et utiliserons le gestionnaire d'états visuels, dont la bonne compréhension repose sur l'ensemble des notions apprises auparavant. Nous verrons quel est son impact en matière de conception applicative ou de flux de production dans notre quotidien.

5-6. Introduction

Silverlight est, entre autres, un moteur d'animations vectorielles. Depuis sa version 3, il permet de gérer la projection d'objets vectoriels en pseudo 3D. Cela signifie qu'il est capable de représenter un objet au sein des quatre dimensions que nous connaissons tous, soit x, y, z (la profondeur) et t pour le temps. Dans ce chapitre, nous n'étudierons pas les animations dans un espace 3D, mais uniquement 2D pour des raisons de clarté (si besoin est, vous pouvez consulter le Chapitre 8 Les bases de la projection 3D dédié à la 3D).

5-6-1. Qu'est-ce qu'une animation ?

Quel que soit l'environnement de développement, créer une animation revient toujours à modifier la valeur d'une propriété d'un objet au cours du temps. Nous verrons à la section 6.3.2 Liaison de modèles que la valeur de départ peut-être récupérée implicitement. Cette particularité propre au lecteur Silverlight permet des comportements très puissants et beaucoup de souplesse de conception.

Dans le langage courant, une animation est forcément fluide, mais c'est un raccourci un peu rapide et incorrect. Gardez à l'esprit que c'est avant tout une propriété d'objet qui varie au cours du temps quel que soit le laps de temps écoulé entre deux valeurs de cette propriété. Ainsi, le rebond d'une balle se résume aux variations de ses coordonnées dans l'espace au cours du temps.

La nature offre de nombreux exemples d'animations invisibles à nos yeux, comme l'érosion d'une montagne ou la croissance d'un arbre. Les propriétés de ces objets évoluent tellement lentement dans le temps que voir ces phénomènes est simplement impossible à l'œil nu. Certains mouvements sont tellement rapides qu'il est également difficile de les analyser. Le galop du cheval en est un exemple flagrant, il ne fut réellement compris scientifiquement par Étienne Jules Marey et Eadweard Muybridge qu'à la fin du xixe siècle. Le naturaliste Étienne Jules Marey pensait qu'au grand galop aucune des pattes du cheval ne touchait le sol en même temps durant un cours instant. Cela est vrai, mais le démontrer n'était pas si simple. Pour le prouver, Eadweard Muybridge a découpé son mouvement à l'aide d'appareils photographiques alignés les uns à côté des autres. Les appareils photographiques de ces scientifiques étaient déclenchés de manière décalée dans le temps selon un intervalle régulier (voir Figure 6.1).

Image non disponible
Figure 6.1 - Découpage du galop d'un cheval par Muybridge

La décomposition et l'étude des mouvements amena de grandes découvertes et perspectives. Ce laps de temps constant entre chaque photographie a introduit la notion de cadence de prise de vues. Plus ce laps est court, plus la cadence est élevée. Par la suite, Muybridge inventa le premier appareil permettant d'afficher les images rapidement les unes après les autres. Cette invention préfigurait sans doute les prémisses du cinéma et de l'animation. Dans la même veine, les premiers dessins animés Disney étaient en huit images par seconde, car cela évitait un travail de dessin trop fastidieux. Toutefois, l'œil humain ne percevant plus l'effet de saccade à partir de 24 images par seconde, ces 8 images par seconde étaient perçues par les spectateurs malgré le talent des animateurs. Les premiers films des frères Lumière connaissaient des limites équivalentes. Les cadences d'affichage, de prise de vues et d'animations furent, durant au moins 50 ans, le centre de nombreux efforts de la part des cinéastes et techniciens, car celles-ci engendrent l'illusion du mouvement et son réalisme. Aujourd'hui encore, leur maîtrise permet de concevoir des effets visuels impressionnants et de mieux connaître notre environnement naturel.

5-6-2. La cadence des animations au sein de Silverlight

Au sein de Silverlight, la cadence des animations est de 60 images par seconde par défaut. Le laps de temps s'écoulant théoriquement entre deux images affichées est donc de 1/60 de seconde. Cette valeur est accessible à travers la propriété MaxFrameRate de l'instance Silverlight. Vous pouvez également afficher la cadence en bas de la fenêtre du navigateur via la propriété EnableCounterFrameRate à l'instanciation du lecteur Silverlight :

 
Sélectionnez
<div id="silverlightControlHost">
   <object data="data:application/x-silverlight," type="application/
                x-silverlight-2" width="100%" height="100%">
      <param name="source" value="ClientBin/sitet.xap"/>
      <param name="onerror" value="onSilverlightError" />
      <param name="background" value="white" />
      <param name="minRuntimeVersion" value="3.0.40128.0" />
      <param name="autoUpgrade" value="true" />
      <param name="maxFrameRate" value="30" />
      <param name="enableFrameRateCounter" value="true" />
      <a href="http://go.microsoft.com/fwlink/?LinkID=141205" 
        style="text-decoration: none;"> <img src="http://go.microsoft.
        com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" 
        style="border-style: none"/>
       </a>
   </object>

Vous pouvez également définir ces deux paramètres à l'exécution en C# :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   Loaded +=new System.Windows.RoutedEventHandler(MainPage_Loaded);
   MouseLeftButtonDown += MainPage_MouseLeftButtonDown;
}

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   App.Current.Host.Settings.EnableFrameRateCounter = true;
}

private void MainPage_MouseLeftButtonDown (object sender, 
                        MouseButtonEventArgs e)
{
   App.Current.Host.Settings.MaxFrameRate = 30;
}

Toutefois, comme dans n'importe quel autre moteur d'animation, et ainsi que vous pouvez le constater grâce à la propriété EnableFrameRateCounter, la cadence n'est pas constante. Ceci est dû aux algorithmes de calcul et aux fluctuations de performance des systèmes d'exploitation, mais également à de nombreux autres facteurs, comme l'occupation mémoire et processeur à l'instant où l'animation est visionnée. La cadence maximum n'est en réalité rien d'autre qu'une valeur souhaitée idéale. Silverlight n'est volontairement pas conçu pour gérer les animations traditionnelles de prime abord, c'est-à-dire les animations image par image. Il est orienté vers la diffusion d'applications riches avant tout.

Il en découle deux spécificités essentielles propres à Silverlight. La première est que l'unité de temps est exprimée en secondes et non en images affichables comme sous Flash. La seconde particularité est que Silverlight dispose d'un moteur vectoriel "autodégradable". Cela signifie que si une animation est trop lourde pour être affichée correctement selon sa cadence, dans tous les cas, le moteur respectera au maximum le temps qui lui est imparti, même s'il lui faut ignorer l'affichage d'images intermédiaires de l'animation. Autrement dit, même saccadée, une animation ne dépassera pas une durée définie, car Silverlight privilégie le temps plutôt que l'affichage de toutes les étapes. De manière générale, l'œil perçoit l'intervalle entre deux images si la cadence est inférieure à 25 images par seconde. Il vaut donc mieux éviter les cadences trop faibles pour ne pas créer un effet saccadé disgracieux.

5-6-3. Une première animation

Vous allez maintenant créer votre première animation. Pour cela, ouvrez Blend et créez un projet de type application. Nommez-le PremiereAnimation par exemple. Nous allons commencer par un peu de pratique, puis nous examinerons ce qui a été produit côté XAML par Blend. Vous pouvez passer en espace de travail Animation via le raccourci F6. Dans le conteneur LayoutRoot, créez un champ texte de type TextBlock, puis entrez la chaîne de caractères animation d'une valeur numérique dans sa propriété Text. Nous allons créer une nouvelle animation de champ texte. Pour cela cliquez sur l'icône plus (+) dans le panneau au-dessus de l'arbre visuel et logique. Une nouvelle fenêtre s'ouvre, vous proposant de nommer la nouvelle animation que vous souhaitez créer. Nommez-la AnimationNumerique (voir Figure 6.2).

Image non disponible
Figure 6.2 - Fenêtre de création d'une nouvelle animation

Vous remarquez que créer une nouvelle animation revient à créer une nouvelle ressource de type Storyboard. Cliquez sur OK. L'interface de Blend affiche désormais une ligne de temps exprimée en secondes. Celle-ci est située à droite de l'arbre visuel et logique. Chaque élément de l'arbre visuel peut posséder sa propre animation qui sera accessible dans la fenêtre de la ligne de temps. Une animation peut cibler une ou plusieurs propriétés. Un cadre rouge entoure la fenêtre de design (il est représenté en noir à la Figure 6.3).

Image non disponible
Figure 6.3 - Arbre visuel attenant au panneau de la ligne du temps

Dans la partie gauche de l'interface, l'encadré rouge indique que toute modification des objets entraînera la création d'une nouvelle clé d'animation ou la modification d'une clé existante. Attention à ne pas changer le type de valeur contenu par la propriété. Un dégradé ne peut pas être interpolé en couleur unie. Il faut un dégradé au départ et en fin d'animation. Déplacez l'instance du TextBlock de la gauche vers la droite. Dès que vous avez réalisé cette étape, une nouvelle clé d'animation est créée à la seconde 0. Elle est représentée par un ovale gris clair, à droite du champ texte (voir Figure 6.4).

Image non disponible
Figure 6.4 - Une nouvelle clé d'animation est créée à la seconde 0

Lorsqu'un objet possède une clé d'animation, un point rouge apparaît en bas à gauche de son icône dans l'arbre visuel et logique.

Une clé d'animation indique une modification de la valeur d'une propriété à un instant donné. Créer une clé d'animation alors qu'aucun changement ne survient peut être utile si vous poursuivez un but précis. Toutefois, évitez de polluer la ligne de temps en ajoutant des clés n'indiquant aucun changement de propriété.

Déplacez maintenant la tête de lecture à la seconde 2, représentée par une ligne jaune surmontée d'un triangle. La position de la tête de lecture permet d'afficher les objets vectoriels à un instant donné de l'animation. La déplacer revient à faire un arrêt sur image à n'importe quel instant de l'animation. Sélectionnez le champ texte, puis repositionnez-le n'importe où ailleurs, au sein du conteneur LayoutRoot. Vous générez à nouveau une image clé, mais cette fois elle est positionnée à la seconde 2. L'image clé est automatiquement créée, car Blend est en mode enregistrement d'animation. Vous pouvez voir le résultat de vos actions en jouant l'animation. Pour cela, il suffit de cliquer sur le bouton de lecture situé au-dessus de la ligne de temps (voir Figure 6.5).

L'aperçu de la lecture est moins performant que celui que vous obtiendrez en compilant le projet. Toutefois, compiler le projet ne déclenche pas l'animation au chargement. Il vous faudra déclencher l'animation à l'exécution, soit par C#, soit grâce aux comportements (voir section 6.2 Style visuel). Comme nous l'avons vu jusqu'à présent, tout ce qui est créé par le designer ou l'intégrateur au sein de Blend est traduit en langage XAML. Nous allons maintenant étudier ce qui a été produit, ainsi que les classes de la plate-forme Silverlight utilisées pour animer les objets.

Image non disponible
Figure 6.5 - Contrôler une animation sous Blend

5-6-4. Les différents types d'animation

Fermez l'animation en cours pour éviter de la modifier. Il suffit pour cela de cliquer sur l'icône de fermeture de l'animation (X) située à droite du nom de l'animation dans l'arbre visuel et logique. Lorsque vous avez créé l'animation, elle a été ajoutée en tant que ressource. Une ressource est un type d'objet particulier dont le but est de pouvoir être accessible et réutilisable au sein de votre projet. Il existe plusieurs types de ressources, mais elles possèdent toutes une portée d'utilisation. Dans le cas des animations, les ressources sont en majorité définies comme ressource de l'élément visuel le plus haut dans l'arbre visuel. Dans notre cas, l'animation sera stockée dans le composant UserControl racine, elle sera donc utilisable et accessible à l'intérieur du nœud XAML UserControl racine. Voici le code XAML correspondant à notre animation :

 
Sélectionnez
<UserControl.Resources>
   <Storyboard x:Name="AnimationNumerique">
      <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
            TargetName="textBlock" Storyboard.
            TargetProperty="(UIElement.RenderTransform).
            (TransformGroup.Children)[3].(TranslateTransform.X)">
         <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-25"/>
         <EasingDoubleKeyFrame KeyTime="00:00:02" Value="75"/>
      </DoubleAnimationUsingKeyFrames>
      
      <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
            TargetName="textBlock" Storyboard.
            TargetProperty="(UIElement.RenderTransform).
            (TransformGroup.Children)[3].(TranslateTransform.Y)">
         <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-20"/>
         <EasingDoubleKeyFrame KeyTime="00:00:02" Value="60"/>
      </DoubleAnimationUsingKeyFrames>
   </Storyboard>
</UserControl.Resources>

Comme vous le constatez, l'animation est représentée par une ressource de type Story-board. Elle possède un nom à l'instar des objets vectoriels que nous avons utilisés. À l'intérieur du nœud élément Storyboard, deux séquences d'animation de type DoubleAnimationUsingKeyFrames cohabitent. L'une cible la propriété X du nœud Render-Transform, l'autre la propriété Y de ce même nœud (voir section 6.3 Bouton générique). Un nœud DoubleAnimationUsingKeyFrames décrit une animation d'une propriété de type Double par l'utilisation de clés d'animations. Comme nous le verrons dans ce chapitre, un changement peut être défini autrement que par des clés d'animation. De plus, il est possible d'animer des types non numériques. Pour le mettre en évidence, vous pouvez modifier la couleur du champ texte (propriété Foreground) à la seconde 2. Il vous faut au préalable accéder à l'animation pour la modifier. Pour cela, cliquez sur l'icône listant les animations créées (   ). Une fenêtre apparaît, sélectionnez l'animation. Une fois la couleur du champ texte modifiée, vous obtiendrez l'équivalent du code XAML généré ci-dessous :

 
Sélectionnez
<Storyboard x:Name="AnimationNumerique"><ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
        TargetName="textBlock" Storyboard.TargetProperty=
        "(TextBlock.Foreground).(SolidColorBrush.Color)" >

      <EasingColorKeyFrame KeyTime="00:00:00" Value="Black"/>
      <EasingColorKeyFrame KeyTime="00:00:02" Value="Red"/>

   </ColorAnimationUsingKeyFrames>

</Storyboard>

Cette fois, Blend a généré une sous-séquence d'animation de type ColorAnimation-UsingKeyFrames au sein de l'objet Storyboard. Vous n'animez plus de valeurs numériques, mais des valeurs de type Color. La Figure 6.6 liste une partie des classes et types d'animation permettant d'animer des objets au sein de Silverlight.

Image non disponible
Figure 6.6 - Les classes utilisées pour animer des objets

La classe Storyboard assure le contrôle de tous les types d'animation, leur lecture ou leur pause, par exemple. De ce fait, vous pouvez considérer l'objet Storyboard comme l'unité d'organisation principale. L'interpolation des valeurs entre deux instants est gérée par les autres classes. Quatre grands types de valeurs peuvent être animés.

  1. Double représente les valeurs de types numériques. L'opacité, par exemple, sera animée grâce à la classe DoubleAnimation, car celle-ci est typée Double.
  2. Point correspond à une structure constituée d'une paire de valeurs nommées X et Y et de type Double. Pour résumer, il représente les coordonnées d'un point dans l'espace. Lorsque vous animez les sommets d'un tracé vectoriel (de type Path), en interne le XAML généré correspondra à PointAnimation.
  3. Color fait référence à toutes les classes utilisant la classe Color. Ainsi, animer les couleurs d'un dégradé linéaire (LinearGradientBrush) repose sur Color-Animation.
  4. Object renvoie à n'importe quel type. Silverlight vous permet d'animer n'importe quel type de valeurs différentes des précédentes. Toutefois interpoler la visibilité (Visi-bility) ou une chaîne de caractères (string) ne produira pas une animation fluide.

Nous allons maintenant essayer de comprendre quels types d'interpolation sont fournis par Silverlight et comment les concevoir.

5-6-5. Les différents types d'interpolation

Comme vous pouvez le constater avec la Figure 6.6, mis à part pour les animations ciblant les valeurs Object, deux grandes familles d'interpolations cohabitent pour chaque type. La première famille utilise des clés d'animation, la seconde possède une propriété pour la valeur de départ et une autre pour la valeur d'arrivée, cette dernière famille n'utilise donc pas de clé d'animation.

5-6-5-1. Interpolations par clé d'animation

Une clé d'animation est une classe constituée au minimum des propriétés Value et KeyTime. La propriété Value indique la valeur de la propriété animée. KeyTime définit l'instant auquel la propriété doit atteindre la valeur spécifiée :

 
Sélectionnez
<EasingColorKeyFrame KeyTime="00:00:00" Value="Black"/>
<EasingColorKeyFrame KeyTime="00:00:02" Value="Red"/>

Ainsi dans le code ci-dessus, à la seconde 0, la propriété animée est affectée de Black (noir) alors qu'à la seconde 2 celle-ci doit atteindre la valeur Red (rouge). Parce qu'il existe quatre grands types d'animation (voir section 5.6.4 Les différents types d'animation), il existe quatre grands types de clés d'animation (voir Figure 6.7).

Une classe abstraite représente chaque type de clé. Pour chaque type de clé (mis à part les clés faisant référence à l'animation ObjectAnimation), il existe quatre manières de jouer l'animation. Au sein de Blend, vous pouvez choisir le type d'interpolation en cliquant sur une image clé via le panneau des propriétés (voir Figure 6.8).

Par défaut, les clés d'animations sont de type Easing, c'est-à-dire qu'elles utilisent des équations d'accélération pour interpoler les valeurs entre elles. Par exemple, lors de la création de notre première animation, celle-ci utilisait une accélération linéaire :

 
Sélectionnez
<EasingColorKeyFrame KeyTime=»00:00:00» Value=»Black»/>
<EasingColorKeyFrame KeyTime=»00:00:02» Value=»Red»/>
Image non disponible
Figure 6.7 - Les différents types d'images clés et d'interpolation
Image non disponible
Figure 6.8 - Choisir le type d'interpolations dans Blend

Lorsqu'aucune accélération n'est définie, celle-ci est linéaire (Linear). Vous trouverez les différents types d'interpolation ci-dessous.

  • Discrete : n'interpole pas les valeurs entre deux clés d'animation. L'animation est jouée de manière brutale. La propriété ciblée par l'animation ne change de valeur (Value) qu'à l'instant où la tête de lecture arrive au temps (KeyTime) défini dans l'image clé. L'animation n'est pas fluide mais saccadée. Il faudra cliquer sur l'onglet Hold In pour utiliser ce type de clé.
  • Easing : les clés préfixées par ce mot possèdent en plus une propriété (Easing-Function) permettant de définir une équation d'accélération. C'est exactement ce type que vous utiliserez pour faire rebondir une balle, par exemple. Nous reviendrons sur ce type de clé à la section 6.3 Bouton générique.
  • Linear : dans la vie réelle, aucun objet ne possède de mouvement linéaire. Plus vous utiliserez ce type de clé et moins votre animation sera intéressante, car prévisible. Vous aurez parfois besoin de ce type d'animation pour les rotations, par exemple. Toutefois, les animateurs traditionnels les évitent le plus possible. Il n'y a pas de moyen simple dans Blend de spécifier ce type de clé on utilise plutôt une accélération linéaire - ce qui revient au même résultat.
  • Spline : vous pouvez définir vous-même la courbe d'accélération manuellement, ce qui peut être plus précis dans certains cas. Pour gérer vous-même l'accélération, cliquez sur l'onglet KeySpline (voir section 6.3 Bouton générique).

5-6-5-2. Interpolations sans clé d'animation

Silverlight apporte beaucoup de souplesse et de facilité dans la création de transitions grâce aux propriétés From correspondant à la valeur de départ, To qui est la valeur de destination et By une valeur de destination relative à la valeur de départ. Ces propriétés sont utilisables par les animations qui n'emploient pas de clés (indiquées par le suffixe UsingKeyFrames), soit :

  • ColorAnimation ;
  • DoubleAnimation ;
  • PointAnimation.

Voici une manière d'écrire une telle animation. Remplacez le XAML décrivant l'animation de positionnement X et Y par le code suivant :

 
Sélectionnez
<DoubleAnimation BeginTime="00:00:00" From="-25" To="75" 
   Duration="00:00:02" Storyboard.TargetName="textBlock" Storyboard.
   TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)" />

<DoubleAnimation BeginTime="00:00:00" From="-20" To="60" 
   Duration="00:00:02" Storyboard.TargetName="textBlock" Storyboard.
   TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)
   [3].(TranslateTransform.Y)" />

Comme aucune propriété KeyTime n'est précisée, il vous faut définir la durée de l'animation via la propriété Duration. Ce type d'animation ne permet de définir qu'une valeur de départ et d'arrivée et dans ce cas, Blend n'affiche pas de clés. Dépliez l'arborescence des objets animés pour visualiser les propriétés animées (voir Figure 6.9).

Image non disponible
Figure 6.9 - Affichage d'une animation sans clé d'animation

Blend ne crée ce type d'animation que dans le cas où vous utilisez le gestionnaire d'états visuels (voir section 6.4 Le gestionnaire d'états visuels).

5-6-6. La classe Storyboard

Une instance de la classe Storyboard représente l'enveloppe de l'animation. Chaque occurrence de Storyboard peut avoir plusieurs sous-séquences d'animation distinctes possédant des comportements et des propriétés qui leurs sont propres. Par exemple, l'une de ces séquences ciblera un rond et l'autre un bouton. L'une pourra débuter à la seconde 2 du Storyboard, et l'autre ciblera une propriété de type couleur. Toutes les combinaisons sont possibles. Les animations sont conçues comme des objets indépendants complètement séparés de l'aspect visuel des objets. Autrement dit, elles n'appartiennent à aucun élément graphique, mais peuvent cibler n'importe lequel d'entre eux (voir Figure 6.10).

Image non disponible
Figure 6.10 - Principe du Storyboard

La classe Storyboard possède des propriétés communes aux autres classes héritant de TimeLine. Toutefois, c'est la seule classe possédant des méthodes de contrôle de l'animation, dont voici une liste :

  • Begin ; cette méthode permet de lire l'animation depuis la seconde 0, à chaque appel de la méthode, le Storyboard est joué depuis le début ;
  • GetCurrentState : cette méthode renvoie l'état actuel du Storyboard, s'il est en cours de lecture ou s'il est stoppé ;
  • GetCurrentTime : retourne une valeur de type TimeSpan correspondant au temps écoulé depuis le départ de l'animation, et cela à l'instant où la méthode est invoquée ;
  • Pause : met la lecture en pause ;
  • Resume : redémarre une animation mise en pause ;
  • Seek : cette méthode attend une instance de TimeSpan afin de positionner la tête de lecture à un temps donné de l'animation ;
  • Stop : arrête la lecture du Storyboard et repositionne la tête de lecture à la seconde 0.
    Les trois méthodes SetTarget, SetTargetName et SetTargetProperty sont statiques et définies dans la classe Storyboard. Elles sont donc invoquées par celle-ci. Elles sont très importantes, car elles montrent à quel point le modèle d'animation de Silverlight est souple. Une animation au sein de la plate-forme Silverlight est un objet indépendant de l'objet animé ou de la propriété ciblée. Ces trois méthodes prennent en premier paramètre une instance de type TimeLine, donc tout type d'animation. Les animations de type ColorAnimation, Storyboard ou Double-Animation-UsingKeyFrame, entre autres, en font partie. En second paramètre, il vous faudra passer une valeur attendue par la méthode. La méthode SetTarget attend, par exemple, une instance de Dependency-Object comme second argument.
  • SetTarget : précise la référence de l'objet graphique à animer.
  • SetTargetName ; définit le nom de la cible à animer, elle est à utiliser côté XAML.
  • SetTargetProperty : cible la propriété à interpoler sur l'objet ciblé, par exemple l'opacité (Opacity) ou la largeur (Width).

Ces méthodes statiques sont très utiles. Prenons le cas d'un designer interactif. Il réalise une animation sous Blend pour un menu spécifique. L'un de ses collègue développeur souhaite réutiliser son animation pour la réaffecter à d'autres menus. Afin d'éviter un travail fastidieux de recopie et de réaffectation au sein de Blend, le développeur récupère la référence du Storyboard du designer, puis il réaffecte dynamiquement, comme cible, une autre instance de DependencyObject au Storyboard :

 
Sélectionnez
Storyboard.SetTarget( MonAnimationCool, MonNouveauMenu) ;
MonAnimationCool.Begin(;

Nous allons maintenant passer à la pratique afin d'assimiler tous les concepts et principes que nous avons appris. Cependant, n'oubliez pas que Blend est un outil de mise en forme XAML. La nature profonde du XAML, basée sur les relations familiales, rend impossible la gestion de tous les cas d'imbrication ou d'écriture directement par l'interface graphique de Blend. Parfois, écrire ou copier-coller du XAML dans l'éditeur de code sera plus pratique et rapide, voire incontournable.

5-7. Animer avec Expression Blend

5-7-1. Les bonnes pratiques

Lorsque vous souhaitez animer un visuel, plusieurs bonnes pratiques importantes doivent être prises en compte. Si vous avez déjà de bonnes notions en animation traditionnelle, vous pouvez ignorer ce passage. Dans tous les cas, les règles sont faites pour être outrepassées, mais elles servent de cadre et évoluent avec les mœurs. Voici certaines d'entre elles :

  • évitez d'animer trop d'objets en même temps ;
  • arrangez-vous pour que l'œil ne soit pas accroché à trop d'endroits en même temps ; essayez toujours de définir un sens de lecture et une unité d'animation ;
  • les animations de menus ne doivent pas excéder une seconde, dans le cas contraire, les animations deviennent prévisibles et pesantes lors d'une utilisation prolongée ;
  • évitez autant que possible les animations linéaires, rien n'est linéaire dans la nature, seules les mathématiques décrivent de tels mouvements, et cela donne un aspect plat, monotone et robotique aux animations ; parfois un simple ralenti fait des merveilles ;
  • simplifiez vos animations et concentrez-vous avant tout sur les sensations que vous souhaitez créer, l'animation doit être complètement intégrée dans le site ou l'application et en prolonger l'unité graphique.
  • les transitions sont aussi importantes que les interfaces, elles représentent un lien logique et sensible entre chaque interface, soignez-les particulièrement, mais restez sobre ; Silverlight cible avant tout les applications riches ;
  • essayez d'introduire des éléments inattendus au cours de vos animations, cela ajoute de la profondeur à vos applications et l'utilisateur voudra les explorer un peu plus pour découvrir de nouveaux détails ;
  • Rythmez vos animations.

5-7-2. Créer une animation d'introduction

Dans Blend, ouvrez le projet SiteAgencePortfolio créé dans les précédents chapitres. Nous allons l'utiliser pour affiner notre compréhension du modèle d'animation à travers la création d'une animation d'introduction. Vous trouverez également ce projet dans l'archive pleinEcran_maquette.zip du dossier chap6 des exemples du livre.

Nous allons découper les étapes de notre animation afin de l'enrichir. Cela permet de réfléchir et d'imaginer le tempo facilement sans pour autant nous plonger directement dans l'aspect technique de la réalisation. Voici chaque étape de notre animation d'introduction :

  • rien n'est présent sur l'écran au lancement ;
  • les menus du haut apparaissent les uns après les autres avec un léger effet de déplacement vertical (du haut vers le bas) et. la durée totale de cette partie de l'animation ne doit pas excéder une seconde ;
  • le centre du site apparaît au milieu de l'écran ;
  • les liens en bas apparaissent comme s'ils émergeaient du cadre central en 4/10e de seconde environ ;
  • le bouton plein écran apparaît pratiquement en même temps.

En totalité, l'animation ne doit pas excéder trois secondes. Créez une nouvelle animation et nommez la ressource Storyboard AnimIntro. Vous n'êtes pas obligé de créer les séquences d'animation dans l'ordre chronologique. Vous allez commencer par créer l'apparition du centre de la page. Sélectionnez le composant Border et nommez-le Contenu-Page.

Nous nommons l'objet, car en XAML une animation ne peut cibler que des objets nommés. Si vous l'animez sans lui donner de nom, Blend le nommera à votre place. Cela est gênant, car le nom donné sera trop générique et peu explicite.

Une fois nommé, définissez son opacité à 0 %. Il s'agit bien de pourcentage, car la plage de valeurs acceptées en C# ou en XAML se situe entre 0 et 1. Positionnez maintenant la tête de lecture à la seconde 1,6, puis cliquez sur l'icône carrée à droite de la propriété Opacity. Un nouveau menu apparaît, cliquez sur l'option "Record Current Value" (voir Figure 6.11).

Image non disponible
Figure 6.11 - Création d'un nouvelle clé d'animation avec la valeur courante d'une propriété

Vous venez de créer une nouvelle clé d'animation à la seconde 1,6. Cela signifie que durant la première clé et la seconde, l'opacité ne change pas et reste à zéro. Comme l'animation n'a pas besoin d'être fluide, vous pouvez également sélectionner la seconde clé, puis choisir l'option Hold In pour en faire une clé d'animation de type Discrete (voir Figure 6.12).

Image non disponible
Figure 6.12 - Clé d'animation sans interpolation

Déplacez la tête de lecture à la seconde 2 et modifiez l'opacité avec une valeur de 100. Testez votre animation. Le centre du site reste transparent durant une seconde et demie, puis apparaît. Votre ligne de temps doit correspondre à la Figure 6.13.

Image non disponible
Figure 6.13 - Animation du composant Border

Chacun des objets animés du site va suivre cette logique d'animation avec quelques différences de tempo. Pour vous simplifier la vie, vous pouvez dupliquer le code XAML créé et cibler un autre composant comme, par exemple, le premier de nos menus qui est contenu au sein du WrapPanel. Vérifiez tout d'abord que tous les boutons sont nommés pour qu'ils puissent être ciblés en XAML (voir Figure 6.14).

Image non disponible
Figure 6 .14 - Nommage des menus

Vous pouvez maintenant passer en mode d'édition XAML. Voici la partie du code généré par Blend :

 
Sélectionnez
<Storyboard x:Name="AnimIntro">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
         Storyboard.TargetName="ContenuPage" 
         Storyboard.TargetProperty="(UIElement.Opacity)">
      <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
      <DiscreteDoubleKeyFrame KeyTime="00:00:01.6" Value="0"/>
      <EasingDoubleKeyFrame KeyTime="00:00:02" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
</Storyboard>

Animer d'autres objets graphiques est très simple à partir de cette base. Dupliquez le nœud XAML DoubleAnimationUsingKeyFrame et remplacez la cible, soit la valeur de Storyboard.TargetName, par NouveautesBtn. Ensuite, pour les deuxième et troisième clés d'animation, définissez la valeur de la propriété KeyTime respectivement à 0 seconde et 3/10e et 0 seconde et 6/10e :

 
Sélectionnez
<Storyboard x:Name="AnimIntro">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
        TargetName="ContenuPage" Storyboard.TargetProperty=
        "(UIElement.Opacity)">
      <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
      <DiscreteDoubleKeyFrame KeyTime="00:00:01.60000" Value="0"/>
      <SplineDoubleKeyFrame KeyTime="00:00:02" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
        TargetName="NouveautesBtn" Storyboard.TargetProperty=
        "(UIElement.Opacity)">
      <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
      <DiscreteDoubleKeyFrame KeyTime="00:00:00.3" Value="0"/>
      <EasingDoubleKeyFrame KeyTime="00:00:00.6" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
</Storyboard>

Répétez cette opération pour les autres menus en décalant la seconde et la troisième clés d'1/10e de seconde. Pour cela, copiez et collez le code XAML que vous venez d'écrire, puis changez le nom de l'objet ciblé à chaque fois (voir Figure 6.15).

Image non disponible
Figure 6.15 - Ligne de temps avec menus animés

Testez votre animation. Il semble qu'il s'écoule un peu trop de temps entre l'apparition du dernier menu et l'apparition du fond central. Cela ne pose pas de problème. Vous pouvez glisser-déplacer les clés d'animation du fond central pour les rapprocher de celles des menus.

Comme vous l'avez constaté, concevoir une animation en XAML par de simples copier-coller ne prend que quelques secondes. Vous pourrez toujours réaliser en mode création vos propres animations. Passer par le code peut vous sembler éloigné de la création pure, mais cela est très rapide quand vous connaissez précisément vos objectifs. Vous pouvez également affiner les animations générées en mode création. Il suffira de les centraliser au sein d'un projet dédié pour les récupérer à n'importe quel moment.

Nous allons maintenant finir l'animation en faisant apparaître le composant StackPanel contenant le pied de page, puis le bouton plein écran. Procédez exactement de la même manière et faites en sorte que le pied de page et le bouton apparaissent en dernier. L'animation d'introduction est presque terminée. Celle-ci est encore un peu fade, car nous ciblons uniquement l'opacité des objets. De plus, nous n'avons défini aucune accélération sur chaque animation.

5-7-3. Déclencher des animations

Pour tester votre animation vous pouvez ouvrir la bibliothèque de composants, puis sélectionner l'onglet des comportements (Behaviors). Ensuite, glissez le comportement ControlStoryboard-Action sur le composant UserControl racine. Celui-ci est ajouté au sein de l'arbre visuel, mais ce n'est pas un composant graphique, il va simplement nous aider à jouer l'animation au chargement de l'application. Pour la propriété EventName, définissez la valeur Loaded. Pour le champ Story-board, sélectionnez AnimIntro (voir Figure 6.16).

Image non disponible
Figure 6.16 - Configuration du comportement

Ce comportement permet de jouer l'animation lorsque le contrôle UserControl racine est chargé. Toutefois l'action à entreprendre peut être différente. Par défaut, l'action est de lire le Storyboard. Appuyez sur la touche F5 de votre clavier pour tester votre animation dans des conditions réelles. Pour revoir plusieurs fois l'animation se jouer, vous n'avez qu'à rafraîchir la page au sein du navigateur. Nous apprendrons à la section 6.3 Bouton générique à contrôler des Story-boards avec C#.

5-7-4. Les transformations relatives

Pour donner un peu de relief, nous allons animer d'autres propriétés que l'opacité. Pour les menus, nous pourrions par exemple les faire apparaître tout en les déplaçant de haut en bas. Sélectionnez le premier menu, positionnez la tête de lecture à la seconde 0. Tout en maintenant la touche Maj enfoncée, appuyez deux fois sur la flèche du haut. De cette manière, le menu est déplacé de 20 pixels vers le haut. Déplacez la tête de lecture de 3/10e, puis repositionnez le menu de 20 pixels vers le bas. Celui-ci a repris sa position d'origine. À ce stade, notre objet est animé alors qu'il n'est toujours pas visible. Jusqu'à maintenant, pour décaler nos animations, nous avions besoin de trois clés d'animation. Toutefois, il existe une autre manière de procéder.

Toutes les classes héritant de la classe abstraite TimeLine possèdent la propriété BeginTime. Dépliez l'arborescence du menu NouveautesBtn et sélectionnez la ligne RenderTransform. Passez ensuite en mode d'édition mixte pour faire apparaître le code XAML correspondant à ce nœud, modifiez la valeur de la propriété BeginTime à 0 seconde et 4/10e :

 
Sélectionnez
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00.3" 
      Storyboard.TargetName="NouveautesBtn" Storyboard.   
      TargetProperty="(UIElement.RenderTransform).(TransformGroup.
      Children)
      [3].(TranslateTransform.Y)">
   <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-20"/>
   <EasingDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
   </DoubleAnimationUsingKeyFrames>

Vous venez de décaler l'animation de transparence de 3/10e de seconde. Cela vous permet d'animer le déplacement du menu au même instant où vous le faites apparaître. Au sein de Blend, la zone grise du déplacement est décalée vers la droite de 0/10e (voir Figure 6.17).

Image non disponible
Figure 6.17 - Décaler une séquence d'animation au sein de Blend

Regardez maintenant la propriété ciblée par notre séquence d'animation :

 
Sélectionnez
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[3].(TranslateTransform.Y)">

L'accès à la propriété Y n'est pas simple. En fait, dès que nous avons modifié la position de notre menu, Blend y a automatiquement généré un nœud de transformation relative. Pour rappel, les transformations relatives permettent de s'affranchir des contraintes liées à la mise en page du conteneur (voir section 5.5.3.2 Introduction aux RenderTransform). Lorsque vous animez des objets, Blend privilégiera toujours l'utilisation des transformations relatives. Pour des raisons pratiques, le nœud RenderTransform est toujours généré par Blend de la même manière. Il existe toutefois de nombreuses façons d'écrire ce type de nœud. Ne vous formalisez pas si vous rencontrez une écriture différente. Voici le nouveau code XAML décrivant notre menu :

 
Sélectionnez
<Button x:Name="NouveautesBtn" Height="Auto" Width="Auto" 
      Content="Nouveautés" Margin="0,0,20,0" FontSize="14" 
      FontFamily="Trebuchet MS" Visibility="Visible" 
      RenderTransformOrigin="0.5,0.5">
   <Button.RenderTransform>
      <TransformGroup>
         <ScaleTransform/>
         <SkewTransform/>
         <RotateTransform/>
         <TranslateTransform/>
      </TransformGroup>
   </Button.RenderTransform>
</Button>

La conséquence directe du ciblage d'un nœud RenderTransform par une animation est que, si celui-ci n'existe pas réellement dans la balise du composant, Blend lève une erreur de ciblage. Pour le démontrer, il suffit de copier-coller notre nouvelle animation tout en ciblant les autres boutons à chaque fois et en décalant de 1/10e de seconde la valeur de BeginTime (voir Figure 6.18).

Image non disponible
Figure 6.18 - Erreur levée lorsque l'on cible un nœud RenderTransform existant

Pour résoudre ce problème, vous devez coller le nœud RenderTransform suivant au sein de chaque menu :

 
Sélectionnez
   <Button.RenderTransform>
      <TransformGroup>
         <ScaleTransform/>
         <SkewTransform/>
         <RotateTransform/>
         <TranslateTransform/>
      </TransformGroup>
   </Button.RenderTransform>
</Button>

Chaque menu est maintenant animé du haut vers le bas. L'animation prend peu à peu plus d'ampleur. Vous pouvez procéder de la même manière pour le composant StackPanel représentant le pied de page. Vous risquez cependant de rencontrer quelques difficultés lors de la compilation. Le nœud RenderTransform que nous copions débute par Button.RenderTransform. Or, comme il est situé dans le nœud élément StackPanel, le compilateur lèvera une erreur. Pour éviter tout problème, il suffit de remplacer Button par Framework-Element de cette manière :

 
Sélectionnez
   <FrameworkElement.RenderTransform>
      <TransformGroup>
         <ScaleTransform/>
         <SkewTransform/>
         <RotateTransform/>
         <TranslateTransform/>
      </TransformGroup>
   </FrameworkElement.RenderTransform>
</StackPanel>

La classe FrameworkElement est héritée de tous les composants visuels et elle possède la propriété RenderTransform. Pour cette raison, quel que soit le composant visuel auquel vous souhaitez ajouter un nœud RenderTransform, cette classe fera l'affaire pour le déclarer. En programmation orientée objet, ce concept très puissant est appelé polymorphisme. Il est réalisable à travers l'héritage de classes ou l'implémentation d'interfaces. Concrètement, vous l'utiliserez lorsque vous ne connaîtrez pas à l'avance le type de l'objet - ce qui est le cas ici. Comme les objets graphiques héritent tous de FrameworkElement, vous n'avez pas à connaître le type précis de chacun d'eux pour affecter le nœud RenderTransform.

Voici le code XAML permettant d'animer le pied de page :

 
Sélectionnez
<DoubleAnimationUsingKeyFrames BeginTime="00:00:01.3" Storyboard.
      TargetName="PiedDePage" Storyboard.TargetProperty=
         "(UIElement.RenderTransform).(TransformGroup.Children)[3].
         (TranslateTransform.Y)">
   <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-20"/>
   <EasingDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>

Vous remarquez que le StackPanel est renommé en PiedDePage. L'animation subit un décalage de une seconde et 3/10e afin d'être lue en même temps que celle concernant l'opacité. Pour finir, nous allons faire apparaître le bouton plein écran. Positionnez la tête de lecture à la seconde 0, puis modifiez l'échelle du bouton via l'onglet des transformations relatives. Définissez une échelle ScaleX et ScaleY à 0,5. Cela signifie qu'à cette seconde, il ne possédera que la moitié de ses dimensions d'origine. Déplacez ensuite la tête de lecture à la seconde 0 et 3/10e et définissez l'échelle à 1 pour ScaleX et ScaleY. Décalez cette animation pour qu'elle corresponde exactement à celle de l'opacité. Nous nous approchons d'un premier résultat, il nous reste encore quelques réglages pour voir aboutir l'animation. Vous pouvez récupérer cette étape de l'animation (pleinEcran_maquetteAnimee_1.zip) dans le dossier chap6 des exemples.

5-8. Gérer l'accélération

Comme nous l'avons évoqué dans les bonnes pratiques, il faut éviter les animations linéaires dans la majorité des cas. Silverlight vous permet d'éviter cet écueil grâce à trois types de mode d'animation : Spline, Easing et Discrete. Le mode Discrete n'est pas pertinent, car aucune interpolation n'est réalisée par Blend. Dans ce cas, la transition est abrupte. Il nous reste les modes Spline et Easing qui font référence aux même principes.

5-8-1. Les principes

Il nous faut d'abord comprendre ce qu'est une accélération avant d'aborder sa gestion au sein de Blend. Pour résumer, l'accélération, qu'elle soit positive ou négative, est une modification de la vitesse au cours du temps. La vitesse est le rythme du changement. Lorsque vous lâchez une balle au-dessus du sol, elle subit l'accélération de la pesanteur correspondant à un G (G pour Gravité), soit 9,82 m/s-2. Cela signifie que lorsque la balle n'est plus tenue, elle tombe de plus en plus rapidement vers le sol. S'il n'y avait pas d'accélération, elle tomberait de manière constante vers le sol (voir Figure 6.19).

Image non disponible
Figure 6.19 - Mouvement sans accélération et avec accélération

Dans le premier cas, le déplacement de la balle est constant, cela est visible, car l'espace entre deux instants égaux est toujours le même. A contrario, dans le second cas, l'espace entre chaque affichage de la balle grandit, car la vitesse augmente au fur et à mesure du temps. Il est également possible de représenter l'accélération grâce à un petit graphique. La Figure 6.20 illustre la représentation d'un ralenti. On s'aperçoit qu'à 20 % du temps de l'animation, 50 % de celle-ci est déjà effectuée. Cela signifie qu'il reste 50 % de l'animation à réaliser en 80 % de temps restant. La vitesse est donc en forte diminution.

Image non disponible
Figure 6.20 - Représentation d'une décélération sous forme de graphique

Nous allons maintenant aborder ce sujet au sein de l'environnement Silverlight et étudier les moyens mis en œuvre par Blend pour gérer l'accélération. Dans Silverlight, l'accélération est toujours définie sur la clé d'arrivée. Lorsque vous souhaiterez en créer une spécifique entre deux clés d'animation, il vous faudra donc sélectionner la clé d'arrivée, puis modifier ses propriétés. L'avantage de gérer l'accélération sur l'image clé d'arrivée est qu'une clé initiale n'est pas nécessaire pour créer une animation. Pour mieux l'expliquer et le démontrer, nous allons animer le rebond d'une balle avec une seule clé.

5-8-2. Une animation de rebond en une seule clé d'animation

Une animation, dans Silverlight, peut-être définie grâce à une unique clé d'arrivée. Dans ce cas, l'animation est en fait une interpolation entre la valeur en cours de la propriété de l'objet et la valeur de destination définie par la clé. Cela signifie que l'animation sera calculée en fonction de la valeur de départ de manière dynamique, ce qui facilite grandement le travail. Nous allons étudier et créer ce type d'animation dans cette section. Nous aurons à nouveau l'occasion de le faire au Chapitre 6 Boutons personnalisés.

5-8-2-1. Créer la balle

Au sein de Blend, créez une nouvelle application Silverlight nommée AnimRebond. Nous allons commencer par créer une balle. Créez deux cercles parfaits, l'un sur l'autre, en instanciant le composant Ellipse. Sélectionnez-les tous les deux, puis groupez-les au sein d'une grille en utilisant le raccourci Ctrl+g. Nommez la grille MaBalle (voir Figure 6.21).

Image non disponible
Figure 6.21 - Imbrication de la balle

Cachez l'instance d'Ellipse qui est à l'avant-plan. Cela vous permet de travailler sur l'autre de manière plus simple. Définissez-lui un dégradé radial partant de l'orange clair vers l'orange foncé. Pour créer un dégradé, au sein du panneau des propriétés, sélectionnez Fill puis le mode dégradé radial. Modifiez ensuite les picots pour gérer la couleur du début de dégradé et celle de fin de dégradé. Ceux-ci sont situés sur la réglette des dégradés (voir Figures 6.22 et 6.23).

Image non disponible
Figure 6.22 - Créer un dégradé radial

Depuis la version 3 de Blend, il est possible de modifier les picots directement au sein de la vue de création. Pour cela cliquez sur l'icône de gestion des dégradés (voir Figure 6.23).

Image non disponible
Figure 6.23 - Modifier un dégradé radial au sein de la fenêtre de création

Grâce à cet outil, vous pouvez directement, et de manière sensible, modifier et gérer le remplissage d'un dégradé. Faites apparaître le second cercle, puis au sein du panneau des transformations relatives, définissez une échelle en ScaleX à 0,6 et laissez l'échelle ScaleY à sa valeur. Définissez également un dégradé de couleur, mais dans les tons vert ou bleu. Le résultat est reproduit à la Figure 6.24.

Image non disponible
Figure 6.24 - Une balle en plastique

5-8-2-2. Les équations d'accélération

Maintenant que vous avez créé la balle, il ne reste plus qu'à réaliser l'animation. Pour cela, nous allons utiliser les équations d'accélération. Cliquez sur l'icône correspondante (Image non disponible) et nommez l'animation AnimRebond. Une fois en mode d'enregistrement, déplacez le composant Grid MaBalle de 200 pixels vers le bas et de 100 pixels vers la droite. Vous venez de créer deux clés d'animation à la seconde 0, la première pour le déplacement en X et la seconde pour le déplacement en Y. Déplacez la clé principale à la seconde 2, puis dépliez complètement l'arbre visuel afin de sélectionner la clé générée par le déplacement vertical. Dans le panneau des propriétés, cliquez sur la liste déroulante contenant les équations d'accélération et choisissez celle décrivant un rebond à l'arrivée (voir Figure 6.25).

Vous pouvez régler cette accélération grâce à deux paramètres : le facteur de rebond (Bounciness) et le nombre de rebonds (Bounces). Plus le facteur de rebond est faible, plus les rebonds gagneront en amplitude, plus le facteur sera élevé, moins les rebonds seront visibles. Pour chaque composant visuel, il est possible de définir, en une seule fois, une accélération différente pour chaque clé ou encore pour toutes les clés situées à la même seconde. Il suffit pour cela de configurer la clé d'animation située au même niveau que l'objet. De cette manière, toutes les propriétés sont interpolées de manière identique.

Image non disponible
Figure 6.25 - Choisir une accélération de type rebond à l'arrivée

5-8-2-3. Les courbes d'accélération

Nous allons choisir un autre type d'accélération pour l'axe des X. Sélectionnez la clé correspondante et dans le panneau des propriétés choisissez l'onglet KeySpline. Configurez la courbe d'accélération manuellement pour créer un léger effet de ralenti. Déplacez le point jaune situé en haut à droite du graphique vers l'intérieur du graphe (voir Figure 6.26).

Image non disponible
Figure 6.26 - Personnaliser une courbe d'accélération

L'extrémité de la tangente est déterminée en sortie de courbe par les valeurs X2 et Y2. Voici le XAML généré pour les deux types d'accélération que nous venons de définir :

 
Sélectionnez
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
      TargetName="MaBalle" Storyboard.TargetProperty="(UIElement.
      RenderTransform).(TransformGroup.Children)[3].
     (TranslateTransform.Y)">
   <EasingDoubleKeyFrame KeyTime="00:00:01" Value="200">
      <EasingDoubleKeyFrame.EasingFunction>
         <BounceEase Bounciness="2"/>
      </EasingDoubleKeyFrame.EasingFunction>
   </EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
      TargetName="MaBalle" Storyboard.TargetProperty="(UIElement.
      RenderTransform).(TransformGroup.Children)[3].
        (TranslateTransform.X)">
      <SplineDoubleKeyFrame KeyTime="00:00:01" Value="50" 
         KeySpline="0,0,0.3,0.8"/>
</DoubleAnimationUsingKeyFrames>

Vous pouvez remarquer, en gras, les deux types de clés correspondant aux modifications que nous avons apportées. La propriété KeySpline contient les coordonnées des points de tangence soit, dans l'ordre, X1, Y1, X2 et Y2. Testez votre animation au sein de Blend, puis dans votre navigateur préféré. Vous pouvez utiliser un comportement de type Control-StoryboardAction afin de déclencher la lecture de l'animation à l'exécution (voir la section 5.9.2 Instanciation dynamique de ressources Storyboard). Comme vous le constatez, une seule clé d'animation est nécessaire.

Le principal avantage de ce comportement est de permettre une mise à jour plus facile de vos animations en évitant de figer par une clé leur point de départ. Nous allons le démontrer simplement. Fermez le Storyboard AnimRebond et déplacez votre balle n'importe où au sein du conteneur LayoutRoot, puis recompilez votre application et testez-la. Comme l'animation utilise les transformations relatives, elle est indépendante des contraintes du conteneur. De plus, comme elle ne possède qu'une clé d'arrivée, elle a exactement le même effet quelle que soit la position par défaut de l'objet. Autrement dit l'animation est totalement indépendante de la scène principale ou de l'objet ciblé.

Pour vous en convaincre, créez un rectangle n'importe où dans LayoutRoot et nommez-le MonCarre. Créez-y un nœud de transformations relatives, RenderTransform (voir section 5.10 Les transformations relatives), puis dupliquez l'animation en copiant-collant le code XAML.

Vous pouvez également dupliquer l'animation au sein de l'interface visuelle de Blend. Pour cela, ouvrez l'animation AnimRebond en cliquant sur l'icône de liste d'actions (Image non disponible). Une fois à l'intérieur, cliquez sur l'icône déroulante (Image non disponible) située juste à droite de l'icône d'ajout et sélectionnez l'option Dupliquer. Vous êtes désormais dans l'animation AnimRebond_copy. Cliquez à nouveau sur l'icône et choisissez Renommer.

Changez le nom par AnimRebondCarre. Passez en mode d'édition mixte, puis modifiez la cible des deux sous-séquences d'animation de type DoubleAnimation par MonCarre. Vous venez de dupliquer l'animation et vous avez changé sa cible en quelques secondes. Vous pouvez tester l'animation directement dans Blend.

Grâce à l'icône de liste d'actions, vous pouvez ajouter, supprimer, renommer et dupliquer une ressource Storyboard. Vous pouvez même inverser l'ordre des clés d'animation. Toutefois, cette dernière opération n'est pas sans surprises, car seul l'ordre des enfants de la propriété KeyFrames est inversé. Autrement dit, le timing de l'animation ne l'est pas réellement. Il vous faudra donc un peu plus de travail si vous avez plus de deux clés pour la même propriété.

L'exercice finalisé est dans l'archive AnimRebond.zip du dossier chap6 des exemples.

5-8-3. Améliorer l'animation d'introduction

Nous allons maintenant améliorer notre animation principale en gérant l'accélération des clés d'animation. Cela ne devrait pas poser beaucoup de problèmes. Nous allons cependant limiter à deux le nombre de types d'accélération. Ouvrez la solution faisant référence au projet Site-AgencePortFolio là où nous l'avions laissé à la section 6.2 Style visuel. Vous trouverez aussi ce fichier dans l'archive pleinEcran_maquetteAnimee_1.zip du dossier chap6 des exemples du livre.

Sélectionnez la dernière image clé de chaque animation de modification relative, Render-Transform, et définissez une accélération de type Back Out (voir Figure 6.27).

Cette équation d'accélération est assez pratique, car la valeur de la propriété dépasse un peu celle de destination avant de l'atteindre avec un effet de ralenti. Ce genre d'accélération est utile, car impossible à réaliser avec les courbes d'accélération de type Spline. Il est en effet impossible de définir un point de tangence dont les coordonnées dépassent 1 en Y, soit 100 % de réalisation du mouvement.

Si vous trouvez le tempo trop rapide, espacez légèrement chaque clé. De même, si l'œil de l'utilisateur n'est pas assez attiré en bas à droite, pour l'animation du pied de page en fin d'introduction, il vous suffit de ralentir l'animation. Toutefois, d'un point de vue design, de nombreux changements seront mis en œuvre. Le gris clair des contrôles par défaut est neutre et n'attire pas le regard. D'autres moyens sont encore à notre disposition pour soutenir cette animation et l'intégrer au sein d'une charte graphique.

Un autre facteur est également très important : les dimensions du site. Comme le site occupe 100 % de la fenêtre du navigateur, si ce dernier accapare tout votre écran, les difficultés de décryptage pour l'utilisateur en seront accrues. Le redimensionnement est très pratique, mais peut influencer l'animation et son impact visuel au sein d'une application ou d'un site Web. Pour finir, sélectionnez la dernière image clé de chaque animation d'opacité, puis définissez soit une courbe d'accélération, soit une équation d'accélération avec ralenti.

Image non disponible
Figure 6.27 - Personnalisez l'accélération des nœuds RenderTransform

5-9. Animer avec C#

5-9-1. L'intérêt d'animer avec C#

Il nous faut tout d'abord lever toute ambiguïté en précisant que l'animation d'un site incombe avant tout à l'animateur, au designer interactif ou au directeur artistique. Celle-ci est directement liée à la charte graphique, à l'expérience utilisateur et donc à l'ergonomie. Toutefois, notre but dans cette section est d'apprendre à créer des animations dynamiquement. Les langages C# ou VB ne remplaceront bien sûr jamais un logiciel comme Blend d'un point de vue créativité, mais l'utilisation d'un langage logique ouvre de nombreuses possibilités et offre une grande souplesse de production.

On le constate aisément avec des technologies du type Processing. Cette dernière présente l'avantage de générer des animations très esthétiques en temps réel. Pour de plus amples informations sur cette technologie, rendez-vous sur le site : http://processing.org. Toutefois ces technologies s'inscrivent souvent dans une démarche artistique. Les outils et les bibliothèques proposés sont par nature orientés vers le visuel et l'interactivité et non vers la fonctionnalité, au contraire de C#. La création d'animations via C# ou VB présente tout de même de nombreux intérêts, si celle-ci est encadrée par des créatifs. Grâce à C# :

  • on évite le travail rébarbatif de recopie des animations sous Blend sans enlever la conception de celle-ci aux animateurs ;
  • il est possible d'ajouter une couche d'interactivité utilisateur supplémentaire en mettant à jour les animations dynamiquement ;
  • on peut déclencher des animations aléatoirement ou de manière rythmée ;
  • l'animation de particules est à notre portée, cela nous permet de simuler des fluides, de la fumée, de la pluie ou un vol d'oiseau.

Tous ces avantages sont attrayants et justifient un investissement dans ce domaine. Nous allons aborder chacun d'eux d'un point de vue pratique.

5-9-2. Instanciation dynamique de ressources Storyboard

Ouvrez le projet AnimRebond que vous avez réalisé dans un précédent exercice (AnimRebond.zip). Dans un premier temps, nous allons créer la même animation que celle que nous avions réalisée, mais uniquement avec C# pour apprendre les concepts.

5-9-2-1. Créer l'animation de rebond en C#

La première étape consiste à dupliquer la balle. Nommez la nouvelle balle MaBalle2. Sélectionnez la grille LayoutRoot, puis dans le panneau des événements de l'objet entrez PlayAnimBalle pour l'événement MouseLeftButtonDown (voir Figure 6.28).

Blend ouvre automatiquement le fichier de code logique C# correspondant à notre fichier XAML et crée une méthode PlayAnimBalle. Elle se déclenchera à chaque fois que l'utilisateur cliquera sur la grille principale. Cette méthode nous permettra de déclencher la lecture du Storyboard, que nous allons créer par code.

Image non disponible
Figure 6.28 - Définir une méthode d'écoute pour l'événement MouseKLeftButtonDown de l'objet LayoutRoot

Le code ci-dessous expose la création d'une nouvelle ressource Storyboard :

 
Sélectionnez
public partial class MainPage : UserControl
{

   Storyboard AnimRebondCode;

   public MainPage()
   {
      InitializeComponent();

      Loaded += new RoutedEventHandler(MainPage_Loaded);
   }
   void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      //on commence par instancier une nouvelle ressource Storyboard
      AnimRebondCode = new Storyboard();
      
      //on crée ensuite la sous-séquence d'animation 
      //en lui définissant une nouvelle destination
      DoubleAnimation DaY = new DoubleAnimation();
      
      //on définit la valeur d'arrivée de la propriété 
      //qui sera ciblée, on a le choix entre To et By 
      DaY.To = 200;
      
      //DaY.By = 200; 
      //By permet d'ajouter une valeur de destination
      //relative à la valeur actuelle 

      //Puis une nouvelle équation d'accélération pour la 
      //sous-séquence d'animation
      DaY.EasingFunction = new BounceEase();
      
      //La classe Storyboard nous permet de modifier l'objet
      //ciblé par l'animation via la méthode statique SetTarget
      Storyboard.SetTarget(AnimRebondCode, MaBalle2);
      
      //La classe Storyboard nous fournit également une méthode 
      //pour cibler la propriété à animer
      Storyboard.SetTargetProperty(DaY, new PropertyPath("(UIElement.
           RenderTransform).(TransformGroup.Children)[3].
          (TranslateTransform.Y)"));

      //On ajoute la sous-séquence d'animation comme enfant du Storyboard
      AnimRebondCode.Children.Add(DaY);
      
      //l'étape finale facultative consiste à ajouter le Storyboard
      //aux ressources du UserControl racine, soit 
      //this Resources.Add("AnimRebondCodeClé", AnimRebondCode);

   }

   private void PlayAnimBalle(object sender, MouseButtonEventArgs e)
   {
      AnimRebondCode.Begin();
   }
}

Deux ou trois concepts méritent d'être expliqués. Tout d'abord, la création, puis l'utilisation d'un Storyboard contiennent pratiquement les mêmes étapes que l'ajout d'un objet Framework-Element à l'arbre visuel d'une application. On le déclare comme membre de classe pour y accéder depuis n'importe quelle méthode. On l'instancie ensuite au chargement de l'application et pour finir, on l'ajoute comme enfant de la propriété Resources. Cette dernière étape est nécessaire uniquement si vous souhaitez utiliser le Storyboard comme ressource, ce qui n'est pas obligatoire depuis Silverlight 3. Il est conseillé de ne pas l'ajouter par défaut, afin de faciliter la libération des ressources en cas de suppression du Storyboard. Cette propriété est propre à tous les objets de type Framework-Element. N'importe quel objet au sein de l'arbre visuel peut donc posséder des ressources. Nous pourrions donc très bien avoir le code XAML suivant :

 
Sélectionnez
<Grid  >
   <Button Width="100" Height="30">
      <Button.Resources>
         <Storyboard x:Name="monAnimAccessibleDansButton"  ></Storyboard>
      </Button.Resources >
   </Button>
</Grid >

Comme nous l'avons montré à la section 5.9.2 Instanciation dynamique de ressources Storyboard, les objets de type Storyboard sont considérés comme des ressources, car ils ne possèdent pas de représentation visuelle concrète. La propriété Resources fait référence aux dictionnaires de ressources. Elle implémente l'interface IDictionnary. Chaque ressource, lorsqu'elle est ajoutée au dictionnaire, doit posséder une clé d'accès. C'est à cela que sert la chaîne de caractères passée en premier paramètre :

 
Sélectionnez
Resources.Add("AnimRebondCodeClé", AnimRebondCode);

Le second argument représente la référence du Storyboard que nous souhaitons ajouter. Une autre difficulté que vous pouvez rencontrer est le ciblage de la propriété à animer. Dans la ligne de code ci-dessous, nous spécifions le chemin d'accès pour la propriété Y de type Render-Transform :

 
Sélectionnez
Storyboard.SetTargetProperty(DaY, new PropertyPath("(UIElement.
   RenderTransform).(TransformGroup.Children)[3].
   (TranslateTransform.Y)"));

Cela peut paraître un peu barbare, mais vous pouvez copier-coller le chemin d'accès grâce au code XAML généré automatiquement par Blend. De plus, ce type de chemin n'est nécessaire que pour les transformations relatives. Pour d'autres propriétés propres aux objets eux-mêmes, comme Opacity ou Width, vous pouvez utiliser des membres statiques de classe suffixés de Property :

 
Sélectionnez
PropertyPath pp = new PropertyPath( Canvas.WidthProperty );
Storyboard.SetTargetProperty(DaY, pp);

Pour finir, il ne faut pas oublier de cibler l'objet à animer. À cette fin, utilisez la méthode statique SetTarget de la classe Storyboard. Vous devez préciser l'instance de type Time-Line et la cible animée par cette dernière :

 
Sélectionnez
//La classe Storyboard nous permet de modifier l'objet
      //ciblé par l'animation via la méthode statique SetTarget
      Storyboard.SetTarget(AnimRebondCode, MaBalle2);

Nous allons maintenant pousser ce concept un peu plus loin.

5-9-2-2. Mise à jour dynamique de l'animation

Pour l'instant, tout ce que nous faisons peut être réalisé dans Blend. Vous allez ajouter un peu de logique afin d'illustrer l'intérêt de C#. Lorsque l'utilisateur cliquera sur la grille principale, la balle se déplacera aux coordonnées où l'événement se sera produit. Tout d'abord, vous allez afficher les informations concernant le clic du bouton de la souris sur LayoutRoot.

Créez un nouveau membre de classe de type TextBlock, nommé InfoTxt, puis ajoutez-le comme enfant du conteneur LayoutRoot lors du chargement de l'application :

 
Sélectionnez
TextBlock InfoTxt = new TextBlock();
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   LayoutRoot.Children.Add(InfoTxt);
  // le code vu précédemment dans cette méthode…
}

Il faut également modifier la méthode PlayAnimBalle pour récupérer les coordonnées de la souris lors de l'événement OnMouseDown :

 
Sélectionnez
private void PlayAnimBalle(object sender,MouseButtonEventArgs e)
{
   Point Position = e.GetPosition(null);
   InfoTxt.Text = Position.ToString();
   AnimRebondCode.Begin();
}

La structure, de type Position, contient deux valeurs de type de double correspondant à X et Y. Celle-ci est récupérée grâce à l'argument de type MouseButtonEventArgs. Nous reviendrons sur ce type d'argument au Chapitre 7 Interactivité et modèle événementiel. Le champ texte indique désormais les coordonnées de la souris à l'instant où vous cliquez sur le conteneur LayoutRoot. Pour se déplacer aux coordonnées X et Y, nous devons définir une deuxième animation ciblant cette fois l'axe des X :

 
Sélectionnez
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   LayoutRoot.Children.Add(InfoTxt);

   //on commence par instancier une nouvelle ressource Storyboard
   AnimRebondCode = new Storyboard();
   
   //on crée ensuite la sous-séquence d'animation 
   //en lui définissant une nouvelle destination
   DoubleAnimation DaY = new DoubleAnimation();
   DoubleAnimation DaX = new DoubleAnimation();

   #region plus besoin de ce code
   //on définit la valeur d'arrivée de la propriété qui sera ciblée
   //on a le choix entre To et By 
   //DaY.To = 200;
   //Dax.By = 200; 
   //By permet d'ajouter une valeur relative à la valeur actuelle 
   #endregion

   //Puis une nouvelle équation d'accélération pour la 
   //sous-séquence d'animation
   DaY.EasingFunction = new BounceEase();
   DaX.EasingFunction = new BounceEase();
      //La classe Storyboard nous permet de modifier l'objet
      //ciblé par l'animation via la méthode statique SetTarget
   Storyboard.SetTarget(AnimRebondCode, MaBalle2);
            
   //La classe Storyboard nous fournit également une méthode 
   //pour cibler la propriété à animer
   Storyboard.SetTargetProperty(DaY, new PropertyPath("(UIElement.
    RenderTransform).(TransformGroup.Children)[3].
    (TranslateTransform.Y)"));
   Storyboard.SetTargetProperty(DaX, new PropertyPath("(UIElement.   
    RenderTransform).(TransformGroup.Children)[3].
    (TranslateTransform.X)"));

   //On ajoute la sous-séquence d'animation comme enfant du Storyboard
   AnimRebondCode.Children.Add(DaY);
   AnimRebondCode.Children.Add(DaX);
   

}

Il suffit maintenant de modifier notre animation pour que les valeurs de destination soient mises à jour. Toutefois, il faut faire la différence entre les coordonnées de la souris, que nous récupérons sur le conteneur, et les transformations relatives X et Y que nous affectons. Le code suivant est un début mais n'est pas suffisant :

 
Sélectionnez
private void PlayAnimBalle(object sender, MouseButtonEventArgs e)
{
   Point Position = e.GetPosition(null);
   InfoTxt.Text = Position.ToString();
   (AnimRebondCode.Children[0] as DoubleAnimation).To = Position.Y;
   (AnimRebondCode.Children[1] as DoubleAnimation).To = Position.X;
   AnimRebondCode.Begin();
}

Vous remarquez qu'il y a un décalage entre le point de destination des animations X et Y et l'endroit où vous avez cliqué. Ceci est dû aux transformations relatives. Dans le code précédent, à chaque fois que vous cliquez, vous ajoutez les coordonnées de votre souris à la position initiale de la balle au sein du conteneur LayoutRoot. Pour remédier à ce problème, vous avez deux choix. Le plus simple consiste à positionner MaBalle2 en haut à gauche de LayoutRoot en supprimant les marges et en choisissant un alignement à gauche et à droite (voir Figure 6.29).

Image non disponible
Figure 6.29 - Modification des marges et de l'alignement de MaBalle2

Recompilez votre application pour voir le résultat. Elle fonctionne mieux, mais cette solution vous oblige à positionner votre balle en haut à gauche lors de la compilation. La seconde solution - plus pratique - consiste à soustraire les marges existantes et à spécifier un alignement en haut et gauche :

 
Sélectionnez
private void PlayAnimBalle(object sender, MouseButtonEventArgs e)
{
   Point Position = e.GetPosition(null);            
   InfoTxt.Text = Position.ToString();
   double NewPosY = Position.Y - MaBalle2.Margin.Top-(MaBalle2.Height/2);
   double NewPosX = Position.X - MaBalle2.Margin.Left-(MaBalle2.Width/2);
   (AnimRebondCode.Children[0] as DoubleAnimation).To = NewPosY;
   (AnimRebondCode.Children[1] as DoubleAnimation).To = NewPosX;
   AnimRebondCode.Begin();
}

Dans le code précédent, la moitié de la largeur et de la hauteur de la balle est également soustraite. Ceci vous permet de positionner le centre de la balle exactement là où vous avez cliqué.

5-9-3. Affectation dynamique de Storyboards

Vous allez affecter la même animation à différents objets contenus dans une grille. Créez un nouveau projet nommé AnimRebond_Dynamic. Commencez par la mise en place du projet sous Expression Blend. Au sein du conteneur LayoutRoot, créez trois copies de notre balle ayant pour nom MaBalle1, MaBalle2 et MaBalle3. En arrière-plan des balles, et toujours dans la grille LayoutRoot, instanciez un nouvel objet de type Grid et appelez-le ZoneClick. Cette grille doit posséder une marge basse de 110 pixels. Vous pouvez lui affecter une couleur d'arrière-plan pour la visualiser plus facilement. La marge vous permet de positionner un composant StackPanel horizontal entre le bas de la zone de clic et le bord inférieur de notre application. Le composant contiendra les boutons qui modifieront la cible de l'animation, ainsi que les coordonnées lors du clic de souris. Autrement dit, nous changerons dynamiquement la référence correspondant à l'objet ciblé. Cela est réalisable grâce à la méthode statique SetTarget de la classe Storyboard. Nommez le StackPanel BoutonsInfos et créez les boutons. Pour chacun d'eux, la propriété Content doit être affectée de la valeur Anim Balle 1, Anim Balle 2 ou Anim Balle 3 (voir Figure 6.30).

Image non disponible
Figure 6.30 - Interface pour animation dynamique

Le code ressemble à celui de notre projet précédent, toutefois de subtiles modifications nous permettent d'améliorer son comportement. Commencez par recopier le code de la précédente animation. Il vous faut ajouter le membre de classe BalleEnCours (de type FrameworkElement). Il correspond à la référence de la balle actuellement ciblée par l'animation. Spécifiez ensuite une méthode pour l'événement MouseLeftButtonDown de chaque bouton. Vous pouvez utiliser le panneau des événements à cette fin. Nommez ces méthodes SetBalle1, SetBalle2 et Setballe3. Au sein de ces fonctions, vous allez redéfinir la cible de l'animation. Ainsi, lorsque vous cliquerez sur le bouton 3, le Storyboard ciblera la balle 3.

Voici la totalité du code mis à jour :

 
Sélectionnez
public partial class MainPage : UserControl
{
   Storyboard AnimRebondCode;
   FrameworkElement BalleEnCours;
   
   public MainPage()
   {
      InitializeComponent();
      Loaded +=new System.Windows.RoutedEventHandler(MainPage_Loaded);
   }
   
   TextBlock InfoTxt = new TextBlock();

   void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      //Par défaut on anime la première balle
      BalleEnCours = MaBalle1;

      BoutonsInfos.Children.Add(InfoTxt);

      AnimRebondCode = new Storyboard();

      DoubleAnimation DaY = new DoubleAnimation();
      DoubleAnimation DaX = new DoubleAnimation();

      DaY.To = 200;

      DaY.EasingFunction = new BackEase();
      DaX.EasingFunction = new ElasticEase();
      
      //La classe Storyboard nous permet de modifier l'objet
      //ciblé par l'animation via la méthode statique SetTarget
      Storyboard.SetTarget(AnimRebondCode, BalleEnCours);
      
      Storyboard.SetTargetProperty(DaY, new PropertyPath("(UIElement.
      RenderTransform).(TransformGroup.Children)[3].
      (TranslateTransform.Y)"));
      Storyboard.SetTargetProperty(DaX, new PropertyPath("(UIElement.
      RenderTransform).(TransformGroup.Children)[3].(
      TranslateTransform.X)"));

      AnimRebondCode.Children.Add(DaY);
      AnimRebondCode.Children.Add(DaX);
      
   
   }

   private void PlayAnimBalle(object sender, MouseButtonEventArgs e)
   {
      Point Position = e.GetPosition(null);            
      InfoTxt.Text = "X :: "+ Position.X + " - Y :: " + Position.Y;

      double NewPosY = Position.Y - BalleEnCours.Margin.Top - 
            (BalleEnCours.Height / 2);

      double NewPosX = Position.X - BalleEnCours.Margin.Left - 
            (BalleEnCours.Width / 2);
      (AnimRebondCode.Children[0] as DoubleAnimation).To = NewPosY;
      (AnimRebondCode.Children[1] as DoubleAnimation).To = NewPosX;
      AnimRebondCode.Begin();
   }


   private void SetBalle1(object sender, RoutedEventArgs e)
   {
      AnimRebondCode.Stop();
      BalleEnCours = MaBalle1;
      Storyboard.SetTarget((AnimRebondCode.Children[0]), BalleEnCours);
      Storyboard.SetTarget((AnimRebondCode.Children[1]), BalleEnCours);
   }

   private void SetBalle2(object sender, RoutedEventArgs e)
   {
      AnimRebondCode.Stop();
      BalleEnCours = MaBalle2;
      Storyboard.SetTarget((AnimRebondCode.Children[0]), BalleEnCours);
      Storyboard.SetTarget((AnimRebondCode.Children[1]), BalleEnCours);
   }

   private void SetBalle3(object sender, RoutedEventArgs e)
   {
      AnimRebondCode.Stop();
      BalleEnCours = MaBalle3;
      Storyboard.SetTarget((AnimRebondCode.Children[0]), BalleEnCours);
      Storyboard.SetTarget((AnimRebondCode.Children[1]), BalleEnCours);
   }
}

Revenons un peu sur ce code. Le moteur Silverlight ne permet pas à un Storyboard d'être utilisé plusieurs fois, tant que celui-ci est actif. Il nous faut donc libérer la ressource Story-board en invoquant la méthode Stop, avant de pouvoir lui affecter une nouvelle cible :

 
Sélectionnez
private void SetBalle3(object sender, RoutedEventArgs e)
{
   AnimRebondCode.Stop();
   BalleEnCours = MaBalle3;
   Storyboard.SetTarget((AnimRebondCode.Children[0]), BalleEnCours);
   Storyboard.SetTarget((AnimRebondCode.Children[1]), BalleEnCours);
}

Lorsque vous relâchez le bouton gauche de la souris n'importe où sur la zone de clic, la méthode PlayAnimBalle est exécutée. Il est assez pratique dans notre cas d'appeler une méthode lorsque le bouton gauche de la souris est enfoncé (MouseLeftButtonDown), puis d'en appeler une autre quand celui-ci est relâché (MouseLeftButtonUp). Cela vous permet de contrôler sereinement l'enchaînement des étapes. Nous pourrions par exemple définir la nouvelle balle à animer lorsque l'utilisateur appuie sur l'une d'entre elles, puis déclencher l'animation lorsqu'il relâche le bouton. Les objets présents au sein de l'arbre visuel sont tous interactifs, cela vous évite d'instancier des composants Button pour tout et n'importe quoi.

5-9-4. Dupliquer un Storyboard créé dans Blend via C#

Pour réaliser cet exercice, il vous faudra désarchiver MenuAnim_Dynamique.zip du dossier chap6 des exemples du livre. Vous pouvez ouvrir le projet dans Blend ou Visual Studio.

Ce projet est très simple, il contient plusieurs primitives Rectangle, mais seul le premier enfant de la grille est animé. Notre objectif va consister à récupérer l'animation générée dans Blend par le designer interactif, puis à la dupliquer dynamiquement afin de l'affecter à chaque Rectangle contenu dans la grille. Cloner dynamiquement un Storyboard n'est malheureusement pas une opération simple à réaliser, ceci pour deux raisons. La première est que la méthode MemberWiseClone héritée de Object est protégée, donc inaccessible depuis l'extérieur (de plus cette méthode n'est pas satisfaisante, car elle ne clone l'objet qu'en surface). La seconde raison est que la classe XamlWriter, utilisée pour récupérer la chaîne de caractères XAML de toute référence sous WPF, n'est pas supportée par Silverlight et qu'elle est disponible uniquement au sein de WPF. Nous pourrions utiliser l'API de réflexion qui permet de parcourir les types, méthodes, propriétés, etc. Toutefois un tel code serait fastidieux et ne servirait pas notre propos.

Nous allons donc commencer par récupérer la chaîne de caractères correspondant au Storyboard. Il suffit de copier le code XAML généré dans Blend, puis de le coller directement au sein du fichier C# dans le constructeur. Une fois cette étape réalisée, supprimez toutes les propriétés Storyboard.TargetName="Rectangle1", les propriétés BeginTime="00:00:00", ainsi que la propriété x:Name="Anim3D". Vous obtiendrez le résultat ci-dessous :

 
Sélectionnez
<Storyboard>
   <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
      "(UIElement.Projection).(PlaneProjection.RotationY)">
     <EasingDoubleKeyFrame KeyTime="00:00:00" Value="650"/>
     <EasingDoubleKeyFrame KeyTime="00:00:01" Value="0">
        <EasingDoubleKeyFrame.EasingFunction>
            <CubicEase/>
         </EasingDoubleKeyFrame.EasingFunction>
      </EasingDoubleKeyFrame>
   </DoubleAnimationUsingKeyFrames>
   <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
      "(UIElement.Opacity)">
      <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
      <EasingDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
   <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
      "(UIElement.Projection).(PlaneProjection.GlobalOffsetZ)">
      <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-700"/>
      <EasingDoubleKeyFrame KeyTime="00:00:01.5000000" Value="0">
         <EasingDoubleKeyFrame.EasingFunction>
            <CubicEase/>
         </EasingDoubleKeyFrame.EasingFunction>
      </EasingDoubleKeyFrame>
   </DoubleAnimationUsingKeyFrames>
   <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
      "(FrameworkElement.Width)">
      <EasingDoubleKeyFrame KeyTime="00:00:01" Value="51"/>
      <EasingDoubleKeyFrame KeyTime="00:00:01.5000000" Value="150">
         <EasingDoubleKeyFrame.EasingFunction>
            <CubicEase/>
         </EasingDoubleKeyFrame.EasingFunction>
      </EasingDoubleKeyFrame>
   </DoubleAnimationUsingKeyFrames>
</Storyboard>

Visual Studio lève plusieurs erreurs, mais ce n'est que temporaire. Il va nous falloir transformer le XAML en objet de type String. C'est la partie du code un peu rébarbative, mais c'est une phase sensible, car au moindre faux pas le compilateur lèvera une erreur.

Nous devons utiliser la classe StringBuilder pour concaténer une chaîne de caractères. Concaténer revient à mettre plusieurs chaînes de caractères à la suite. Favoriser l'utilisation de StringBuilder optimise grandement la mémoire allouée pour ce type d'opération. Référencez l'espace de noms System.Text via l'instruction using. Nous allons parcourir tous les enfants de la grille LayoutRoot et créer un Storyboard pour chaque UIElement trouvé :

 
Sélectionnez
//espace de noms à ajouter
using System.Windows.Markup;
using System.Collections.Generic;
using System.Text;
//On crée un dictionnaire pour stocker chaque animation créée
Dictionary<UIElement, Storyboard> DicoElementsSB = new 
          Dictionary<UIElement, Storyboard>();

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   CreateStoryboard();
}

private void CreateStoryboard()
{
   foreach (UIElement Ui in LayoutRoot.Children)
   {
      //on récupère l'index de chaque objet dans la liste d'enfants
      int i = LayoutRoot.Children.IndexOf(Ui);
   }
}

Notre objectif est d'injecter dynamiquement la chaîne de caractères XAML. Vous allez maintenant placer le code XAML récupéré au sein de la boucle foreach, puis utiliser la classe StringBuilder. Toutefois, pour que cette opération soit possible, nous devons respecter certaines règles :

  • vous devez toujours ajouter les espaces de noms XAML à l'élément le plus élevé dans la hiérarchie ; dans notre cas, il faudra donc ajouter les espaces de noms au nœud élément Storyboard ;
  • la propriété Name doit toujours prendre une valeur différente si vous la définissez, cependant, il n'est pas utile de conserver et d'affecter cette propriété.
  • pour transformer le XAML en chaîne de caractères, il faut remplacer les apostrophes doubles de chaque propriété par de simples apostrophes, puis encadrer la totalité de la ligne par deux apostrophes doubles, cela vous évitera les erreurs ; Par exemple, KeyTime="00:00:00" deviendra KeyTime='00:00:00'.

Voici le début de ce que doit être votre code :

 
Sélectionnez
foreach (UIElement Ui in LayoutRoot.Children)
{
   int i = LayoutRoot.Children.IndexOf(Ui);

   StringBuilder sb = new StringBuilder("<Storyboard x:Name='Anim3D"+ 
      i +"' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' 
      xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>");
   sb.AppendLine("<DoubleAnimationUsingKeyFrames BeginTime='00:00:00." + 
      i + "' Storyboard.TargetProperty = '(UIElement.Projection).
      (PlaneProjection.RotationY)'>");
   sb.AppendLine("<EasingDoubleKeyFrame KeyTime='00:00:00' Value='650'/>");}

La méthode AppendLine permet de concaténer chaque nouvelle ligne XAML. Pour réaliser cette opération rapidement, vous pouvez utiliser l'outil de remplacement de texte de Visual Studio ou de Blend. Une fois fait, il faut encore transformer les chaînes de caractères créées à chaque itération de la boucle en instance de type Story-board. L'espace de noms System.Windows.Markup que nous avons référencé contient la classe XamlReader qui permet de transformer la chaîne de caractères en instance :

 
Sélectionnez
foreach (UIElement Ui in LayoutRoot.Children)
{
   int i = LayoutRoot.Children.IndexOf(Ui);

   StringBuilder sb = new StringBuilder("<Storyboard x:Name='Anim3D"+ 
      i +"' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/
      presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/
      xaml'>");
   sb.AppendLine("<DoubleAnimationUsingKeyFrames BeginTime='00:00:00." + 
      i + "' Storyboard.TargetProperty = '(UIElement.Projection).
      (PlaneProjection.RotationY)'>");
   sb.AppendLine("<EasingDoubleKeyFrame KeyTime='00:00:00' Value='650'/>");
…
   sb.AppendLine("</Storyboard>");

   //on transforme la chaîne de caractères créée 
   //en une instance de Storyboard 
   Storyboard NewClonedStoryboard = (Storyboard) XamlReader.Load 
                  (sb.ToString());

   //on définit un décalage de temps permettant aux 
   //objets Storyboard de se lancer les uns après les autres
   NewClonedStoryboard.BeginTime = TimeSpan.FromMilliseconds(i * 200);

   //on définit la nouvelle cible à animer sur le Storyboard
   //lui-même au lieu de chaque DoubleAnimation
   Storyboard.SetTarget(NewClonedStoryboard, Ui);

   
   //on ajoute le Storyboard créé au sein d'un dictionnaire, 
   //avec comme clé la référence de l'instance animée
   DicoElementsSB.Add(Ui, NewClonedStoryboard);

}

Compilez l'application. Si à ce stade vous n'avez aucune erreur levée par le compilateur, c'est que tout s'est bien passé. Dans le cas contraire, l'erreur provient dans 80 % des cas de la concaténation via la méthode AppendLine ou de l'oubli des espaces de noms comme attribut de la balise <Storyboard xmlns="…" xmlns:x="…">. Comme vous le constatez, il est utile de conserver un accès aux instances Storyboard créées. La meilleure manière d'y avoir accès est d'utiliser un dictionnaire. Vous pourriez également utiliser la propriété Resource de votre application. Toutefois celle-ci peut contenir bien d'autres ressources que des animations. Ce choix vous appartient. Dans tous les cas, nous avons besoin de stocker les occurrences de Storyboard, pour les réutiliser plus tard. Nous allons maintenant déclencher les animations lors de chaque clic de la souris sur la grille principale. Au sein de Blend, dans le panneau des événements entrez la chaîne DeclencheAnim pour l'événement MouseLeftButtonDown. Dans le code C#, il suffit de parcourir le dictionnaire et d'appeler la méthode Begin pour chaque animation contenue :

 
Sélectionnez
private void DeclencheAnim(object sender, MouseButtonEventArgs e)
{
   foreach (Storyboard sb in DicoElementsSB.Values)
   {
      sb.Begin();
   }
}

Cet exercice permet de comprendre comment les développeurs et les designers interactifs peuvent travailler les avec les autres sans se gêner et tout en conservant le travail de chacun. Vous trouverez l'exercice finalisé dans l'archive MenuAnim_Dynamique_Final.zip du dossier chap6 des exemples de l'ouvrage.

5-10. Les transformations relatives

Dans cette section, nous allons aborder les transformations relatives de manière plus approfondie. Vous remarquez que lorsque vous modifiez la position d'un objet au sein d'une animation, Blend privilégie l'utilisation des transformations relatives. Nous aurions tendance à penser qu'au sein d'un composant Canvas, les propriétés Canvas.Left et Canvas.Top seraient employées pour animer la position, mais il n'en est rien. Là encore, Blend choisira d'animer X et Y du nœud TranslateTransform, plutôt que d'interpoler les propriétés attachées par le conteneur (Canvas.Left et Canvas.Top). Les transformations relatives nous permettent d'échapper aux contraintes de mise en forme posée par le contexte conteneur. Pour cette raison, Blend les choisit en priorité. Il est donc pratique de savoir créer des instances de ce type dynamiquement via C#. Une autre raison plus terre à terre nous invite à générer ce type de nœud. Lorsque vous avez dupliqué la balle dans l'exercice précédent, celle-ci possédait déjà un nœud RenderTransform.Transform-Group, qui a donc été transmis aux copies de la balle, de ce fait, l'animation de rebond pouvait cibler les balles copiées. Toutefois, si l'affectation de cette propriété avait manqué, Blend aurait levé une erreur d'accès à la compilation. Dans certains cas, il vous faudra donc ajouter ce nœud dynamiquement.

5-10-1. Principes

Il existe quatre types de transformations relatives (voir Figure 6.31).

Image non disponible
Figure 6.31 - les quatre types de transformations relatives

Du point de vue d'un développeur, il est possible d'affecter de différentes manières des transformations relatives aux instances. La première consiste simplement à affecter la propriété d'une transformation de son choix :

 
Sélectionnez
TranslateTransform Tt = new TranslateTransform();
Tt.X = 200;
MonFrameworkElement.RenderTransform = Tt; 
//décale MonFrameworkElement de 200 pixels vers la droite

ScaleTransform St = new ScaleTransform();
St.ScaleX = 2;
MonFrameworkElement.RenderTransform = St;
//écrase l'ancienne affectation de translation
//Multiplie par 2 la largeur de MonFrameworkElement

La deuxième est d'affecter plusieurs transformations relatives au même objet en les groupant dans une instance de type TransformGroup :

 
Sélectionnez
TranslateTransform Tt = new TranslateTransform();
Tt.X = 200;
ScaleTransform St = new ScaleTransform();
St.ScaleX = 2;

TransformGroup Tg = new TransformGroup();
Tg.Children.Add(Tt);
Tg.Children.Add(St);

MonFrameworkElement.RenderTransform = Tg;
//Affecte les deux transformations sans que l'une écrase l'autre

Pour un designer, cela est légèrement différent, car Blend génère par défaut un nœud XAML de type TransformGroup qui contient les quatre types de transformation :

 
Sélectionnez
<UIElement.RenderTransform>
   <TransformGroup>
      <ScaleTransform/>
      <SkewTransform/>
      <RotateTransform/>
      <TranslateTransform/>
   </TransformGroup>
</UIElement.RenderTransform>

Dès lors, pour des raisons de communication et d'homogénéité, le développeur doit, dans certains cas, cibler ou affecter les transformations contenues dans un nœud XAML TransformGroup. Cela pour la bonne raison que le nœud XAML est utilisé par le designer interactif au sein de Blend.

5-10-2. Tester la présence d'une instance TransformGroup

Avant d'ajouter ou de cibler un nœud de type TransformGroup, la première chose à faire est de tester la propriété RenderTransform. Quelle que soit l'instance de UIElement, cette propriété n'est pas nulle, car sa valeur correspond à la matrice de transformation de l'objet par défaut (MatrixTransform). Toutefois, vous pouvez tester si elle contient une instance de type TransformGroup. Si c'est le cas, cela signifie que la propriété RenderTransform de l'instance a été modifiée sous Blend ou qu'elle a été affectée par code, et vous devrez si possible éviter tout écrasement des transformations modifiées par le graphiste dans Blend.

Créez un nouveau projet dans Blend et deux instances de Rectangle dans LayoutRoot. Nommez l'un R et l'autre Rt. Sur le Rectangle Rt, modifiez les transformations relatives via le panneau des propriétés. Ouvrez le projet sous Visual Studio. Nous allons utiliser cet environnement pour tester les transformations relatives de chaque Rectangle :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   bool Rb = R.RenderTransform is TransformGroup;
   bool Rtb = Rt.RenderTransform is TransformGroup;
}

Ce code ne suffit pas, le mieux serait de poser un point d'arrêt pour vérifier quelles valeurs prennent Rb et Rtb. Cliquez à gauche de la ligne contenant la déclaration de Rtb (voir Figure 6.32).

Image non disponible
Figure 6.32 - Poser un point d'arrêt dans Visual Studio

Lancez la compilation. Elle est interrompue lorsque la méthode MainPage_Loaded est déclenchée. Pour connaître la valeur de Rb et de Rtb, il vous suffit de survoler l'opérateur is qui évalue le type. La variable Rtb n'est pas encore affectée à cet instant, c'est pourquoi le survol de la variable Rtb renvoie false. Visual Studio affiche la valeur sous chaque variable (Figure 6.33).

Image non disponible
Figure 6.33 - Afficher les valeurs à l'exécution grâce au débogueur

Non seulement Visual Studio affiche la valeur au survol, mais il renseigne la valeur des variables accessibles depuis la méthode MainPage_Loaded au sein du panneau Locals. Pour tester une référence ou une valeur, vous pouvez également utiliser la fenêtre de sortie de Visual Studio. La classe statique Debug possède la méthode WriteLine permettant d'écrire en fenêtre de sortie. Elle se trouve dans l'espace de noms System.Diagnostics. Voici un exemple d'utilisation :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   bool Rb = R.RenderTransform is TransformGroup;
   bool Rtb = Rt.RenderTransform is TransformGroup;

   Debug.WriteLine(" TransformGroup de R :: {0} - TransformGroup de Rt :: 
                   {1}", Rb, Rtb);
}

Le raccourci F5 sous Visual Studio lance automatiquement le débogueur. Tant que celui-ci est exécuté, le code logique ou déclaratif sous Visual Studio n'est pas modifiable. Désactivez le débogueur sous Visual Studio pour y accéder de nouveau via le raccourci Maj+F5. Cet exercice est disponible dans TestRenderTransform.zip du dossier chap6.

5-10-3. Affecter la propriété RenderTransform

Maintenant que nous savons tester la présence du nœud élément TransformGroup, nous allons le créer de toute pièce lorsqu'il est absent. Pour des raisons de standard d'écriture XAML, nous allons le créer de la même manière que le ferait Blend. L'exemple de code ci-dessous gère entièrement l'animation, la création des objets graphiques et des transformations relatives :

 
Sélectionnez
public partial class MainPage : UserControl
{
   Storyboard Sb;
   DoubleAnimation DaX;
   DoubleAnimation DaY;

   public MainPage()
   {
      // Required to initialize variables
      InitializeComponent();

      Loaded += new RoutedEventHandler(MainPage_Loaded);
   }

   void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      Ellipse Balle = CreateBalle();

      Balle = (Ellipse)CreateRenderTransformNode(Balle);

      CreateAnimationBalle(Balle);

      LayoutRoot.MouseLeftButtonUp += OnLayoutClick;

   }

   private Ellipse CreateBalle()
   {
      Ellipse MaBalle = new Ellipse();
      MaBalle.Width = 100;
      MaBalle.Height = 100;
      MaBalle.HorizontalAlignment = HorizontalAlignment.Left;
      MaBalle.VerticalAlignment = VerticalAlignment.Top;
      MaBalle.Fill = new SolidColorBrush(Colors.Gray);
      LayoutRoot.Children.Add(MaBalle);
      return MaBalle;
   }

   private UIElement CreateRenderTransformNode(UIElement balle)
   {
     //l'objectif est de standardiser l'écriture du nœud
     //RenderTransform. Pour cette raison, nous le créons 
     //de la même manière que Blend
      if ( (balle.RenderTransform is TransformGroup))
      {
         TransformGroup Tg = new TransformGroup();
         Tg.Children.Add(new ScaleTransform());
         Tg.Children.Add(new SkewTransform());
         Tg.Children.Add(new RotateTransform());
         Tg.Children.Add(new TranslateTransform());
         balle.RenderTransform = Tg;
      }
      return balle;
   }

   private void CreateAnimationBalle(Ellipse balle)
   {
      Sb = new Storyboard();
      DaX = new DoubleAnimation();
      DaY = new DoubleAnimation();

      DaX.Duration = TimeSpan.FromSeconds(0.6);
      DaY.Duration = TimeSpan.FromSeconds(0.6);

      DaX.EasingFunction = new ElasticEase();
      DaY.EasingFunction = new ElasticEase();

      Storyboard.SetTarget(DaX, balle);
      Storyboard.SetTarget(DaY, balle);

      Storyboard.SetTargetProperty(DaX, new PropertyPath("(UIElement.
      RenderTransform).(TransformGroup.Children)[3].
      (TranslateTransform.X)") );
      Storyboard.SetTargetProperty(DaY, new PropertyPath("(UIElement.
      RenderTransform).(TransformGroup.Children)[3].
      (TranslateTransform.Y)") );

      Sb.Children.Add(DaX);
      Sb.Children.Add(DaY);

   }

   void OnLayoutClick (object sender, MouseButtonEventArgs e)
   {
      // la balle se déplacera lorsque vous cliquerez 
      //n'importe  au sein du conteneur LayoutRoot 
      DaX.To = e.GetPosition(null).X - 50 ;
      DaY.To = e.GetPosition(null).Y - 50 ;
      Sb.Begin();
   }

Compilez votre application. Lorsque vous cliquez n'importe où sur la scène, l'animation est mise à jour et la balle se déplace à l'endroit cliqué. Vous trouverez cet exercice ici : chap6/plein-Ecran_maquetteAnimee_1.zip.

Créer un nœud élément RenderTransform peut se révéler assez fastidieux. De plus, son chemin d'accès n'est pas forcément simple à renseigner. À cette fin, vous pouvez soit vous aider de Blend, soit utiliser la bibliothèque ProxyRenderTransform. Nous allons voir son utilisation dans la prochaine section.

5-10-4. La bibliothèque ProxyRenderTransform

5-10-4-1. Principes et utilisation simple

Assurez-vous d'avoir téléchargé la bibliothèque au préalable. Elle est à votre disposition sur le portail CodePlex, à l'adresse : http://proxyrd.codeplex.com. Puis, référencez-la au sein d'un nouveau projet nommé UsingProxyRD (voir Figure 6.34).

Image non disponible
Figure 6.34 - Référencer la bibliothèque ProxyRenderTransform

Ensuite, importez l'espace de noms au sein de votre code :

 
Sélectionnez
using ProxyRenderTransform;

Cette bibliothèque fournit un certain nombre d'avantages. Elle peut ajouter dynamiquement un groupe de transformations relatives aux objets qui n'en possèdent pas. Elle permet également de récupérer ou d'affecter la valeur de chaque transformation via l'utilisation de méthodes d'extension. Vous pourrez par exemple écrire :

 
Sélectionnez
MonUIElement.SetScaleX(2);
//ou encore
MonUIElement.SetX(50);

Ces méthodes sont accessibles directement grâce à l'IntelliSense que vous fournit Visual Studio (voir Figure 6.35).

Image non disponible
Figure 6.35 - IntelliSence de la bibliothèque ProxyRenderTransform dans Visual Studio

Comme vous pouvez le voir, cette bibliothèque donne également accès aux projections 3D (voir Chapitre 8 Les bases de la projection 3D). Si vous souhaitez récupérer la référence d'un nœud de transformation contenu dans le groupe généré par défaut sous Blend, vous pouvez appelez les méthodes GetRotateNode, GetScaleNode, etc. Pour finir, la bibliothèque vous permet de récupérer les chemins d'accès aux différents nœuds en renvoyant des objets typés Property-Path (voir Figure 6.36).

Image non disponible
Figure 6.36 - Récupération des chemins d'accès aux transformations

Vous pouvez désormais écrire :

 
Sélectionnez
Storyboard.SetTargetProperty(DaX, ProxyRD._TRANSLATE_X_PATH );

Cette écriture est non seulement simple, mais également facile d'accès via l'IntelliSense de Visual Studio. L'IntelliSense de Blend ne donne pas accès dynamiquement aux méthodes d'extension.

5-10-4-2. Animer avec le métronome DispatcherTimer

Nous allons faire une animation très simple pour illustrer son utilisation. Dans un nouveau projet nommé AnimProg, ajoutez la bibliothèque ProxyRenderTransform, puis créez un composant Rectangle au sein de la grille principale LayoutRoot. Placez-le en haut à gauche de la grille et nommez-le Suiveur. Dans le panneau des événements de la grille principale, entrez GetMouse-Pos pour l'événement MouseMove. Vous allez utiliser la classe DispatcherTimer pour modifier la position du rectangle selon un intervalle de temps. Voici le code commenté permettant au rectangle de suivre constamment la souris avec un léger effet de ralenti :

 
Sélectionnez
using ProxyRenderTransform;
using System.Windows.Threading;
public partial class MainPage : UserControl
{
//la destination du rectangle mis à jour lors de l'événement Tick
//qui se déclenche selon l'intervalle de temps spécifié
double NewDestX=0;
double NewDestY=0;
//Stocke les coordonnées actuelles de la souris
double MouseX;
double MouseY;

public MainPage()
{
   // Required to initialize variables
   InitializeComponent();

   DispatcherTimer dt = new DispatcherTimer();

   dt.Interval = TimeSpan.FromMilliseconds(10);

   dt.Tick += new EventHandler(dt_Tick);
   dt.Start();

}
   void dt_Tick(object sender, EventArgs e)
   {
      //ces expressions permettent de créer l'effet de déplacement ralenti
      //0.1 correspond au facteur de ralenti, plus ce facteur est faible
      //plus le ralentissement s'accentue
      NewDestX += ((MouseX - Suiveur.Width/2) - (double)Suiveur.GetX()) * 0.1;
      NewDestY += ((MouseY - Suiveur.Height/2)- (double)Suiveur.GetY()) * 0.1;
      Suiveur.SetX(NewDestX);
      Suiveur.SetY(NewDestY);
   }

   //lors du déplacement de la souris, on récupère sa position
   private void GetMousePos(object sender, MouseEventArgs e)
   {
      MouseX = e.GetPosition(null).X;
      MouseY = e.GetPosition(null).Y;
   }
}

L'animation est très fluide et ne repose pas sur la classe Storyboard. Vous pouvez pratiquement réaliser n'importe quel type d'animation grâce à la classe DispatcherTimer. Dans tous les cas, l'animation - même écrite avec des nœuds éléments Storyboard - est basée en interne sur ce type de fonctionnement.

Il n'est pas réellement possible de récupérer les coordonnées de la souris en dehors d'un gestionnaire d'événements pour la bonne raison que la classe "Mouse" n'existe pas. Dans le cas contraire, nous n'aurions pas besoin d'écouter l'événement MouseMove et notre code serait plus optimisé.

Vous trouverez le projet dans l'archive AnimProg.zip du dossier chap6 des exemples.

Nous allons maintenant animer avec la classe Math. Cette dernière centralise de nombreuses méthodes dont certaines sont liées à la trigonométrie. Cette discipline est assez intéressante, car elle permet de coder des animations complexes en quelques lignes seulement. Nous allons utiliser deux méthodes, Math.Sin et Math.Cos, pour générer un mouvement circulaire. Créez un nouveau projet et nommez-le AnimationCirculaire_Math. Référencez la bibliothèque ProxyRenderTransform. Créez ensuite deux instances de type Ellipse. La première, nommée Axe, symbolise l'axe de rotation. La deuxième, appelée Satellite, va subir une rotation autour de l'axe. Elles doivent toutes deux être alignées horizontalement et verticalement au centre de la grille. Voici le code permettant de générer une rotation via DispatcherTimer :

 
Sélectionnez
//Vitesse angulaire exprimée en radians
//elle dépend directement de l'intervalle de l'objet DispatcherTimer
double vitesseAngulaire = .1;
//angle initial
double angle = 0; //exprimé en radians
//Le rayon de rotation
double rayon = 100;
//l'axe de rotation
Point CentreRotation;

DispatcherTimer Dt = new DispatcherTimer();

public MainPage()
{
   InitializeComponent();
   Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   //Lorsque l'application est chargée, on récupère les coordonnées
   //de l'objet servant d'axe de rotation
   CentreRotation = new Point((double)Axe.GetX(), (double)Axe.GetY());
   Dt.Interval = TimeSpan.FromMilliseconds(30);
   Dt.Tick += new EventHandler(OnTick);
   Dt.Start();
}

void OnTick(object sender, EventArgs e)
{

   //On récupère les nouvelles coordonnées du satellite
   //en fonction de l'angle, du rayon et du centre de la rotation
   Satellite.SetX(CentreRotation.X + Math.Sin(angle) * rayon);
   Satellite.SetY(CentreRotation.Y + Math.Cos(angle) * rayon);
   
   //À chaque appel de la méthode, on redéfinit l'angle 
   //en lui ajoutant la vitesse angulaire
   angle += vitesseAngulaire;
}

Cet exercice est disponible dans l'archive AnimationCirculaire_Math.zip, du dossier chap6 des exemples de cet ouvrage.

5-10-5. Effets antagonistes et complémentaires

Ce type d'effet est très utile en matière de graphisme, car il permet de créer des visuels à la fois faciles à mettre à jour et impressionnants. Ces effets reposent essentiellement sur l'imbrication, l'ordre hiérarchique des composants, ainsi que sur les transformations relatives. Nous allons simuler de la 3D sans utiliser la projection qui est disponible depuis la version 3 de Silverlight. La projection 3D est pratique, mais elle offre le désavantage d'un rendu parfois pixélisé (voir Chapitre 8 Les bases de la projection 3D). Nous n'aurons pas ce problème puisque nous utiliserons le mode vectoriel 2D uniquement. Créez un nouveau projet nommé DisqueAnim. Instanciez une ellipse de 200 pixels de largeur par 200 pixels de hauteur avec un fond blanc et une bordure de couleur noire. Faites-en une deuxième mais de 100 x 100 pixels. Sélectionnez-les, puis via le menu d'alignement centrez-les verticalement et horizontalement (voir Figure 6.37).

Image non disponible
Figure 6.37 - Les deux instances de ellipse alignées

Vous allez soustraire la plus petite à la plus grande afin de percer cette dernière. Pour cela, vous devez utiliser le menu Combiner accessible via un clic droit ou par le menu Objet. Sélectionnez les deux instances d'Ellipse puis, dans le menu Combiner, choisissez l'opération Soustraire. L'ordre de sélection compte. Vous devriez récupérer un tracé vectoriel de type Path ressemblant à un tore (voir Figure 6.38).

Image non disponible
Figure 6.38 - Opération de soustraction sur les Ellipses

Créez un triangle droit grâce à l'outil Plume, celui-ci doit partir du centre du tore vers la droite (voir Figure 6.39).

Image non disponible
Figure 6.39 - Création d'un triangle droit via l'outil Plume

Sélectionnez le triangle, puis le tore et répétez l'opération de soustraction. Nous allons maintenant animer le tracé obtenu avec une rotation. Créez une nouvelle animation nommée Anim-Rotation, puis ouvrez le panneau des transformations relatives. À la seconde 0, créez une clé d'animation avec une rotation de 0 degré. Positionnez la tête de lecture à la seconde 2, puis saisissez une rotation de 360 degrés. Sélectionnez le Storyboard, pour cela cliquez sur son nom au-dessus de l'arbre visuel. Au sein du panneau des propriétés, dans le champ RepeatBehavior, sélectionnez la valeur Forever. L'animation de rotation se répétera ainsi indéfiniment (voir Figure 6.40).

Image non disponible
Figure 6.40 - Lecture d'un Storyboard en boucle via la propriété RepeatBehavior

Sélectionnez la grille principale, dans le panneau des événements, entrez OnLoaded pour Loaded. Au sein du code C#, déclenchez l'animation via la méthode Begin :

 
Sélectionnez
private void OnLoaded(object sender, RoutedEventArgs e)
{
   AnimRotation.Begin();
}

Le disque est maintenant animé dès le chargement de l'application. Vous allez créer un effet antagoniste. Pour cela, groupez le disque au sein d'un Canvas. Sélectionnez ce dernier, puis au sein de l'onglet des transformations relatives, entrez la valeur 0,25 pour l'échelle ScaleY. Recompilez ; cette fois, le disque subit toujours la rotation, mais la totalité de cette dernière est aplatie, simulant un effet de perspective.

Si vous aviez modifié l'échelle directement au sein du disque, vous n'auriez pas eu ce type de résultat, car vous auriez aplati le disque et non l'interpolation elle-même (voir Figures 6.41 et 6.42).

Ce n'est qu'un simple exemple et vous pouvez imaginer un grand nombre d'utilisations différentes de ce concept. Vous trouverez le projet final, AnimProg.zip, dans le dossier chap6 des exemples du livre.

Image non disponible
Figure 6.41 - Modification de l'échelle directement sur le tracé
Image non disponible
Figure 6.42 - Effet complémentaire suite à la modification de l'échelle sur le conteneur Canvas

5-11. Animer des particules

Si un type d'animation passe pour être rébarbatif et difficile auprès des animateurs, il s'agit bien de l'animation des particules. Aucun outil dédié n'est présent au sein de Blend à cette fin. De plus, une particule possède des comportements soit aléatoires soit dictés par des ensembles complexes. Par exemple, la poussière, la pluie, les oiseaux et même les humains peuvent être considérés comme des particules dès lors qu'on les examine de loin. Ainsi, un animateur traditionnel, dont le métier est avant tout d'animer un nombre restreint d'objets, est dépassé par la somme colossale de travail à réaliser pour ce genre de visuel.

Toutefois, cela n'est pas inaccessible : les mouvements de foule du film Akira ont été réalisés par des dizaines d'animateurs et sans développement informatique. Cela reste un cas particulier. Dans le film d'animation du Bossu de Notre-Dame, la foule regroupée autour de Notre-Dame est quant à elle générée par ordinateur. L'idée est d'associer différents mouvements, comportements, couleurs et formes à chaque individu présent sur la place. Cela confère au dessin animé beaucoup de réalisme, mais ne requiert pas pour autant une armée d'animateurs. Par la suite, un algorithme informatique associe aléatoirement chaque composant constituant un individu, afin de lui donner un caractère unique au sein de la foule. C'est exactement ce que nous allons faire dans cette section pour donner l'illusion de la diversité.

5-11-1. Exemples d'un fond sous-marin

Téléchargez le projet chap6/BubbleParticule.zip. Décompressez le fichier, puis chargez le projet dans Expression Blend. Vous avez un aperçu du visuel final de cet exercice à la Figure 6.43.

Image non disponible
Figure 6.43 - Visuel du projet finalisé

5-11-1-1. Mise en place

Au sein du panneau des projets, vous constatez la présence d'un composant personnalisé nommé Bulle. Vous n'avez pas besoin de savoir pour l'instant comment celui-ci a été réalisé (vous pourrez vous en faire une idée au Chapitre 11 Composants personnalisés).

Un composant personnalisé peut être instancié de la même manière que n'importe quel autre via le code C# ou la bibliothèque de composants accessible dans Blend. La grille principale du projet contient deux composants dont seul le premier nous intéresse : le composant Canvas nommé Emetteur. C'est ce composant qui va contenir nos particules. Celles-ci seront en fait des bulles qui remonteront vers la surface en zigzagant. Pour déplacer les bulles, nous n'utiliserons pas les Render-Transform, car cela engendrerait la création d'une transformation relative de type Translate-Transform et ajouterait une charge processeur supplémentaire.

Il est parfois utile d'éviter l'utilisation des nœuds RenderTransform. Vous pourrez vous en passer dans deux cas précis : lorsque vous souhaiterez déplacer dans l'espace un objet vectoriel ou le redimensionner. Les opérations de rotation (RotateTransform) ou d'inclinaison (Skew-Transform) ne sont en revanche pas réalisables sans l'utilisation des transformations relatives ou d'une matrice de transformation. Pour déplacer simplement un contrôle, il vous suffit d'utiliser le composant Canvas. Ce composant permet un placement des objets sans contrainte de repositionnement. Cela est très pratique si vous souhaitez vous écarter au maximum d'une mise en forme traditionnelle.


Pour redimensionner un composant sans sa propriété RenderTransform, il vous faut utiliser le composant ViewBox. Vous trouverez ce contrôle dans la bibliothèque Silverlight Toolkit à l'adresse : http://www.codeplex.com/Silverlight. Lorsque vous changez la largeur (Width) et la hauteur (Height) de ce conteneur à enfant unique, il redimensionne son contenu comme si celui-ci avait eu sa propriété RenderTransform affectée d'une instance ScaleTransform modifiée.

Vous constatez également la présence d'une animation si vous ouvrez la liste des objets de type Storyboard dans Blend. Vous pouvez modifier cette liste à loisir pour donner plus de réalisme à la scène. Ouvrez le fichier MainPage.xaml.cs. Vous constatez que l'on commence par démarrer l'animation des rais de lumière. Vous remarquez également que celle-ci est cinq fois moins rapide que l'aperçu dans Blend, car sa propriété SpeedRatio est affectée à 0.2 :

 
Sélectionnez
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
   //initialisation de l'animation des rais de lumière
   RaiesLumieres.SpeedRatio = 0.2;
   RaiesLumieres.Begin();
}

Nous allons maintenant créer des instances du composant personnalisé Bulle selon un laps de temps.

5-11-1-2. Créer les particules

Il faudra limiter le nombre de particules. Pour cela, il nous suffit de définir une constante représentant le nombre maximum de bulles générées :

 
Sélectionnez
public partial class MainPage : UserControl
{
   //On définit un nombre de bulles maximum
   private const int NOMBRE_BULLE = 50;}

Chaque bulle de notre visuel est conçue à un instant différent. Pour générer les bulles, le mieux est d'utiliser la classe DispatcherTimer contenue dans l'espace de noms System.Windows.-Threading. Nous pouvons ajouter une instance de cette classe comme membre de la classe principale, puis la configurer dans notre méthode Loaded :

 
Sélectionnez
public partial class MainPage : UserControl
{
   //On définit un nombre de bulles maximum
   private const int NOMBRE_BULLE = 50;

   //on crée une instance de l'objet DispactherTimer
   //qui va nous servir à créer des bulles selon un intervalle de temps
   DispatcherTimer monTimer = new DispatcherTimer();

   public MainPage()
   {
      InitializeComponent();
      Loaded += new RoutedEventHandler(MainPage_Loaded);
   }

   void MainPage_Loaded(object sender, RoutedEventArgs e)
   {
      //initialisation de l'animation des rais de lumière
      RaiesLumieres.SpeedRatio = 0.2;
      RaiesLumieres.Begin();

      //Intervalle de temps entre la création de chaque bulle
      monTimer.Interval = TimeSpan.FromMilliseconds(200);
      
      //Souscription de l'écouteur monTimer_Tick à l'événement Tick
      monTimer.Tick += new EventHandler(monTimer_Tick);
      
      //Démarrage du métronome
      monTimer.Start();
   }

Il nous faut un objet de type Random. Celui-ci possède plusieurs méthodes capables de calculer un nombre aléatoire. C'est donc grâce à lui que nous positionnerons les bulles aléatoirement. Sous la déclaration de l'instance DispatcherTimer, créez une instance de l'objet Random. En le déclarant comme membre de classe, nous le rendons accessible depuis n'importe quelle méthode de la classe MainPage :

 
Sélectionnez
public partial class MainPage : UserControl
{
   //On définit un nombre de bulles maximum
   private const int NOMBRE_BULLE = 50;

   //on crée une instance de l'objet DispactherTimer
   //Il va nous servir à créer des bulles selon un intervalle de temps
   DispatcherTimer monTimer = new DispatcherTimer();

   //On initialise une instance Random dont on va se servir 
   //pour gérer les mouvements aléatoires
   private Random rnd = new Random();

Il ne nous reste plus qu'à instancier puis à positionner aléatoirement les bulles au sein de la méthode monTimer_Tick. Chacune des bulles sera un enfant du Canvas Emetteur.

 
Sélectionnez
void monTimer_Tick(object sender, EventArgs e)
{
   //On commence par vérifier que l'on n'a pas atteint le nombre maximum
   //de bulles générées.
   if (Emetteur.Children.Count==NOMBRE_BULLE)
   {
      //Si c'est le cas, on arrête le métronome
      monTimer.Stop();
      //et on sort de la méthode
      return;
   }
   
   //dans le cas contraire, on crée une nouvelle bulle
   Bulle MaBulle = new Bulle();

   //Pour donner l'impression de la diversité
   //on affecte la largeur et la hauteur de la bulle avec
   //une valeur aléatoire entre 10 et 20
   MaBulle.Width = MaBulle.Height = rnd.Next(10, 20);

   //on modifie l'opacité par défaut aléatoirement entre 0.2 et 0.7
   //soit entre 20 % et 70 %
   MaBulle.Opacity = (rnd.NextDouble()*0.5) + 0.2D;

   //on crée deux valeurs aléatoires
   double PosX = rnd.NextDouble() * Emetteur.ActualWidth;
   double PosY = rnd.NextDouble() * Emetteur.ActualHeight;

   //La première est affectée à la propriété Canvas.Left
   Canvas.SetLeft(MaBulle, PosX);
   //la seconde à la propriété Canvas.Top de chaque bulle 
   Canvas.SetTop(MaBulle, PosY);

   //On ajoute la bulle créée comme enfant du Canvas Emetteur
   Emetteur.Children.Add(MaBulle);

   //Pour finir, on appelle la méthode qui va créer une animation
   //propre à la bulle créée
   CreerAnimationAleatoire(MaBulle);
}

Nous allons voir en détail certaines parties de ce code. La première concerne l'affectation de la hauteur et de la largeur de chaque objet Bulle. La méthode Next de l'objet Random renvoie des entiers. Lorsque vous passez deux arguments, vous demandez en fait un entier aléatoire dans une plage de valeurs. Les propriétés Width et Height étant typée Double, nous procédons ainsi à un transtypage implicite. La valeur entière récupérée est convertie en type Double. Sur cette ligne de code, nous effectuons également une triple égalité, cela nous permet d'avoir des dimensions dont le rapport initial est conservé. Concernant l'opacité, elle est obtenue grâce à la méthode Next-Double de l'objet Random et celle-ci renvoie un nombre de type Double aléatoire situé entre 0 et 1. Multiplier le retour de cette méthode par 0.5 revient à demander un nombre entre 0 et 0.5. Ajouter 0.2D signifie que vous ajoutez la valeur 0.2 de type Double. Nous obtenons donc une opacité située entre 0.2 et 0.7, cela donne un effet de profondeur à la scène. De la même manière, pour positionner la bulle aléatoirement, nous utilisons la méthode NextDouble. Les propriétés ActualWidth et ActualHeight renvoient en fait la largeur et la hauteur mises à jour selon les contraintes de redimensionnement du site. Ces valeurs sont réellement utiles lorsque votre conteneur possède des propriétés Width et Height en mode Auto. C'est notre cas, la largeur (Width) du Canvas Emetteur est constamment mise à jour selon le redimensionnement de la fenêtre de navigation. Les propriétés Canvas.Left et Canvas.Top sont des propriétés attachées aux objets contenus dans un conteneur de type Canvas. C'est le cas des instances de Bulle que nous créons. Il existe deux manières d'affecter des propriétés attachées, les voici dans le cas de Left et Top :

 
Sélectionnez
//1 - on utilise les méthodes statiques de la classe Canvas

Canvas.SetTop(MonInstance, 100); 
//positionne mon instance à 100 pixels du bord haut 
Canvas.SetLeft(MonInstance, 100); 
//positionne mon instance à 100 pixels du bord gauche 

//2 - on utilise la méthode SetValue propre aux instances de   
// DependencyObject

MonInstance.SetValue(Canvas.TopProperty, 100);
//on obtient le même résultat
MonInstance.SetValue(Canvas.LeftProperty, 100);

Compilez votre projet pour voir le résultat. Les bulles sont créées aléatoirement au sein du conteneur Canvas (Figure 6.44).

Image non disponible
Figure 6.44 - Création de bulles aléatoirement dans le composant Emetteur

5-11-2. Créer l'animation des bulles

Nous allons maintenant nous concentrer sur l'animation de chaque bulle. Le code sera centralisé dans la méthode CreerAnimationAleatoire :

 
Sélectionnez
private void CreerAnimationAleatoire(Bulle maBulle)
{
   //on commence par instancier une nouvelle ressource Storyboard
   Storyboard AnimBulle = new Storyboard();}

Cette méthode reçoit en paramètre la nouvelle bulle créée, puis lui affecte deux animations uniques. La première animation cible l'axe vertical Y représenté par la propriété Canvas.Top de chaque Bulle. La seconde interpole la position des particules sur l'axe X, défini par la propriété Canvas.Left.

5-11-2-1. Animation verticale

Pour l'axe vertical, cela est assez simple, car l'animation générée avec C# est semblable à celle créée à la section 6.4.2.1 Principes. Pourtant elle diffère en plusieurs points. Tout d'abord l'animation débute à partir de l'endroit où la bulle a été créée aléatoirement. Pour récupérer la position d'un objet au sein d'un Canvas, il vous suffit d'utiliser ses méthodes statiques :

 
Sélectionnez
Canvas.GetTop(MonObjetContenu);
//renvoie la position de l'objet sur l'axe vertical si celui-ci 
//est contenu dans un Canvas
Canvas.GetLeft(MonObjetContenu);
//renvoie la position de l'objet sur l'axe horizontal si celui-ci est 
//contenu dans un Canvas
//Dans les deux cas, il est inutile de préciser 
//la référence du conteneur, car celui-ci est implicitement 
//le parent de l'instance MonObjetContenu

Une fois récupérée, nous pouvons affecter cette valeur à la propriété From de notre animation. Nous définissons l'opposé de la hauteur de la page comme valeur de destination. La deuxième particularité de cette animation est qu'elle est jouée en boucle. Ceci se fait en affectant la valeur Forever à la propriété RepeatBehavior, celle-ci est héritée de la classe abstraite TimeLine. Concrètement, lorsqu'une bulle a terminé son parcours, elle revient à son point de départ et rejoue son déplacement. Cela évite de générer constamment de nouvelles bulles et allège la charge processeur en réutilisant celles qui sont déjà instanciées :

 
Sélectionnez
//Cette animation se joue en boucle indéfiniment sur l'axe vertical
//Une fois arrivée à sa destination, la bulle recommence son trajet 
DaY.RepeatBehavior = RepeatBehavior.Forever;

La dernière particularité est de définir une durée aléatoire pour chaque animation verticale via l'objet Random :

 
Sélectionnez
//L'animation durera entre 8 et 14 secondes maximum 
//grâce à l'utilisation d'une valeur aléatoire
DaY.Duration = TimeSpan.FromSeconds((rnd.NextDouble() * 6) + 8);

Voici le code complet générant cette animation :

 
Sélectionnez
//on crée une sous-séquence d'animation
DoubleAnimation DaY = new DoubleAnimation();

//Celle-ci cible l'axe Y grâce à la propriété Canvas.Top de chaque Bulle
Storyboard.SetTargetProperty(DaY, new PropertyPath(Canvas.TopProperty));

//Cette seconde sous-séquence cible également l'instance maBulle
Storyboard.SetTarget(DaY, maBulle);

DaY.RepeatBehavior = RepeatBehavior.Forever;

DaY.From = Canvas.GetTop(maBulle);

DaY.To = -LayoutRoot.ActualHeight - 50;

DaY.Duration = TimeSpan.FromSeconds((rnd.NextDouble() * 6) + 8);

Cette animation est assez simple, ce n'est pas vraiment elle qui donnera son caractère à l'animation, car l'objet Random est très peu utilisé. Nous allons maintenant voir comment déplacer aléatoirement chaque bulle sur l'axe X.

5-11-2-2. Animation horizontale

Cette animation est un peu plus compliquée. Nous allons tout d'abord essayer de comprendre l'interpolation dont nous avons besoin. La bulle doit remonter lentement tout en zigzagant à gauche et à droite. Si elle possède une amplitude constante à gauche et à droite, nous aurons un mouvement trop mécanique et donc pas assez aléatoire et chaotique. Il faut donc que celle-ci possède une amplitude différente lorsqu'elle va de droite à gauche. Si l'on additionne l'animation verticale à une animation horizontale de gauche à droite, on obtient aisément l'effet recherché (voir Figure 6.45).

Image non disponible
Figure 6.45 - Conjugaison de l'animation verticale et horizontale

Notre objectif consiste à créer des clés d'animation qui prendront une valeur aléatoire autour de la position d'origine (Canvas.Left) de la bulle. Nous avons besoin de créer une animation un peu plus complexe qu'une DoubleAnimation puisque celle-ci doit contenir des clés d'animation. Comme vu à la section 6.1 Créer un lecteur vidéo, il nous faut instancier une animation de type DoubleAnimationUsing-KeyFrames. De plus, celle-ci doit être un mouvement en boucle sans à-coup. Il suffit de préciser qu'elle joue à l'envers une fois arrivée à son terme, puis qu'elle joue en boucle. Voici le code permettant de la générer :

 
Sélectionnez
//On va créer une animation contenant des clés d'animation, l'avantage
//est de pouvoir générer une animation plus complexe et chaotique
DoubleAnimationUsingKeyFrames DaX = new DoubleAnimationUsingKeyFrames();

//Cette seconde sous-séquence cible la propriété Canvas.Left
//pour déplacer les bulles sur l'axe X
Storyboard.SetTargetProperty(DaX, new PropertyPath(Canvas.LeftProperty));

//Cette seconde sous-séquence cible l'instance maBulle
Storyboard.SetTarget(DaX, maBulle);

//l'animation sur cet axe doit être lue en boucle
DaX.RepeatBehavior = RepeatBehavior.Forever;

//Elle doit également se jouer dans les deux sens
DaX.AutoReverse = true;

//on crée 5 clés d'animation avec une boucle
for (int i = 0 ; i< 5; i++) 
{
   //Pour lisser le mouvement, on utilise une clé d'animation 
   //de type accélération
   EasingDoubleKeyFrame dkx = new EasingDoubleKeyFrame();

   //on définit donc un type de easing
   ExponentialEase Ee = new ExponentialEase();
   
   //afin de lisser l'animation de droite à gauche
   //l'accélération doit être la même au départ comme
   //à l'arrivée de l'interpolation
   Ee.EasingMode = EasingMode.EaseInOut;

   //on affecte cette équation à la propriété EasingFunction 
   //de chaque clé
   dkx.EasingFunction = Ee;

   //Chaque clé doit posséder une valeur qui affectera
   //la propriété Canvas.Left de la bulle
   dkx.Value = (double)Canvas.GetLeft(maBulle)+(rnd.NextDouble()*120) - 60;
   
   dkx.KeyTime = TimeSpan.FromMilliseconds((rnd.NextDouble()*1500) +
                                           ( (i+1) * 1500));
   DaX.KeyFrames.Add(dkx);
}

Comme vous le constatez, les animations utilisant des clés d'animation possèdent la propriété KeyFrames, qui est en fait une collection de DoubleKeyFrame. Chaque clé d'animation fait donc partie d'un type précis d'accélération.

Vous aurez le choix entre Easing, représentant les équations d'accélération, Spline pour les courbes d'accélération (voir section 6.3.2.3Contraintes), Linear permettant les interpolations linéaires et Discrete pour des clés d'animation sans interpolation. L'un des aspects importants consiste à éviter des mouvements trop saccadés sur l'axe horizontal. Pour cela, il faudra éviter de positionner trop de clés dans l'animation horizontale. Il nous reste maintenant à ajouter les deux animations à l'instance de Storyboard et à la lire :

 
Sélectionnez
//On ajoute la sous-séquence d'animation comme enfant du Storyboard
AnimBulle.Children.Add(DaY);
AnimBulle.Children.Add(DaX);

//Puis on demande à l'animation de se jouer
AnimBulle.Begin();

Finalement, on pourrait encore créer deux animations de fondu pour l'apparition progressive d'une bulle et pour sa disparition. Il suffit pour cela de stocker la durée aléatoire de l'animation verticale et de jouer une animation d'opacité 2 secondes avant sa fin. Vous trouverez ce projet finalisé dans l'archive BubbleParticule_final.zip du dossier chap6 des exemples.

Au Chapitre 6 Boutons personnalisés, nous reviendrons sur des notions d'animation dans un contexte complètement différent puisqu'elles seront conçues via le gestionnaire d'états visuels. Nous aborderons la création de boutons personnalisés à travers la conception d'un lecteur vidéo simple.

6. Boutons personnalisés

Tout au long de ce chapitre, vous concevrez le design d'un lecteur vidéo simple. À travers cet exemple, vous allez créer des composants de type ButtonBase entièrement personnalisés. C'est une première étape facilitant l'apprentissage des notions propres à la personnalisation de tous types de contrôles. Ainsi, vous connaîtrez la différence entre style et modèle de composant, au sein de la plate-forme Silverlight. Vous étudierez la liaison de modèles qui assouplit et facilite la maintenance des styles et des modèles tout en évitant leur multiplication. Dans un second temps, vous découvrirez le gestionnaire d'états visuels qui permet aux designers de gérer les transitions aussi bien au sein des contrôles qu'au niveau de l'application elle-même. Pour finir, vous apprendrez à créer un bouton interrupteur à deux états et utiliserez le système d'agencement fluide pour faciliter les transitions entre états.

6-1. Créer un lecteur vidéo

Nous allons concevoir le lecteur vidéo correspondant à la Figure 7.1. Dans un premier temps l'idée est de mettre en forme ce projet d'un point de vue design.

Image non disponible
Figure 7.1 - Lecteur vidéo finalisé

6-1-1. Mettre en place le projet

Afin de simplifier les opérations, vous pouvez télécharger le projet du dossier : chap7/Player-Video.zip. L'application se redimensionne automatiquement en fonction du navigateur. Vous trouverez plusieurs objets, dans des grilles nommées, répartis dans l'arbre visuel. Il s'agit en réalité d'une base visuelle pour les futurs contrôles du lecteur (voir Figure 7.2).

Image non disponible
Figure 7.2 - Arbre visuel du projet PlayerVideo

Comme vous le constatez, le lecteur vidéo est centralisé dans un Canvas. Le contrôle Media-Element contenant le fichier vidéo n'est pas encore présent (nous l'étudierons au Chapitre 12 Médias et données). Cela est assez logique, car il ne sera pas redimensionné par l'utilisateur. De plus, l'agencement de ses composants est facilité par le placement libre que propose ce type de conteneur. Le premier de ses enfants, nommé TimeLineProgress, permettra à l'utilisateur de déplacer la tête de lecture à un instant précis. Le second, Download-Progress, est un tracé qui affichera l'état de téléchargement des vidéos en cours de lecture. Les troisième et quatrième enfants, correspondants à LargeColorRing et SmallColorRing, sont des indicateurs de couleur recouvrant pour l'instant complètement les précédents. Lorsqu'un des boutons sera survolé, ces deux disques changeront de couleur. Au centre du lecteur, un interrupteur nommé PlayPause permettra de lire la vidéo ou de la mettre en pause et sera de type ToggleButton. Il affichera alternativement l'icône de lecture ou l'icône de pause. Nous aborderons son aspect visuel plus tard au sein de ce chapitre. Juste en dessous de l'interrupteur, un tracé nommé Background ainsi qu'une grille (Grid), IconeForward, constitueront notre bouton d'avance rapide. Les trois derniers enfants du Canvas sont en fait les icônes que nous utiliserons pour les prochains boutons.

6-1-2. Insérer une image d'arrière-plan

Avant de créer les boutons, nous allons afficher une image à l'arrière-plan du contrôleur vidéo. Sélectionnez la grille principale LayoutRoot comme conteneur contexte. Au sein du panneau Project, ouvrez le répertoire bitmaps et double-cliquez sur l'image nommée back.jpg. Celle-ci prend place au sein d'un conteneur spécialisé de type Image.

Vous pouvez cliquer droit sur l'image au sein du projet et choisir l'option Insert pour réaliser cette opération. Il est également possible de rechercher le composant Image dans la bibliothèque de composants Silverlight pour l'instancier directement sur la scène. Ensuite, vous devrez lui définir un chemin d'accès pointant vers l'image que vous souhaitez afficher via sa propriété Source. Une image bitmap ne peut être affichée que de deux manières. La première consiste à utiliser un conteneur Image. La seconde est d'utiliser un pinceau de type ImageBrush (voir Chapitre 8 Les bases de la projection 3D).

Sélectionnez le composant Image nouvellement créé. Placez-le à l'index 0 de la liste d'affichage de LayoutRoot. Définissez une opacité de 20 % pour le composant Image, centrez-le horizontalement et alignez-le en haut.L'idéal serait d'empêcher la déformation de l'image bitmap, pour cela, dans les options propres au composant Image, choisissez un étirement de type UniformToFill. Ce mode de remplissage permet d'éviter la déformation de l'image lorsque le composant est étiré tout en évitant les zones de remplissage vides (voir Figure 7.3).

Image non disponible
Figure 7.3 - Mode d'étirement UniformToFill

Pour configurer ce type de remplissage en C#, il suffit de cibler la propriété Stretch du composant Image comme suit :

 
Sélectionnez
//Instanciation d'un composant Image
Image myImage = new Image();
//On définit son mode de redimensionnement
myImage.Stretch = Stretch.UniformToFill;

Uri adresseImage = new Uri("fond.png",UriKind.Relative);

BitmapImage bi = new BitmapImage( adresseImage );
myImage.Source = bi;

Nous ne nous attarderons pas sur ce code, car nous aborderons le chargement dynamique de médias au Chapitre 9 Prototypage dynamique avec SketchFlow.

6-1-3. Le rôle du composant Grid

La grille est le conteneur à enfants multiples sur lequel repose l'architecture de la majorité des composants Silverlight. Lorsque vous souhaiterez créer un composant personnalisé, vous devrez souvent centraliser les objets graphiques au sein d'une grille avant de créer le composant. Celle-ci est très performante et offre de nombreux avantages exploités par l'équipe de développeurs chargée de créer la bibliothèque de composants. C'est pour cette raison que le contrôle Grid, nommé PlayPause dans l'arbre visuel de notre application, contient tous les éléments qui constitueront le futur composant ToggleButton. Vous allez commencer par créer le bouton d'avance rapide. Pour cela, sélectionnez le tracé nommé Background ainsi que le conteneur IconeForward, puis utilisez le raccourci Ctrl+G pour les imbriquer au sein d'une nouvelle grille (voir Figure 7.4).

Image non disponible
Figure 7.4 - Arbre visuel de la maquette finalisée

Vous trouverez la maquette finalisée dans l'archive PlayerVideo_Maquette.zipdu dossier chap7 des exemples du livre. Maintenant que vous avez centralisé tous les éléments graphiques, il est temps de créer un bouton personnalisé.

6-2. Style visuel

Personnaliser l'affichage d'un composant visuel consiste souvent à définir un nouveau style à ce type d'objet. On peut donc se demander ce que représente un style. C'est assez simple, un style est un ensemble de propriétés prédéfinies propres à un type de composant. Concrètement, vous pourriez vouloir que tous vos boutons contiennent un texte par défaut et qu'ils aient une largeur de 150 pixels. Ce paramétrage prédéfini de propriétés d'objet est appelé Style. Le principe des feuilles de style pour le contenu HTML repose sur cette définition.

La définition d'un style peut également être abordée d'un point de vue purement technique. Tous les composants héritant de Control possèdent la propriété Style. C'est le cas de nombreux composants tels que Button, RadioButton ou Slider. Ils ont la même architecture que notre application. Ils sont constitués d'un fichier XAML déclaratif ainsi que d'un fichier C# contenant le code logique du composant. Toutefois, ils ne sont ouverts au changement que du point de vue visuel. Autrement dit, leur code logique est fermé à la modification, à l'opposé de leur code déclaratif qui est accessible et facilement modifiable. La raison en est simple : la fonctionnalité (donc le code logique d'une barre de défilement ou d'un bouton) ne changera jamais alors que son apparence dépendra grandement de la charte graphique. Pour affecter ou atteindre le code XAML permettant de personnaliser un composant de type Control, on utilisera sa propriété Style.

6-2-1. Créer un style personnalisé

Sélectionnez la grille contenant les objets graphiques d'avance rapide et dans le menu Tools, cliquez sur le menu Make Into Control... Une boîte de dialogue correspondant à la Figure 7.5 s'affiche. Elle vous permet de créer un nouveau composant personnalisé à partir du conteneur sélectionné et de ses enfants. Comme vous pouvez le constater, la liste de composants possibles est longue. Dans certains cas, cette tâche demande un minimum d'expérience, nous verrons comment aborder cet apprentissage au Chapitre 8 Les bases de la projection 3D.

Image non disponible
Figure 7.5 - Création d'un style

Nommez le style ArcButtonStyle. En dessous de Define in, laissez l'option This document cochée. Cette option place le style au sein de la balise UserControl correspondant à notre composant racine. Ainsi, tous les composants de type Button qui sont dans le composant UserControl auront la possibilité de recevoir ce style. Ceux qui seront situés à l'extérieur du UserControl n'auront pas accès au style que nous avons créé. Cette série d'options définit la portée d'accès au style généré (voir Chapitre 10 Ressources graphiques). Cliquez sur OK, Blend vous place par défaut et immédiatement au sein du modèle Button généré, et non au niveau du style lui-même. Seul l'arbre visuel du composant Button apparaît (voir Figure 7.6).

Image non disponible
Figure 7.6 - Arbre visuel du bouton personnalisé

Comme vous le constatez, la liste d'affichage se différencie de l'arbre visuel de MainPage.xaml par de nombreux points. Tout d'abord, l'élément le plus haut n'est plus User-Control, mais l'élément Template qui signifie modèle. Le modèle d'un composant constitue son arbre visuel et logique. C'est l'une des propriétés prédéfinies qu'un style peut posséder. En second lieu, nous savons que nous sommes dans un modèle propre aux composants de type bouton, car vous pouvez lire Button Template à droite du nom du style ArcButtonStyle. Il peut arriver de se perdre un peu dans l'interface lorsque l'on débute avec Blend, car elle est entièrement contextuelle. Veillez bien à conserver des repères visuels tels que le nom de l'objet le plus haut dans la hiérarchie. Vous remarquez, à gauche de Arc-ButtonStyle, l'icône (Image non disponible) qui est devenue active. Elle ne l'était pas au sein de l'arbre visuel principal de l'application. Si vous la cliquez, vous pourrez revenir sur l'arbre visuel principal. C'est l'une des manières de naviguer entre différents niveaux de composants.

Pour finir, vous trouverez en bas de l'arbre visuel un composant de type Content-Presenter généré lors de la création du style. L'explication en est simple : la classe Button hérite elle-même de ContentControl et cette dernière ajoute un comportement d'imbrication par défaut à toutes ses classes héritées. Grâce à la propriété Content, vous pouvez afficher n'importe quel type de contenu au sein du bouton en le glissant directement sur le bouton dans l'arbre visuel principal de l'application. Le composant ContentPresenter assure cette fonction. Sélectionnez-le et passez sa propriété Visibility à Collapsed pour désactiver momentanément cette fonctionnalité. La chaîne de caractères Button affichée par défaut disparaît.

6-2-2. Naviguer entre style, modèle et application principale

La navigation entre chaque niveau d'imbrication représente l'une des difficultés rencontrées lors de la prise en main de Blend. Vous aurez souvent besoin de corriger ou de mettre à jour un style personnalisé. Mais nous allons commencer par analyser le code XAML généré afin de mieux comprendre le fonctionnement de la navigation dans Blend. Ouvrez le mode d'édition XAML et placez-vous au niveau de la balise <UserControl.Resources>. Vous devriez visualiser l'équivalent non abrégé du code XAML montré ci-dessous :

 
Sélectionnez
<UserControl.Resources>
   <Style x:Key="ArcButtonStyle" TargetType="Button">
      <Setter Property="Template">
         <Setter.Value>
            <ControlTemplate TargetType="Button">
               <Grid>
                  <VisualStateManager.VisualStateGroups></VisualStateManager.VisualStateGroups>
                  <Path x:Name="Background" 
            </ControlTemplate>
         </Setter.Value>
      </Setter>
   </Style>
<UserControl.Resources>

Lorsque le style est généré dans un UserControl, il fait partie de sa propriété Resources. Cette dernière représente un ensemble de ressources de n'importe quel type. L'attribut x:Key de la balise Style désigne le nom du style. Un style est une ressource en tant que telle, on utilise une clé de ressource plutôt qu'un nom de référence (voir Chapitre 8 Les bases de la projection 3D). La propriété TargetType spécifie le type d'objet ciblé par ce style. La balise Setter définit une propriété spécifique au sein du style. Dans notre cas, lorsque le Style sera appliqué à un bouton, la propriété Template de ce bouton sera affectée indirectement par notre Style nommé ArcButtonStyle. Le modèle n'est donc qu'une propriété parmi d'autres que l'on peut trouver au sein d'un style. Cette importante notion conditionne la navigation au sein de l'interface d'Expression Blend.

Passez en mode création, puis cliquez sur l'icône Image non disponible , située à gauche du nom Arc-Button-Style. Blend affiche à nouveau l'arbre visuel principal. La grille qui contenait les éléments visuels du bouton a été remplacée par un composant Button. Nous avons, jusqu'à présent, accédé au modèle mais pas au style. Il existe trois méthodes différentes pour accéder à un style.

  1. Image non disponible
    La première méthode est la plus simple. En haut de la fenêtre de design, vous apercevez trois icônes vous permettant d'accéder soit à l'instance du bouton, soit au style, soit directement au modèle du bouton sélectionné (voir Figure 7.7). Attention toutefois au fait que ces icônes sont visibles uniquement si le Style ou le Template ont déjà été modifiés ou du moins atteints dans Blend.
  2. L'icône contenant [Button] représente l'instance du bouton actuellement sélectionné au sein du Canvas. Si vous nommez votre bouton, c'est le nom qui apparaîtra. Le pictogramme en forme de palette de peintre (Image non disponible ) représente, quant à lui, un raccourci vers le style. Il suffit donc de cliquer dessus pour y accéder. La dernière icône, [ContentPresenter], correspond au dernier élément sélectionné dans le modèle et donne accès à celui-ci.

    Lorsque vous venez d'ouvrir un projet, seule la première des trois icônes est présente en haut de la fenêtre de design. Ce n'est pas un problème. Il suffit de cliquer sur cette icône, puis de sélectionner le menu Edit Template > Edit Current (voir Figure 7.8). Toutefois, vous accédez alors directement au modèle et non au style. Vous devrez donc remonter d'un niveau pour afficher le style.

    Image non disponible
    Figure 7.8 - Accéder au modèle d'un composant
  3. La deuxième possibilité est d'utiliser le menu Object > Edit Style, puis de cliquer sur Edit Current (voir Chapitre 10 Ressources graphiques pour les options de création de style).

  4. La dernière méthode consiste à utiliser le panneau Resources situé à droite, à déplier l'arborescence du UserControl et à cliquer sur l'icône du style auquel vous souhaitez accéder (voir Figure 7.9). Vous pouvez également utiliser le panneau des propriétés, cliquer sur le bouton des options avancées situé au niveau de la propriété Style et cliquer sur Edit Resource.
Image non disponible
Figure 7.9- Icônes d'accès au style, au modèle et nouvel arbre visuel du lecteur vidéo

Une fois au sein du style, vous constatez qu'aucun arbre visuel n'est représenté. C'est tout à fait normal, car nous ne sommes pas encore au sein du modèle de notre bouton, mais simplement au niveau des propriétés propres à la classe Button. Modifiez la propriété Cursor en sélectionnant la valeur Hand. De cette manière, lorsque le bouton sera survolé, le curseur de la souris sera remplacé par une main. Compilez le projet si nécessaire pour le vérifier. Étudiez maintenant le code XAML généré :

 
Sélectionnez
<UserControl.Resources>
   <Style x:Key="ArcButtonStyle" TargetType="Button">
      <Setter Property="Template">
         <Setter.Value>
            <ControlTemplate TargetType="Button">
               <Grid>
                  <VisualStateManager.VisualStateGroups></VisualStateManager.VisualStateGroups>
                  <Path x:Name="Background" 
            </ControlTemplate>
         </Setter.Value>
      </Setter>
      <Setter Property="Cursor" Value="Hand"/>
   </Style>
<UserControl.Resources>

Une nouvelle balise Setter est contenue dans la balise Style. Elle indique que la propriété Cursor doit prendre Hand pour valeur par défaut lorsque le style est appliqué. Cela montre à quel point le modèle, représenté par la propriété Template des objets de type Control, n'est qu'une propriété parmi d'autres. Vous allez maintenant modifier le modèle en créant un léger reflet. Définissez un dégradé transparent pour le remplissage du tracé nommé Background. Supprimez également le contour du tracé (voir Figure 7.10).

Image non disponible
Figure 7.10 - Bouton avec reflet

Vous avez finalisé la première étape de création du bouton. Il serait possible de créer autant de styles que nécessaire pour chaque bouton présent dans le lecteur. Vous allez toutefois utiliser le style actuel pour créer d'autres boutons ce qui est bien plus optimisé en termes de production.

6-3. Bouton générique

Les quatre boutons principaux présents sur le disque du lecteur partagent de nombreuses similitudes. Par exemple, afin de respecter la charte graphique, ils doivent avoir le même comportement au survol de la souris. De plus, leur structure interne est identique et seul le pictogramme diffère. Créer autant de styles que de boutons serait inutile en plus d'être fastidieux. Il existe trois méthodes différentes pour résoudre ce type de problématique et minimiser le nombre de styles créés.

  1. La première est d'utiliser du code logique, c'est un peu ennuyeux dans notre situation. Bien que cette solution soit envisageable, cela serait un peu prématuré d'utiliser du code logique pour résoudre cette problématique. Lorsque le nombre d'opérations de ce type est limité, il est préférable que le graphiste garde cette tâche pour lui et ne dépende pas du planning d'un développeur. Toutefois, cela est salutaire et inévitable dans certains cas lorsque le nombre de boutons à gérer est trop important. De manière générale, plus une tâche est rébarbative, plus il est souhaitable qu'elle soit automatisée via le code logique.
  2. La deuxième manière de procéder consiste à utiliser la propriété Content propre aux classes héritant de ContentControl.
  3. La troisième possibilité est d'utiliser la liaison de modèles que nous aborderons en détail au Chapitre 10 Ressources graphiques. Nous choisirons donc la deuxième solution.

6-3-1. La propriété Content

Finalement la seule différence entre chacun de nos boutons est l'icône elle-même. La propriété Content, héritée de la classe ContentContent, permet d'afficher n'importe quelle instance d'objet. Si nous externalisons l'icône présente dans le modèle de bouton, en l'affectant à la propriété Content des instances, chaque exemplaire de Button possédera sa propre icône tout en ayant un visuel homogène, car affecté du même style.

Une fois au sein du modèle, sélectionnez la grille nommée IconeForward, puis faites un couper-coller via le raccourci Ctrl+X. Ensuite, sélectionnez le composant Content-Presenter et passez sa propriété Visibility à Visible. Revenez au niveau de l'application et sélectionnez le bouton comme contexte conteneur si ce n'est pas le cas. Ensuite, utilisez le raccourci Ctrl+V pour imbriquer l'icône à l'intérieur du bouton. Cela revient à affecter la grille à la propriété Content du bouton. Vous pouvez recentrer l'icône à l'intérieur du bouton facilement en utilisant la propriété Margin de la grille IconeForward. Créez les trois autres boutons à partir de celui existant. Après l'avoir dupliqué trois fois, il vous reste à positionner ses copies avec une simple rotation via l'onglet Render-Transform du panneau des propriétés. Vous pouvez vous faciliter la tâche en déplaçant le centre de transformation du bouton au centre du cercle. Ainsi, chaque copie du bouton sera placée correctement via une rotation adéquate (voir Figure 7.11).

Image non disponible
Figure 7.11 - Copie du bouton généré avec centre de transformation déplacé

Vous pouvez supprimer la grille nommée IconeBackward dans ce cas précis. La rotation du bouton d'avance rapide correspond en effet au visuel du bouton de retour rapide. Ensuite, il reste à renommer la grille contenue dans le nouveau bouton par IconeBackward. Pour les deux autres copies, glissez les icônes correspondant à la gestion du volume en utilisant l'arbre visuel et logique.

Utiliser une rotation fonctionne, car tous nos boutons sont symétriques sur leur axe horizontal. Cette manipulation n'est donc pas valable dans tous les cas de figure. La propriété Content, bien que très utile, ne répond pas à toutes les problématiques. Toutefois, son utilisation repose sur un concept plus large que nous allons maintenant étudier. Il est également possible d'opérer une symétrie horizontale.

6-3-2. Liaison de modèles

La liaison de modèles est un concept provenant de WPF et peut s'appliquer à diverses propriétés. L'instance ContentPresenter au sein de tout modèle d'objet de type ContentControl repose sur ce principe. Ainsi, sans nous en rendre compte, la liaison de modèles nous a permis de créer quatre boutons différents en apparence, mais ayant pourtant le même style.

6-3-2-1. Principes

Pour mieux le constater, il vous suffit d'aller dans le code XAML du modèle :

 
Sélectionnez
<ControlTemplate  ><Grid>
      <ContentPresenter HorizontalAlignment="
         {TemplateBinding HorizontalContentAlignment}" 
         VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
   </Grid>
</ControlTemplate>

Comme vous le remarquez, les deux propriétés d'alignement sont affectées d'une valeur entre accolades. Cela signifie que ces propriétés ne sont pas affectées d'une valeur définie en dur, mais d'une expression dont le résultat peut évoluer. Dans notre cas, c'est même un peu plus compliqué, car l'expression entre accolades commence par le mot-clé TemplateBinding. Il signifie que les valeurs d'alignement de l'instance Content-Presenter ne sont pas définies directement au sein du modèle, mais au niveau de l'exemplaire du bouton. C'est exactement ce qui permet d'utiliser la propriété Content de chaque exemplaire afin de spécifier une icône différente à afficher.

Par défaut, une instance Content-Presenter possède une propriété Content liée à la valeur affectée sur la propriété Content de l'exemplaire. Celle-ci n'est donc pas précisée dans le code XAML du ContentPresenter. Vous pourriez donc écrire le code XAML suivant, le résultat serait exactement le même :

 
Sélectionnez
<ControlTemplate  ><Grid><ContentPresenter HorizontalAlignment=
         "{TemplateBinding HorizontalContentAlignment}" 
         VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
         Content="{TemplateBinding Content}"/>
   </Grid>
</ControlTemplate>

Vous pouvez consulter la Figure 7.12 qui décrit le schéma de deux liaisons de modèles. La première correspond à la liaison de modèles propre à la propriété Content. Elle est créée par défaut. La seconde expose une liaison de modèles entre la propriété Fill, du tracé présent à l'intérieur du modèle, et la propriété Background de chaque exemplaire de Button.

Image non disponible
Figure 7.12 - Le principe de la liaison de modèles

En tant que designer interactif, vous pouvez définir une liaison de modèles directement via l'interface d'Expression Blend. Nous allons permettre à chaque exemplaire de bouton, au sein de notre lecteur vidéo, d'afficher une couleur de remplissage différente.

6-3-2-2. Créer une liaison de modèles

Afin de mieux comprendre l'intérêt de ce type de liaison, sélectionnez n'importe lequel des boutons créés. Modifiez sa propriété Background en choisissant n'importe quelle couleur via le nuancier. Comme vous le constatez, modifier la couleur d'arrière-plan des boutons ne change en rien leur affichage. C'est tout à fait normal, car les propriétés de couleurs ne sont reliées à aucun objet présent dans le modèle de nos boutons. C'est cette problématique que nous allons maintenant traiter.

Sélectionnez le bouton d'avance rapide, puis affichez l'arbre visuel de son modèle. Sélectionnez le tracé nommé Background. Il possède un remplissage que nous avons défini en dur. Cela signifie que si vous modifiez ce tracé directement à l'intérieur du modèle, tous les exemplaires du bouton seront affectés de ces modifications. Cliquez maintenant sur l'icône carrée, située à droite de la propriété Fill du tracé. Ensuite, sélectionnez le menu Template Binding, puis la propriété Background (voir Figure 7.13).

Image non disponible
Figure 7.13 - Définir une liaison de modèles au sein d'expression Blend

Tous les boutons du lecteur possédant ce modèle affichent désormais la couleur définie sur leur propriété Background. Revenez au niveau de l'application principale, puis définissez une couleur d'arrière-plan différente pour chaque exemplaire. Cela peut-être un dégradé, une couleur pleine ou n'importe quelle instance de type Brush. Choisissez des couleurs vives afin de donner un peu plus de vie à notre lecteur. Des couleurs bonbon fluo correctement dosées seraient les bienvenues (voir Figure 7.14).

Voici le code XAML généré dans Blend :

 
Sélectionnez
<ControlTemplate x:Key="ArcButtonStyle" TargetType="Button"><Grid>
      <Path Fill="{TemplateBinding Background}" Data="" /></Grid>
</ControlTemplate>

En quelques opérations assez simples, nous avons créé une impression de richesse visuelle alors qu'un seul style est utilisé.

Image non disponible
Figure 7.14 - Chaque exemplaire de bouton possède désormais sa propre couleur

Décliner plusieurs visuels à partir d'un unique modèle est une technique de marketing utilisée depuis bien longtemps et toujours d'actualité (voir Figure 7.15). Il est donc normal que le langage XAML fournisse les moyens aux designers de réaliser simplement cette tâche.

Image non disponible
Figure 7.15 - Déclinaisons à partir d'un unique objet

Vous pouvez télécharger le projet finalisé, PlayerVideo_ButtonsGenerique.zip, dans le dossier chap7 des exemples.

6-3-2-3. Contraintes

La liaison de modèles est une fonctionnalité accessible pour la grande majorité des propriétés d'objets présents au sein des modèles. Comme nous l'étudierons aux Chapitres 10 Ressources graphiques et 11 Composants personnalisés, toute propriété de type DependencyProperty peut être la cible d'une liaison de modèles. La seule condition de fonctionnement est que la propriété de l'objet présent dans le modèle ne peut définir de liaison que vers une propriété de même type. Ainsi, lorsque vous avez affiché la liste des propriétés acceptant une liaison pour Fill, seules quatre propriétés de Button ont été disponibles : Background, BorderBrush, Fore-ground et OpacityMask. Elles sont disponibles, car elles acceptent des valeurs de type Brush au même titre que Fill ou Stroke (qui sont propres aux objets héritant de Shape). Lorsqu'un transtypage implicite est possible, la liaison de modèles est également accessible. Par exemple, si vous essayez de définir une liaison de modèles sur la propriété Content d'un ContentPresenter, toutes les propriétés de l'instance seront accessibles. Ceci est normal puisque par définition la propriété Content est typée Object et que toutes les classes héritent de Object. Content peut donc être liée à n'importe quelle propriété (voir Figure 7.16).

Image non disponible
Figure 7.16 - Liste des propriétés disponibles pour une liaison de modèles à la propriété Content

D'un point de vue logique, on pourrait considérer cette fonctionnalité comme une béquille permettant de résoudre certaines problématiques de conception. En effet, nous avons lié la propriété Fill à Background, car elles ont des rôles très proches. Toutefois, rien ne nous empêcherait de lier Fill à OpacityMask. Cela n'est pas très logique, mais la liaison de modèles nous l'autorise. Il faudra parfois passer par des contorsions de ce type pour concevoir un composant générique et facile à maintenir. Le désavantage de ces contorsions est qu'il faut se souvenir d'une liaison de modèles qui n'est pas forcément naturelle lorsque vous ouvrez à nouveau le projet après un long moment.

Dans certains cas, vous ne souhaiterez pas récupérer directement la valeur en provenance de la propriété d'instance mais la convertir à la volée pour en avoir une représentation légèrement différente au sein du modèle. Dans ce cas, vous utiliserez une classe implémentant l'interface IValueConverter. Cela peut-être très pratique et se révèle simple à coder. Nous en étudierons un exemple concret lorsque nous aborderons la création de modèles personnalisés plus complexes (ListBox, Slider, ProgressBar).

La dernière contrainte concerne en particulier les designers. Les propriétés liées ne sont plus modifiables au sein du modèle, car elles récupèrent leur valeur dynamiquement. Il est donc tout à fait logique de ne plus accéder à la modification de ces propriétés au sein du modèle. Lorsqu'une propriété est liée dans un modèle d'objet, elle est entourée d'un liseré interdisant toute manipulation (voir Figure 7.17).

Dans la grande majorité des situations, vous pouvez toutefois réinitialiser la valeur de la propriété en cliquant sur l'icône carrée située à sa droite et en sélectionnant l'option Reset (voir Figure 7.18).

Image non disponible
Figure 7.17 -Affichage d'une propriété liée au sein d'Expression
Image non disponible
Figure 7.18 - Supprimer une liaison de modèles

Cette dernière contrainte paraît évidente, mais il faut compter avec, surtout lorsque vous souhaiterez animer la propriété en question. Pour l'instant notre bouton est fonctionnel, mais il ne réagit pas aux interactions utilisateur les plus simples. Vous allez y remédier en utilisant le gestionnaire d'états visuels abordé dans la prochaine section.

6-4. Le gestionnaire d'états visuels

Le gestionnaire d'états visuels est un module introduit depuis Silverlight 2. Il est représenté par une API dont la classe principale est VisualStateManager. Celle-ci a pour objectif de gérer les transitions entre chaque interface utilisateur au sein d'une application. Cette démarche et le concept de gestion d'états visuels représentent l'un des axes majeurs de réflexion pour les ergonomes et les designers interactifs. Si l'esthétique et l'ergonomie des interfaces visuelles composant une application furent considérées comme importantes au milieu des années 90, les transitions qui les mettent en scène ne furent quant à elles prises en compte que bien plus tard. Aujourd'hui encore, les transitions restent négligées au sein de nombreux développements applicatifs. Pour les développements de type Windows, celles-ci ne furent considérées d'un point de vue technique qu'à partir de .Net 3, c'est-à-dire lors de l'apparition de Windows Presentation Foundation début 2007.

6-4-1. Évolution de l'expérience utilisateur

Pour mieux comprendre l'importance des transitions, nous allons faire un rapide retour sur l'évolution des interfaces utilisateurs. Ainsi, nous comprendrons mieux le positionnement du lecteur Silverlight dans cette évolution.

6-4-1-1. Les différentes générations d'interfaces

Trois grandes familles d'interfaces cohabitent aujourd'hui. Elles furent successivement découvertes depuis la première interface informatique homme-machine (voir Figure 7.19).

Image non disponible
Figure 7.19 - Les trois grands types d'interfaces utilisateur

Le type CLI (Command Line Interface) fut la première famille d'interface réellement disponible. Encore aujourd'hui, Unix, Linux, FreeBsd, MS-DOS, le terminal sous Mac OS X et de nombreux autres environnements reposent sur cette conception de l'accès aux fonctionnalités. Concrètement, l'utilisateur doit connaître le langage propre au système d'exploitation pour l'utiliser. Ainsi, lister un répertoire sous MS-DOS ou Linux Debian est réalisé en exécutant une commande prédéfinie, immuable et propre au système d'exploitation. La grammaire et le vocabulaire de ce langage étant tous deux propres à chaque système, cela force l'utilisateur à se spécialiser ou à ingurgiter un nombre de connaissances important et anecdotique tant leur utilisation peut s'avérer ponctuelle. L'utilisation du clavier comme médium permettant la communication homme-machine est obligatoire. La souris n'a simplement aucun intérêt, car naviguer dans l'interface est impossible.

GUI est l'acronyme de Graphic User Interface. Une équipe de Xerox PARC, composée entre autres de Alan Kay et Douglas Engelbar, a créé, à la toute fin des années 70, la première interface utilisateur graphique. Par la suite, Steve Jobs a poursuivi ce travail à Apple, aidé de nombreux anciens de Xerox. Parallèlement à Apple et PARC, X Window est développé au MIT sous le nom de Projet Athena et devient accessible dès 1983. Windows 1 représentant l'interface graphique de MS-DOS devient disponible à partir de 1985. Au sein d'une interface GUI, on privilégie l'exploration de l'utilisateur ainsi qu'un apprentissage empirique (la pédagogie par l'erreur ou l'expérience, au choix). Cela est réalisé à travers un mode fenêtré grâce auquel l'utilisateur peut exécuter des programmes, trouver ses fichiers et cliquer sur des icônes. Windows, Mac OS X, X-Window sont tous basés sur ce principe. Celui-ci n'a pas réellement évolué depuis 1983 et Vista, Windows 7, Snow Leopard ainsi que la grande majorité des systèmes d'exploitations sont basés sur ce principe. La souris devient dès ce moment un instrument incontournable pour utiliser ce type d'interfaces. Comme pour les interfaces de type CLI de nombreuses différences existent encore entre chaque système d'exploitation. Cela repose constamment la problématique de formation pour la prise en main et l'administration du système.

Le type NUI (Natural User Interface) est aujourd'hui visible sur de très rares périphériques tels que les tables de type Surface, l'iPhone - plus proche du grand public - ou encore le projet NATAL de Microsoft. L'objectif est simple : éviter à l'utilisateur l'apprentissage d'une interface en se reposant sur l'expérience déjà acquise dans son environnement naturel. Ainsi, pour un jeu de voiture sur l'iPhone, tourner le volant sera accompli en inclinant l'iPhone. L'utilisateur par sa propre nature possède tous les outils et instincts nécessaires pour interagir avec l'interface. La voix, le toucher, la reconnaissance visuelle des objets, la géo-localisation, la détection du mouvement ainsi que sa direction, tous ces éléments sont directement considérés comme des stimuli au même titre que le clic de la souris dans un environnement de type GUI.

L'interface NUI y réagit directement. L'utilisateur n'est donc que très peu limité par son environnement social, son âge ou son investissement dans l'apprentissage de telle ou telle technologie. La table Surface est à ce jour le périphérique supportant le mieux ce type d'interfaces. De plus, développer pour Surface est assez simple, car son framework repose sur la technologie WPF. Tous les périphériques supportant les interfaces NUI possèdent un point commun : ils s'accordent et se marie à l'environnement physique. Poser un objet physique sur une table Surface, se retrouver entre amis autour, collaborer, jouer à plusieurs est non seulement possible mais souhaitable. Surface est un environnement massivement multi-touch à 360 degrés. De nombreuses autres technologies de ce type existent dont certaines ont été développées par des universitaires et sont complètement libre d'accès moyennant un peu de bricolage fait maison. Dans ce type d'interfaces, les transitions sont extrêmement importantes, car aucune fenêtre d'exploration n'est présente. En effet, les interfaces utilisateur NUI étant entièrement contextuelles, c'est-à-dire liées au contexte d'utilisation, il devient crucial que l'utilisateur connaisse sa position au sein du processus d'utilisation. Il doit également savoir son point de départ et ses destinations possibles.

Si vous souhaitez plus d'informations sur les interfaces de type NUI, vous pouvez consulter le blog de Dr Neil, très investi dans la technologie Surface de Microsoft, à cette adresse : http://blogs.msdn.com/surface/archive/2009/03/13/friday-afternoon-chat-with-dr-neil-on-education.aspx, ou encore le site de Douglas Edric Stanley concernant l'hypertable et ses expérimentations : http://www.abstractmachine.net/blog/.

Nous allons maintenant essayer de comprendre comment se positionne Silverlight dans cette évolution.

6-4-1-2. L'apport des moteurs vectoriels

Bien que les moteurs vectoriels, comme Flash ou Silverlight, soient contenus par défaut au sein d'un navigateur Web (donc d'une interface GUI), ils proposent des interfaces utilisateur novatrices échappant complètement aux contraintes GUI. Le fichier compilé peut en effet proposer n'importe quel type d'interfaces utilisateur puisqu'il est complètement indépendant du système d'exploitation. Cela est possible, car les designers d'interfaces sont partie prenante dans la conception de l'application, mais également parce que chaque développement est unique et industrialisable au prix de nombreux efforts. Contrairement à une application Windows Forms, limitant grandement la manière de présenter les informations mais facile à développer, une application WPF est entièrement personnalisable mais plus difficile à concevoir (car intégrant de nouveaux profils métier). Flash est sans doute, jusqu'à présent, le moteur vectoriel qui a le plus bénéficié de la communauté des designers. Cela a permis de s'affranchir des modes de réflexion et des standard d'ergonomie liés au système d'exploitation. L'exemple de l'application de réservation en ligne du célèbre hôtel Broadmoor au Colorado est la preuve de l'efficacité d'une bonne ergonomie (voir Figure 7.20). Bien que l'interface (voir Figure 7.20) fût développée il y a maintenant sept ans, elle n'a été modifiée que très récemment. Nous pourrions procéder à un lifting complet d'un point de vue esthétique, mais la manière dont elle est envisagée répond complètement aux attentes du client. Lors de sa mise en ligne, les réservations de l'hôtel ont simplement triplé.

Image non disponible
Figure 7.20 - L'interface de réservation du Broadmoor avec ses trois étapes de réservation

Ainsi les applications vectorielles Cross Platform bénéficient d'un statut intermédiaire puisqu'elles sont conçues indépendamment du système ou du navigateur qui les affiche. L'application de réservation en est un exemple criant et les transitions qu'elle propose à l'utilisateur entre chaque étape du processus sont pertinentes encore aujourd'hui. Celles-ci bénéficient, en outre, de nombreux comportements asynchrones tels que la connexion socket XML ou binaire, le peer to peer, l'appel de services distants, le streaming vidéo live ou enregistré, l'affichage dynamique de médias. Ces fonctionnalités favorisent grandement l'essor d'applications à caractère social et la diffusion de contenus riches, deux thèmes majeurs du NUI. La grande limite reste donc la souris et le clavier comme medium incontournable de l'expérience utilisateur pour ce type d'environnement.

6-4-1-3. L'importance des transitions

Les transitions sont considérées comme aussi importantes que les interfaces applicatives. Chaque interface visuelle au sein d'une application peut être considérée comme une fonctionnalité de cette dernière, une brique applicative. Les transitions jouent le rôle de mortier entre chaque brique fonctionnelle. Autrement dit, elles font le lien entre deux interfaces visuelles. Elles possèdent de ce fait trois rôles importants.

  1. Esthétique et ludique. Elles doivent être esthétiques et amusantes, car ce qui est ludique est directement connecté à l'affect de l'utilisateur. Le plaisir ressenti à l'utilisation est aujourd'hui un domaine qui n'est plus exclusivement relié aux jeux vidéo. L'expérience utilisateur, lorsqu'elle est agréable, peut faire la différence commercialement.
  2. Sens. Bien que ludiques, les transitions ne sont pas pour autant gratuites. Elles ont donc pour mission de renforcer le sens donné à chaque interface visuelle. Nous pourrions par exemple utiliser des transitions 3D sur la profondeur des objets pour permettre à l'utilisateur de ressentir leur importance à un instant donné du processus tout en évitant une navigation trop linéaire.
  3. Localisation. Elles ont également pour objectif de faciliter l'immersion de l'utilisateur en améliorant l'ergonomie. Afin d'atteindre ces objectifs, elles doivent fournir une indication supplémentaire sur l'emplacement, la position ou l'étape du processus dans lequel est situé l'utilisateur. Il ne suffit pas de savoir ce que l'on fait, il faut également savoir d'où l'on vient et où l'on va. Cela permet à l'utilisateur de se repérer et d'avoir une idée claire des tâches restant à accomplir ou les fonctionnalités accessibles. Cette notion est très importante pour les interfaces de type "naturelle non fenêtrée" pour des périphériques comme Surface, l'iPhone, etc. Pour ces interfaces de nouvelle génération, il faut éviter autant que possible à l'utilisateur d'avoir à découvrir l'application par une exploration systématique ou par l'expérimentation empirique. L'expérience des interactions dans la vie réelle est déjà accomplie, le designer d'expérience utilisateur doit récupérer ce savoir acquis pour accompagner la démarche d'utilisation.

Pour finir sur ce sujet, il est important de concevoir les transitions le plus tôt possible dans le processus de conception, mais également d'équilibrer les trois composantes décrites plus haut, car privilégier trop l'une au dépend des autres engendre des comportements non souhaités. Une application trop agréable, mais dans laquelle l'utilisateur ne sait pas se situer, est intéressante dans une démarche artistique, mais n'est pas satisfaisante d'un point de vue commercial. À l'inverse, savoir se situer, mais n'avoir aucune surprise ou plaisir dans la navigation, peut provoquer une désaffection et de l'ennui. Sur Internet, l'ennui est le grand ennemi des Webmasters. Nous allons maintenant aborder la conception des transitions.

6-4-2. Au sein d'un control

Gérer les transitions est réalisable à deux niveaux différents. Vous pouvez gérer celles appartenant à l'application elle-même, ou celles prédéfinies dans les composants personnalisables tels que Button. D'un point de vue développeur, il n'y a pas réellement de différences entre ces deux niveaux. L'application peut, en effet, être considérée comme un composant géant très spécifique, car son architecture est similaire aux composants standard déjà fournis. Toutefois d'un point de vue organisationnel et utilisateur final, la différence est flagrante. Nous allons commencer par gérer les états d'un des boutons personnalisés pour mieux comprendre l'utilisation des transitions au sein de Silverlight.

6-4-2-1. Principes

Sélectionnez le bouton d'avance rapide, cliquez droit et sélectionnez successivement les menus Edit > Template > Edit Current. Vous atteignez une nouvelle fois le modèle du bouton. C'est à ce niveau que les transitions sont gérées pour tous les composants et non au niveau du style. Ouvrez le panneau contenant les états visuels States. Les objets de type Button possèdent tous les mêmes états (voir Figure 7.21).

Image non disponible
Figure 7.21 - Les états visuels au sein d'un composant Button

Tout d'abord, les états visuels sont contenus au sein de groupes d'états visuels représentés par la classe VisualStateGroup. Le code XAML, formalisant nos états visuels, est situé dans la balise VisualStateManager.VisualStateGroup :

 
Sélectionnez
<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="FocusStates">
      <VisualState x:Name="Focused"/>
      <VisualState x:Name="Unfocused"/>
   </VisualStateGroup>
   <VisualStateGroup x:Name="CommonStates">
      <VisualState x:Name="Normal"/>
      <VisualState x:Name="MouseOver"/>
      <VisualState x:Name="Pressed"/>
      <VisualState x:Name="Disabled"/>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Les groupes d'états visuels sont répartis par fonctionnalités. Dans le code XAML ci-dessus, le premier groupe gère le visuel selon le focus utilisateur, le second en fonction de ses états d'interactions utilisateur. Ainsi, un bouton peut ne pas posséder l'intérêt utilisateur tout en étant survolé. Il peut donc afficher deux états visuels à la fois, mais un seul par groupe d'états. Il peut avoir une opacité différente pour le survol de la souris et posséder une petite ombre pour indiquer le fait qu'il possède le focus utilisateur. Deux états centralisés dans des groupes différents seront indépendants.

Un bouton possédant le focus utilisateur est cliquable par défaut via la barre d'espace ou la touche Entrée. C'est assez pratique dans certains cas d'utilisation, notamment pour les formulaires d'inscription ou pour les fenêtres de connexion. Vous devrez toutefois gérer l'ordre de tabulation pour éviter au focus utilisateur de se placer sur n'importe quel composant interactif.

Avant d'aller plus loin, sachez que Base fait référence au visuel du bouton lorsque celui-ci ne possède aucun état visuel actif. Lorsque vous sélectionnez Base, puis que vous modifiez une propriété d'objet, tous les états visuels seront, par défaut, affectés de cette modification. Voici la description des états interactifs du groupe CommonStates :

  • Normal : l'état normal est toujours actif par défaut, il représente l'état d'interaction normal du bouton, c'est lui qui est, en réalité, affiché lorsque vous êtes à l'extérieur du modèle ;
  • MouseOver : représente le visuel du bouton lorsque celui-ci est survolé par la souris ;
  • Pressed : affiche l'état du bouton lorsque l'utilisateur clique dessus en appuyant sur le bouton gauche de la souris.
  • Disabled : définit le visuel affiché lorsque le bouton n'est pas actif ; vous pourriez désactiver un bouton de souscription tant qu'un formulaire n'est pas rempli de manière conforme, par exemple, et vous pouvez directement l'activer en modifiant la propriété Enabled d'un exemplaire de composant.

Considérez ces états comme une liste de choix. Vous ne pourrez en afficher qu'un seul à la fois par groupe d'états. Nous abordons cette contrainte à la section 6.4.3.2 Créer des états visuels. Chaque type de composant possède sa propre liste d'états visuels. Vous trouverez pourtant certains points communs entre ceux-ci. Les principes que nous exposons ici sont, pour leur part, communs à tous composants

6-4-2-2. Modifier les états visuels

Modifier un état est assez simple. Cliquez sur l'état MouseOver, vous passez en mode enregistrement d'état et un contour rouge vous le signale. Vous pourriez être tenté de modifier la couleur à ce stade. Ainsi, le bouton changerait de couleur lors du survol. Toutefois cela est impossible, car la propriété Fill est liée à la propriété Background de l'instance. Celle-ci est donc fermée à la modification au sein du modèle. Ce n'est pas grave, nous allons passer outre cette contrainte en utilisant l'état Normal. Sélectionnez-le, puis cliquez sur l'objet représentant le fond de couleur. Affectez la propriété Opacity du tracé de la valeur 0. Comme vous le constatez, tous les boutons perdent leur couleur d'arrière-plan à ce stade. C'est tout à fait logique puisque l'état Normal est affiché par défaut. Les autres états visuels utilisent les propriétés définies par Base, la couleur est donc visible au survol (MouseOver), sur le clic de l'utilisateur (Pressed) et lorsque le bouton est inactif (Disabled).

Vous allez maintenant agrandir de 20 % les dimensions de l'icône lors du survol de la souris. Comme l'icône n'est pas contenue au sein du modèle, mais affectée à la propriété Content des boutons, il suffit d'augmenter pour cela les dimensions du composant ContentPresenter. C'est ce composant qui affiche la valeur de la propriété Content de chaque exemplaire. Il est nécessaire d'utiliser les transformations relatives afin de s'affranchir des contraintes de positionnement liées au conteneur Grid. Sélectionnez l'état MouseOver, puis ContentPresenter. Ouvrez l'onglet des transformations relatives et modifiez l'échelle en x (ScaleX) et en y (ScaleY) en affectant à chacune d'elles la valeur 1.2. L'icône est dorénavant agrandie lorsque vous passez de l'état Normal à l'état MouseOver (voir Figure 7.22).

Image non disponible
Figure 7.22 - Modification de l 'état MouseOver

Lorsque vous testez votre application après l'avoir compilée, vous remarquez que tout correspond à nos actions. Toutefois lorsque vous cliquez sur les boutons, l'icône retrouve une taille normale, car vous passez implicitement à l'état Pressed, cela serait plus élégant si nous n'avions pas de différences marquées entre Pressed et MouseOver. Revenez dans Blend et cliquez-droit sur l'état MouseOver, puis cliquez sur l'option Copy to Pressed State. Toutes les modifications réalisées sur l'état MouseOver viennent d'être directement affectées aux objets sur Pressed. Ce menu caché vous évitera des manipulations rébarbatives dans de nombreux cas.

Tous les états sont maintenant configurés, il ne nous reste plus qu'à les animer.

Vous pouvez afficher un état visuel sans pour autant le sélectionner. Pour cela, cliquez sur Base, puis sur le cercle noir présent à gauche de l'état dont vous souhaitez avoir un aperçu (voir Figure 7.23). Une fois activé, l'icône d'un œil apparaît (Image non disponible). Cela ne fonctionnera toutefois pas forcément lorsque vous utiliserez des animations personnalisées.

Image non disponible
Figure 7.23 - Affichage d'états visuels sans sélection

6-4-2-3. Animer les états visuels

Pour l'instant, chaque transition est brutale donc peu élégante. Pour y remédier, il faut définir des interpolations pour chacune. Il existe trois possibilités via l'interface de Blend pour créer des transitions animées. Le code XAML est assez proche pour chacune d'elles, mais elles apportent une finesse de personnalisation différente. La première méthodologie consiste à définir une durée d'animation générique qui sera propre à l'ensemble des états visuels contenus dans un groupe. C'est très pratique en terme de productivité. Toujours au sein du modèle dans le panneau States, dans le groupe d'états visuels CommonStates, vous trouverez l'option Default transition. Dans le champ de saisie situé à droite et indiquant 0s, définissez une durée de 0.4s (voir Figure 7.24).

Image non disponible
Figure 7.24 - Création d'une transition générique du groupe

Vous venez en une seule manipulation de définir trois transitions différentes. Quel que soit l'état interactif actuel du bouton, une transition de 4/10e de seconde sera, par défaut, jouée afin d'atteindre n'importe quel autre état du groupe CommonStates. Voici le code XAML généré :

 
Sélectionnez
<VisualStateGroup x:Name="CommonStates">
   <VisualStateGroup.Transitions>
      <VisualTransition GeneratedDuration="00:00:00.4"/>
   </VisualStateGroup.Transitions>
   <VisualState x:Name="Normal"></VisualStateGroup>

Nous allons maintenant outrepasser l'animation de base. Utiliser une seule interpolation générique par groupe d'états est très bien pour un prototype rapide, mais cela est parfois un peu trop sobre ou standard pour une version finale de l'application. Vous aurez besoin de marquer la différence d'importance ou de définir une accélération propre à chaque transition. L'esthétique entre également en ligne de compte. Normaliser ou industrialiser trop les transitions, bien qu'efficace en terme de temps, n'est pas une bonne pratique. Il faut travailler chaque transition indépendamment les unes des autres. Cela ne signifie pas pour autant que vous devez trop en faire, bien au contraire. Les transitions les moins importantes prendront moins de temps. Les plus importantes sont situées au niveau de l'application et permettent de valider les différentes étapes d'utilisation. Celles-ci doivent être plus travaillées.

La seconde méthodologie permet de définir une durée et une équation d'accélération propre à chaque état. Cliquez sur l'icône représentant une flèche et un signe plus (Image non disponible) située à droite de l'état MouseOver. Une autre série d'icônes vous permet de définir une transition spécifique. Choisissez celle définie par une étoile suivie de MouseOver. Celle-ci définit une transition unique depuis n'importe quel état d'origine à destination de Mouse-Over. Comme vous le constatez, la valeur par défaut correspond à celle que vous avez défini pour la transition par défaut, soit 0.4. Spécifiez un délai de 0.6s, ainsi qu'une équation d'accélération de type CubicOut (voir Figure 7.25).

Image non disponible
Figure 7.25 - Création d'une transition d'état

Les spécificités de chaque transition sont contenues dans la balise VisualState-Group.-Transitions. Vous le constatez dans le code XAML ci-dessous :

 
Sélectionnez
<VisualStateGroup.Transitions>
   <VisualTransition GeneratedDuration="00:00:00.4"/>
   <VisualTransition GeneratedDuration="00:00:00.6" To="MouseOver">
      <VisualTransition.GeneratedEasingFunction>
         <CubicEase EasingMode="EaseOut"/>
      </VisualTransition.GeneratedEasingFunction>
   </VisualTransition>
</VisualStateGroup.Transitions>

Lorsque les états de départ ou de destination, au sein de la balise VisualTransition, ne sont pas précisés, cette balise concerne toutes les transitions d'état à état quel que soit leur nombre. Les transitions dont vous souhaitez différencier le comportement passeront outre le réglage de base.

Vous pouvez dès à présent tester ces transitions en utilisant le mode de prévisualisation. Cliquez sur l'icône contenant une flèche et l'icône de lecture pour activer ce mode (Image non disponible). Celle-ci est située tout en haut du panneau des états. Sélectionnez l'état Normal, puis MouseOver, la transition est automatiquement jouée de manière optimisée. Cette fonctionnalité est vraiment très utile et évite de compiler systématiquement l'application pour tester les transitions.

La dernière manière de gérer les transitions consiste à créer une animation personnalisée à l'aide d'un objet Storyboard personnalisé. Cette manière de procéder offre bien plus de possibilités mais il est moins facile de la maintenir sur le temps. Toutefois c'est également celle qui vous permettra de créer des transitions plus rythmées et plus originales. Sélectionnez l'état MouseOver et cliquez sur l'icône permettant d'afficher le scénario : Image non disponible . Vous remarquez qu'une clé d'animation est présente pour l'objet ContentPresenter à la seconde 0. C'est le comportement par défaut adopté par le mode d'enregistrement d'état. Passez ensuite en mode d'édition XAML, Le code ci-dessous montre clairement l'intégration de l'objet Storyboard :

 
Sélectionnez
<VisualStateGroup x:Name="CommonStates">
   <VisualStateGroup.Transitions>
      <VisualTransition GeneratedDuration="00:00:00.4"/>
      <VisualTransition GeneratedDuration="00:00:00.6" To="MouseOver">
         <VisualTransition.GeneratedEasingFunction>
            <CubicEase EasingMode="EaseOut"/>
         </VisualTransition.GeneratedEasingFunction>
      </VisualTransition>
   </VisualStateGroup.Transitions><VisualState x:Name="MouseOver">
      <Storyboard>
         <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
               Duration="00:00:00.001" Storyboard.
               TargetName="contentPresenter" 
               Storyboard.TargetProperty="(UIElement.RenderTransform).
               (TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="1.4"/>
         </DoubleAnimationUsingKeyFrames>
         <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
               Duration="00:00:00.001" Storyboard.
               TargetName="contentPresenter" Storyboard.
               TargetProperty="(UIElement.RenderTransform).
               (TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="1.4"/>
         </DoubleAnimationUsingKeyFrames>
      </Storyboard>
   </VisualState></VisualStateGroup>

La structure de chaque état est simple, les transitions sont par défaut centralisées dans la balise Visual-StateGroup.Transitions. La définition de chaque état contenant les propriétés modifiées est stockée, quant à elle, dans une balise VisualState. Cette balise contient au moins l'attribut x:Name correspondant au nom de la transition. Chaque objet de type VisualState contient un Storyboard. Cette animation est déclenchée chaque fois que l'état doit être affiché, mais par défaut les clés d'animation se situent toutes à la seconde 0. Autrement dit, la durée du Storyboard lui-même est par défaut de 0 seconde. La durée de la transition, qui est différente, est gérée au niveau des transitions, via la propriété GeneratedDuration, qui est configurable via le panneau States au sein de Blend. Supprimez la transition que nous avions définie pour l'état MouseOver en passant soit par le code XAML, soit par l'interface de Blend. Pour le faire au sein de Blend, il vous suffit de cliquer sur l'icône moins (-). Déplacez ensuite la clé d'animation à la seconde 0.6. Pour finir, définissez une équation d'accélération de type CubicEase sur celle-ci. Vous obtenez le code XAML ci-dessous :

 
Sélectionnez
<VisualStateGroup x:Name="CommonStates">
   <VisualStateGroup.Transitions>
      <VisualTransition GeneratedDuration="00:00:00.4"/>
   </VisualStateGroup.Transitions><VisualState x:Name="MouseOver">
      <Storyboard>
         <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
               Storyboard.TargetName="contentPresenter" Storyboard.
               TargetProperty="(UIElement.RenderTransform).
               (TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
            <EasingDoubleKeyFrame KeyTime="00:00:00.6" Value="1.4">
               <EasingDoubleKeyFrame.EasingFunction>
                  <CubicEase EasingMode="EaseOut"/>
               </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
         </DoubleAnimationUsingKeyFrames>
         <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.
               TargetName="contentPresenter" Storyboard.
               TargetProperty="(UIElement.RenderTransform).
               (TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
            <EasingDoubleKeyFrame KeyTime="00:00:00.6" Value="1.4">
               <EasingDoubleKeyFrame.EasingFunction>
                  <CubicEase EasingMode="EaseOut"/>
               </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
         </DoubleAnimationUsingKeyFrames>
      </Storyboard>
   </VisualState></VisualStateGroup>

Maintenant que vous connaissez toutes les manières de créer des transitions, finalisez le design de chaque état interactif au sein du groupe CommonStates. Vous avez le choix : soit différencier l'état Pressed de l'état MouseOver, soit les cloner. Dupliquer l'état, par copier-coller du code XAML déjà généré, reste le plus simple.

Si vous créez un Storyboard personnalisé comme réalisé pour l'état MouseOver, vous ne pourrez pas avoir d'aperçu de cet état en utilisant l'icône de l'œil. C'est tout à fait normal puisque cet œil ne montre que la seconde 0 du Storyboard (Image non disponible). L'idéal serait évidemment d'afficher la clé d'animation finale.

Pour l'état Disabled, vous pouvez passer l'opacité de l'arrière-plan de couleur à 0 et passer sa propriété Visibility à Collapsed. Cela ne suffit pas pour le différencier de l'état Normal. Affectez l'opacité et la visibilité du ContentPresenter de la même manière. Ainsi lorsque le bouton est désactivé, il est complètement caché. Dépliez la ligne du temps ou scénario de cet état. Décalez ensuite toutes les clés d'animation de 6/10e de seconde. La propriété Visibility est animée grâce à une clé de type Discrete. Cela signifie qu'elle ne prendra la valeur Collapsed que lorsque la tête de lecture atteindra la clé. L'opacité sera, quant à elle, interpolée, car la plage de valeurs admises est de type Double (voir Figure 7.26).

Image non disponible
Figure 7.26 - Scénario de l'état Disabled

6-4-2-4. Contraintes logiques

Vous pouvez décider de personnaliser les états Focused et Unfocused. Si tel est le cas, sachez que vous ne pourrez pas réutiliser des propriétés déjà modifiées dans le groupe d'états CommonStates. Pour vous en rendre compte, sélectionnez l'état Unfocused et modifiez l'opacité du tracé vectoriel assurant le fond de couleur. Au sein du panneau des états, une nouvelle icône apparaît. Elle vous indique une utilisation illogique des états (voir Figure 7.27).

Image non disponible
Figure 7.27 - Erreur logique d'enregistrement d'état

Le dilemme est simple, les groupes d'états sont indépendants les uns des autres du point de vue de leur affichage. Le bouton peut posséder le focus ou non, et être survolé par la souris en même temps. Dès lors, les deux états vont être tous deux affichés. Toutefois, comme la propriété Opacity du tracé Background a été affectée dans chaque état, le gestionnaire d'états visuels ne saura pas laquelle des deux affectations choisir. L'affectation est due au fait que le bouton n'a pas le focus ou elle est générée par le survol de la souris. C'est un choix impossible à résoudre de manière élégante et cela est tout à fait logique d'un point de vue conception. Il n'y a aucune manière simple de résoudre ce type de conflit, car c'est le raisonnement à la source qui est faux.

Avoir le focus utilisateur n'est pas la même chose que survoler le bouton. Le visuel doit donc être différent. Supprimez les modifications apportées à l'état Unfocused. Le mieux serait de rajouter une forme de type Ellipse sous le ContentPresenter, avec un léger dégradé du blanc vers le blanc transparent pour simuler une lueur. Lorsque le bouton n'a pas l'intérêt utilisateur, cette lueur est désactivée en passant son opacité à 0. Compilez et testez votre application en utilisant la touche Tabulation (voir Figure 7.28).

Image non disponible
Figure 7.28 - Gestion visuelle du focus utilisateur

Pour voir l'affichage du focus sans l'arrière-plan de couleur, vous pouvez sélectionner l'état Focused, puis cliquer sur l'icône d'aperçu à gauche de l'état Normal (voir Figure 7.28). De cette manière, vous pouvez tester le visuel de l'état Focused avec n'importe quel autre état interactif, si celui-ci n'appartient pas au groupe FocusStates.

Vous trouverez cet exercice corrigé dans : chap7/PlayerVideo_ButtonsAnime.zip.

6-4-3. Au niveau de l'application

Nous allons maintenant créer des états au niveau de l'application elle-même. Les états que nous avons configurés jusqu'à présent étaient propres à la classe Button. Une application Silverlight ne possède pas d'états visuels par défaut, il vous faudra donc les créer manuellement. Avant de commencer, il importe de faire un rapide tour des différents affichages que nous pourrions envisager.

6-4-3-1. Découper l'application

Nous partons du principe que le lecteur vidéo s'occultera de lui-même après un certain temps d'inactivité de la souris. Il sera donc parfois visible et parfois invisible. Cette fonctionnalité peut faire l'objet d'un groupe d'états nommé VisibleStates. Pour l'instant le lecteur vidéo ne semble présenter que des fonctionnalités sommaires, toutefois on pourrait imaginer plusieurs affichages différents.

Par exemple, à chaque fois que la souris survolerait l'un des boutons de contrôle, le disque se teinterait partiellement pour illustrer l'action en cours. Pour créer ce type d'interaction, il nous faudra créer un groupe d'états nommé ColorStates. Nous pourrions également proposer deux modes d'affichage. L'utilisateur pourrait soit utiliser les contrôles de base que nous avons déjà réalisés, soit consulter l'état de téléchargement et naviguer grâce à une tête de lecture directement sur une ligne de temps représentée par le disque. Au final, cette fonctionnalité constituerait également un groupe d'états visuels nommé ControlStates.

Nous aurons donc trois groupes d'états indépendants qui géreront chacun l'affichage de l'une de ces fonctionnalités (voir Figure 7.29).

Image non disponible
Figure 7.29 - Schéma des groupes d'états visuels propres à notre application

6-4-3-2. Créer des états visuels

Positionnez-vous au niveau de l'application et affichez le panneau States qui, pour l'instant est vide. Créez trois groupes via l'icône d'ajout de groupe (Image non disponible) et nommez-les respectivement VisibleStates, ColorStates et ControlStates. Vous allez créer tous les états décrits à la Figure 7.29. À droite du groupe VisibleStates, cliquez sur l'icône d'ajout d'état (Image non disponible). Nommez le nouvel état créé VisiblePlayer et répétez l'opération autant de fois que nécessaire pour tous les groupes. Vous allez commencer par gérer les transitions du groupe ColorStates. Pour celui-ci, définissez une transition générique de 4/10e de seconde ainsi qu'une équation d'accélération de votre choix. Vous n'avez pas réellement besoin d'animations personnalisées pour l'interpolation de couleur. Utiliser une animation générique est dans ce cas le meilleur moyen de gagner du temps (voir Figure 7.30).

Image non disponible
Figure 7.30 - Groupes d'états visuels du lecteur vidéo

Il ne vous reste plus qu'à sélectionner chaque état au sein de ce groupe pour modifier la couleur du dégradé du disque situé sous les contrôles utilisateur. Pour récupérer les bonnes nuances de couleur, vous pouvez temporairement modifier l'état Normal du bouton générique. En passant la valeur d'opacité du bouton à 100 %, vous faites apparaître les couleurs de chacun des contrôles. Ensuite, sélectionnez l'état Purple, puis dans l'arbre visuel de l'application, l'objet Large-ColorRing. C'est en fait l'un des disques que vous allez colorer. Modifiez son remplissage pour un dégradé du rose vers le blanc. Vous pouvez vous baser sur la couleur du bouton de volume VolumeUpBtn. Sélectionnez ensuite le tracé SmallColorRing et recommencez le processus. Procédez de même pour chaque état de couleur en utilisant les couleurs de base de chaque bouton. Retournez dans le modèle de bouton générique, au sein de l'état Normal, et redéfinissez un arrière-plan à 100 % d'opacité. Avant de piloter le gestionnaire d'états visuels, vous pouvez créer une ou deux animations personnalisées pour les états du groupe VisibleStates. Ne vous occupez pas pour l'instant du groupe ControlStates.

6-4-3-3. Gérer les transitions grâce aux comportements interactifs

Les comportements ou Behaviors au sein de Blend et de l'API XAML permettent aux designers interactifs de gérer par eux-mêmes les interactions simples. Ouvrez le panneau Assets et le menu Behaviors, la liste des comportements accessibles par défaut apparaît (voir Figure 7.31).

Image non disponible
Figure 7.31 - Liste des comportements disponibles

Vous les utiliserez tous, tôt ou tard dans vos développements ainsi que dans ce livre. Nous les aborderons plus en détail au Chapitre 7 Interactivité et modèle événementiel. Glissez-déposez le comportement nommé GoToStateAction sur chacun des quatre contrôles utilisateur déjà réalisés. Ils permettent d'accéder à un état visuel lors de l'interaction utilisateur de votre choix. Dans le panneau des propriétés, configurez-les pour qu'ils réagissent au survol de la souris (MouseEnter) et ciblent l'état de couleur correspondant au sein du groupe ColorStates (voir Figure 7.32).

Image non disponible
Figure 7.32 - Configuration du comportement GoToStateAction

Compilez et testez le projet. Vous remarquez que dès que vous survolez un bouton, l'application change d'état visuel. La couleur des deux disques se met à jour dynamiquement. Toutefois, l'application ne réaffiche pas l'état Normal. Il faudrait pour cela définir un nouveau comportement ciblant cet état lorsque la souris quitte le survol des boutons. C'est assez simple à réaliser.

Afin de vous éviter un travail fastidieux, groupez tous les contrôles au sein d'un conteneur de type Canvas. Déposez le comportement GoToStateAction sur ce dernier. Ensuite, configurez-le afin qu'il se déclenche lorsque la souris quitte son survol et qu'il cible l'état Normal. Cette manière de procéder vous évite de recréer le même comportement quatre fois (une fois par contrôle). L'arbre visuel de notre application s'est légèrement étoffé, vous pouvez voir la portion correspondant aux comportements ajoutés sur la Figure 7.33.

Image non disponible
Figure 7.33 - Partie de l'arbre visuel avec comportements

Testez et compilez votre application : celle-ci réagit de manière fluide lors du survol de chaque contrôle.

6-4-3-4. Piloter le gestionnaire d'états visuels avec C#

Piloter et gérer le déclenchement des transitions en C# est assez simple. Nous devrons pour cela utiliser la classe VisualStateManager. Nous allons faire en sorte que tant que la souris bouge, l'état VisiblePlayer soit l'état de destination. Au bout de deux secondes d'inactivité, l'état ciblé sera HiddenPlayer. Le code ci-dessous constitue la première étape :

 
Sélectionnez
public partial class MainPage : UserControl
{

   public MainPage()
   {
      InitializeComponent();

      //Par défaut le lecteur est caché
      VisualStateManager.GoToState(this, "HiddenPlayer", true);

      //on écoute l'événement MouseMove
      LayoutRoot.MouseMove += LayoutRoot_MouseMove;
   }

   //Chaque fois que celui-ci est diffusé
   //donc lorsque la souris bouge
   void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
   {
      //on utilise le gestionnaire d'états visuels 
      //afin d'afficher l'état VisiblePlayer
      VisualStateManager.GoToState(this, "VisiblePlayer", true);
   }
}

Dans le code ci-dessus, les deux lignes importantes concernent la classe VisualState-Manager. Elle possède la méthode statique GoToState qui permet d'afficher l'état visuel de n'importe quel contrôle. Le premier argument est le contrôle dont vous souhaitez afficher l'état. Dans notre cas, nous souhaitons atteindre l'état du UserControl racine, soit this, l'instance de la classe MainPage. Le second paramètre est la chaîne de caractères correspondant au nom de l'état. Le dernier argument permet de spécifier si nous souhaitons utiliser ou non une transition animée. Toutefois, cette option ne concerne pas les transitions reposant sur un objet Storyboard spécifique. Une transition personnalisée sera jouée quelle que soit la valeur du dernier argument, car ce n'est pas l'objet State qui définit la durée d'animation (via la propriété GeneratedDuration) mais, dans ce cas, le Storyboard. Pour finaliser l'interaction utilisateur, il va falloir créer un compte à rebours. Ce dernier sera géré par une instance d'objet de type DispatcherTimer. L'idée consiste à réinitialiser le compte à rebours à chaque fois que la souris bouge. Lorsque la souris ne bouge plus, le compte à rebours égrène les secondes. Au bout de deux secondes d'inactivité, on déclenche l'animation vers l'état HiddenPlayer. On occulte donc celui-ci.

 
Sélectionnez
//Ajouter l'espace de noms Threading
//pour instancier DispatcherTimer
using System.Windows.Threading;

namespace PlayerVideo
{
   public partial class MainPage : UserControl
   {
      //on crée un métronome
      DispatcherTimer Dt = new DispatcherTimer();
      //on lui définit un intervalle de temps de deux secondes
      Dt.Interval=TimeSpan.FromSeconds(2);

      public MainPage()
      {
         InitializeComponent();

         //Par défaut le lecteur est caché
         VisualStateManager.GoToState(this, "HiddenPlayer", true);

         //on écoute l'événement MouseMove
         LayoutRoot.MouseMove += LayoutRoot_MouseMove;

         //on écoute le métronome (DispatcherTimer)
         Dt.Tick += Dt_Tick;
      }

      //Chaque fois que celui-ci est diffusé
      //donc lorsque la souris bouge
      void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
      {
         //on utilise le gestionnaire d'états visuels 
         //afin d'afficher l'état VisiblePlayer
         VisualStateManager.GoToState(this, "VisiblePlayer", true);

         //puis on réinitialise le compte à rebours à deux secondes
         Dt.Start();
      }

      //lorsque les deux secondes sont atteintes
      //c'est-à-dire lorsque la souris 
      //ne bouge plus depuis deux secondes
      void Dt_Tick(object sender, EventArgs e)
      {
         //on cache le lecteur vidéo grâce à la classe
         //VisualStateManager
         VisualStateManager.GoToState(this, "HiddenPlayer", true);
         
         //on stoppe le métronome pour optimiser les performances
         Dt.Stop();
      }
   }

Testez et compilez l'application, les transitions sont fluides et agréables. Les contrôles du lecteur vidéo ne sont accessibles que lorsque l'utilisateur en a réellement besoin. Le lecteur est donc contextuel, ce qui est encore la meilleure manière de concevoir l'expérience utilisateur. Vous trouverez cet exercice dans : chap7/PlayerVideo_AppAnime.zip.

6-5. Le bouton interrupteur ou ToggleButton

Nous allons maintenant personnaliser un bouton de type interrupteur (ToggleButton). Vous pouvez considérer ce type de bouton comme une case à cocher (CheckBox), car seule leur forme diffère. Ces deux composants possèdent trois états visuels supplémentaires par rapport à un bouton traditionnel : Checked, Unchecked et Indeterminate. Habituellement, on trouve ces boutons dans les formulaires. Grâce au XAML et à la personnalisation de contrôle utilisateur, nous utiliserons le comportement on/off de ce type de bouton pour alterner entre la lecture de la vidéo et sa mise en pause.

6-5-1. Créer le bouton

Dans l'arbre visuel et logique de l'application, sélectionnez la grille nommée PlayPause, cliquez droit dessus et choisissez l'option Make Into Control… Dans la liste affichée, sélectionnez ToggleButton, nommez le style PlayPauseStyle. Vous pouvez vous aider du champ de recherche pour afficher le bouton de type ToggleButton (voir Figure 7.34). Confirmez ensuite la création du style en cliquant sur OK.

Image non disponible
Figure 7.34 - Création du style PlayPause

Commencez par cacher le composant ContentPresenter en passant sa propriété Visibility sur Collapsed. Vous utiliserez ce composant ultérieurement pour indiquer une information à l'utilisateur. Vous allez faire apparaître le reflet lors du survol de la souris uniquement. Pour cela, sélectionnez l'état Normal et créez une animation de votre choix pour faire disparaître le reflet de manière élégante. Vous pouvez, par exemple, concevoir une animation personnalisée sur les dégradés simulant l'impact lumineux et le reflet. Ceux-ci correspondent respectivement aux valeurs de la propriété Fill pour les composants Ellipse RefletHaut et RefletBase.

Créer une animation personnalisée pour l'état Normal vous simplifiera grandement la tâche. Il suffit de faire disparaître les reflets déjà présents. Pour les autres états, vous pouvez utiliser des transitions génériques afin d'alléger au maximum votre travail (voir Figure 7.35). Testez et compilez le projet.

Image non disponible
Figure 7.35 - Exemple d'animation de disparition pour l'état normal

6-5-2. Le système d'agencement fluide

Les trois états supplémentaires, propres au ToggleButton et déjà évoqués, sont présents au sein d'un groupe d'états nommé CheckStates. Nous n'avons pas besoin d'utiliser l'état Indeterminate, car nous avons besoin d'un interrupteur à deux états. Lorsque le bouton sera coché, nous ferons apparaître la grille nommée IconePause, indiquant ainsi à l'utilisateur que la vidéo est en cours de lecture et qu'il doit appuyer sur le bouton pour la mettre en pause. À l'opposé, lorsque le bouton sera décoché, la vidéo sera sur pause et la grille IconePlay sera visible, indiquant à l'utilisateur qu'il peut la remettre en lecture à tout instant.

Sélectionnez l'état Checked et affectez la propriété Visibility de IconePause à Visible, ensuite passez la propriété Visibility de la grille IconePlay à Collapsed. Compilez et testez votre projet, les icônes apparaissent alternativement l'une et l'autre lors de chaque clic gauche de la souris.

Toutefois, il n'y a pas de transitions fluides entre les états cochés et non cochés. Cela est très simple à résoudre grâce à une fonctionnalité présente depuis Silverlight 3, nommée système d'agencement fluide ou Fluid Layout System. Les propriétés qui ne sont pas de type Point, Double ou Color ne sont pas interpolées et subissent des animations de type Discreet. Le système d'agencement fluide résout en partie cette limitation en générant une animation de ce que pourrait être l'interpolation, la plus logique, de telles propriétés. Par exemple, vous pourriez décider qu'un des enfants d'une grille soit aligné à gauche dans un état visuel, puis à droite dans un autre état visuel. Habituellement, une transition fluide serait impossible, car l'alignement est géré par la propriété Horizontal-Alignment qui est une énumération. Toutefois, le système d'agencement fluide permet de simuler ce que serait l'animation la plus logique entre les deux positions d'alignement.

Commencez par définir une transition générique d'une durée de 6/10e de seconde pour le groupe d'état CheckStates. Cliquez sur l'icône représentant des vaguelettes (Image non disponible) située à droite de l'état CheckStates afin d'activer le système d'agencement fluide. Compilez et testez à nouveau le bouton de lecture/pause. Cette fois une animation est jouée ; elle n'était pas vraiment facile à prévoir, on a l'impression d'un redimensionnement des icônes permettant de les faire apparaître ou disparaître. En un seul clic, nous avons permis de simuler l'animation de la propriété Visibility qui n'est pourtant pas interpolable. Du côté XAML, le code est assez simple :

 
Sélectionnez
<VisualStateGroup x:Name="CheckStates" 
      ic:ExtendedVisualStateManager.UseFluidLayout="True">
   <VisualStateGroup.Transitions>
      <VisualTransition GeneratedDuration="00:00:00.6"/>
   </VisualStateGroup.Transitions>
   <VisualState x:Name="Unchecked"/>
   <VisualState x:Name="Indeterminate"/>
   <VisualState x:Name="Checked">
      <Storyboard>
         <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" 
               Duration="00:00:00.001" Storyboard.TargetName="IconePause" 
               Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
               <DiscreteObjectKeyFrame.Value>
                  <Visibility>Visible</Visibility>
               </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
         </ObjectAnimationUsingKeyFrames>
         <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" 
               Duration="00:00:00.001" Storyboard.TargetName="IconePlay" 
               Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
               <DiscreteObjectKeyFrame.Value>
                  <Visibility>Collapsed</Visibility>
               </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
         </ObjectAnimationUsingKeyFrames>
      </Storyboard>
   </VisualState>
</VisualStateGroup>

La propriété UseFluidLayout possède la valeur true, c'est celle-ci qui permet d'activer le gestionnaire d'états visuels. Vous remarquez cependant que la propriété Visibility n'est pas interpolée, car elle subit des clés d'animation de type DiscreteObjectKeyFrame. L'animation réelle ressemblant à un redimensionnement (RenderTransform) nous est complètement occultée. C'est le gestionnaire d'états qui gère cette animation en arrière-plan. Il ne vous reste plus qu'à créer un nouveau comportement pour le bouton de lecture, qui indiquera à l'application de naviguer vers l'état Black lors de son survol (voir section 6.4.3.3 Gérer les transitions grâce aux comportements interactifs).

D'un point de vue design, le lecteur vidéo est terminé, vous finaliserez son développement au Chapitre 11 Composants personnalisés, dédié aux composants personnalisés. Vous trouverez le lecteur vidéo dans : chap7/PlayerVideo_ToggleButton.zip.

Nous ne finalisons pas le lecteur vidéo dans ce chapitre pour diverses raisons. L'une d'elles est que la création du code logique pour ce type d'application repose grandement sur la notion de programmation événementielle. Depuis le début du livre, nous avons évoqué à de nombreuses reprises ce concept, sans vraiment l'étudier. Cette notion est très importante au sein des langages de haut niveau tels que C#. Nous allons donc l'aborder au Chapitre 7 Interactivité et modèle événementiel et voir comment une utilisation adéquate de ses concepts peut nous faciliter grandement la vie.

7. Interactivité et modèle événementiel

Le concept d'interactivité repose sur la notion de stimulus, et d'une ou plusieurs actions invoquées en réponse à celui-ci. Ce principe, pour les applications, est essentiellement lié à l'utilisateur, il fait appel à la logique événementielle. Dans ce chapitre, vous étudierez en profondeur les mécanismes du modèle événementiel dans Silverlight, pour le langage C#. Ainsi, vous apprendrez la diffusion, la souscription et le désabonnement d'événements. Puis, vous verrez comment optimiser votre code grâce au couplage faible et à la propagation des événements. Pour finir, vous aborderez la création de comportements interactifs personnalisés afin de faciliter et d'améliorer le flux de production entre designers et développeurs.

7-1. Les fondements du modèle événementiel

Le modèle événementiel, à la base de tout développement orienté utilisateur, est souvent relié aux langages de haut niveau, tels que C#. Il occupe également une place important au sein des technologies asynchrones comme Silverlight ou Ajax. Il correspond pleinement à la notion de connexion distante sur laquelle repose Internet ; sur le Web, rien n'est synchrone. La durée écoulée entre l'instant où vous cliquez sur un lien et le moment où la nouvelle page s'affiche varie de manière impossible à prévoir. Cette variation s'explique assez bien : les données sont distantes et doivent donc être téléchargées avant d'être affichées. Le débit des lignes Internet, la fréquentation d'un site ou l'occupation mémoire comme processeur d'un serveur à un instant t ne sont jamais identiques. La logique événementielle prend tout son sens sur ce type de réseau. Il en va de même pour les interactions utilisateur.

Le développeur ne peut jamais prévoir quand un utilisateur va cliquer sur un bouton ou quand il va bouger la souris. Il peut, en revanche, décider d'un comportement à adopter lorsque cela arrivera. Pour cela, il utilise les outils fournis par le modèle événementiel, propres à chaque langage. Nous allons maintenant essayer de comprendre l'origine et les mécanismes du modèle événementiel.

7-1-1. Le pattern Observateur

Les modèles de conception (ou design patterns) sont nés à la fin des années 80 avec l'avènement de la programmation orientée objet (POO). À cette époque, la majeure partie, soit 99,99 %, des problèmes de conception avait déjà été résolue par les développeurs en matière de POO et n'avait plus rien d'originale. Les patterns sont nés du besoin de formaliser des solutions génériques répondant aux différentes problématiques rencontrées. Dès 1991, quatre développeurs, connus sous le nom de "Gang of Four", écrivent le premier ouvrage de référence sur ce sujet : Design Patterns: Elements of Reusable Object-Oriented Software. Il s'agit d'Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides. Ces concepteurs formalisent ainsi les bonnes pratiques de conception orientée objet répondant à la majorité des problématiques. Le modèle Observateur, ou Observer en anglais est l'un d'entre eux, il fait partie des patterns liés à la gestion du comportement des objets. Il définit une relation de un à plusieurs objets, et permet lorsqu'un objet change d'état, de le notifier à plusieurs autres qui en dépendent. Ceux-ci peuvent ainsi réagir ou être mis à jour automatiquement. Pour que les objets soient notifiés ou informés des changements, ils doivent souscrire auprès de l'objet qui diffuse les notifications. Ce modèle de conception repose donc sur deux notions essentielles : la souscription et la diffusion de notifications. C'est exactement le principe de la programmation événementielle. Dans ce contexte, une notification est appelée événement. Prenons un exemple de la vie courante.

  1. Vous désirez acheter ou louer un bien immobilier. Vous souhaitez être informé lorsqu'un nouvel appartement ou qu'une nouvelle maison est disponible à l'achat ou à la location pour ne pas vous déplacer pour rien.
  2. Vous souscrivez aux lettres d'informations de plusieurs agences immobilières. Vous vous abonnez en fait aux événements "nouvel appartement à louer" et "nouvel appartement à acheter" diffusés par les agences immobilières.
  3. Lorsqu'une agence reçoit une nouvelle offre de location ou d'achat, elle vous en fait part. Pourquoi ? Simplement parce que vous êtes abonné. Les non abonnés ne sont pas notifiés de l'événement.
  4. Vous pouvez réagir différemment en fonction des caractéristiques du bien immobilier. Le descriptif de celui-ci est contenu le plus souvent dans un objet événementiel, par exemple, la lettre d'information.
  5. Lorsque vous avez trouvé le bien adéquat et que vous l'avez acquis, vous vous désabonnez, car il n'est plus nécessaire de chercher.

Nous venons de décrire exactement le principe du modèle événementiel. Chaque agence immobilière possède plusieurs abonnés et diffuse des événements de type "nouveau bien disponible" à chacun d'eux. Le diffuseur est également appelé sujet, les abonnés sont appelés écouteurs (voir Figure 8.1).

Nous allons maintenant démontrer en quoi ces principes sont souples et faciles à maintenir à travers une introduction au couplage faible.

Image non disponible
Figure 8.1 - Principes du pattern Observateur à travers l'exemple d'annonces immobilières

7-1-2. Introduction au couplage faible

Lorsque deux objets n'ont pas besoin de se connaître pour collaborer ensemble, on dit qu'ils sont faiblement couplés, c'est-à-dire qu'ils n'entretiennent pas de relations spécifiques fortes. Une relation forte signifie qu'un objet fait explicitement référence à un autre objet pour être fonctionnel. Le couplage faible représente exactement la nature des relations entretenues par les écouteurs et les diffuseurs d'événements :

  • le sujet (ou diffuseur) n'a pas besoin de connaître les objets qui écoutent ses événements diffusés ; ainsi, il est possible de supprimer des écouteurs sans que le diffuseur ne soit dérangé dans son fonctionnement ;
  • de même, il est possible d'ajouter des écouteurs à tout instant en cours d'exécution à n'importe quel diffuseur ;
  • les écouteurs ou les diffuseurs peuvent avoir d'autres activités que la souscription ou la diffusion d'événements de manière totalement indépendante ;
  • modifier un objet abonné ou diffuseur ne change pas les relations ou le fonctionnement de chacun des acteurs du processus.

L'avantage principal du couplage faible est de permettre des conceptions souples et faciles à maintenir. Il est possible de concevoir des combinaisons d'écouteurs /diffuseurs plus complexes, sans mettre en danger le fonctionnement ou la cohérence des liaisons. Par exemple, vous pouvez vous abonner aux lettres d'informations de plusieurs agences immobilières (voir Figure 8.2).

De plus, la suppression d'un écouteur ou d'un sujet n'affecte en rien le fonctionnement de l'un ou de l'autre. Nous verrons à la section 8.2 Les propriétés 3D comment désabonner un écouteur à l'exécution.

Image non disponible
Figure 8.2 - Un schéma plus complexe

7-1-3. Souscrire à un événement en C#

Dans la majorité des conceptions, l'écouteur est une méthode. Ainsi, définir un écouteur d'événements consiste à définir une méthode s'exécutant à chaque diffusion de l'événement. Toutefois, nous allons voir que ces méthodes sont particulières. Si vous développez pour le Web en Java-Script ou ActionScript, vous avez pu rencontrer l'écriture suivante :

  • uneInstance.addEventListener("événement", écouteur ) ;

L'écriture en C# est assez comparable. Le code générique ci-dessous permet d'écouter un événement en langage C# :

 
Sélectionnez
UneInstance.Evenement += Ecouteur ; //l'écouteur est une méthodevoid Ecouteur ( Diffuseur, ObjetEvenementiel)
{
   //Fait quelque chose
}

L'opérateur d'affectation += permet d'ajouter la méthode à la liste des écouteurs. En pratique, déclencher la méthode ClickButton chaque fois que l'utilisateur clique sur un bouton nommé MonBouton est réalisé comme ci-dessous :

 
Sélectionnez
MonBouton.Click += new RoutedEventHandler(ClickButton);void ClickButton ( object sender, RoutedEventArgs e)
{
   //Fait quelque chose
}

Voici la traduction en "bon français" de ce qui est réalisé : l'écouteur, représenté par la méthode ClickButton, souscrit à l'événement Click de MonBouton. Lorsque l'utilisateur clique sur le bouton durant l'exécution, ce dernier diffuse l'événement Click. La méthode ClickButton est alors invoquée, car elle fait partie des abonnés à l'événement Click de MonBouton

Nous allons maintenant mettre en pratique cet exemple.

7-1-4. Cas pratique

Ouvrez le projet nommé PlayerVideo_ToggleButton réalisé au Chapitre 6 Boutons personnalisés. Vous pouvez également le trouver dans l'archive PlayerVideo_ToggleButton.zip du dossier du chap8. Sauvegardez-en une copie nommée PlayerVideo_Evenement via le menu File d'Expression Blend, puis ouvrez cette copie ; cela vous évite de modifier le projet original. Nous allons remplacer les comportements (ou Behaviors) définis dans Expression Blend, pour naviguer d'un état visuel de l'application à un autre, par du code C#. Commencez par supprimer tous les comportements que vous trouverez sur les boutons (dont le Toggle-Button). Nommez le ToggleButton de lecture/pause PlayPauseBtn et le conteneur Canvas des contrôles ControlsRingArea. Ensuite, dans le code C#, ajoutons l'équivalent de l'interactivité utilisateur produite par les comportements. Pour cela, il faut écouter chaque bouton de contrôle du lecteur vidéo séparément comme montré dans le code ci-dessous :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   //Par défaut le lecteur est caché
   VisualStateManager.GoToState(this, "HiddenPlayer", true);

   //on écoute l'événement MouseMove
   LayoutRoot.MouseMove += new MouseEventHandler(LayoutRoot_MouseMove);

   Dt.Tick += new EventHandler(Dt_Tick);

   #region gestion des transitions vers les états de 
   #couleurs ColorStates Group
   ForwardBtn.MouseEnter += new MouseEventHandler(ForwardBtn_MouseEnter);

   BackwardBtn.MouseEnter += new MouseEventHandler(BackwardBtn_MouseEnter);

   VolumeDownBtn.MouseEnter += new MouseEventHandler(VolumeDownBtn_MouseEnter);

   VolumeUpBtn.MouseEnter += new MouseEventHandler(VolumeUpBtn_MouseEnter);

   PlayPauseBtn.MouseEnter += new MouseEventHandler(PlayPauseBtn_MouseEnter);

   ControlsRingArea.MouseLeave += new MouseEventHandler(ControlsRingArea_MouseLeave);
   #endregion

}

   ...

void ControlsRingArea_MouseLeave(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Normal", true);
}

void VolumeUpBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Purple", true);
}

void VolumeDownBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Cyan", true);
}

void BackwardBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Orange", true);
}

void ForwardBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Green", true);
}

void PlayPauseBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Black", true);
}

Le code généré est un peu verbeux, vous pourriez l'éviter en définissant l'écoute de l'événement en langage déclaratif XAML via le panneau Properties. Toutefois, cela peut se révéler plus problématique qu'autre chose. Le développeur n'ira pas naturellement vers cette solution propre aux designers interactifs. Si l'écoute est définie dans le XAML, elle est partiellement cachée au développeur, car un fichier déclaratif peut être fastidieux à explorer. Cela pourrait engendrer des pertes de performance ou des conflits, si le développeur n'a pas pris connaissance de l'écoute d'un événement.

Testez et compilez le lecteur vidéo, vous obtenez exactement la même interactivité qu'avec les comportements. Vous trouverez plusieurs pistes à la section 7.5 Les comportements afin de choisir entre comportements interactifs et programmation événementielle C#. Ce choix sera fonction de la situation et du contexte de production que vous rencontrerez. Il est tout de même utile de préciser que l'avantage de la programmation événementielle reste le contrôle total des performances par le développeur. En effet, ce dernier peut, à tout moment, ajouter ou supprimer l'écoute d'un événement à l'exécution. Ainsi, l'application ne consomme que la mémoire et la ressource processeur qui lui est utile à un instant donné. Nous allons maintenant étudier plusieurs techniques d'optimisation.

7-2. Supprimer l'écoute d'un événement

La première technique consiste tout simplement à arrêter l'écoute d'un événement lorsqu'il n'est plus utile d'y réagir.

7-2-1. Principe

Dans la vie réelle, c'est une tâche courante et saine. Dans l'exemple de l'agence immobilière, lorsque l'un des abonnés aux lettres d'informations a trouvé un appartement ou une maison, il ne souhaite plus être informé des nouvelles offres. Traduire cette notion en langage C# est simple : à l'opposé de l'opérateur d'affectation += qui signifie ajouter, supprimer se traduit par l'opérateur -=. Le code ci-dessous montre l'écriture complète :

 
Sélectionnez
UneInstance.Evenement -= Ecouteur ;

Ainsi, pour supprimer l'écoute d'un bouton, vous pouvez écrire :

 
Sélectionnez
MonBouton.Click -= ClickButton;

Pour éviter toute confusion, vous pouvez traduire ce code en français par : l'écouteur Click-Button se désabonne de l'événement Click diffusé par MonBouton

Il ne faut pas vous imaginer que vous supprimez la méthode ClickButton de cette manière. Dans les faits, vous supprimez juste la référence ClickButton dans la liste des écouteurs de l'événement Click (voir Figure 8.3).

Vous pourrez donc souscrire à nouveau ClickButton à cet événement plus tard, si besoin est. Si la souscription auprès d'événements diffusés est importante, le désabonnement l'est tout autant pour diverses raisons. La première est de contrôler l'interactivité. La seconde raison est liée à la gestion des ressources. Écouter un événement consomme de la mémoire, parfois inutilement. Par exemple, écouter le déplacement de la souris occupe beaucoup de ressources processeur. Ainsi, il est fortement conseillé de supprimer l'écoute de l'événement MouseMove quand vous n'en avez plus besoin. Le langage C# est dit géré, car l'allocation des ressources mémoire n'est pas directement gérée par le développeur, mais par le ramasse-miettes ou Garbage Collector. Toutefois, pour que ce dernier libère les ressources le plus tôt possible, il est important de lui présenter des objets qui ne sont plus référencés par d'autres, lorsque vous n'en avez plus besoin. Arrêter la souscription d'un événement entre dans ce cadre.

Image non disponible
Figure 8.3 - Suppression d'un événement

7-2-2. Un cas concret d'utilisation

Dans cet exemple, nous allons utiliser le projet nommé PlayerVideo_Optimisations. Il se trouve dans l'archive PlayerVideo_Optimisations.zip du dossier chap8 des exemples. L'idée de ce projet est simple : deux nouveaux états permettent de positionner le lecteur vidéo soit au centre de la fenêtre, soit en bas. Pour changer de position dynamiquement, il suffit de cliquer alternativement sur le bouton interrupteur, nommé FixedPositionBtn. Lorsque les contrôles utilisateur ne sont pas placés au centre, il n'y a pas d'effet de disparition au bout de deux secondes. Pour réaliser cette tâche, le développeur a écouté les événements Checked et Unchecked du bouton Fixed-PositionBtn. Ainsi, il peut à la fois arrêter le compte à rebours de disparition et déplacer les contrôles utilisateurs vers le bas ou le centre de l'application. De même, il teste lors du déplacement de la souris (événement MouseMove) si le bouton est coché ou décoché. Ainsi, il peut également arrêter ou lancer le compte à rebours en fonction du ToggleButton nommé Fixe-PositionBtn. Voici le code logique correspondant à ce développement :

 
Sélectionnez
public MainPage()
{
…
   LayoutRoot.MouseMove += new MouseEventHandler(LayoutRoot_MouseMove);
…
   FixePositionBtn.Checked += new RoutedEventHandler(FixePositionBtn_Checked);
   FixePositionBtn.Unchecked += new RoutedEventHandler(FixePositionBtn_Unchecked);
}

void FixePositionBtn_Unchecked(object sender, RoutedEventArgs e)
{
   VisualStateManager.GoToState(this, "Center", true);
   //la position est centrée, car le bouton est décoché,
   //on cache donc automatiquement les contrôles utilisateur
   //au bout de deux secondes
   Dt.Start();
}

void FixePositionBtn_Checked(object sender, RoutedEventArgs e)
{
   VisualStateManager.GoToState(this, "Bottom", true);
   //le bouton fixe position est coché, 
   //on ne fait donc pas disparaître le lecteur
   //en stoppant le compte à rebours de disparition
   Dt.Stop();
}//lorsque la souris bouge
void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
{
   //on teste la position centrée ou basse des contrôles
   //utilisateur, si la position est basse on arrête
   //le compte à rebours sinon on le relance
   if ((bool)FixePositionBtn.IsChecked)  Dt.Stop();
   else Dt.Start();

   //on utilise le gestionnaire d'état visuel 
   //afin d'afficher l'état VisiblePlayer
   VisualStateManager.GoToState(this, "VisiblePlayer", true);
   
}

Ce code présente plusieurs problématiques. Tout d'abord, la logique concernant l'arrêt du compte à rebours de disparition est répartie dans trois écouteurs différents. La logique de la méthode LayoutRoot_MouseMove ne devrait pas tester l'état du bouton Fixe-Position-Btn en dur. Après tout, les instructions ont toutes le même rôle : relancer le compte à rebours de disparition chaque fois que la souris bouge, et effectuer une transition vers l'état VisiblePlayer, si besoin est. Cela empêche le fondu des contrôles lorsque l'utilisateur a une quelconque activité.

La seconde problématique concerne les performances. Le fait que l'événement MouseMove de LayoutRoot continue d'être écouté lorsque nous positionnons le lecteur en bas de la fenêtre, consomme inutilement des ressources processeur et mémoire. Comme nous ne souhaitons pas d'effet de disparition dans l'état Bottom, l'écoute de MouseMove qui gère cet effet devient superflue sur cet état. Autant supprimer cette écoute lorsque le bouton FixePositionBtn est coché, puis se réabonner à l'événement uniquement lorsque le bouton est décoché (indiquant l'état centré). La désinscription et la souscription de l'événement MouseMove au bon moment génèrent un code plus propre et moins gourmand en ressources :

 
Sélectionnez
public MainPage()
{
…
   LayoutRoot.MouseMove += new MouseEventHandler(LayoutRoot_MouseMove);
…
   FixePositionBtn.Checked += new RoutedEventHandler(FixePositionBtn_Checked);
   FixePositionBtn.Unchecked += new RoutedEventHandler(FixePositionBtn_Unchecked);
}

void FixePositionBtn_Unchecked(object sender, RoutedEventArgs e)
{
   VisualStateManager.GoToState(this, "Center", true);
   //la position est centrée, car le bouton est décoché,
   //on cache donc automatiquement les contrôles utilisateur
   //au bout de deux secondes
   Dt.Start();
   //On écoute l'événement MouseMove que lorsque c'est nécessaire
   LayoutRoot.MouseMove += LayoutRoot_MouseMove;

}

void FixePositionBtn_Checked(object sender, RoutedEventArgs e)
{
   VisualStateManager.GoToState(this, "Bottom", true);
   //le bouton fixe position est coché, 
   //on ne fait donc pas disparaître le lecteur
   //en stoppant le compte à rebours de disparition
   Dt.Stop();
   //On supprime la souscription de l'écouteur lorsqu'il n'est pas 
   //nécessaire de faire disparaître les contrôles
   LayoutRoot.MouseMove -= LayoutRoot_MouseMove;

}//lorsque la souris bouge
void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
{
   //Il n'y a plus besoin de tester l'état du bouton FixePositionBtn
   //ici, car l'écoute de l'événement MouseMove est désactivée avant

   //on utilise le gestionnaire d'états visuels 
   //afin d'afficher l'état VisiblePlayer
   VisualStateManager.GoToState(this, "VisiblePlayer", true);
   Dt.Start();
}

Testez en compilant votre projet. Pour mettre en valeur l'optimisation apportée par ce code, vous pouvez utiliser le mode "debug" de Visual Studio. Vous pourrez ainsi écrire un message en fenêtre de sortie chaque fois que la souris se déplace :

 
Sélectionnez
Using System.Diagnostics;

Int nMessage = 0;

//lorsque la souris bouge
void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
{
   //Il n'y a plus besoin de tester l'état du bouton FixePositionBtn ici, 
   //car l'écoute de l'événement MouseMove est désactivée avant
   //On écrit un message lorsque la souris bouge
   Debug.WriteLine("la souris bouge pour la {0}ème fois",++nMessage);

   //on utilise le gestionnaire d'état visuel 
   //afin d'afficher l'état VisiblePlayer
   VisualStateManager.GoToState(this, "VisiblePlayer", true);
   Dt.Start();

}

Vous constaterez qu'en position basse, le lecteur ne consomme plus du tout de ressources. L'événement MouseMove n'est plus capté par l'écouteur LayoutRoot_MouseMove. Les messages en fenêtre de sortie ne sont donc plus incrémentés. Nous pouvons encore améliorer notre code et optimiser notre application grâce au couplage faible.

7-3. Le couplage faible en pratique

Comme nous l'avons vu précédemment, le couplage faible est avant tout le résultat d'une bonne pratique du développement orienté objet. C'est une manière de créer des liens faibles entre les objets : la modification et la suppression d'objets collaborent de concert et n'impactent pas les objectifs de fonctionnement propres à chacun.

7-3-1. Principe

Pour le modèle événementiel, le couplage faible est réalisé grâce aux deux paramètres récupérés par les méthodes d'écoute. Le premier argument représente une référence au diffuseur de l'événement, il est de type object, car on ne peut savoir à l'avance quel est le type de l'objet diffuseur. Ceci pour la bonne raison que des objets de types différents peuvent diffuser le même événement. Par exemple, l'événement Click peut être diffusé par les composants héritant de la classe ButtonBase, soit RadioButton, CheckBox, ToggleButton ou Button. L'événement Loaded est diffusable, quant à lui, par toutes les classes héritant de UIElement. Le fait de typer object nous permet donc de nous affranchir du type diffuseur puisque tous les types ont pour origine object. Le deuxième argument est nommé objet événementiel. Il donne des informations complémentaires sur l'événement diffusé, comme la position à l'instant précis du clic de la souris. Grâce à ces deux paramètres, il n'est nul besoin de référencer l'objet diffuseur en dur. Dans le code ci-dessous, vous trouverez deux manières de réaliser la même tâche. Voici la mauvaise manière de récupérer la valeur d'un Slider dont la valeur vient de changer :

 
Sélectionnez
//On diffuse l'événement ValueChanged chaque fois que l'utilisateur 
//modifie la position du curseur
MonSlider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(MonSlider_ValueChanged);

//la méthode d'écoute se déclenche chaque fois que l'utilisateur 
//modifie la position du curseur
void MonSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
   //lorsque c'est le cas on trace en fenêtre 
   //de sortie la nouvelle valeur
   Debug.WriteLine("nouvelle valeur de MonCurseur : "+MonSlider.Value);
}

Ce n'est pas la bonne manière de procéder, car MonSlider est référencé fortement dans la méthode d'écoute. Voici une autre manière de procéder qui présente l'avantage de ne pas faire directement référence au Slider :

 
Sélectionnez
void MonSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
   if ( (sender as Slider)!=null ) 
      Debug.WriteLine("nouvelle valeur de MonCurseur : "+ (sender as Slider).Value);
}

Avec cette méthode, le développeur utilise l'argument nommé sender faisant référence à l'objet diffuseur. Comme sender est de type object, il nous faut transformer son type grâce au mot-clé as. Ce mot-clé permet de renvoyer null si la conversion n'est pas possible. Avec une conversion de type explicite notée (Slider)sender, nous serions susceptible d'avoir une erreur levée à l'exécution et nous serions obligés d'utiliser une instruction try / catch consommant plus de ressources en cas d'exception levée. Une seconde manière de procéder consiste à utiliser l'objet événementiel pour récupérer la nouvelle valeur ainsi que l'ancienne :

 
Sélectionnez
void MonSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
   Debug.WriteLine("ancienne valeur de MonCurseur : "+e.OldValue);
   Debug.WriteLine("nouvelle valeur de MonCurseur : "+e.NewValue);
}

Ces deux méthodes ne font pas référence au Slider directement. Ainsi, la méthode écouteur MonSlider_ValueChanged pourrait souscrire à l'événement ValueChanged diffusé par plusieurs objets Slider :

 
Sélectionnez
//On crée plusieurs contrôles Slider
Slider MonSlider1 = new Slider(){Name="Slider1"};
Slider MonSlider2 = new Slider(){Name="Slider2"};
Slider MonSlider2 = new Slider(){Name="Slider3"};//On définit le même écouteur pour chacun d'eux 
MonSlider1.ValueChanged += new RoutedPropertyChangedEventHandler<double>(MonSlider_ValueChanged);

MonSlider2.ValueChanged += new RoutedPropertyChangedEventHandler<double>(MonSlider_ValueChanged);

MonSlider3.ValueChanged += new RoutedPropertyChangedEventHandler<double>(MonSlider_ValueChanged);void MonSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
   Debug.WriteLine("ancienne valeur de " + (sender as Slider).Name + " : " + e.OldValue);
   Debug.WriteLine("nouvelle valeur de " + (sender as Slider).Name + " : " + e.NewValue);
}

Dans le code ci-dessus, vous récupérez le nom du Slider dont la valeur a été modifiée par l'utilisateur et cela de manière complètement dynamique. Bien sûr, l'intérêt d'un tel code est relatif à la tâche de chaque Slider. Si ceux-ci font sensiblement la même chose, le couplage faible apporte un réel confort de conception et factorise votre code.

7-3-2. Simplifier le code du lecteur vidéo

Nous allons appliquer ce concept à notre lecteur vidéo. Vous le trouverez dans les exemples du livre : chap8/PlayerVideo_Couplage.zip. Ouvrez le projet au sein de Blend et de Visual Studio. Notre code est pour l'instant redondant, peu optimisé et inélégant. En voici un exemple criant : si vous voyez un jour ce type de code, c'est qu'il y a une erreur de conception  :

 
Sélectionnez
//Gère les transitions vers les états de couleurs ColorStates Group
ForwardBtn.MouseEnter += new MouseEventHandler(ForwardBtn_MouseEnter);

BackwardBtn.MouseEnter += new MouseEventHandler(BackwardBtn_MouseEnter);

VolumeDownBtn.MouseEnter += new MouseEventHandler(VolumeDownBtn_MouseEnter);

VolumeUpBtn.MouseEnter += new MouseEventHandler(VolumeUpBtn_MouseEnter);

PlayPauseBtn.MouseEnter += new MouseEventHandler(PlayPauseBtn_MouseEnter);

ControlsRingArea.MouseLeave += new MouseEventHandler(ControlsRingArea_MouseLeave);void ControlsRingArea_MouseLeave (object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Normal", true);
}

void VolumeUpBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Purple", true);
}

void VolumeDownBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Cyan", true);
}

void BackwardBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Orange", true);
}

void ForwardBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Green", true);
}

void PlayPauseBtn_MouseEnter(object sender, MouseEventArgs e)
{
   VisualStateManager.GoToState(this, "Black", true);
}

Comme vous le constatez, chaque écouteur fait exactement la même chose. Lorsqu'un objet est survolé, les écouteurs exécutent une transition vers un état de couleur spécifique via le gestionnaire d'états visuels. Un seul écouteur pourrait réaliser tout ce travail simplement. Si les boutons survolés contenaient chacun un indice de l'état de couleur ciblé, il deviendrait simple de référencer le diffuseur et de pointer vers le bon état de couleur. Nous pourrions renommer chaque bouton d'une autre manière pour faire référence à l'état ciblé.

Toutefois, ce n'est pas une bonne pratique, car le nom d'un objet indique avant tout sa fonctionnalité et non la forme ou la transition à laquelle il fait référence. Le mieux est d'utiliser la propriété Tag (de type Object) propre à tout objet FrameworkElement et d'y stocker le nom de l'état de couleur ciblé.

Dans Blend, sélectionnez chaque bouton les uns après les autres et entrez le nom de l'état de couleur ciblé pour chacun. Par exemple, pour le bouton ForwardBtn, dépliez complètement le panneau Common Properties, vous pouvez entrer la chaîne de caractères Green dans le champ de saisie Tag (voir Figure 8.4).

N'oubliez pas de modifier également la propriété Tag de l'objet ControlsRingArea qui permettra de revenir à l'état visuel Normal.

Image non disponible
Figure 8.4 - Configuration de la propriété Tag

Une fois que vous avez finalisé cette partie, vous pouvez simplifier le code existant en ne définissant qu'une seule méthode d'écoute :

 
Sélectionnez
//Gère les transitions vers les états de couleurs ColorStates Group
ForwardBtn.MouseEnter += new MouseEventHandler(GotoColorState);

BackwardBtn.MouseEnter += new MouseEventHandler(GotoColorState);

VolumeDownBtn.MouseEnter += new MouseEventHandler(GotoColorState);

VolumeUpBtn.MouseEnter += new MouseEventHandler(GotoColorState);

PlayPauseBtn.MouseEnter += new MouseEventHandler(GotoColorState);

PlayPauseBtn.MouseEnter += new MouseEventHandler(GotoColorState);

ControlsRingArea.MouseLeave += new MouseEventHandler(GotoColorState);void GotoColorState(object sender, MouseEventArgs e)
{
   //on vérifie que l'objet cliqué est de type ButtonBase
   if (sender as FrameworkElement != null)
   {
      var TargetedState = (sender as FrameworkElement).Tag;
      VisualStateManager.GoToState(