IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Pratique de Silverlight


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 où 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éthode
…
void 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(this, TargetedState.ToString(), true);
   }
}

Transtyper un objet vers FrameworkElement permet de récupérer la propriété Tag pour n'importe quel type de contrôles Silverlight. Comme l'événement est diffusé par trois types différents d'objets (Canvas, Button, ToggleButton), utiliser FrameworkElement - dont ces objets héritent - est pratique.

Dans le code précédent, chaque événement est écouté par la même méthode nommée Goto-ColorState. Grâce au couplage faible entretenu entre l'écouteur et le diffuseur, nous sommes capables de récupérer dynamiquement la valeur de la propriété Tag. Celle-ci étant typée Object, il nous faut utiliser la méthode ToString pour récupérer la valeur sous forme de chaîne de caractères.

La propriété Tag est de type Object, elle peut donc recevoir n'importe quel type d'objet. Comme nous ne sommes pas dans un langage dynamique, tel que JavaScript ou Action-Script, il n'est pas possible d'ajouter dynamiquement des propriétés aux objets d'affichage du framework. La propriété Tag permet de contourner cette limitation tout en centralisant le contenu que vous souhaitez ajouter aux objets. Selon les écoles, et notamment pour les adeptes de la POO, ajouter dynamiquement des propriétés aux objets natifs n'est pas une bonne pratique de conception. Cela génère un code souvent difficile à maintenir même si cela permet parfois plus de souplesse de développement dans certaines situations.

Vous pouvez trouver le lecteur finalisé dans les exemples du livre : chap8/PlayerVideo_CouplageFinal.zip.

7-3-3. Affectation dynamique d'animations

Nous allons voir comment réaffecter une animation dynamiquement en fonction de l'objet diffuseur. Ouvrez le projet nommé AnimRebond_CouplageDynamic (). Trois balles sont présentes sur LayoutRoot. La première est ciblée par un Storyboard nommé Anim-Rebond. Lors de l'exécution, si vous cliquez sur celle-ci, l'animation est jouée grâce au code ci-dessous :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();

   MaBalle1.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle1_MouseLeftButtonUp);
}

void MaBalle1_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   AnimRebond.Begin();
}

Nous pourrions créer autant d'animations que de balles, mais nous allons profiter du couplage faible pour réassigner dynamiquement l'animation à la balle cliquée. Nous pouvons ainsi écrire :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();

   MaBalle1.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
   MaBalle2.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
   MaBalle3.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
}

void MaBalle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   Storyboard.SetTarget(AnimRebond, (sender as Grid));
   AnimRebond.Begin();
}

Toutefois, si vous faites le test, vous remarquez que l'assignation dynamique ne fonctionne qu'une seule fois. Ceci est dû à la gestion en mémoire des instances de Storyboard par Silverlight. Les ressources de type Storyboard doivent tout d'abord être libérées avant d'être accessibles à la modification. Il suffit d'invoquer la méthode Stop sur le Story-board, puis de lui réassigner une nouvelle cible :

 
Sélectionnez
void MaBalle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   AnimRebond.Stop();
   Storyboard.SetTarget(AnimRebond, (sender as Grid));
   AnimRebond.Begin();
}

Cette fois, vous pouvez cliquer sur chaque balle pour réaffecter la cible et jouer l'animation. L'ancienne balle animée revient à sa position de départ. Ce principe est intéressant à plus d'un titre, puisque cela évite aux designers de créer autant d'animations que d'objets. Pour éviter le côté saccadé dû à l'appel de la méthode Stop, vous pouvez également affecter une animation de retour à l'ancien objet ciblé. Cela serait particulièrement utile pour un grand nombre d'objets à animer. Pour éviter un appel injustifié de la méthode Stop, vous pouvez également tester l'état actuel de l'animation :

 
Sélectionnez
void MaBalle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   if (AnimRebond.GetCurrentState() != ClockState.Stopped) 
      AnimRebond.Stop();
   Storyboard.SetTarget(AnimRebond, (sender as Grid));
   AnimRebond.Begin();
}

Le principal avantage réside au niveau de la rapidité de production entre développeur et designer. La maintenance est également plus facile, puisqu'il suffit au designer de modifier une seule animation pour mettre à jour l'application. Le développeur, quant à lui, ne fait référence qu'à une seule animation pour un groupe d'objet ayant une fonctionnalité similaire.

7-4. Propagation événementielle

La propagation événementielle est propre aux objets graphiques imbriqués. Cette logique événementielle ne s'applique donc qu'aux objets héritant de la classe UIElement. Cette spécificité du modèle événementiel permet d'améliorer les performances et épure le code logique produit.

7-4-1. Principe

Le principe est simple : un événement, lorsqu'il est diffusé, parcourt l'arborescence des objets graphiques de manière verticale. Au sein de Silverlight, une seule direction existe. L'événement part de l'élément diffuseur le plus bas dans la hiérarchie et se dirige vers son parent le plus haut. Durant ce trajet, chaque parent de l'objet source peut, à son tour, diffuser cet événement. C'est la phase de remontée événementielle, ou Bubbling. On considère que les événements sont comme des bulles d'air remontant à la surface de l'eau. Vous pouvez écouter l'événement sur chaque parent de l'objet qui est à l'origine de l'événement. Cette notion explique le sens de nombreux comportements.

Par exemple, lorsque vous placez des formes au sein d'une grille, vous pouvez écouter l'événement MouseLeftButtonUp sur cette grille même si elle ne possède pas de remplissage. Pourquoi ? Simplement parce que l'événement est diffusé par l'une des formes contenues et que celui-ci remonte vers son conteneur parent qui le diffuse à son tour. En l'occurrence, le conteneur parent en question est la grille (voir Figure 8.5).

Image non disponible
Figure 8.5 - Principe de la propagation événementielle

Comme vous le constatez à la Figure 8.5, l'avantage principal réside dans le fait qu'il suffit au développeur d'écouter un événement sur un conteneur plutôt que sur chaque objet contenu. Il est possible de connaître l'objet diffuseur originel via la propriété OriginalSource de l'objet événementiel. Attention toutefois au fait que seuls certains des événements acheminés (dits routés et typés RoutedEvent) utilisent la propagation événementielle. En voici une liste :

  • MouseLeftButtonUp est un événement diffusé lors du relâché du bouton gauche de la souris ;
  • MouseLeftButtonDown est notifié lors de l'appui sur le bouton gauche de la souris ;
  • MouseMove est déclenché lorsque la souris bouge ;
  • KeyUp est diffusé lorsqu'une touche du clavier est relâchée ;
  • KeyDown est diffusé lors de l'appui sur une touche du clavier ;
  • GotFocus est un événement diffusé lorsqu'un composant bénéficie de l'intérêt utilisateur ou focus ;
  • LostFocus est diffusé lorsqu'un composant perd le focus utilisateur ;
  • MouseWheel est un événement diffusé lors de l'utilisation de la molette de la souris ;
  • BindingValidationError est un événement diffusé lorsqu'une erreur est levée lors de l'assignation d'une valeur non conforme.

Chacun des événements ci-dessus est une interaction fondamentale de l'utilisateur. Ainsi, l'événement Click n'est qu'un événement spécifique propre aux composants orientés utilisateur, alors que MouseLeftButtonUp est un événement diffusable par toutes les classes dérivées de UIElement.

7-4-2. Un exemple simple de propagation

Nous allons mettre en pratique les notions que nous venons d'apprendre. Téléchargez le projet nommé "Propagation" depuis les exemples du livre : chap8/Propagation.zip. Ce projet constitue un cas très simple de propagation événementielle. Un conteneur de type Grid contient une forme de type Rectangle. Le premier est rempli d'une couleur gris foncé ; le rectangle, quant à lui, affiche une couleur de fond gris clair (voir Figure 8.6).

Image non disponible
Figure 8.6 - Visuel du projet Propagation

Ajoutez un champ TextBlock nommé Resultat en haut à gauche au sein du conteneur Layout-Root. Écoutez ensuite l'événement MouseLeftButtonUp sur chacun des deux objets (ConteneurGrille et UnRectangle) de cette manière :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   UnRectangle.MouseLeftButtonUp += new MouseButtonEventHandler(MouseLeftButtonUp_generique);
   ConteneurGrille.MouseLeftButtonUp += new MouseButtonEventHandler(MouseLeftButtonUp_generique);
}

void MouseLeftButtonUp_generique(object sender, MouseButtonEventArgs e)
{
   string NomObjet = (sender as FrameworkElement).Name;
   Resultat.Text += "Diffuseur :: " + NomObjet +"\n";
}

Lorsque vous cliquez sur le rectangle gris clair, le champ texte vous confirme que les deux composants ont diffusé le relâché de la souris. Lorsque vous cliquez sur la grille sombre en revanche, seule celle-ci diffuse l'événement. Grâce à la propagation événementielle, vous n'êtes pas obligé d'écouter l'événement deux fois. Vous pouvez utiliser la propriété OriginalSource pour déterminer quel objet a été cliqué au sein de la grille. Ajoutez deux autres rectangles nommés Deux-Rectangle et TroisRectangle au sein de la grille et répartissez-les équitablement (voir Figure 8.7).

Image non disponible
Figure 8.7 - Mise en place du projet Propagation

Vous pouvez optimiser le code en ne définissant l'écouteur que sur la grille nommée ConteneurGrille :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   ConteneurGrille.MouseLeftButtonUp += new MouseButtonEventHandler(MouseLeftButtonUp_generique);
}
void MouseLeftButtonUp_generique(object sender, MouseButtonEventArgs e)
{
   string NomObjet = (sender as FrameworkElement).Name;
   Resultat.Text = " Objet écouté en dur :: " + NomObjet+"\n";
   Resultat.Text += " Objet diffuseur originel de l'événement :: " 
                 + (e.OriginalSource as FrameworkElement).Name + "\n";
}

Le code en gras permet de tracer l'objet à la source de la diffusion. Celui-ci peut être différent de l'objet écouté directement dans votre code. Compilez et testez votre application en cliquant sur chacun des rectangles, puis sur ConteneurGrille. Chaque rectangle peut être responsable de la diffusion de l'événement MouseleftButtonUp, vous pouvez récupérer la référence de l'objet diffuseur source grâce à la propriété OriginalSource de l'objet événementiel. Le code est ainsi beaucoup plus simple à maintenir.

7-4-3. Éviter la diffusion d'événements

Parfois, vous devrez empêcher les événements d'être diffusés. Reprenons l'exercice des balles, nous pourrions être tenté d'améliorer le code en gras ci-dessous grâce à la propagation événementielle :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   
   MaBalle1.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
   MaBalle2.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
   MaBalle3.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
}

Il suffit pour cela d'écouter le conteneur parent commun à chaque balle. Dans ce cas, il correspond à la grille principale LayoutRoot :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   
   LayoutRoot.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
}

void MaBalle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   if (AnimRebond.GetCurrentState() != ClockState.Stopped)
      AnimRebond.Stop();
   Storyboard.SetTarget(AnimRebond, (e.OriginalSource as Grid));
   AnimRebond.Begin();
}

Si vous testez ce code, vous vous apercevrez qu'il ne fonctionne pas pour diverses raisons. La première étant que le premier objet à l'origine de la diffusion de l'événement est forcément de type Ellipse. Les balles contiennent chacune trois ellipses qui diffuseront l'événement en premier. L'affectation de cible ne fonctionnera pas. La seconde raison est du même ordre. L'événement aura tendance à être diffusé par LayoutRoot lui-même. Lorsque vous cliquerez sur la grille LayoutRoot, l'animation ne sera pas jouée, car elle ne contient pas le nœud RenderTransform lui permettant d'être ciblée par l'animation. Il nous faut donc réaliser deux tâches différentes qui ont toutes deux pour objectif d'éviter la diffusion d'un événement. Tout d'abord, il faut que les objets de type Ellipse ne puissent pas diffuser l'événement MouseleftButtonUp. Pour cela, nous pouvons passer leur propriété IsHitTestVisible à false, nous y aurons accès en dépliant complètement l'onglet Common Properties, situé au sein du panneau Properties. Ensuite, il est nécessaire de tester le nom de la référence renvoyée par e.OriginalSource. Si cette référence est LayoutRoot, on sort de la méthode sans rien faire via l'instruction return. Voici le code définitif :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();
   
   LayoutRoot.MouseLeftButtonUp += new MouseButtonEventHandler(MaBalle_MouseLeftButtonUp);
}

void MaBalle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   //on teste le nom de l'objet diffuseur
   if ((e.OriginalSource as Grid).Name == "LayoutRoot") 
      return;
   //on teste ensuite l'état actuel de l'animation
   if (AnimRebond.GetCurrentState() != ClockState.Stopped) 
      AnimRebond.Stop();
   Storyboard.SetTarget(AnimRebond, (e.OriginalSource as Grid));
   AnimRebond.Begin();
}

Testez et compilez, les balles sont cette fois réactives au clic gauche de la souris. Les instances d'Ellipse ne diffusent plus l'événement MouseLeftButtonUp, laissant ce rôle aux grilles MaBalle1, MaBalle2 et MaBalle3. Gardez cependant à l'esprit que cela est possible parce que MaBalle1, MaBalle2 et MaBalle3 possèdent un fond transparent. Leur propriété Fill possède donc une valeur hexadécimale de type 0x00xxxxxx. Une grille qui ne possède pas de remplissage ne peut diffuser les événements que grâce aux objets qu'elle contient. Comme les ellipses au sein des balles ne sont plus cliquables, il est obligatoire que les grilles MaBalle1, MaBalle2 et MaBalle3 possèdent un fond. Toutefois, cette solution n'est pas entièrement satisfaisante dans notre cas. La surface cliquable par l'utilisateur correspond en fait aux dimensions rectangulaires de chaque grille. L'utilisateur pourra donc cliquer dans l'espace invisible situé entre la surface de la balle et la surface restante de la grille. Nous allons maintenant étudier une manière d'empêcher la diffusion d'événements propagés.

7-4-4. Arrêter la propagation événementielle

Stopper la propagation événementielle est pratique dans le cas où vous ne souhaitez pas qu'un objet conteneur diffuse un événement déjà écouté par ses enfants. Par exemple, vous pourriez avoir besoin d'écouter le même événement sur un conteneur et sur ses enfants tout en invoquant deux méthodes d'écoutes différentes. L'une concernerait le conteneur, l'autre se déclencherait lorsque l'un des enfants diffuse l'événement. Vous pourriez décider que l'une ou l'autre méthode d'écoute se déclenche, mais pas les deux en même temps. C'est exactement le cas dans le projet que nous allons voir maintenant. Ouvrez le projet nommé Propagation_Handled (chap8/Propagation_Handled.zip). Compilez et testez l'application. En haut à gauche du conteneur principal, vous apercevez un composant TextBlock nommé TestTxt. Il va nous permettre d'afficher toutes les méthodes d'écoute déclenchées dans l'ordre de diffusion. Au centre de LayoutRoot se trouve un conteneur StackPanel. Il contient plusieurs rectangles gris qui représentent chacun un voyant (voir Figure 8.8).

Image non disponible
Figure 8.8 - Visuel du projet Propagation_Handled

Au sein de ce projet, deux méthodes d'écoute sont déclenchées pour le même événement MouseLeftButtonDown diffusé.

  • La première méthode, ConteneurMenus_MouseLeftButtonDown, est diffusée par le StackPanel. Ce dernier possède un remplissage sous forme de dégradé linéaire représentant trois zones horizontales. Lorsque l'utilisateur clique sur ce conteneur, nous testons l'endroit où l'utilisateur a cliqué (événement MouseLeftButtonDown), puis nous repositionnons le conteneur dans l'application en fonction de la zone de clic. Cette tâche est réalisée grâce au gestionnaire d'agencement fluide vu au Chapitre 6 Boutons personnalisés. Par exemple, si l'utilisateur clique dans la partie gauche du StackPanel, il s'alignera à gauche de l'application. Voici le code permettant de gérer ce comportement :

     
    Sélectionnez
    public MainPage()
    {
       // Required to initialize variables
       InitializeComponent();
       
       ConteneurMenus.MouseLeftButtonDown +=new MouseButtonEventHandler(ConteneurMenus_MouseLeftButtonDown);
       …
    }
    
    private void ConteneurMenus_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
       StackPanel MonConteneur = sender as StackPanel;
    
       //À chaque fois que l'on clique sur le conteneur 
       //on passe la couleur d'arrière-plan de tous ses enfants en gris
       foreach (Rectangle rec in MonConteneur.Children)
       {
          rec.Fill = new SolidColorBrush (Colors.Gray);
       }
    
       //on teste l'endroit qui a été cliqué 
       //et qui est relatif aux conteneurs
       double XClic = e.GetPosition(MonConteneur).X;
       string pos="";
       
       if ( XClic >= MonConteneur.ActualWidth*2/3)
       {
          VisualStateManager.GoToState(this,"RightPosition",true);
          pos = " - clic dans la zone droite";
       }
       else if (XClic < MonConteneur.ActualWidth*2/3 && XClic >= MonConteneur.ActualWidth/3)
       {
          VisualStateManager.GoToState(this,"CenterPosition",true);
          pos = " - clic dans la zone du centre";
       }
       else if (XClic < MonConteneur.ActualWidth/3)
       {
          VisualStateManager.GoToState(this,"LeftPosition",true);
          pos = " - clic dans la zone gauche";
       }   
       
       //On finit en incrémentant le champ TextBlock
       //afin d'avertir de la diffusion de l'événement
       TestTxt.Text += "\n - "+MonConteneur.Name + pos; 
    }
  • La seconde méthode déclenchée est commune à tous les objets de type Rectangle qui sont contenus au sein du StackPanel (soit Menu1, Menu2, etc.). L'objectif est simple : nous souhaitons leur donner un rôle d'indicateur visuel en changeant leur couleur de remplissage par du blanc. Cela est réalisé lors de chaque clic de la souris sur l'un des rectangles (MouseLeftButtonDown). Gardez également à l'esprit que lorsque l'utilisateur clique sur leur conteneur StackPanel, leur couleur est réinitialisée. Cette fonctionnalité est gérée par le code en gras ci-dessus dans la méthode Conteneur-Menus_Mouse-Left-ButtonDown. Voici l'écouteur déclenché lorsque les menus diffusent l'événement MouseLeftButtonDown :
 
Sélectionnez
public MainPage()
{
   InitializeComponent();

   ConteneurMenus.MouseLeftButtonDown += …

   //À chaque fois que l'on trouve un enfant au sein du conteneur on 
   //souscrit la méthode d'écoute Menu_MouseLeftButtonDown
   foreach (UIElement e in ConteneurMenus.Children)
   {
      e.MouseLeftButtonDown +=new MouseButtonEventHandler(Menu_MouseLeftButtonDown);
   }
}

private void Menu_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
   Rectangle MenuClic = (sender as Rectangle);
   MenuClic.Fill = new SolidColorBrush(Colors.White);
   
   TestTxt.Text = "Objets diffusant MouseLeftButtonDown dans l'ordre de remontée :: ";
   TestTxt.Text += "\n - "+(sender as FrameworkElement).Name; 
}

Si vous compilez et cliquez sur chaque menu, le résultat attendu par ce code ne fonctionne pas. La couleur d'arrière plan des rectangles ne devient pas blanche. C'est en fait très logique. La méthode appelée par le conteneur est déclenchée après celle exécutée par les menus. La couleur de chaque menu est réellement modifiée, mais ensuite la méthode ConteneurMenus_MouseLeftButtonDown la réinitialise. Le champ texte en haut à gauche le prouve, les rectangles diffusent l'événement avant le conteneur (voir Figure 8.9).

Image non disponible
Figure 8.9 - Ordre de diffusion mis en valeur par le champ TestTxt

Il est tout à fait possible d'écrire ce code sans connaître le concept de propagation événementielle. Il devient alors difficile de diagnostiquer certains comportements comme celui que nous avons rencontré à l'instant. Pour remédier à ce problème, nous devons arrêter la propagation de l'événement après que les enfants du conteneur l'ont diffusé. La propriété Handled de l'objet événementiel, lorsqu'elle est passée à true, permet d'attraper l'événement et l'empêche de remonter au niveau supérieur pour être diffusé par le parent.

Voici le code mis à jour pour la méthode d'écoute de chaque rectangle :

 
Sélectionnez
private void Menu_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
   Rectangle MenuClic = (sender as Rectangle);
   MenuClic.Fill = new SolidColorBrush(Colors.White);
   
   TestTxt.Text = "Objets diffusant MouseLeftButtonDown dans l'ordre de remontée :: ";
   
   TestTxt.Text += "\n - "+(sender as FrameworkElement).Name;

   //On stoppe la propagation
   e.Handled = true;
}

Cette fois-ci, tous les rectangles changent de couleur. Si vous cliquez directement sur l'arrière-plan du conteneur, il se repositionne en fonction de la zone cliquée et réinitialise les couleurs de chaque enfant en gris. Vous pouvez ainsi définir des interactions à la fois différentes et complémentaires pour un conteneur et ses enfants à partir d'un unique événement.

7-4-5. Glisser-déposer et capture des événements souris

Les opérations de glisser-déposer sont aujourd'hui communes et n'étonnent personne au sein des interfaces riches. Ce n'est pas un effet de mode, puisque cela existe depuis les premières interfaces graphiques. C'est un type d'interaction largement adopté, offrant une ergonomie performante et accentuant l'impression de liberté d'action. Ce type d'interaction, au sein de Silverlight, n'est pas si simple à réaliser du fait de la nature même de l'événement MouseMove. Cet événement est assez particulier : il subit la propagation événementielle sans pour autant permettre l'arrêt de sa propagation via la propriété Hand-led. L'explication en est simple : l'objet événementiel, qui est de type MouseEventArgs, ne possède pas cette propriété. De plus, le type de propagation est légèrement différent, l'événement n'effectue pas vraiment la phase de remontée. En réalité, chaque parent diffuse l'un après l'autre l'événement par lui-même. Il est ainsi impossible d'arrêter cette pseudo-propagation.

Nous allons maintenant générer cette interaction en connaissance de cause. Créez un projet nommé ClickAndDrag. Vous utiliserez les transformations relatives de translation pour gérer le glisser-déposer. À cette fin, il est préférable de télécharger la bibliothèque ProxyRender-Transform que je mets à disposition à cette adresse :http://proxyrd.codeplex.com/. L'avantage des Render-Transform est de pouvoir déplacer un objet quelque soit le type de contrainte imposée par le conteneur parent de l'objet à déplacer. Référencez le fichier dll que vous avez téléchargé cliquant droit sur le répertoire Reference, puis en choisissant le menu Add Reference. Attention ensuite à bien faire référence à la bibliothèque au sein du code C# via l'instruction using Proxy-RenderTransform. Créez un rectangle nommé MonRectangle sur la grille principale Layout-Root. Ouvrez maintenant le fichier MainPage.xaml.cs au sein de Blend ou de Visual Studio.

Si vous décomposez l'interaction de glisser-déposer, vous remarquez que trois événements sont nécessaires à son fonctionnement :

  • l'événement MouseLeftButtonDown sur l'objet à déplacer permettra de lier l'objet aux coordonnées de la souris ;
  • l'événement MouseLeftButtonUp, correspondant au relâché de la souris diffusé par l'objet, aura la charge de libérer l'objet du déplacement de la souris ;
  • pour finir, MouseMove - se déclenchant à chaque déplacement de la souris - gérera le déplacement de l'objet.

Attention toutefois au fait que MouseMove est coûteux en termes de performance. L'idéal serait de ne le diffuser que lorsque c'est nécessaire. C'est tout à fait possible de limiter sa diffusion. Dans la vie courante, vous ne dessinez sur le papier que lorsque la mine de votre crayon est appuyée et qu'elle se déplace sur la feuille, mais pas lorsqu'elle est au-dessus. Cela paraît évident et c'est tant mieux, car le comportement de glisser-déposer est similaire. Vous ne déplacez l'objet que lorsque vous cliquez dessus tout en laissant le bouton appuyé. Ainsi, vous n'avez pas besoin de diffuser l'événement MouseMove de manière permanente, mais uniquement quand le bouton gauche de la souris reste appuyé. Vous pouvez commencer par définir l'écoute des événements MouseLeft-ButtonDown et MouseLeftButtonUp dans le constructeur de MainPage :

 
Sélectionnez
public MainPage()
{
   InitializeComponent();

   MonRectangle.MouseLeftButtonDown += new MouseButtonEventHandler(MonRectangle_MouseLeftButtonDown);

   MonRectangle.MouseLeftButtonUp += new MouseButtonEventHandler(MonRectangle_MouseLeftButtonUp);
}

Comme vous l'avez vu plus haut, vous pouvez définir l'écoute de l'événement MouseMove seulement quand l'utilisateur appuie sur l'objet. Il faut également arrêter l'abonnement de l'écouteur au relâché du bouton. Ainsi vous pouvez déjà écrire les méthodes d'écoute correspondantes :

 
Sélectionnez
void MonRectangle_MouseLeftButtonDown(object sender,MouseButtonEventArgs e)
{
   //Lorsqu'on appuie sur le rectangle
   //on écoute le déplacement de la souris
   MonRectangle.MouseMove += new MouseEventHandler(MonRectangle_MouseMove);

}

void MonRectangle_MouseLeftButtonUp(object sender,MouseButtonEventArgs e)
{
   MonRectangle.MouseMove -= new MouseEventHandler(MonRectangle_MouseMove);
}

Pour vous persuader de l'efficacité en termes de performance, vous pouvez tracer une variable incrémentée chaque fois que MouseMove est diffusée. Vous verrez que celle-ci ne s'incrémente que lorsque vous déplacez la souris au-dessus du rectangle après avoir appuyé dessus :

 
Sélectionnez
int i =0;
void MonRectangle_MouseMove(object sender, MouseEventArgs e)
{
   //On incrémente une variable
}

Il faut relâcher le bouton de la souris au-dessus du rectangle pour arrêter la souscription à l'événement MouseMove. Il y a en effet une différence entre relâcher au-dessus de l'objet et relâcher à l'extérieur. Dans le premier cas, l'événement MouseLeftButtonUp sera bien diffusé, pas dans le second.

À ce stade, il nous reste à définir la méthode d'écoute MonRectangle_MouseMove et à créer l'algorithme de déplacement. Voici le code complété, notez qu'il utilise la position de la souris ainsi que les transformations relatives sans aucun besoin des marges de l'objet dans la grille. Il est beaucoup plus simple de procéder ainsi :

 
Sélectionnez
void MonRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   MonRectangle.MouseMove -= new MouseEventHandler(MonRectangle_MouseMove);
}

void MonRectangle_MouseMove(object sender, MouseEventArgs e)
{
   //On redéfinit les nouvelles transformations relatives
   //par rapport au repère d'origine
   Point p = e.GetPosition(null);
   MonRectangle.SetX(p.X - coordX);
   MonRectangle.SetY(p.Y - coordY);
}

void MonRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{

   //Lorsqu'on appuie sur le rectangle, on écoute le déplacement 
   //de la souris
   MonRectangle.MouseMove += new MouseEventHandler(MonRectangle_MouseMove);

   //On récupère la différence entre les coordonnées de la souris
   //et les transformations relatives de l'objet. 
   coordX = e.GetPosition(null).X - (double)MonRectangle.GetX();
   coordY = e.GetPosition(null).Y - (double)MonRectangle.GetY();
}

Testez et compilez le projet. Si vous déplacez lentement l'objet MonRectangle, tout fonctionne bien. Toutefois, dès que la souris va trop vite et sort des limites du rectangle, plus rien ne va. Les bogues qui apparaissent ont deux raisons similaires. Tout d'abord, MouseMove n'est diffusé que lorsque la souris est dans les limites imparties par le rectangle. Or, si vous allez un peu trop vite dans les déplacements de la souris, celle-ci sort des limites de l'enveloppe et l'objet n'est plus déplacé. En second lieu, si vous relâchez le bouton de la souris en dehors de l'enveloppe, encore une fois parce que vous allez un peu trop vite, l'événement MonRectangle_MouseLeftButtonUp n'est pas diffusé. La conséquence directe de ce comportement, c'est que vous allez souscrire une seconde fois à l'événement MouseLeftButtonDown, lorsque vous allez le recliquer. Il devient alors impossible d'arrêter l'écoute de l'événement MouseMove.

Vous allez régler ces deux problèmes en une seule fois en capturant tous les événements liés à la souris. Ceci est réalisé grâce aux méthodes CaptureMouse et ReleaseMouse-Capture propres aux objets héritant de la classe UIElement. Ainsi, l'événement MouseMove continuera d'être diffusé même lorsque la souris sortira des dimensions du rectangle. Pour la même raison, l'événement MouseLeftButtonUp sera diffusé même si l'utilisateur relâche la souris en dehors du rectangle. Voici le code finalisé de l'application :

 
Sélectionnez
using …
using ProxyRenderTransform;

namespace DragAndDropObject
{
   public partial class MainPage : UserControl
   {

      double coordX;
      double coordY;

      public MainPage()
      {
         InitializeComponent();

         MonRectangle.MouseLeftButtonDown += new MouseButtonEventHandler(MonRectangle_MouseLeftButtonDown);

         MonRectangle.MouseLeftButtonUp += new MouseButtonEventHandler(MonRectangle_MouseLeftButtonUp);
      }

      void MonRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
      {
         //on arrête la capture des événements propres à la souris
         MonRectangle.ReleaseMouseCapture();
         MonRectangle.MouseMove -= new MouseEventHandler(MonRectangle_MouseMove);
      }

      void MonRectangle_MouseMove(object sender, MouseEventArgs e)
      {
         MonRectangle.SetX(e.GetPosition(null).X - coordX);
         MonRectangle.SetY(e.GetPosition(null).Y - coordY);
      }

      void MonRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      {
         //MonRectangle capture tous les événements 
         //diffusés par la souris
         MonRectangle.CaptureMouse();

         //Lorsqu'on appuie sur le rectangle on écoute 
         //le déplacement de la souris
         MonRectangle.MouseMove += new MouseEventHandler(MonRectangle_MouseMove);

         //On récupère les coordonnées de la souris sur LayoutRoot
         coordX = e.GetPosition(null).X - (double)MonRectangle.GetX();
         coordY = e.GetPosition(null).Y - (double)MonRectangle.GetY();
      }
   }
}

Testez et compilez votre projet, vous verrez que cette fois, tout se déroule comme prévu. Vous trouverez cet exercice corrigé dans : chap8/DragAndDropObject.zip.

7-5. Les comportements

Le concept de comportement repose sur le pattern Décoration ou Decorator. Ce modèle de conception permet d'attacher des fonctionnalités ou des caractéristiques non natives aux objets de manière dynamique et non intrusive. Décorer un objet ne modifie pas son code natif, mais étend ses possibilités. Cela permet des architectures objet bien plus facile à produire et à maintenir que l'héritage. Ajouter des comportements à un objet va dans ce sens. Utiliser des comportements ou ajouter des fonctionnalités via un langage logique sont deux manières équivalentes d'arriver à un même résultat, l'interactivité. Pourtant ces deux méthodologies se différencient sur plusieurs points : la souplesse d'utilisation selon les cas de figure, l'optimisation des performances et la philosophie du flux de production entre designers et développeurs. Il est également possible de vouloir ajouter des capacités à un objet sans pour autant que celles-ci soient liées à une quelconque interaction utilisateur.

7-5-1. Comportements versus programmation événementielle

Au sein de Silverlight, le mot comportement est relié à l'interface de Blend et aux designers. Les comportements sont représentés sous forme de petites icônes que vous pouvez placer sur un objet. Utiliser des comportements se révèle efficace dans deux situations.Dans le premier cas, le designer souhaite atteindre un objectif rapidement sans coder pour produire une maquette semi-fonctionnelle. Il peut alors utiliser les comportements de navigation fournis en standard pour déclencher un Storyboard, affecter une propriété ou accéder à un état visuel. La seconde situation apparaît lorsque l'objectif à atteindre est très complexe. Par exemple, vous souhaitez ajouter des interactions physiques à vos objets comme la gravité ou les collisions, ou capturer les mouvements de la souris. C'est typiquement le genre d'interactivité difficile à mettre en place, mais qui pourrait être paramétrée par un designer. L'apport d'un développeur est alors crucial. Celui-ci peut fournir un jeu de comportements que le designer pourra librement affecter aux objets de son choix et paramétrer à loisir dans Blend.

Coder les événements est pourtant plus souple d'un autre point de vue. Le comportement est instancié dans le projet par le designer dans la plupart des cas. Le développeur pourrait donc ne pas être informé de leur existence puisqu'ils apparaissent au sein du code déclaratif. Cela peut générer des quiproquos ou des conflits d'interactivité si la communication intermétiers n'est pas correctement établie. La deuxième raison, qui pousse les développeurs à gérer eux-mêmes l'interactivité, est la centralisation du code logique et l'optimisation des performances de l'application. S'il est possible pour un graphiste d'ajouter des comportements au sein de Blend à n'importe quel élément visuel, il est en revanche impossible pour lui de le faire durant l'exécution de l'application. De même, supprimer des comportements dynamiquement n'est pas réalisable pour lui sans code. Il devra donc faire appel à un développeur pour ces tâches. La programmation événementielle reste donc utilisée dans 90 % des cas - ce qui est heureux. Il faut maintenir un équilibre constant en fonction des équipes, des compétences de chacun et surtout justifier l'utilisation des comportements soit par une phase de production rapide, soit par des besoins spécifiques.

7-5-2. Les différents types de comportements

On distingue deux grandes familles de comportements : les comportements simples héritant de la classe Behavior et les comportements d'action déclenchée qui sont typés TriggerAction et TargetedTriggerAction. Au sein de Blend, ces deux familles sont évoquées par des icônes différentes que vous pouvez voir dans le panneau Assets. Les comportements simples sont représentés par un pictogramme en forme d'engrenage et leur nom se termine par Behavior. Les comportements d'action ciblés, ou non, ont quant à eux une icône composée d'un engrenage sur lequel s'ajoute une flèche de lecture. Ils sont suffixés du mot Action (voir Figure 8.10).

Image non disponible
Figure 8.10 - Les deux familles de comportements
7-5-2-1. Principe des comportements simples

Le but d'un comportement simple est d'ajouter des fonctionnalités. Il contient à cette fin des gestionnaires d'événements, toutefois, la logique événementielle permettant ce type de comportements n'est pas accessible au designer sous Blend, celle-ci est fermée à la modification. C'est exactement le cas du comportement MouseDragElement-Behavior. Ouvrez un nouveau projet dans Blend, créez un simple rectangle dans LayoutRoot, puis glissez-déposez le comportement MouseDragElementBehavior sur celui-ci. Dans le panneau des propriétés, vous pouvez voir les propriétés paramétrables du comportement simple (voir Figure 8.11).

Image non disponible
Figure 8.11 - Propriétés du comportement simple MouseDragElementBehavior

Comme vous pouvez le voir, il n'est pas possible d'accéder à la logique événementielle, mais seulement à des propriétés qui vont influencer la manière dont le comportement s'exécutera. Dans le cas présent, vous pouvez choisir si l'objet à déplacer restera ou non dans les limites imparties par le conteneur.

7-5-2-2. Les comportements d'action déclenchée

Les comportements simples affectent les fonctionnalités de l'objet auquel ils sont attachés, une fois pour toute, à la compilation. Les comportements d'action sont plus évolués, car ils vous permettent de choisir l'événement qui déclenchera leur exécution ainsi que l'objet diffuseur. Cela peut-être très utile pour un designer, car il n'aura pas à coder en C# la logique événementielle, mais simplement à paramétrer les propriétés du comportement. C'est exactement le cas de GoToState-Action. Créez deux états visuels, nommez le premier RedState et le second GreenState. Sélectionnez l'état RedState, puis assignez une couleur rouge pâle au remplissage de LayoutRoot ; procédez de même pour l'état GreenState en lui assignant une couleur verte. Placez ensuite deux boutons sur Layout-Root et glissez le comportement GoToStateAction sur les deux boutons. Vous pouvez maintenant permettre à l'utilisateur de passer d'un état à un autre en paramétrant les deux comportements (voir Figure 8.12).

Image non disponible
Figure 8.11 - Propriétés du comportement d'action ciblée GoToStateAction

Les comportements d'action reproduisent complètement la logique événementielle et donnent accès à son paramétrage. L'onglet Trigger (déclencheur) centralise les propriétés gérant la diffusion et permet notamment de choisir l'objet diffuseur ainsi que l'événement à écouter. Par défaut, l'objet diffuseur est l'objet auquel le comportement a été attaché. Vous pouvez ensuite décider de l'événement déclencheur. Dans ce projet, l'événement à choisir est Click, car les boutons ne diffusent pas MouseLeftButtonDown par défaut. Vous avez la possibilité de choisir un objet cible ainsi que l'état à afficher et l'utilisation ou non d'une transition animée. Le paramétrage est au final très simple et ne demande aucune ligne de code. Le comportement GoToStateAction est particulier, car il vous permet de choisir une instance de Control. Pour cette raison, il est de type TargetedTriggerAction.

7-5-3. Créer des comportements personnalisés

7-5-3-1. Ajouter la fonctionnalité filigrane aux champs de saisie

Nous allons maintenant créer un comportement simple personnalisé. Son objectif est simple, nous le déposerons sur un champ de saisie. Tant qu'il n'aura pas été rempli par l'utilisateur ou n'aura pas le focus utilisateur, il affichera une chaîne de caractères en gris clair permettant d'indiquer le type d'information à saisir. Nous allons créer, de cette manière, un champ de saisie avec filigrane, ou WatermarkTextBox. Créer le comportement est relativement aisé, puisque Blend et Visual Studio génèrent une partie du code initial.

Créez un nouveau projet Silverlight nommé YourBehaviors. Au sein du panneau Projects, cliquez-
droit sur le projet et choisissez le menu Add New Item… Dans la boîte de dialogue qui s'affiche, vous avez le choix entre un comportement simple (Behavior) et un comportement d'action (TiggerAction). Sélectionnez Behavior, puis nommez la classe WatermarkBehavior.cs (voir Figure 8.13).

Image non disponible
Figure 8.13 - Création du comportement WatemarkBehavior

Comme vous le constatez, dans le code logique du comportement par défaut, la classe est paramétrée avec DependencyObject par défaut. Cette classe est la plus générique possible, car elle est parente de UIElement. Cela n'est pas pratique, car ce comportement n'a de sens que s'il est affecté sur des instances de TextBox, il nous faut donc définir le type d'objet paramétré comme étant de type TextBox :

 
Sélectionnez
public class WatermarkBehavior : Behavior<TextBox>
{
   public WatermarkBehavior()
   {
      …
   }
…
}

Outre le constructeur de la classe WatermarkBehavior, deux méthodes ont été générées : On-Attached et OnDetaching. Bien que visible dans l'arbre visuel et logique sous Blend, un comportement n'est pas un objet visuel mais purement logique. Il ne possède pas la méthode d'initialisation Loaded propre aux objets d'affichage, mais OnAttached la remplace. Celle-ci se déclenche à la compilation ou à l'exécution lorsque le comportement est attaché à un objet. La méthode OnDetaching est invoquée lorsque le comportement est supprimé ou détaché de l'objet à l'exécution. Pour être fonctionnel, notre comportement doit posséder au moins une propriété permettant au designer de choisir le message à afficher lorsque le champ de saisie n'est pas rempli. Nous pouvons commencer par créer cette propriété ; la voici en C# au sein de la classe :

 
Sélectionnez
public class WatermarkBehavior : Behavior<TextBox>
{
   private string watermark = "Veuillez saisir une information";
   public string Watermark 
   { 
      get { return watermark; } 
      set { watermark = value; } 
   }
   …
}

Le scénario d'utilisation est simple : lorsque le champ de saisie sera initialisé ou perdra le focus utilisateur, un test sera réalisé sur le contenu de sa propriété Text. Si la valeur de Text est vide, alors le champ affichera la chaîne de caractères, contenue par la propriété WatermarkText, en grisé. Lorsque le champ texte obtiendra le focus utilisateur, si sa propriété Text contient une chaîne de caractères correspondant à la propriété Watermark-Text, alors il se videra pour laisser l'utilisateur saisir les informations personnalisées nécessaires. Tout se joue donc sur les événements propres à la gestion du focus utilisateur : LostFocus et GotFocus. Toutefois, dans notre cas, le sujet (ou diffuseur de l'événement) est en fait le champ de saisie sur lequel vous glisserez le comportement. Pour récupérer une référence du futur champ de saisie attachant le comportement, vous pouvez utiliser la propriété héritée de la classe Behavior : AssociatedObject. Attention toutefois au fait que AssociatedObject n'est affectée de l'instance de l'objet attaché qu'à l'instant où celui-ci l'est effectivement dans l'interface de Blend. Autrement dit, au sein du constructeur de la classe, la propriété AssociatedObject ne contient aucune instance de TextBox. L'initialisation des écouteurs doit donc être centralisée dans la méthode OnAttached. Voici le code qui contient la gestion des événements nécessaires :

 
Sélectionnez
public class WatermarkBehavior : Behavior<TextBox>
{
   private string watermarkText = "Please enter information here";
   public string WatermarkText
   { 
      get { return watermarkText; } 
      set { watermarkText = value; } 
   }

   public WatermarkBehavior()
   {
   }

   protected override void OnAttached()
   {
      base.OnAttached();
      //on teste par défaut le champ de saisie pour afficher le filigrane 
      //si le champ est vide
      AssociatedObject_LostFocus(this.AssociatedObject, null);

      AssociatedObject.GotFocus += new RoutedEventHandler(AssociatedObject_GotFocus);
      AssociatedObject.LostFocus += new RoutedEventHandler(AssociatedObject_LostFocus);

   }

   //lorsque le champ de saisie perd le focus
   void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
   {
      //on teste le nombre de caractères contenus si rien n'est
      //rempli alors on affecte le champ texte de la valeur en filigrane
      if (AssociatedObject.Text.Length == 0)
      {
         AssociatedObject.Text = WatermarkText;
      }
   }

   //lorsque le champ de saisie obtient le focus
   void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
   {
      //on regarde si la valeur de la propriété Text correspond 
      //au filigrane. Si elle correspond, on vide le champ texte
      //pour que l'utilisateur puisse saisir les informations
      if (AssociatedObject.Text.Contains(WatermarkText))
      {
         AssociatedObject.Text = "";
      }
   }
   …
}

Compilez le projet. Pour le tester efficacement, vous devez créer plusieurs champs de saisie sur LayoutRoot - créer cinq champs serait idéal. Au sein du panneau Assets, sélectionnez l'onglet Project. Vous faites ainsi apparaître tous les composants créés pour le projet. Le comportement WatermarkBehavior est donc présent dans la liste. Déposez-le sur chacun des champs de saisie. Supprimez leur contenu texte, puis dans les paramètres de chaque comportement, définissez respectivement les chaînes de caractères : "saisir votre nom", "saisir votre prénom", "saisir votre adresse", "saisir votre code postal", "saisir votre ville". Vous pouvez également répartir ces champs au sein d'un StackPanel. Une fois cette tâche réalisée, il est plus facile de tester le projet. Au sein du navigateur, utilisez la touche Tabulation afin de passer d'un champ à l'autre (voir Figure 8.14).

Image non disponible
Figure 8.14 - Test des champs de saisie au sein du navigateur

Pour parfaire le comportement, il faudrait griser le texte lorsque le filigrane est affiché. Rien de plus simple, il suffit d'affecter la propriété Foreground du champ de saisie. Toutefois, il est obligatoire de sauvegarder au sein d'une variable la couleur d'origine. Ceci nous permettra d'afficher le texte de manière normale lorsque les informations sont saisies correctement. Il serait également avantageux de laisser le soin au designer de choisir la couleur du filigrane afin de respecter la charte graphique. Voici le code complet du comportement :

 
Sélectionnez
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Interactivity;

//permet entre autres choses de gérer la répartition des 
//propriétés dans Blend
using System.ComponentModel;

namespace YourBehaviors
{
   public class WatermarkBehavior : Behavior<TextBox>
   {
      private string watermarkText ;
       [Category("Watermark Properties")]
       [DefaultValue("saisir un texte")]
      public string WatermarkText 
      { 
         get { return watermarkText; } 
         set { watermarkText = value; } 
      }

      //couleur d'origine
      private SolidColorBrush originColor;

      //couleur du filigrane
      private SolidColorBrush watermarkForeground = new SolidColorBrush(Colors.Gray);
      public SolidColorBrush WatermarkForeground
      {
         get { return watermarkForeground; }
         set { watermarkForeground = value; }
      }

      public WatermarkBehavior(){      }

      protected override void OnAttached()
      {
         base.OnAttached();

         //on commence par sauvegarder la couleur d'origine
         originColor = (SolidColorBrush)AssociatedObject.Foreground;

         //on initialise par défaut le champ
         AssociatedObject_LostFocus(this.AssociatedObject, null);

         //on écoute les événements
         AssociatedObject.GotFocus += new RoutedEventHandler(AssociatedObject_GotFocus);
         AssociatedObject.LostFocus += new RoutedEventHandler(AssociatedObject_LostFocus);

      }

      protected override void OnDetaching()
      {
         base.OnDetaching();
         //on reste fidèle aux bonnes pratiques en supprimant
         //l'écoute des événements dont on n'a plus besoin
         AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
         AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
      }

      void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
      {
         //on teste le nombre de caractères contenus
         //si rien n'est rempli alors on affecte le champ texte
         //de la valeur en filigrane
         if (AssociatedObject.Text.Length == 0)
         {
            AssociatedObject.Text = WatermarkText;
            AssociatedObject.Foreground = watermarkForeground;
         }
         else
         {
            AssociatedObject.Foreground = originColor;
         }
      }

      //on regarde si la valeur de la propriété Text correspond 
      //au filigrane. Si elle correspond, on vide le champ texte
      //pour que l'utilisateur puisse saisir les informations
      void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
      {
         AssociatedObject.Foreground = originColor;
         if (AssociatedObject.Text.Contains(WatermarkText))
         {
            AssociatedObject.Text = "";
         }
      }
   }
}

Nous pourrions encore perfectionner ce comportement en affectant, par exemple, le ToolTip du champ de saisie par la valeur de l'indication. Nous pourrions également prévoir tous les cas de mauvaise utilisation. Que se passe-t-il si l'objet auquel on attache le comportement n'est pas un champ de saisie de type TextBox ? Il faudrait pouvoir le gérer directement au sein de Blend. Une autre problématique est que nous utilisons la propriété Text du champ de saisie, ainsi si le formulaire ne vérifie que la présence ou non d'une chaîne de caractères, l'indication sera envoyée en lieu et place d'une saisie correcte de l'utilisateur. Il faudra donc également vérifier que la valeur du champ ne correspond pas à la valeur de la propriété WatermarkText et lever une erreur si tel est le cas. Nous allons maintenant créer un autre comportement, plus évolué par certains côtés puisqu'il rend disponible la gestion des événements.

7-5-3-2. Recopie bitmap d'un composant ou comment simuler les pinceaux visuels WPF

Depuis la version 3 de Silverlight, il est possible de générer une image bitmap à partir de n'importe quelle arborescence au sein de l'arbre visuel. Notre comportement aura pour vocation de créer un cliché bitmap d'un composant auquel il est attaché et de l'affecter soit à la propriété Fill d'une forme (Shape), soit à la propriété Background d'une instance de la classe Control. Au sein du panneau Projects, cliquez-droit sur le projet et choisissez le menu Add New Item... Dans la boîte de dialogue qui s'affiche, optez pour un comportement d'action (TriggerAction), puis nommez la classe SnapshotAction.cs (voir Figure 8.15).

Image non disponible
Figure 8.15 - Création d'un comportement d'action

Blend ouvre automatiquement le fichier SnapshotAction.cs, affichant ainsi le code généré. Supprimez les commentaires générés si leur présence vous gêne :

 
Sélectionnez
namespace YourBehaviors
{
   
   public class SnapshotAction : TriggerAction<DependencyObject>
   {
      public SnapshotAction()
      {
         
      }

      protected override void Invoke(object o)
      {

      }
   }
}

La méthode Invoke sera déclenchée lorsque l'événement que le designer a choisi sera diffusé. Pour mieux comprendre ce concept, compilez votre projet puis créez un rectangle sur LayoutRoot et glissez le comportement d'action SnapshotAction dessus. Le panneau des propriétés met en évidence les propriétés paramétrables par défaut (voir Figure 8.16).

Image non disponible
Figure 8.16 - Propriété d'un comportement d'action standard

Comme vous le constatez, le designer peut définir l'écoute d'un événement diffusé par le sujet de son choix. Par défaut, l'objet diffuseur de l'événement est l'objet attaché. Dans notre cas, il s'agit du rectangle. Le comportement est légèrement plus avancé puisque nous désirons également pouvoir définir une cible afin d'en créer un cliché bitmap. Nous pourrions, par exemple, vouloir cibler le conteneur StackPanel contenant les champs de saisie. Pour arriver à nos fins, il nous faut un comportement de type TargetedTrigger-Action. Il suffit d'étendre cette classe au lieu de TriggerAction. Nous allons cibler des composants utilisateur. Vous pouvez également modifier le type paramétré comme suit :

 
Sélectionnez
namespace YourBehaviors
{
   
   public class SnapshotAction :
         TargetedTriggerAction<UIElement>
   {
      public SnapshotAction()
      {
         
      }

      protected override void Invoke(object o)
      {

      }
   }
}

Puis, compilez à nouveau le projet pour voir les changements apportés dans le panneau des propriétés du comportement (voir Figure 8.17).

La nouvelle propriété TargetName a été ajoutée. Il est maintenant possible de sélectionner l'objet dont nous souhaitons créer un cliché. Le code en lui-même est assez simple, il faut tout d'abord vérifier que l'espace de noms System.Windows.Media.Imaging est référencé.

Image non disponible
Figure 8.17 - Propriété d'un comportement d'action ciblé

Nous allons utiliser la classe WritableBitmap qui permet de faire l'instantané de n'importe quel contrôle :

 
Sélectionnez
protected override void Invoke(object o)
{
   //on déclare une nouvelle image bitmap 
   WriteableBitmap Wb;

   //on l'instancie en lui passant l'objet
   //dont elle doit créer un instantané
   Wb = new WriteableBitmap(this.Target, null);
   //on crée un nouveau pinceau d'image
   ImageBrush Ib = new ImageBrush();

   Ib.Stretch = Stretch.None;
   //on affecte l'image binaire de l'instantané à 
   //la propriété ImageSource du pinceau d'image
   Ib.ImageSource = Wb;

   //Ensuite on teste le type et on affecte 
   //la bonne propriété en fonction de ce dernier
   if (AssociatedObject is Shape)
      ((Shape)AssociatedObject).Fill = Ib;

   else if (AssociatedObject is Panel)
      ((Panel)AssociatedObject).Background = Ib;

   else if (AssociatedObject is Control)
      ((Control)AssociatedObject).Background = Ib;

}

Il n'y a rien d'autre à faire, mis à part définir les événements qui invoqueront la méthode (voir Figure 8.18).

Image non disponible
Figure 8.18 - Paramétrage du comportement d'action ciblé SnapshotAction

Voici un exemple simple du formulaire avec un effet de reflet créé à l'aide du comportement.

Image non disponible
Figure 8.19 - Effet de reflet d'un formulaire en ligne via le comportement d'action SnapshotAction

Nous pourrions, encore une fois, améliorer grandement ce comportement de plusieurs manières. L'utilisation de conditions if / else if est un peu lourde et maladroite. Nous avons la possibilité de récupérer le type de l'objet associé dynamiquement et de rechercher si ce dernier possède la propriété Fill ou Background. Nous pouvons également automatiser la création du reflet grâce à une instance de DispatcherTimer. Elle permettrait de rafraîchir le cliché bitmap selon un laps de temps indiqué par le designer ou le développeur et plus seulement via les événements proposés. Nous n'irons cependant pas plus loin dans ce chapitre, car nous avons abordé l'ensemble des notions indispensables permettant d'élaborer une interactivité performante. Vous trouverez ce projet dans les exemples de ce livre : chap8/YourBehaviors.zip.

Dans le prochain chapitre, nous allons apprendre les principes de base de la projection 3D. Nous pourrons ainsi améliorer le lecteur vidéo que nous avons conçu auparavant. Nous nous servirons de la projection 3D pour prototyper nos applications au sein de Blend avec le nouvel outil révolutionnaire qu'est SketchFlow (Chapitre 9 Prototypage dynamique avec SketchFlow). Il était important d'apprendre les bases du modèle événementiel avant de nous y plonger et c'est maintenant chose faite.

8. Les bases de la projection 3D

Dans ce chapitre, vous allez découvrir les bases de la 3D adaptées à la plate-forme Silverlight. Vous aborderez les propriétés 3D d'objets et leur fonctionnement. Vous apprendrez à cibler et à manipuler ces propriétés, au sein de l'interface graphique Expression Blend, mais également dans Visual Studio via le code logique C#. Pour finir, vous mettrez en pratique les connaissances acquises pour réaliser deux exercices dont l'un vous permettra d'améliorer le lecteur vidéo réalisé dans le chapitre précédent. Ainsi, vous vous confronterez aux problématiques de production les plus courantes lorsqu'il s'agit de créer des interfaces en trois dimensions.

8-1. L'environnement 3D

L'image en trois dimensions, ou 3D, est apparue très tôt dans les Beaux-Arts. Les peintres Pierro Della Francesca et Filippo Brunelleschi furent parmi les premiers à utiliser et à concevoir les techniques de perspective. Leurs œuvres, bien qu'assez proches de celles de leurs contemporains,se démarquèrent par l'utilisation de la perspective. Pierro della Francesca diffuse, à l'époque, plusieurs ouvrages traitant directement de la géométrie dans l'espace. Ainsi, il est considéré de nos jours comme l'un des pionniers du dessin d'environnement réaliste et de la renaissance artistique.L'utilisation de la perspective et par conséquent la représentation en trois dimensions d'une scène mythologique ou religieuse apporte à la peinture une nouvelle vision. Si le point de fuite au sein d'une perspective conique représente l'infini, un nouvel acteur invisible est mis en valeur : le spectateur.Ce dernier, sans faire explicitement partie de la toile, participe à la scène peinte ou dessinée.C'est de son point de vue que celle-ci est représentée.Ainsi, l'imagerie 3D, qu'elle soit de synthèse, de peinture ou de dessin, replace l'être humain au centre de l'action. Elle enflamme notre imagination et nous immerge directement dans l'action.Au sein des interfaces riches, elle n'est pas une fin en soi, mais un moyen d'expression au même titre qu'un autre. L'utilisation d'une interface en pure 3D se révélerait assez catastrophique d'un point de vue utilisateur. Très peu de personnes sont aujourd'hui capables de se localiser dans un environnement de ce type (les pilotes d'avions et les joueurs invétérés en font par exemple partie).

N'en abusez donc pas dans vos interfaces et faites en sorte de toujours mettre la 3D au service de l'expérience utilisateur. L'objectif final étant que ce dernier s'approprie et se plonge dans l'application.Nous allons maintenant étudier son fonctionnement et son intégration dans la plate-forme Silverlight.

8-1-1. Introduction

Au sein des moteurs de rendu, on distingue deux grandes catégories. Les moteurs 3D temps réel (comme en possèdent souvent les jeux vidéo) et les moteurs d'images 3D précalculées. Silverlight étant un moteur vectoriel interactif, il se positionne d'emblée dans la première catégorie. Il vous permet donc, par exemple, de déplacer des objets en redéfinissant leur trajectoire à l'exécution.Toutefois, il ne déplacera pas réellement d'objets 3D, mais simplement des projections plan/aires de ces derniers. Nous allons maintenant aborder ce principe. Des logiciels comme Maya, 3D Studio Max, Blender, Lightwave ou Softimage font partie des deux mondes. D'un côté, ils génèrent des images précalculées, de l'autre, ils donnent accès à une interface d'édition élaborée, en 3D temps réel. Alors qu'un environnement en 2D permet aux graphistes de créer ou modifier les objets selon un axe horizontal et vertical, notés respectivement x et y, les environnements 3Dgèrent également l'axe z représentant la profondeur. Par défaut, quatre vues sont donc proposées à l'utilisateur dans les logiciels que nous avons évoqués : la vue de dessus, de côté, de face et une perspective avec point de fuite. Chacune de ces vues permet à l'infographiste de se repérer dans l'espace pour mieux concevoir les objets et agencer l'espace tridimensionnel. Ces vues sont en réalité des caméras particulières, car mis à part la vue de perspective, elles sont isométriques, donc sans point de fuite. Une scène 3D standard possède toujours au moins une caméra, une lumière ainsi qu'un objet 3D ou 2D à mettre en scène (voir Figure 9.1).

Image non disponible
Figure 9.1 - Exemple d'une scène en trois dimensions

Avec ce type de logiciels, le designer conçoit et interagit avec des objets en vraie 3D. Cela signifie que ceux-ci possèdent une hauteur, une largeur et une profondeur. Il est toutefois possible de mettre en scène des objets en deux dimensions comme un disque ou un plan (voir Figure 9.2).

Image non disponible
Figure 9.2 - Exemple d'une scène en trois dimensions avec plan 2D

Comme dans la réalité, les caméras 3D possèdent un angle d'ouverture ainsi qu'une longueur de focale dont les valeurs sont directement liées l'une à l'autre. Modifier la focale revient à modifier l'angle d'ouverture et inversement. Pour finir, la caméra est plus ou moins distante des objets qu'elle encadre. Au sein de Silverlight, le principe est exactement le même, à la différence près que les caméras ne sont pas des objets directement manipulables par le designer interactif, mais un point de vue abstrait, implicite et non modifiable. Nous aborderons la notion de caméra de manière plus approfondie plus loin dans ce chapitre. Comme vous le constatez aux Figures 9.1 et 9.2, l'axe des y est orienté vers le haut. Ce n'est pas le cas au sein de Silverlight dont les objectifs de production sont différents. L'axe 3D des ordonnées est orienté vers le bas, l'axe z des profondeurs est orienté vers le spectateur (voir Figure 9.3).

Image non disponible
Figure 9.3 - Orientation des axes x,y,z au sein de Silverlight et de l'interface Blend

8-1-2. Le plan de projection

Depuis sa version 3, Silverlight permet d'afficher des objets dans un espace en trois dimensions.Toutefois, Silverlight étant à l'origine un moteur vectoriel temps réel, il n'affiche en premier lieu que des objets en deux dimensions. L'affichage des objets vectoriels 2D est calculé par le processeur de l'ordinateur. Tous les contrôles utilisateur héritant de UIElement ont la capacité d'évoluer dans l'espace 3D. Pour des raisons de performances d'affichage, lorsque les contrôles utilisateur sont définis dans l'espace en trois dimensions, ils sont rendus sous forme d'images bitmap projetées sur un plan. Cela évite un recalcul constant et coûteux au processeur. Les objets restent interactifs et peuvent, si besoin est, changer de visuel.Ainsi, un bouton peut toujours afficher un état différent au survol ou au clic de la souris. Le fait de rendre les vecteurs sous forme bitmap limite grandement le nombre de calculs nécessaires à l'affichage ; les images sont placées en mémoire vive et mises à jour uniquement lorsqu'une interaction survient. Pour placer une instance de UIElement dans l'espace en trois dimensions de Silverlight,il suffit de lui appliquer une transformation 3D. La conversion des vecteurs en images implique une très légère dégradation de l'affichage des objets. C'est tout à fait logique et ne se remarque que lorsque vous effectuez un zoom au sein de Blend ou que vous appliquez des transformations 3Dde manière extrême. Cela est dû au fait que les vecteurs sont rendus sous forme d'images dont les pixels sont définis une fois pour toute (voir Figures 9.4 et 9.5).

Image non disponible
Figure 9.4 - Effet de pixellisation suite à la rotation d'un rectangle sur l'axe y

Comme vous le constatez, les bords sont lissés : pour éviter un effet de crénelage, le rendu des images est automatiquement lissé. Les contrôles utilisateur, quels qu'ils soient, peuvent également subir une transformation 3D (voir Figure 9.5).

Image non disponible
Figure 9.5 - Effet de pixellisation suite au déplacement d'une CheckBox sur l'axe z de 500 pixels

Vous remarquez l'effet pixelisé généré lors du déplacement sur l'axe des profondeurs. Ceci est un effet direct de la projection bitmap. Même si les objets ont la capacité d'évoluer dans un espace en trois dimensions, ils ne possèdent pas de réelle profondeur. Pour cette raison, on utilise le terme de 2,5D lorsque l'on évoque la 3D dans Silverlight. L'espace est en 3D, mais les objets restent en 2D.

8-2. Les propriétés 3D

Nous allons maintenant étudier les propriétés 3D accessibles au sein de Silverlight et voir comment elles nous permettent de manipuler les objets de type UIElement.

8-2-1. Rotation 3D

Les rotations représentent les transformations 3D par excellence, car elles mettent directement le ou les points de fuite en valeur. Créez un nouveau projet nommé "Test3D". Créez ensuite un rectangle de 200 pixels de large par 200 pixels de haut. Puis, assignez-lui un dégradé, en tant que remplissage. Vous pourriez également utiliser un composant Image afin de mieux visualiser les effets de perspective. Pour accéder aux projections 3D, sélectionnez le rectangle ou le contrôle Image, puis, dans le panneau Properties, dépliez l'onglet Transform en dessous de la partie dédiée aux transformations relatives : vous trouverez le menu Projection composé de quatre panneaux.Chacun d'eux représente un type de propriété 3D (voir Figure 9.6).

Image non disponible
Figure 9.6 - Menu des projections 3D

Le premier onglet concerne les rotations. Changez la valeur de chaque propriété de rotation. Procédez par étape afin de visualiser leur effet de manière séparée (voir Figure 9.7). Vous constatez qu'un manipulateur interactif, situé à gauche des champs de saisie, vous permet également de modifier la rotation. Toutefois, cet outil, bien qu'intuitif, est au final assez peu pratique, car trop imprécis.

Image non disponible
Figure 9.7 - Rotation 3D

Le fait de définir des propriétés de projection indépendamment des transformations relatives est assez révélateur. Comme les RenderTransform, les propriétés de projections 3D n'appartiennent pas directement aux objets, mais leurs sont attachées lorsque cela est nécessaire. Un nœud est créé à cette fin, voici le code XAML y faisant référence :

 
Sélectionnez
<Rectangle HorizontalAlignment="Left" Width="200" Height="200"
      RenderTransformOrigin="0.5,0.5">
   <Rectangle.Projection>
      <PlaneProjection RotationY="60"/>
   </Rectangle.Projection>
   <Rectangle.Fill>
      <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
         <GradientStop Color="Black" Offset="0"/>
         <GradientStop Color="White" Offset="1"/>
      </LinearGradientBrush>
   </Rectangle.Fill>
</Rectangle>

Comme nous l'avons déjà précisé, tout UIElement possède la propriété Projection, qui accepte

les instances de type PlaneProjection ou Matrix3DProjection. Comme la gestion de la 3D est centralisée par cette propriété, vous pouvez dès lors considérer qu'il est possible de mélanger les transformations relatives et les projections 3D. Ces deux types de propriétés sont cousines et se ressemblent donc en de nombreux points. Le code XAML ci-dessous est donc non seulement réalisable mais réaliste en termes de production :

 
Sélectionnez
<Rectangle HorizontalAlignment="Left" Width="200" Height="200"
      RenderTransformOrigin="0.5,0.5">
   <Rectangle.RenderTransform>
      <TransformGroup>
         <ScaleTransform ScaleX="2" ScaleY="2"/>
         <SkewTransform/>
         <RotateTransform/>
         <TranslateTransform/>
      </TransformGroup>
   </Rectangle.RenderTransform>
   <Rectangle.Projection>
      <PlaneProjection RotationY="60"/>
   </Rectangle.Projection>
   …
</Rectangle>

Attention, toutefois, à ne pas vous perdre, car il n'est pas toujours facile de conceptualiser à l'avance le résultat visuel émanant de différents types de transformations. Choisissez les unes ou les autres en fonction de vos besoins. Lorsqu'il est possible de simuler, de manière efficace, une projection 3D en utilisant une ou plusieurs transformations relatives, privilégiez toujours ce choix, car les transformations relatives sont plus simples à gérer.

8-2-2. L'axe de rotation

La rotation 3D que vous avez testée a mis en évidence un élément important : l'axe de rotation. Sa position est déterminée par trois propriétés : CenterOfRotationX, Center OfRotationY et CenterOfRotationZ. Le deuxième panneau, au sein du menu Projection, permet de modifier les coordonnées de cet axe afin d'avoir plus de contrôle sur les rotations obtenues (voir Figure 9.8). Comme vous le constatez à la Figure 9.8, les valeurs admises sont relatives sur les axes x et y, il n'est donc pas facile de manipuler ces propriétés de manière intuitive. Sur l'axe x, la valeur 1 correspond à 100 % de la largeur du rectangle, sur l'axe y cette valeur correspond à 100 % de la hauteur de ce dernier. Par défaut, l'axe gère les rotations de manière symétrique, car il est situé à 50 % en x et y de l'instance de UIElement, soit une valeur de 0.5. Ces valeurs n'apparaissent pas dans l'interface de Blend à moins de les modifier. Comme les objets n'ont pas d'épaisseur, la valeur de UIElement sur l'axe des profondeurs (z) est égale à 0. Ainsi, les instances d'objets graphiques subissant une rotation sur les axes x et y ne se sont pas déplacées sur l'axe des profondeurs z. De plus, sur l'axe z, la plage de valeurs admises n'est pas exprimée en pourcentage, mais en pixels, du fait que l'épaisseur est égale à zéro. Cela n'aurait, en effet, aucun sens de demander 200 % de 0 pixel d'épaisseur. Vous pouvez voir, sur la droite de la Figure 9.8, deux vues engendrées par le repositionnement de l'axe de rotation de 50 pixels vers l'avant. On obtient des résultats équivalents en déplaçant le centre sur les axes x ou y. Toutefois, utiliser l'axe z offre plus de souplesse et se révèle pratique pour manipuler par la suite les objets en 3D.

Image non disponible
Figure 9.8 - Rotations avec repositionnement de l'axe de rotation 3D

8-2-3. Les axes de translation

Nous allons maintenant nous concentrer sur les axes de translation. Au sein de Silverlight, deux types de translations cohabitent : les translations locales et globales. Lorsque vous avez effectué une rotation 3D du rectangle, vous avez également modifié la rotation de son axe. Le dernier des quatre onglets vous permet de déplacer le rectangle de manière locale (voir Figure 9.9).

Image non disponible
Figure 9.9 - Inspecteur de propriétés des translations 3D locales

Concrètement, le déplacement local d'un composant UIElement sera soumis à l'orientation du centre de rotation. Quel que soit l'ordre de vos opérations entre rotation et déplacement local, cela revient à faire pivoter l'objet, puis à le déplacer en suivant l'axe de rotation (voir Figure 9.10).

Image non disponible
Figure 9.10 - Déplacement du rectangle le long de l'axe de rotation local après une rotation effectuée sur l'axe Y

Vous pouvez également choisir de déplacer un objet en utilisant l'axe de translation global à l'aide du troisième onglet (voir Figure 9.11).

Image non disponible
Figure 9.11 - Inspecteur de propriétés des translations 3D globales

Ce repère 3D global est invisible, non modifiable et fait référence aux coordonnées propres à l'écran lui-même (Figure 9.12).

Image non disponible
Figure 9.12 -: L'axe global de l'écran

La translation globale d'instances de UIElement sera soumise à l'orientation figée de cet axe 3D immuable, quelle que soit la rotation de l'objet autour de son axe (voir Figure 9.13).

Image non disponible
Figure 9.13 - Rotations avec repositionnement de l'axe de rotation 3D

Quel que soit le type de translations utilisées, elles seront soumises au point de fuite de la caméra. C'est un assez bon moyen de mettre en évidence la perspective et les points de fuite. Pour le constater, créez cinq carrés, puis faites une rotation de 90 degrés sur l'axe y pour chacun d'eux.

Ensuite déplacez-les sur l'axe global x afin de mettre en valeur la perspective (voir Figure 9.14).

Image non disponible
Figure 9.14 - Rotation de 90° de l'axe y, puis translation sur l'axe global x de chaque rectangle.

Il est avantageux de déplacer les rectangles en utilisant l'axe global, car obtenir le même résultat avec l'axe local demande un effort de visualisation en trois dimensions. Il nous faudrait en effet déplacer l'objet sur l'axe de translation local z, ce qui peut devenir rapidement difficile à gérer et assez perturbant.

8-2-4. Accès et modification via C#

Les propriétés de projection 3D sont bien plus faciles à lire et à modifier que les transformations relatives. Toutefois, vous aurez le même type de problématique. Les propriétés de projection 3D sont attachées, donc absentes par défaut, et la propriété Projection de UIElement accepte plusieurs types dont Plane Projection. Elle est nulle par défaut. Ainsi, vous devrez obligatoirement affecter une instance de PlaneProjection pour cibler les propriétés auxquelles vous avez accès par nature dans l'interface de Blend. La conséquence de cette architecture au sein de Blend est directe, vous ne pouvez pas cibler une propriété de Plane Projection, au sein d'un Storyboard, si le nœud XAML n'est pas présent en dur. Téléchargez le projet : chap9/ProjectionPlan.zip. Ce projet va nous permettre d'apprendre à cibler les projections via C#. Il contient huit boutons dont l'objectif est de contrôler le composant Image en trois dimensions (voir Figure 9.15).

Image non disponible
Figure 9.15 - Rotation de 90° sur l'axe y, puis translation sur l'axe global x de chaque rectangle.

Le composant Image ne possède pas de nœud élément XAML PlaneProjection. Le pavé de flèches directionnelles va nous permettre de déplacer l'image sur l'axe global en x et z. Les flèches à gauche et à droite joueront une animation de rotation sur y et celle en haut à gauche, un pivotement de l'image sur x. Le mieux est de stocker l'instance de Plane Projection comme membre de la classe MainPage. Vous pouvez ainsi cibler les propriétés de manière très simple :

 
Sélectionnez
public partial class MainPage : UserControl
{
   PlaneProjection pp = new PlaneProjection();

   public MainPage()
   {
      InitializeComponent();
      Logo.Projection = pp;
      MouseLeftButtonUp += new MouseButtonEventHandler(MainPage_
      MouseLeftButtonUp);
   }

   void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   {
      pp.RotationY += 10;
   }
}

Bien que très simple, cette méthode possède un inconvénient. Si le designer, sous Blend, a déjà modifié certaines propriétés de projection, vous risquez d'écraser leur affectation. Il convient donc de tester la valeur de la propriété Projection. Si celle-ci est nulle, on peut lui associer une nouvelle instance ; si celle-ci possède déjà une valeur, il convient de vérifier également son type. Le code logique C#, en gras ci-dessous, est responsable du test de la propriété Projection :

 
Sélectionnez
public partial class MainPage : UserControl
{
   PlaneProjection pp = new PlaneProjection();

   public MainPage()
   {
      InitializeComponent();
      if (Logo.Projection == null)
         Logo.Projection = pp;
      else if (Logo.Projection != null && Logo.Projection is PlaneProjection)
         pp = (PlaneProjection)Logo.Projection;
      else
         throw new NotImplementedException("la projection est de type Matrix3DProjection");
      MouseLeftButtonUp += new MouseButtonEventHandler(MainPage_MouseLeftButtonUp);
   }

   void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   {
      pp.RotationY += 10;
   }
}

Vous pouvez également utiliser les méthodes héritées de la classe DependencyObject, SetValue et GetValue. Toutefois, le code nécessaire est plus verbeux :

 
Sélectionnez
void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
   //pp.RotationY = pp.RotationY + 10;
   double ActualRYValue = (double) pp.GetValue (PlaneProjection.RotationYProperty );
   pp.SetValue(PlaneProjection.RotationYProperty, ActualRYValue + 10);
}

Nous allons animer le composant Image de manière très simple. Lorsque l'une des flèches sera pressée, le composant se déplacera ou pivotera. Quand vous relâcherez le bouton de la souris, l'animation de déplacement ou de rotation sera stoppée. Nous aurions pu utiliser une instance de la classe DispatcherTimer, mais cela aurait engendré plus de code C#. Afin de simplifier au maximum le code logique, les contrôles utilisateur sont en majorité des instances de Repeat-Button. Seul le bouton ResetPosition est de type Button. L'avantage d'un RepeatButton est d'embarquer en son sein le comportement de répétition. Sélectionnez le bouton nommé RotateYLeft.

Dans le panneau Common Properties, vous constatez que celui-ci possède des capacités supplémentaires (voir Figure 9.16).

Image non disponible
Figure 9.16 - Propriétés Delay et Interval de la classe RepeatButton

La propriété Delay accepte une valeur entière (Int32) et permet de spécifier le nombre de millisecondes detemps d'attente en position appuyée avant que le comportement de répétition ne débute. Le laps de temps entre chaque répétition, lui aussi exprimé en milli-secondes, est défini grâce à la propriété Interval (Int32). Le code finalisé de l'application devient trivial :

 
Sélectionnez
namespace ProjectionCSharp
{
   public partial class MainPage : UserControl
   {
      PlaneProjection pp = new PlaneProjection();
      public MainPage()
      {
         InitializeComponent();
         if (Logo.Projection == null)
            Logo.Projection = pp;
         else if (Logo.Projection != null && Logo.Projection is PlaneProjection)
            pp = (PlaneProjection)Logo.Projection;
         else
            throw new NotImplementedException("la projection est de type Matrix3DProjection");
         RotateXBack.Click += new RoutedEventHandler(RotateXBack_Click);
         RotateYLeft.Click += RotateYLeft_Click;
         RotateYRight.Click += RotateYRight_Click;
         GoForward.Click += GoFrontward_Click;
         GoBackward.Click += GoBackward_Click;
         GoLeft.Click += GoLeft_Click;
         GoRight.Click += GoRight_Click;
         ResetPosition.Click += ResetPosition_Click;
      }

      void ResetPosition_Click(object sender, RoutedEventArgs e)
      {
         //on utilise une triple égalité pour réinitialiser
         //les projections
         Logo.Projection = pp = new PlaneProjection();
      }

      void RotateXBack_Click(object sender, RoutedEventArgs e)
      {
         pp.RotationX -= 5;
      }
      void GoLeft_Click(object sender, RoutedEventArgs e)
      {
         pp.GlobalOffsetX -= 10;
      }
      void GoRight_Click(object sender, RoutedEventArgs e)
      {
         pp.GlobalOffsetX += 10;
      }
      void GoBackward_Click(object sender, RoutedEventArgs e)
      {
         pp.GlobalOffsetZ -= 10;
      }
      void GoFrontward_Click(object sender, RoutedEventArgs e)
      {
         pp.GlobalOffsetZ += 10;
      }
      void RotateYLeft_Click(object sender, RoutedEventArgs e)
      {
         pp.RotationY += 5;
      }
      void RotateYRight_Click(object sender, RoutedEventArgs e)
      {
         pp.RotationY -= 5;
      }
   }
}

Comme vous le constatez, ces propriétés sont facilement accessibles. Avec un peu plus de code, nous pourrions envisager des interactions 3D performantes, sans recourir à des notions de mathématiques complexes. Toutefois, si vous êtes férus de géométrie ou d'arithmétique, vous pouvez également utiliser la classe Matrix3DProjection afin d'améliorer les performances et le code logique de vos interfaces. Vous pouvez télécharger le projet : chap9/ProjectionCSharp.zip.

8-3. La caméra

Nous allons maintenant approfondir les notions de conteneur 3D et de caméra tout en mettant en pratique ce que nous avons déjà appris. Au sein de Silverlight, la caméra n'est pas vraiment manipulable en tant que telle. Toutefois, elle est bien présente, la meilleure preuve de son existence est l'effet de perspective dès que vous déplacez un objet dans l'espace 3D. La déformation des objets engendrée par la perspective est directement dépendante de différentes propriétés de la caméra. Trois facteurs sont importants. Les deux premiers, directement liés, représentent la longueur de la focale et le champ de vision (ou field of view) qui correspond à l'angle d'ouverture verticale de la caméra. Le dernier facteur représente la distance entre la lentille de la caméra et l'objet ciblé (voir Figure 9.17).

Image non disponible
Figure 9.17 - Principe de la caméra.

8-3-1. Position de la caméra dans l'espace de projection

Chaque instance de la classe UIElement possède la capacité d'être projetée dans un espace indépendant. Cela signifie que chaque objet possède sa propre caméra de projection 3D. Par défaut, la caméra possède une position égale à ActualWidth/2 et ActualHeight/2 pour les axes x et y, et cela par rapport à l'origine 2D de l'objet, qui est située en haut à gauche. Les propriétés ActualWidth et ActualHeight représentent la valeur réelle de la largeur ou de la hauteur d'un objet quel que soit son mode de redimensionnement. Si nous déplaçons un objet via les propriétés de mise en forme standard, la caméra de projection accompagnera l'objet dans son déplacement. Nous allons illustrer ce comportement. Créez une nouvelle application Silverlight. Instanciez quatre exemplaires de Rectangle au sein de LayoutRoot et affectez leur largeur et hauteur d'une valeur de 100 pixels. Alignez-les au sein de la grille de manière à créer un damier et appliquez leur une rotation 3D de 60° sur l'axe y (voir Figure 9.18).

Image non disponible
Figure 9.18 - Principe des caméras de projection indépendante.

Comme vous le constatez, les rectangles ne sont pas mis en perspective les uns par rapport aux autres, car les propriétés de mise en forme standard ne font pas partie de l'espace projeté. Ainsi leur caméra est déplacée avec eux. Pour que ces rectangles bénéficient de la même perspective, il faut que leur caméra respective soit placée exactement au même endroit les unes par rapport aux autres. Il ne faut donc pas les aligner grâce aux propriétés de mise en pages standard. Nous devons, à la place, utiliser les propriétés de projections 3D. Supprimez tous ces rectangles et recréez-en un de 100 pixels de large par 100 pixels de haut. Ensuite, faites-en trois copies. Les rectangles sont maintenant superposés. Utilisez les translations globales x et y afin de les aligner pour en faire un damier, ensuite affectez à chacun d'eux une rotation de 60° sur l'axe y. Cette fois, vous remarquez que leur position dans l'espace 3D est cohérente. Vous pouvez également tester en les alignant grâce aux translations locales, puis réaffectez une rotation de 60°. Le résultat est très différent selon le type de translations employées (voir Figure 9.19).

Image non disponible
Figure 9.19 - Principe des caméras de projection indépendantes et alignement grâce aux translations globales.

Les deux types de visuels sont valables, car les caméras de projection propres à chaque rectangle sont exactement placées au même endroit. Nous venons de déterminer un fait essentiel : les caméras de projection appartiennent à chaque objet et sont indépendantes les unes des autres. Il nous reste à déterminer la distance par défaut entre un UIElement et la caméra sur l'axe z. Celle-ci est assez facile à trouver, bien que non documentée. Il vous suffit de déplacer un objet de 1 000 pixels en z pour ne plus voir cet objet. Il n'est plus visible, car il est passé derrière l'objectif de la caméra. Il ne fait plus partie de son champ de vision. Blend vous donne tout de même un aperçu de son enveloppe selon la distance dont l'objet dépasse la caméra. La caméra est donc située à 999 pixels de l'instance UIElement associée. Cette distance n'est modifiable qu'en déplaçant l'objet. Aucune propriété ne permet actuellement de modifier la position de la caméra. La distance en z a été choisie de manière à ce que les objets s'affichent de manière 2D plane face à la caméra et à 100 % de leur dimension réelle lorsque ceux-ci n'ont pas subi de projection 3D planaire.

8-3-2. L'angle d'ouverture

L'angle d'ouverture dans Silverlight est de 57 degrés par défaut. Cette valeur n'est pas modifiable, elle est définie en dur dans le code source et conditionne directement la longueur de focale fixée à 33 mm. Il n'est donc pas possible de modifier la déformation de perspective de manière aisée. Ouvrez le projet nommé ProjectionPlan, que nous avons enrichi à la section 8.2.4 Accès et modification via C#. Compilez-le via le raccourci F5 et faites une rotation sur l'axe y du contrôle Image (voir Figure 9.20).

Image non disponible
Figure 9.20 - Rotation simple de 60° sur l'axe y, la caméra est à une distance de 999 pixels en z.

L'effet de perspective est directement dépendant de l'angle d'ouverture et de la focale de la caméra. Il est possible d'accentuer cet effet avec deux transformations complémentaires. Revenez sous Blend et définissez une translation de +500 pixels sur l'axe z du composant Image. Modifiez ensuite les transformations relatives du composant Image pour diviser par deux l'échelle de l'objet. Pour cela, affectez les propriétés ScaleX et ScaleY chacune de la valeur 0.5. Ainsi, nous aurons l'impression que l'objet est à la même distance de la caméra, mais l'effet de perspective dû à la rotation est très accentué (voir Figure 9.21). C'est un moyen assez simple permettant de simuler un grand angle de prise de vue. Toutefois cette astuce est peu précise et vous oblige à mélanger les deux types de transformations.

Image non disponible
Figure 9.21 - Rotation simple de 60° sur l'axe y, la caméra est à une distance de 499 pixels en z, mais l'échelle de l'objet est divisée par deux.

Bien que l'angle d'ouverture soit de 57°, les objets dépassant le cône représentant le champ de vision de 57° ne sont pas masqués. C'est une particularité du modèle de projection qui est avant tout une représentation mathématique simplifiée d'un environnement 3D. Dans la vie réelle, une caméra ne peut simplement pas filmer les objets qui ne sont pas dans son champ de vision. Ce n'est pas le cas dans Silverlight puisque la caméra n'existe pas réellement.

8-3-3. Les conteneurs 3D

Comme nous l'avons vu précédemment, chaque instance de UIElement possède son propre environnement de projection 3D et donc sa propre caméra. Cette particularité a une conséquence directe sur la manière dont est gérée la projection. Les conteneurs à plusieurs enfants, lorsqu'ils subissent une projection, n'affectent pas cette projection dynamiquement à chacun de leurs enfants. Autrement dit, les conteneurs de projection 3D n'existent pas par défaut dans Silverlight. Le résultat visuel de comportement est assez simple à démontrer. Dans un nouveau projet, créez une grille, puis un rectangle. Affectez sur ce dernier une rotation de 50° sur l'axe y. Pour finir, faites une rotation sur l'axe y de -50° sur la grille parente du rectangle. Si la grille possédait le comportement d'un conteneur 3D, le rectangle en son sein serait affiché comme s'il ne possédait pas de rotation, car la rotation de la grille l'annulerait. Or ce n'est pas le cas, les projections sont calculées séparément et sont appliquées l'une sur l'autre (voir Figure 9.22).

Image non disponible
Figure 9.22 - Projections 3D sur un conteneur de type Panel et son enfant de type Rectangle.

Simuler le comportement d'un conteneur 3D est assez simple mais fastidieux. L'avantage principal de ce type de conteneur résiderait dans sa capacité à affecter les propriétés de projection de tous ses enfants lorsque l'on modifierait les siennes. Faire une rotation de ce conteneur appliquerait une rotation équivalente à tous ces enfants. Pour reproduire ce comportement, il nous faudrait créer les enfants exactement au même endroit dans un conteneur de type Panel, puis les déplacer de manière locale. Ainsi l'axe de rotation serait commun à tous les enfants. Au sein d'un nouveau projet, créez cinq exemplaires de Rectangle au même endroit dans la grille principale. Ceux-ci doivent donc être centrés les uns par rapport aux autres (voir Figure 9.23).

Image non disponible
Figure 9.23 - Rectangles centrés au sein de LayoutRoot.

Le fait de centrer les rectangles les uns par rapport aux autres permet de définir une caméra commune. Espacez-les de 50 pixels sur l'axe local z. Le rectangle le plus éloigné doit ainsi posséder une position de -100 pixels en z et le plus proche une position de +100 pixels sur ce même axe. Comme vous utilisez les propriétés de projection, la caméra reste commune à tous (voir Figure 9.24).

Image non disponible
Figure 9.24 - Rectangles décalés via les axes x et y de translations locales de projection 3D.

Pour finir, sélectionnez-les tous en même temps, puis affectez-leur une rotation afin d'obtenir un effet de perspective (voir Figure 9.25). Nous simulons de cette manière le comportement d'un conteneur 3D. Plusieurs autres techniques existent. Vous pourriez, par exemple, grouper les objets au sein de conteneurs de type Canvas de mêmes dimensions et créés aux mêmes coordonnées. Lorsque vous affecteriez une projection 3D à ces conteneurs, les objets qui y sont contenus seraient transformés dans un environnement 3D homogène.

Image non disponible
Figure 9.25 - Rotation 3D des rectangles.

8-3-4. Le point de fuite

Le point de fuite principal, également appelé centre de projection, correspond par défaut aux coordonnées de la caméra en x et y, à l'instant de la création d'un objet. Pour positionner le point de fuite, il vous suffit de créer l'objet à un endroit précis, puis de le déplacer sur l'axe de translation global ou local. Toutefois, vous n'êtes pas obligé de reconstruire un projet depuis le début. Il vous suffit de modifier l'alignement des objets à mettre en perspective dans l'environnement 2D habituel, puis de redéfinir une translation locale ou globale dans l'espace 3D. Sur l'exemple précédent, supprimez les rotations 3D affectées aux rectangles ; vous obtenez un visuel équivalent à celui de la Figure 9.24. Sélectionnez-les tous et modifiez leurs options d'alignement pour les fixer en bas à droite de la grille LayoutRoot. Pour finir, déplacez-les sur l'axe global de -100 en x et de -100 en y. Vous obtenez automatiquement des lignes de fuite dirigées vers le coin en bas à gauche de la grille, soit le point précis sur lequel ces rectangles étaient alignés dans l'espace 2D (voir Figure 9.26).

Image non disponible
Figure 9.26 - Effet de perspective 3D par simple déplacement autour du point de fuite.

Pour que des objets partagent le même point de fuite, il vous faudra simplement les créer aux mêmes coordonnées.

Jusqu'à maintenant, nous avons vu deux moyens différents de gérer l'ordre de superposition des objets. L'ordre des enfants d'un conteneur de type Panel affecte directement leur superposition.

L'affectation de la propriété ZIndex permettait d'affiner cette gestion en passant outre l'ordre d'affichage de l'index de l'enfant. Un troisième moyen existe, il n'est toutefois activé que lorsque la propriété Projection des instances d'UIElement possède une valeur, donc uniquement lorsque les enfants sont projetés dans un espace 3D. La distance de l'objet à la caméra va directement conditionner l'ordre de superposition. Au sein de Silverlight, le tri sur l'axe z, donc sur l'axe caméra objet, est géré sans aucun besoin d'un quelconque calcul de votre part. Ce tri est automatique.

Vous pouvez le voir à l'œuvre sur la Figure 9.26. De la même manière qu'avec le tri par index d'enfants, vous pouvez outrepasser le tri sur l'axe z via la propriété 2D ZIndex.

8-4. Introduction aux matrices

Jusqu'à maintenant, nous avons abordé les transformations 3D du point de vue d'un graphiste.

Bien que nous puissions affecter des transformations 3D par C# en utilisant la classe PlaneProjection, celle-ci apparaîtra limitée lorsque vous aurez besoin d'appliquer des projections 3D plus complexes aux instances d'UIElement. La compréhension des matrices permet de s'affranchir des limitations de la classe PlaneProjection, puisque vous pourrez utiliser Matrix3DProjection à sa place. Attention, cette section est une introduction aux matrices, le but est de vous familiariser avec ce concept. Toutefois, si vous n'êtes pas développeur, vous pouvez complètement occulter cette section et utiliser la classe simplifiée PlaneProjection qui répond à 60 % des cas. Nous mettrons également en parallèle l'API 3D fournie par Silverlight et celle fournie par DirectX.

8-4-1. Définition et principes

Une matrice est une notation mathématique permettant de représenter des équations, des expressions ou des données, de manière à simplifier les calculs complexes. Elles sont particulièrement utiles aux développeurs d'environnement 3D, mais également aux graphistes qui les emploient, sans forcément le savoir (via les outils de modification d'objets). Elles permettent, entre autres, d'appliquer des transformations aux objets dans un environnement 2D ou 3D. Concrètement, une matrice contient une série de nombres (ou d'expressions renvoyant des nombres) formalisée sous forme de n lignes et de n colonnes encadrées par des crochets (voir Figure 9.27).

Image non disponible
Figure 9.27 - Différents exemples de matrices.

Les indices de ligne et de colonne permettent aux valeurs contenues dans une matrice d'être accessibles. Ils constituent un système de coordonnées sous forme de numéros de ligne et de numéro de colonne. Par convention, ces coordonnées sont notées MnLigne,nColonne (voir Figure 9.28).

Image non disponible
Figure 9.28 - Coordonnées de membres de matrices.

Nous avons déjà vu les matrices de transformations dans ce livre, même si nous ne les avons pas formalisées, notamment lorsque nous abordé les transformations relatives (voir Chapitre 4 Un site plein écran en deux minutes). Le nœud TransformGroup contenant tous les types de transformation est en fait la représentation simplifiée d'une matrice de transformation 2D :

 
Sélectionnez
<Rectangle.RenderTransform>
   <TransformGroup>
      <ScaleTransform/>
      <SkewTransform/>
      <RotateTransform Angle="15"/>
      <TranslateTransform X="60" Y="40" />
   </TransformGroup>
</Rectangle.RenderTransform>

Son principal avantage réside dans sa facilité d'utilisation. Il permet, par exemple, aux designers de faire des rotations 2D, ce qui n'est pas aussi simple qu'il y paraît. Comme nous l'avons déjà évoqué, la propriété RenderTransform des instances d'UIElement accepte également une instance de la classe MatrixTransform. Celle-ci possède la propriété Matrix acceptant une valeur (structure) de type Matrix. Cette dernière est la version brute d'une matrice mathématique constituée de trois colonnes et de trois lignes. Elle permet de contrôler entièrement les déformations de manière colinéaire (voir Figure 9.29).

Image non disponible
Figure 9.29 - Représentation de la matrice de transformation vectorielle 2D de Silverlight.

La matrice correspondant aux transformations du nœud RenderTransform (celui que nous avons décrit en XAML et qui affecte au rectangle +15 degrés pour la rotation, 60 pixels de déplacement en X et 40 en Y) est décrite à la Figure 9.30.

Image non disponible
Figure 9.30 - Représentation de la matrice de transformation correspondant au nœud RenderTransform.

Au sein d'une matrice de transformation, la propriété exprimant la rotation n'existe pas réellement. Elle est en réalité générée par la combinaison de l'inclinaison et du redimensionnement de l'objet. Voici la traduction de notre matrice côté XAML :

 
Sélectionnez
<Rectangle.RenderTransform>
   <MatrixTransform>
      <MatrixTransform.Matrix>
      <Matrix OffsetY="60" OffsetY="40" M11="0.966" M22="0.966"
         M12="0.259" M21="-0.259" />
      </MatrixTransform.Matrix>
   </MatrixTransform>
</Rectangle.RenderTransform>

Vous remarquez que les indices aux coordonnées M31 et M32 sont directement notés OffsetX et OffsetY au sein de la structure Matrix. Les autres propriétés sont accessibles par la notation Mn,m. Cela permet à un développeur d'identifier et d'utiliser simplement les propriétés de la matrice qui correspondent aux translations en x et y.

La matrice de transformation 2D possède trois colonnes. La troisième colonne contient M1,3=0, M2,3=0 et M3,3=1. Ces valeurs ne sont pas modifiables. Sans rentrer dans une explication mathématique complexe, cela permet d'éviter les déformations ou les distorsions des objets 2D lorsque ceux-ci subissent des rotations, des mises à l'échelle ou des inclinaisons. La matrice 2D de Silverlight gère ainsi des transformations dites affines, également appelées transformations colinéaires (voir Figure 9.31).

Image non disponible
Figure 9.31 - Transformations affines et non affines.

Du côté C#, une instance de MatrixTransform est affectée à la propriété RenderTransform d'un objet héritant de UIElement. La classe MatrixTransform possède la propriété Matrix qui contient réellement les valeurs entre crochets étudiées plus haut :

 
Sélectionnez
MatrixTransform Mt = new MatrixTransform();
MonUIElement.RenderTransform = Mt;
/*
* Renvoie Identity qui fait référence
* à la matrice d'identité par défaut :
* 1,0,0
* 0,1,0
* 0,0,1
*/
Debug.WriteLine(Mt.Matrix.ToString());

Dans l'exemple ci-dessus, l'objet n'est pas transformé. Lorsque vous instanciez un nouvel objet

MatrixTransform, sa propriété Matrix correspond par défaut à une matrice d'identité. Une matrice d'identité est neutre et n'applique aucune transformation à l'objet. Elle représente l'état de la matrice lorsque l'objet n'a subi aucune transformation. Vous pouvez également instancier la structure Matrix afin d'appliquer une transformation personnalisée à l'objet de type UIElement lors de l'initialisation :

 
Sélectionnez
Matrix M = new Matrix(1,1,
                      0,1,
                      0,0);
MatrixTransform Mt = new MatrixTransform();
Mt.Matrix = M;
bt.RenderTransform = Mt;
/*
* Renvoie la matrice affectée :
* 1,1,
* 0,1,
* 0,0
*/
Debug.WriteLine(Mt.Matrix.ToString());

Comme vous le constatez, la matrice ne renvoie pas la dernière colonne, celle-ci n'est simplement pas accessible ou modifiable.

8-4-2. Les matrices 3D

Au sein de Silverlight, les matrices de transformations 3D suivent les mêmes principes que ceux évoqués ci-dessus. Elles gèrent toutefois les transformations non affines, car elles ont pour but d'afficher des objets en perspective 3D, ce qui déforme les objets dans une certaine mesure. Elles sont constituées de quatre lignes et de quatre colonnes et possèdent une notation par ligne équivalente à celle que propose DirectX. À l'opposé, les matrices 3D fournies par OpenGL et Flash ont une notation par colonne. Vous pouvez facilement identifier le type d'écriture utilisé en fonction du positionnement des valeurs exprimant le déplacement (voir Figure 9.32).

Image non disponible
Figure 9.32 - Représentation des matrices 3D selon les plates-formes.

Il est également intéressant de noter que l'axe z n'est pas dirigé vers l'utilisateur, mais vers l'horizon pour OpenGL et Flash, à l'opposé de DirectX et Silverlight. On évoque à ce sujet la notion de repère main droite ou gauche : DirectX utilise le repère main gauche alors qu'OpenGL est basé sur un repère main droite. Il faut également remarquer que même si les matrices au sein de Silverlight héritent de DirectX, l'axe y ne suit pas la même direction. Au sein du lecteur Silverlight, il est dirigé vers le bas et sous DirectX vers le haut, mais cela ne change rien aux valeurs contenues par la matrice.

La classe Matrix3DProjection est l'équivalent de la classe MatrixTransform qui permet les transformations 2D. Les instances de Matrix3DProjection ne sont pas affectées à la propriété RenderTransform des exemplaires d'UIElement, mais à leur propriété Projection. La structure Matrix3D est homologue à Matrix. Il est donc possible d'affecter des matrices 2D et 3D en même temps. Toutefois, cela est déconseillé dans la majorité des situations, car le résultat devient très vite difficile à prédire. Dans tous les cas de projection, il faudra utiliser la propriété ProjectionMatrix de la classe Matrix3DProjection et lui affecter une instance de la structure Matrix3D. L'écriture en XAML est assez similaire à celle que nous avons déjà étudiée plus haut, mais un peu plus verbeuse :

 
Sélectionnez
<Rectangle.Projection>
   <Matrix3DProjection>
      <Matrix3DProjection.ProjectionMatrix>
      <Matrix3D
         M11="1" M12="0" M13="0" M14="0"
         M21="0" M22="1" M23="0" M24="0"
         M31="0" M32="0" M33="1" M34="0"
         OffsetX="50" OffsetY="0" OffsetZ="0" M44="1" />
      </Matrix3DProjection.ProjectionMatrix>
   </Matrix3DProjection>
</Rectangle.Projection>

Vous pouvez également utiliser cette écriture simplifiée :

 
Sélectionnez
<Rectangle.Projection>
   <Matrix3DProjection
      ProjectionMatrix="1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      50, 0, 0, 1"
   />
</Rectangle.Projection>

En voici une version C# :

 
Sélectionnez
//on crée une nouvelle instance de matrice
Matrix3D MyMatrix = new Matrix3D();
//On effectue une translation de 50 pixels sur l'axe x.
MyMatrix.M11=1; MyMatrix.M12=0; MyMatrix.M13=0; MyMatrix.M14=0;
MyMatrix.M21=0; MyMatrix.M22=1; MyMatrix.M23=0; MyMatrix.M24=0;
MyMatrix.M31=0; MyMatrix.M32=0; MyMatrix.M33=1; MyMatrix.M34=0;
MyMatrix.OffsetX=50; MyMatrix.OffsetY=0; MyMatrix.OffsetZ=0; MyMatrix.M44=1;
//on crée une nouvelle instance de la classe
//enveloppante Matrix3DProjection
Matrix3DProjection MyMatrix3DProjection = new Matrix3DProjection();
//on lui affecte MyMatrix créée auparavant
MyMatrix3DProjection.ProjectionMatrix = MyMatrix;
//on affecte la propriété Projection du rectangle
MonRectangle.Projection = m3dProjection;

Comme nous l'avons dit plus haut, la notation apportée par les matrices présente l'avantage de faciliter les calculs mathématiques complexes. L'opération la plus courante est la multiplication, nous allons l'aborder dans la prochaine section.

8-4-3. Opérations sur les matrices

À l'origine, les matrices de transformation possèdent de nombreuses méthodes facilitant leur utilisation. L'API 3D de Silverlight étant toute récente, la plupart des méthodes que vous trouverez sous DirectX ne sont pas intégrées nativement dans le lecteur. Microsoft en fournit toutefois un certain nombre au sein de la documentation de Silverlight, vous pourrez les utiliser pour bénéficier de plus de capacités. En premier lieu, il nous faut comprendre certains concepts de base permettant de manipuler des matrices de transformation 3D. Si vous possédez un niveau équivalent à un DEUG de Maths de première année, vous connaissez sans doute les matrices et pouvez passer votre chemin. Dans ce cas, vous pourriez éventuellement vous lancer dans le portage d'une partie de l'API fournie dans DirectX vers la plate-forme Silverlight. Pour appliquer une transformation à un objet, nous devons utiliser une matrice de transformation. Celle-ci décrit précisément ce que vous souhaitez modifier. Si vous voulez, par exemple, changer l'échelle d'une instance de 200 % en y, il faut commencer par créer la matrice de transformation décrivant le redimensionnement, puis multiplier celle de l'objet par cette dernière. L'opération de multiplication est la plus utilisée, car elle permet d'ajouter de nouvelles transformations aux objets. Elle obéit à des règles précises (voir Figure 9.33)

Image non disponible
Figure 9.33 - Multiplication de matrices.

Comme vous le constatez, nous multiplions les valeurs contenues dans les colonnes par les valeurs présentes au sein des lignes. Vous pouvez multiplier une matrice A par une matrice B à partir de l'instant ou le nombre de colonnes de A est égal au nombre de lignes de B. Il est ainsi possible de multiplier une matrice définie sur une ligne et quatre colonnes, par une autre de quatre lignes et de quatre colonnes.

Du fait de la notation par ligne utilisée par Silverlight et héritée de DirectX, l'ordre de la multiplication est spécifique. Dans notre cas, la première matrice symbolise l'objet à transformer alors que la seconde désigne la matrice de transformation. À l'opposé, dans l'environnement de développement OpenGL et Flash, la première des deux matrices représente la matrice de transformation, la seconde est la matrice de l'objet à modifier.

Il est peut-être utile de multiplier deux matrices de transformation entre elles. Par exemple, l'une pourrait modifier l'échelle en y et l'autre affecterait l'inclinaison en x (voir Figure 9.34).

Image non disponible
Figure 9.34 - Multiplication de matrices de transformation.

Il reste à multiplier la matrice générée par celle de l'objet pour appliquer les deux transformations en une seule fois. Il est aisé de comprendre pourquoi la multiplication de matrice n'est pas commutative, c'est-à-dire que l'opération Matrice1 x Matrice2 est différente de Matrice2 x Matrice1. Si nous reprenons le même exemple en inversant l'ordre des matrices, nous n'obtenons pas le même résultat (voir Figure 9.35).

Image non disponible
Figure 9.35 - Ordre inversé de la multiplication de matrice initiale.

Visuellement, la différence est flagrante entre les deux multiplications (voir Figure 9.36).

Image non disponible
Figure 9.36 - Deux résultats visuels différents selon l'ordre de multiplication.

Comme nous l'avons déjà évoqué plus haut, la rotation n'est en fait qu'une combinaison d'inclinaisons et de redimensionnements.

Pour le vérifier, vous pouvez prendre n'importe quelle instance de la classe UIElement, puis modifier ses transformations relatives AngleX et AngleY et les affecter respectivement d'un angle et de son opposé. Si vous utilisez les valeurs 30 et -30, vous remarquez que l'objet subit une rotation ainsi qu'un agrandissement engendré par les deux inclinaisons combinées. Pour palier ce redimensionnement, il faut également affecter l'échelle en x et y (propriétés ScaleX et ScaleY) par le cosinus de l'angle d'inclinaison.

C'est exactement le même principe pour les matrices 3D sauf que nous devons prendre en compte l'axe z. Il faut en fait réfléchir par plan. La rotation s'effectue dans tous les cas sur un plan à deux axes. C'est également le cas pour les matrices de transformation 2D permettant les rotations sur le plan xy, car la rotation s'effectue autour d'un axe z abstrait. Nous ne rentrerons pas dans l'explication des formules mathématiques, car cela sort du cadre de ce livre. Toutefois, il suffit d'assimiler la rotation dans un environnement 2D pour comprendre ces dernières. Nous allons identifier leur emplacement (voir Figure 9.37).

Image non disponible
Figure 9.37 - Identification des opérations de rotation d'angle a dans la matrice 3D Silverlight.

Le sens de rotation est directement lié au fait que les inclinaisons x et y possèdent des valeurs

opposées. Pour l'inverser, vous pouvez affecter la valeur -sin(a) à M21 et sin(a) à M12. Dans ce cas, le sens de rotation est l'opposé de celui généré par la matrice montrée à la Figure 9.37. Il serait pratique de centraliser les méthodes qui facilitent la manipulation de matrice 3D. À cette fin, nous pouvons créer une classe statique Matrix3DUtils contenant, entre autres, des méthodes statiques de rotation, de translation ou de redimensionnement :

 
Sélectionnez
public static Matrix3D RotateZTransform ( double a )
{
   //l'angle a est exprimé en radians
   double sin = Math.Sin(a);
   double cos = Math.Cos(a);
   Matrix3D m = new Matrix3D();
   m.M11 =cos; m.M12 =sin; m.M13 = 0; m.M14 = 0.0;
   m.M21 =-sin; m.M22 =cos; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = 0; m.M32 = 0.0; m.M33 = 0.0; m.M34 = 0.0;
   m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
   //on retourne la matrice permettant d'appliquer la rotation
   return m;
}

Son utilisation faciliterait grandement la transformation d'instances de type UIElement :

 
Sélectionnez
Matrix3DProjection M3P = new Matrix3DProjection();
Matrix3D M3D = new Matrix3D();
void MainPage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
   //on applique une rotation de 15° lors du clic gauche de la souris
   M3D *= Matrice3DUtils.RotateZTransform(15*Math.PI/180);
   M3P.ProjectionMatrix = M3D;
   MonRectangle.Projection = M3P;
}

8-4-4. Initialiser une vue perspective

La classe Matrix3D ne fournit pas de perspective par défaut. Microsoft a volontairement fourni les briques les plus élémentaires afin que les développeurs puissent réaliser leur propre moteur 3D. Cela permet d'ouvrir le champ des possibilités mais demande quelques connaissances en mathématique. Vous devez donc passer par plusieurs étapes pour simuler un espace en 3D.

  1. Choisir la valeur de l'angle d'ouverture de la caméra factice (nommé Field Of View ou FOV en anglais) dont nous avons évoqué le rôle à la section 9.3.2 Collaboration transverse avec SketchFlow.
  2. Créer une matrice définissant le centre de transformation 3D de l'objet projeté.
  3. Positionner l'objet dans l'espace 3D à l'initialisation.
  4. Générer une matrice simulant l'effet de perspective en fonction de l'angle d'ouverture.
  5. Définir les limites du champ d'ouverture.

Toutes ces initialisations importantes sont fournies aux designers par la classe Plane Projection.

Cette classe est malgré tout limitée et ne permet pas de modifier la focale, la direction de la caméra ou un redimensionnement 3D de l'objet projeté. Pour bénéficier de ce type de contrôle personnalisé, vous devrez générer toutes les matrices décrites par les étapes que nous venons de lister. Heureusement, Microsoft les propose dans la documentation de Silverlight. Toutefois le code fourni n'est pas commenté, et les valeurs passées dans l'appel de certaines de ces fonctions ainsi que la définition de l'une d'entre elles méritent quelques réajustements afin d'être utilisées facilement.

Gardez à l'esprit que l'ordre dans lequel sont affectées les matrices est réellement important. Voici le code modifié et commenté de l'appel des méthodes générant chaque matrice ainsi que leur affectation à la propriété Projection d'une instance :

 
Sélectionnez
private void AppliquerScene3D(object sender, MouseButtonEventArgs e)
{
   //on définit un angle d'ouverture de 60°
   double FovY = 60*Math.PI / 180;
   //On crée une matrice déplaçant l'objet afin de
   //repositionner au milieu son centre de transformation
   //Par défaut le centre de transformation est au milieu
   Matrix3D RecentrerPivotDeRotation = TranslationTransform
      (-BeachImage.ActualWidth / 2.0,
      -BeachImage.ActualHeight / 2.0,
      0);
   //angle de rotation
   double Angle = 60.0 * Math.PI / 180.0;
   //diverses matrices de transformation
   Matrix3D RotationAutourDeX = RotateXTransform(Angle);
   Matrix3D TranslationSurX = TranslationTransform(200,0,0);
   Matrix3D RotationAutourDeY = RotateYTransform(Angle);
   Matrix3D RotationAutourDeZ = RotateZTransform(Angle);
   //Translation de l'objet afin qu'il prenne
   //par défaut la largeur et la hauteur correspondant
   //à sa représentation 2D.
   double TranslationZ = -(LayoutRoot.ActualHeight/2)/Math.Tan(FovY/2) ;
   //on génère une matrice qui éloigne l'objet de la caméra sur z
   Matrix3D EloignerObjetDeLaCamera = TranslationTransform
      (0, 0, TranslationZ);
   //Création de la matrice simulant la perspective
   //Param 1 : Angle d'ouverture
   //Param 2 : Rapport largeur/hauteur
   //Param 3 : Le plan de visibilité le plus proche
   //Param 4 : Le plan de visibilité le plus éloigné
   Matrix3D EffetDePerspective = PerspectiveTransformFovRH
      (fovY, LayoutRoot.ActualWidth / LayoutRoot.ActualHeight,
      1.0, 2000.0);
   //Matrice décrivant la fenêtre d'ouverture
   Matrix3D CadreVisible = ViewportTransform
   (LayoutRoot.ActualWidth, LayoutRoot.ActualHeight,
   BeachImage.ActualWidth, BeachImage.ActualHeight);
   //1 - on replace l'objet par rapport
   //à son centre de transformation
   Matrix3D M = RecentrerPivotDeRotation;
   //2 - on lui applique toutes les transformations que l'on souhaite,
   //une rotation ainsi qu'un déplacement sur x dans le cas suivant
   M = M * RotationAutourDeX;
   M = M * TranslationSurX;
   //M = M * RotationAutourDeY;
   //M = M * RotationAutourDeZ;
   //3 - on le positionne à distance de la caméra
   M = M * EloignerObjetDeLaCamera;
   //4 - on lui applique l'effet de perspective
   M = M * EffetDePerspective;
   //5 - on l'affiche dans un espace délimité
   M = M * CadreVisible;
   //6 - affectation de la matrice finalisée m
   Matrix3DProjection M3DProjection = new Matrix3DProjection();
   M3DProjection.ProjectionMatrix = M;
   MonUIElement.Projection = M3DProjection;
}

Vous remarquez que les transformations à appliquer à un objet doivent être affectées avant que les matrices simulant la perspective soient apposées à ce dernier. Vous pourriez ainsi stocker dans une matrice unique le positionnement de l'objet, la perspective et les limites du cadre de visualisation. De cette manière, vous finiriez toujours par appliquer cette matrice en dernier lieu après avoir modifié l'objet projeté. Voici les définitions de méthodes générant les matrices de transformation 3D simples :

 
Sélectionnez
private Matrix3D TranslationTransform(double tx, double ty, double tz)
{
   Matrix3D m = new Matrix3D();
   m.M11 = 1.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
   m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
   m.OffsetX = tx; m.OffsetY = ty; m.OffsetZ = tz; m.M44 = 1.0;
   return m;
}
private Matrix3D ScaleTransform(double sx, double sy, double sz)
{
   Matrix3D m = new Matrix3D();
   m.M11 = sx; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
   m.M21 = 0.0; m.M22 = sy; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = 0.0; m.M32 = 0.0; m.M33 = sz; m.M34 = 0.0;
   m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
   return m;
}
private Matrix3D RotateYTransform(double theta)
{
   double sin = Math.Sin(theta);
   double cos = Math.Cos(theta);
   Matrix3D m = new Matrix3D();
   m.M11 = cos; m.M12 = 0.0; m.M13 = sin; m.M14 = 0.0;
   m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = -sin; m.M32 = 0.0; m.M33 = cos; m.M34 = 0.0;
   m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
   return m;
}
private Matrix3D RotateZTransform(double theta)
{
   double cos = Math.Cos(theta);
   double sin = Math.Sin(theta);
   Matrix3D m = new Matrix3D();
   m.M11 = cos; m.M12 = sin; m.M13 = 0.0; m.M14 = 0.0;
   m.M21 = -sin; m.M22 = cos; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
   m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
   return m;
}
private Matrix3D RotateXTransform(double theta)
{
   double cos = Math.Cos(theta);
   double sin = Math.Sin(theta);
   Matrix3D m = new Matrix3D();
   m.M11 = 1.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
   m.M21 = 0.0; m.M22 = cos; m.M23 = sin; m.M24 = 0.0;
   m.M31 = 0.0; m.M32 = -sin; m.M33 = cos; m.M34 = 0.0;
   m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
   return m;
}

Voici les méthodes qui gèrent les effets de perspective, ainsi que le positionnement de ce dernier au sein d'une fenêtre :

 
Sélectionnez
private Matrix3D PerspectiveTransformFovRH
   (double fieldOfViewY, double aspectRatio,
   double zNearPlane, double zFarPlane)
{
   double height = 1.0 / Math.Tan(fieldOfViewY / 2.0);
   double width = height / aspectRatio;
   double d = zNearPlane - zFarPlane;
   Matrix3D m = new Matrix3D();
   m.M11 = width; m.M12 = 0; m.M13 = 0; m.M14 = 0;
   m.M21 = 0; m.M22 = height; m.M23 = 0; m.M24 = 0;
   m.M31 = 0; m.M32 = 0; m.M33 = zFarPlane / d; m.M34 = -1;
   m.OffsetX = 0; m.OffsetY = 0;
   m.OffsetZ = zNearPlane * zFarPlane / d;
   m.M44 = 0;
   return m;
}
private Matrix3D ViewportTransform
   (double viewPortWidth, double viewPortHeight,
   double projectedObjectWidth,
   double projectedObjectHeight)
{
   Matrix3D m = new Matrix3D();
   m.M11 = viewPortWidth / 2.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
   m.M21 = 0.0; m.M22 = viewPortHeight / 2.0; m.M23 = 0.0; m.M24 = 0.0;
   m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
   m.OffsetX = projectedObjectWidth / 2.0;
   m.OffsetY = projectedObjectHeight / 2.0;
   m.OffsetZ = 0.0;
   m.M44 = 1.0;
   return m;
}

Dans tous les cas, nous ne faisons que simuler un environnement 3D à travers la déformation de l'objet lui-même. Les méthodes de perspective et de cadrage sont une combinaison de redimensionnements, d'inclinaisons et de translations de l'objet. Vous pouvez maintenant vous amuser à modifier la valeur de l'angle d'ouverture pour obtenir des effets de caméra intéressants (voir Figure 9.38).

Image non disponible
Figure 9.38 - Différentes longueurs de focale.

Le projet finalisé est dans l'archive des exemples du livre : chap9/Simple3DScene.zip. Vous pouvez améliorer le code proposé ci-dessus en créant, par exemple, des méthodes ou des propriétés de translations locales et globales équivalentes à celles fournies par la classe simple Plane-Projection. Vous pourriez également afficher les deux faces d'un objet de manière distincte et ainsi commencer à développer votre propre minimoteur de projection 3D pour Silverlight. Dans cette optique, je mets à disposition une bibliothèque gratuite nommée 3DLightEngine. Vous la trouverez sur mon blog : http://www.tweened.org. Son code est complètement ouvert à la modification et vous pouvez l'adapter selon vos besoins. Dans le prochain chapitre, nous aborderons le prototypage d'applications riches via la technologie SketchFlow. La 3D jouera sans aucun doute un rôle prépondérant en matière d'expérience utilisateur dans les prochaines années. Au Chapitre 9 Prototypage dynamique avec SketchFlow, nous utiliserons nos connaissances acquises en 3D pour ajouter une touche de profondeur aux différents panneaux de notre application, et améliorer ainsi son ergonomie.

9. Prototypage dynamique avec SketchFlow

Dans ce chapitre, vous découvrirez un nouvel outil de prototypage nommé SketchFlow. Bien qu'il soit intégré à Expression Blend, vous n'êtes pas obligé d'avoir lu la totalité des chapitres précédents ou d'avoir une connaissance approfondie de Blend pour utiliser SketchFlow. Sa facilité d'utilisation est l'un de ses grands avantages. Que vous soyez responsable de production, directeur technique ou artistique ou encore designer, ce chapitre vous concerne pleinement. Nous y aborderons le prototypage et nous créerons un premier prototype grâce à SketchFlow. L'un des objectifs de SketchFlow est de faciliter la communication entre les différents acteurs du développement afin d'arriver à un consensus. Dans cette optique, nous listerons les moyens mis à disposition par SketchFlow pour œuvrer en ce sens. Pour finir, nous étudierons les outils facilitant l'interactivité utilisateur, ainsi que les différentes approches existantes pour la conception d'interfaces utilisateur.

9-1. L'environnement

Si SketchFlow est un outil de prototypage, la notion même de prototype revêt des formes nuancées ainsi qu'une mise en pratique différente selon les environnements de production rencontrés. Dans cette section, nous allons définir ce terme et étudier l'intégration de SketchFlow au sein du flux de production. Pour finir, nous listerons les principes et moyens mis en œuvre par ce dernier lui permettant de se positionner comme outil de prototypage dynamique.

9-1-1. Le prototypage

Le prototype, même s'il n'a pas toujours été formalisé comme tel, est sans doute l'une des notions les plus vieilles de l'histoire de l'humanité. Il y a toujours une première fois. La première massue, le premier vélo, le premier avion et même la première souris (voir Figure 10.1) ont tous été inventés et rapidement mis à l'épreuve un beau jour. Même s'ils n'ont pas été dès le départ de francs succès techniques ou populaires, ils sont devenus des objets indispensables que l'homme n'a jamais cessé d'améliorer par la suite.

Image non disponible
Figure 10.1 - L'une des premières souris, créée en 1967 (source Wikipédia)

En industrie, un prototype est le premier ou l'un des premiers exemplaires fonctionnels d'un produit industriel (que l'on peut produire en grande quantité de manière rentable). Aujourd'hui, le prototypage est très étroitement lié au design. Un objet "design" peut se définir par trois termes : industrialisable, esthétique et ergonomique. Le but des écoles de design est concentré exclusivement autour de la création de prototypes industriels qui doivent réunir ces trois qualités. Si aujourd'hui ce concept est totalement intégré à l'industrie, cela n'est pas forcément de mise concernant le développement informatique. L'industrialisation du développement informatique n'en est aujourd'hui qu'à ses balbutiements, n'oublions pas que l'informatique est un domaine très jeune comparé aux industries traditionnelles. Il est de plus assez difficile d'appliquer les recettes de succès industriels dans la conception informatique. En premier lieu, une application est théoriquement unique. Son déploiement ou encore les processus de production peuvent être industrialisés, mais il ne sert à rien de reproduire la même application 250 fois. Lorsque l'on parle d'industrialisation en matière de développement informatique, on évoque plus généralement la standardisation des procédés et des étapes de développement en équipe. En second lieu, une agence Web, un studio de création ou une société de développement informatique (dans une moindre mesure) doivent produire un site ou une application interactive riche dans des délais parfois très courts. Cette phase est donc très souvent occultée ou alors complètement fusionnée à la phase de conception graphique. Ce n'est pas le cas de produits hardware industriels qui peuvent bénéficier de plusieurs années de production.

Il existe toutefois deux phases essentielles, communes au développement informatique et à l'industrie : une première phase de croquis et une seconde de prototypage, qui lancera ou non la mise en production. La moindre bouteille d'eau en plastique est le résultat d'un prototype, lui-même étant le résultat d'un schéma technique ou d'un croquis. Certains logiciels, tels que Catia de Dassault industrie ou Alias Design de la société Autodesk, permettent aux créatifs de s'exprimer tout en avançant les phases de prototypage et d'industrialisation. La souris Arc Mouse de Microsoft a suivi ces étapes avant de pouvoir être produite en masse et commercialisée (voir Figure 10.2).

Image non disponible
Figure 10.2 - Prototype de la souris Aro Mouse (source : site Microsoft Expression)

De nos jours, les phases de croquis et de prototypage sont très rapprochées. Elles sont parfois fondues en une seule grâce à l'outil informatique (cette pratique est encore souvent contestée). L'un des buts poursuivi par SketchFlow est de réduire le temps de conception global. Il entre donc précisément dans la catégorie d'outils liant ces deux phases, mais il permet également de se rapprocher de l'application finale. Jusqu'à quel point le prototype doit se rapprocher ou être transformé en application est une question actuellement en débat. Il facilite ainsi la communication entre les créatifs et les ingénieurs. Quel que soit l'environnement, plusieurs objectifs sont atteints grâce au prototype :

  • il permet de valider les choix de recherche, d'approche et de conception ;
  • il valide le bon fonctionnement, l'ergonomie et peut mettre en valeur d'éventuelles intentions de couleurs;
  • il facilite la correction des erreurs de conception.

Tous ces objectifs sont réalisés à travers la communication et l'échange d'idées entre les différentes parties impliquées. Le prototype permet donc d'atteindre un consensus nécessaire à la mise en production. Il faut parfois beaucoup de prototypes avant d'arriver à la version industrialisable d'un objet.

Quelles que soient les parties impliquées dans sa création, financière, technique, artistique, etc. Le prototype permet à chacun d'échanger et de communiquer ses impressions, et c'est là sa grande force. Nous allons maintenant voir comment SketchFlow reprend cette philosophie et engendre plus de productivité et des développements de meilleure qualité.

9-1-2. Qu'est-ce que SketchFlow ?

SketchFlow n'est pas un, il est en réalité constitué d'un ensemble d'éléments complémentaires qui ont été introduits dans la version 3 d'Expression Blend. Il est tout d'abord représenté par un type de projet spécifique généré via Expression Blend. Visual Studio n'est pas l'outil de prédilection pour prototyper. Cela est logique : la notion de prototype est inhérente à celle de design et Blend est avant tout un outil de designer. De plus, l'objectif est de mettre un outil de prototypage à disposition des profils créatifs et techniques. Le logiciel Visual Studio n'est pas approprié, car il demande beaucoup d'expérience et fait avant tout appel à des compétences de codeur. Son interface en termes de design est limitée, comparée à celle de Blend.

SketchFlow se veut simple et accessible afin de fédérer les acteurs d'une production autour d'un outil accessible à tous. Vous pouvez également le percevoir comme un enjeu social. Au sein de ce livre, nous avons souvent évoqué le flux de production créatif et technique. SketchFlow est l'un des outils permettant de rassembler les deux mondes trop longtemps séparés dans l'histoire du développement informatique.

Les projets SketchFlow permettent d'accéder à un ensemble de panneaux dédiés. Ils sont inaccessibles au sein de l'interface de Blend dans le cadre de projets standard. Nous verrons leur utilisation tout au long de ce chapitre. Vous pourrez générer des prototypes basés sur SketchFlow aussi bien pour WPF que pour Silverlight (voir Figure 10.3). De ce point de vue, il n'y a pas de différence entre les fonctionnalités proposées sur l'une ou l'autre de ces plates-formes.

Image non disponible
Figure 10.3 - Les projets SketchFlow sous Expression Blend pour Silverlight et WPF

Le prototypage via SketchFlow consiste avant tout à concevoir la navigation, l'ergonomie, les transitions et l'expérience utilisateur de manière globale. Les projets SketchFlow contiennent par défaut le style Sketch (croquis) pour les contrôles utilisateur. Ainsi, l'on s'affranchit totalement du design de la charte graphique, qui vient après ces étapes. Cela peut paraître inutile, puisque les composants par défaut connaissent les mêmes fonctionnalités. Ce style est pourtant l'un des éléments indispensables constituant SketchFlow. Il permet aux non initiés d'Expression Blend de prendre en main rapidement un projet avec une utilisation minimale du panneau des propriétés.

L'objectif est également de placer l'utilisateur dans une réflexion sur l'ergonomie, l'expérience utilisateur, en mode croquis rapide. L'erreur serait de se focaliser sur la production graphique directe, il vaut mieux rendre cette phase abstraite et moins importante dans un premier temps. Vous trouverez souvent plusieurs styles Sketch pour un même composant. Le contrôle TextBlock existe en version aligné à droite, à gauche, titre ou bloc de texte, etc. Ainsi, les adeptes de Blend ne sont plus les seuls à pouvoir mettre en forme une application. Vous pouvez accéder aux styles via le panneau Assets (voir Figure 10.4).

Encore une fois, le but est de permettre au plus grand nombre d'accéder à la création de prototypes. Le code logique est donc, par défaut à éviter, car seuls les développeurs ou les designers interactifs confirmés peuvent le créer ou le modifier. Un jeu de comportements interactifs spécifiques (Behaviors) est fourni à cette fin (voir Figure 10.5). Ils sont accessibles via le panneau Assets et sont utilisables de manière simplifiée grâce à de nouveaux menus contextuels. Ceux-ci permettent de les utiliser sans avoir à les déposer sur un contrôle et à les configurer. La création des prototypes en est grandement accélérée.

Image non disponible
Figure 10.4 - Le panneau Assets sous Blend donnant, entre autres, accès aux styles
Image non disponible
Figure 10.5 - Les comportements spécifiques à Sketchflow

Pour finir, nous avons évoqué depuis le début la collaboration intermétiers, mais cela ne serait pas possible sans un outil centralisant et permettant à chacun de fournir ses propres impressions. C'est le rôle du lecteur SketchFlow. Lorsque vous testez un projet dans le navigateur, le prototype est affiché au sein de ce lecteur (voir Figure 10.6). Ce dernier donne accès à un certain nombre de fonctionnalités aux utilisateurs de l'application, quels qu'ils soient. Nous étudierons son utilisation à la section 10.3 Les polices de caractères.

Image non disponible
Figure 10.6 - Le lecteur Sketchflow visible lors de la première compilation

Vous pouvez finalement remarquer que la compilation ne donne pas accès à l'application finalisée. Produire une application finalisée et optimisée n'est évidemment pas le but de SketchFlow. Vous pourrez toutefois détacher le cœur de votre application grâce aux compétences de développeurs et de designers interactifs, mais ce n'est pas toujours conseillé. Plus l'application que vous développerez sera complexe et moins cette manière de procéder sera viable.

9-1-3. Le flux de production

SketchFlow est utilisable à deux niveaux. Dans un premier temps, les projets sont créés par les acteurs directs de la production, qu'ils soient concepteurs techniques ou artistiques.

  • Comme nous l'avons évoqué, prototype et design sont liés. SketchFlow s'adresse donc avant tout aux designers dans un sens large. Les designers d'expérience utilisateur (UX Designers) sont la cible la plus évidente. Ces derniers ont à charge de proposer des interfaces intuitives et ergonomiques sans pour autant s'attarder sur le graphisme proprement dit. En second lieu, viennent les designers interactifs qui possèdent une connaissance de Blend et occupent un rôle central dans le flux de production. Ils sont à même d'établir une communication entre les profils techniques et artistiques. Si vous êtes graphiste ou directeur artistique, la prise en main technique simplifiée de SketchFlow a été conçue afin de vous donner un maximum de confort et de productivité.
  • Les développeurs d'applications clientes Winforms ou WPF et de RIA pour Silverlight utiliseront rapidement SketchFlow dans leurs futurs projets afin de concevoir les arborescences et écrans de leurs applications. Ils pourront de cette manière acquérir de nouvelles compétences, de prime abord inhérentes aux designers, tout au long des projets.
  • Les architectes d'applications ou d'informations, dont le rôle est de proposer des structures organisées et performantes, peuvent également détourner SketchFlow afin de créer des arborescences complexes, faciles à maintenir et à partager.

Sur un tout autre plan, SketchFlow est utilisable par les responsables.

  • Les responsables de projets, les directeurs artistiques, les responsables de production peuvent participer pleinement au projet par le biais de leur feedback. Pour cela, ils peuvent utiliser le lecteur SketchFlow qui permet de communiquer sur le prototype durant sa création.
  • Le client fait partie du processus de création. Avec SketchFlow, il devient facile de trouver un consensus, de proposer plusieurs maquettes fonctionnelles ainsi qu'une direction avant de se lancer dans la production pure et dure.

Vous l'aurez compris, SketchFlow est un outil de prototypage dynamique et transverse. Dynamique, du fait de la souplesse et de l'efficacité qu'il propose pour passer d'une idée à une autre et donner un aperçu en temps réel des décisions prises. Transverse, car il est facile d'accès et qu'il favorise la communication intermétiers au sein d'une production. En bref, le prototype est fait pour être testé et approuvé, SketchFlow est donc l'affaire de tous. Attention toutefois à un axiome bien connu des designers : le cœur d'une idée est émis par une personne ou deux et elle peut-être étoffée, mais il faut une direction à tous projets. Quand une application doit plaire à de nombreuses parties et que ces dernières sont décideuses, le risque de perdre la force de l'idée originale est élevé. Il faudra souvent soumettre plusieurs prototypes avant de trouver le juste milieu, celui-ci pourrait toutefois ne pas être si enthousiasmant que cela.

Pour finir, SketchFlow impose d'abord une méthodologie efficace dans le flux de production. C'est un lien direct avec le client. La relation doit être gagnant / gagnant entre prestataire de services et client. Il faut commencer par proposer des maquettes fonctionnelles et ainsi faire vivre le projet. Ce qui avant était figé est maintenant évolutif.

Auparavant plusieurs storyboards étaient dessinés et représentaient le scénario interactif de l'utilisateur. Le client en choisissait un ou deux, émettait des commentaires et on se lançait dans une sorte de "proof of concept" qui prenait non seulement du temps et de l'argent, mais qui en plus n'était peut-être pas validée en fin de parcours. Aujourd'hui, à travers SketchFlow, nous nous appuyons sur les croquis et storyboards pour créer un prototype dont le scénario, la mise en forme et l'interactivité peuvent évoluer dans le temps en un minimum de temps et d'efforts. Le prototype fourni fait en quelque sorte figure de cahier de recettes et de contrat autour duquel clients et prestataires peuvent s'accorder.

9-2. Prototype simple

Nous allons maintenant créer un premier prototype simple que nous étofferons au fur et à mesure des notions abordées. Pour réaliser les exercices qui vont suivre, décompressez les fichiers : chap10/Assets.zip.

9-2-1. Problématique cliente

Dans tous projets, il faudra proposer des croquis de différentes maquettes répondant à la problématique du client. Ce dernier peut même vous fournir lui-même des croquis des différentes pages de l'application. Nous allons prendre l'exemple d'un concessionnaire de véhicule automobile. Celui-ci souhaite un site vitrine afin de mettre en valeur son savoir-faire et la qualité des voitures vendues. Faisant partie d'un secteur très concurrentiel, il souhaite également se démarquer, en proposant aux visiteurs du site la possibilité de choisir la voiture de leur rêve à travers l'utilisation d'un configurateur riche. Nous détaillerons ce terme à la section 10.5.2 Un Slider personnalisé. Le configurateur riche doit être accessible au sein du site sous forme d'applications Silverlight. Afin de créer un premier prototype, le client fournit une première ébauche de maquette sous forme de cinq croquis simples que nous avons numérisés, puis retouchés sous Photoshop. Nous avons ajouté un code couleur pour chaque page et menu afin de reconnaître facilement les pages du futur site. Ces croquis représentent l'ensemble des pages du site qu'il souhaite mettre en ligne (voir Figure 10.7).

La première page représente la page d'accueil du site donnant accès à toutes les autres pages. La deuxième liste les critères certifiant la qualité écologique des véhicules. La troisième décrit les prestations diverses et le service après-vente. La quatrième page met en avant l'innovation technologique et l'ergonomie d'utilisation des véhicules les plus récents, grâce à une galerie d'images et à un lecteur vidéo. La cinquième page contient le configurateur riche, celui-ci permettra au visiteur de trouver le véhicule idéal en fonction de critères. Nous allons utiliser SketchFlow pour lui proposer une maquette fonctionnelle du site global, puis nous mettrons l'accent sur le configurateur riche. De ce point de vue, SketchFlow peut vous permettre de prototyper n'importe quel type d'arborescence visuelle.

Image non disponible
Figure 10.7 -Les cinq pages de croquis retouchées sous Photoshop

Nous allons maintenant créer une première solution SketchFlow.

9-2-2. Le projet SketchFlow

Ouvrez Expression Blend et créez un prototype nommé CarReseller. Vous remarquez d'emblée l'apparition de nouveaux panneaux, dont nous verrons l'utilisation ultérieurement. Affichez le panneau Project s'il n'est pas visible, vous constatez que la solution est scindée en deux projets distincts (voir Figure 10.18).

Image non disponible
Figure 10.18 - Arborescence du projet SketchFlow CarReseller

Le premier projet correspond au lecteur SketchFlow lui-même, il porte le nom original que vous avez défini (CarReseller). Le second contient le prototype, c'est le même nom suffixé du mot Screens. C'est dans ce dernier que vous travaillerez. Vous remarquez plusieurs différences avec les applications standard. Le second prototype possède tout d'abord un répertoire Fonts contenant trois polices utilisées par le style Sketch (croquis). Ce style est un élément important des projets SketchFlow, il est fourni par le fichier SketchStyles.xaml présent à la racine du projet. Ce type de fichier est assez nouveau pour nous. Il s'agit en fait d'un dictionnaire de ressources qui permet d'externaliser et de partager certains types de ressources utiles à un ou plusieurs projets.

Il est facile de réutiliser le style Sketch au sein d'un projet standard. Il vous suffira d'ajouter le fichier SketchStyles.xaml, ainsi que les polices du répertoire Fonts à votre projet. Les styles définis dans le dictionnaire de ressources utilisent ces polices, c'est pourquoi il ne faut pas les oublier.

Comme nous l'avons déjà précisé, ce type de projet contient des comportements (Behaviors) spécifiques à SketchFlow. Attendez-vous donc à trouver des références à de nouvelles bibliothèques dynamiques dans le répertoire References du projet. Nous n'avons pas réellement besoin de décrire le rôle de chaque bibliothèque, mais celles-ci sont indispensables au bon fonctionnement du projet.

Le fichier Sketch.Flow est le dernier que nous évoquerons. Vous pouvez l'ouvrir sous l'application NotePad, par exemple. Il s'agit d'un fichier au format XML, qui décrit une partie du travail réalisé dans le prototype (écrans, transitions, etc.), ainsi que d'un ensemble de paramètres propres au projet, qu'il peut être utile de personnaliser (comme les couleurs d'écran par exemple). Vous pouvez modifier ce fichier de deux manières différentes, soit directement avec un éditeur de texte, soit en passant par le menu Project, puis en cliquant sur SketchFlow Project Settings… Une boîte de dialogue vous permettra de modifier quelques-unes des options présentes dans le fichier XML (voir Figure 10.9).

Image non disponible
Figure 10.9 - Options propres au projet SketchFlow en cours

Vous allez modifier certains des paramètres afin de travailler plus confortablement. Comme nous l'avons précisé, le site est créé avec un code à quatre couleurs. Elles vont nous être utiles pour générer une carte de navigation avec ce code couleur. Si vous préférez modifier le fichier, vous pouvez remplacer les quatre dernières couleurs définies au sein de la balise VisualTags par celles exposées ci-dessous :

 
Sélectionnez
<VisualTags>
   …
   <VisualTag>
      <Name>OurServices</Name>
      <Color>FFFF9711</Color>
   </VisualTag>
   <VisualTag>
      <Name>MyCar</Name>
      <Color>FF48FEFF</Color>
   </VisualTag>
   <VisualTag>
      <Name>Eco2Way</Name>
      <Color>FF2EF872</Color>
   </VisualTag>
   <VisualTag>
      <Name>Innovation</Name>
      <Color>FFF93CA5</Color>
   </VisualTag>
</VisualTags>

Sauvegardez le fichier, puis retournez dans Blend. Celui-ci a détecté la modification et vous propose de recharger le fichier Sketch.Flow. Dans la boîte de dialogue, cliquez sur OK ; si tout ce passe bien le fichier est rechargé proprement. Si les nœuds XML sont mal formatés, Blend vous proposera de recréer un nouveau fichier. Nous verrons le résultat de cette opération dans très peu de temps. Il reste encore un paramètre à régler avant de commencer notre prototype.

Chaque écran correspondra à un croquis fourni par le client, l'idéal serait que chaque nouvel écran que nous générerons possède, par défaut, des dimensions identiques à celles des croquis. Il existe plusieurs manières de procéder. Vous pouvez, par exemple, définir des dimensions d'écran par défaut pour l'ensemble des projets SketchFlow. Dans la barre du haut, choisissez Tools > Options… Dans la fenêtre qui s'affiche, sélectionnez l'onglet SketchFlow. Une série d'options propres à ce type de projets est proposée. Cochez l'option Default size for new screens, puis définissez une largeur de 660 pixels et une hauteur de 517 pixels. Ces dimensions correspondent aux croquis réalisés par le client et retouchés par nos soins. Tous les écrans que nous générerons posséderont par défaut ces dimensions (voir Figure 10.10).

Image non disponible
Figure 10.10 - Options de mise en page propres à l'ensemble des projets Sketchflow

Cette méthode présente un désavantage : si vous travaillez avec de nombreux prototypes aux dimensions différentes, ce réglage n'est pas vraiment pertinent et vous devrez le changer régulièrement. Vous pouvez également utiliser la carte de navigation représentée par le panneau SketchFlow Map. Cliquez sur Screen 1, dans l'arbre visuel, sélectionnez le UserControl racine et affectez-lui une largeur (Width) de 660 pixels et une hauteur (Height) de 517 pixels. Faites ensuite un clic droit sur Screen 1 dans la carte, puis cliquez sur l'option Set As default Navigation Screen Size. Cette option modifie les valeurs de l'une des balises XML contenues dans le fichier Sketch.flow. Lorsque vous concevrez de nouveaux écrans, ceux-ci auront par défaut des dimensions correspondantes à l'écran d'origine. L'avantage de ce réglage est d'être lié aux propriétés du projet. Il est donc rechargé par défaut lorsque vous ouvrirez à nouveau le projet dans Blend.

9-2-3. La carte de navigation

Nous allons générer un premier prototype grâce au panneau SketchFlow Map. Cette fenêtre représente le cœur du travail de prototypage au sein de Blend (voir Figure 10.11).

Les prototypes sont constitués de différents écrans. Chacun d'eux représente une interface utilisateur. Ce panneau permet de gérer l'arborescence et l'enchaînement logique des interfaces utilisateur de manière globale. Celui-ci donne également accès à la gestion des transitions animées.

Image non disponible
Figure 10.11 - Le panneau Sketchflow Map affiche la carte de navigation du projet

En bas du panneau se trouve une série d'outils sous forme d'icônes et voici leur fonctionnalité de gauche à droite.

  • Vous bénéficiez tout d'abord d'un zoom équivalent à celui présent dans le panneau de création et fonctionnant de manière identique.
  • Les deux icônes représentant des flèches (Image non disponible) permettent d'annuler ou de refaire une action SketchFlow. Celles-ci sont particulièrement utiles, car SketchFlow génère un couple de fichiers XAML et C# pour chaque nouvel écran créé. L'annulation classique ne fonctionnera donc pas toujours puisque SketchFlow manipule une arborescence de fichiers. Si vous avez créé un écran par erreur, utilisez ces actions pour revenir sur vos pas.
  • Les troisième (Image non disponible) et quatrième icônes (Image non disponible) servent à ajouter un nouvel écran ou un nouveau composant d'écran. Nous reviendrons sur le principe des composants d'écran par la suite.
  • Le cinquième pictogramme (Image non disponible) efface un écran sélectionné au sein de la carte SketchFlow, quel que soit son type (composant d'écran ou écran). Lorsque vous effacez un écran, le couple de fichiers qui lui est associé n'est pas réellement supprimé du projet. Si vous souhaitez réellement supprimer ces derniers, il vous faudra utiliser le panneau Projects. À l'opposé, vous pouvez définir un couple XAML/C# en écran SketchFlow (voir Figure 10.12).
  • Les deux boutons suivants (Image non disponible) vous permettent de zoomer, soit sur la totalité de la carte, soit sur les écrans en cours de sélection. Ces deux derniers sont vraiment très pratiques pour accéder aux écrans que vous souhaitez modifier. Vous pouvez également maintenir la barre d'espace et le bouton gauche de la souris appuyés pour vous déplacer dans la carte. La molette de la souris est également prise en compte pour le zoom.
  • Vous pouvez diminuer l'opacité des liaisons de navigation ou de composants qui ne sont pas sélectionnées grâce aux deux dernières icônes : (Image non disponible). Cela permet d'y voir un peu plus clair lorsque votre carte commencera à s'étoffer.
Image non disponible
Figure 10.12 - Ajouter un couple XAML / C# comme écran Sketchflow

Tous ces outils vous permettent de créer une carte de navigation assez simplement. Blend propose toutefois d'autres options qui sont directement accessibles lors du survol d'un écran ou d'une liaison de la carte. Dans ce cas, un menu apparaît en dessous de l'écran survolé (voir Figure 10.13).

Image non disponible
Figure 10.13 - Menu déroulant affiché au survol de l'écran

Il existe deux manières de créer des écrans. La première consiste à utiliser l'icône appropriée dans le menu bas de la fenêtre. En procédant ainsi, l'écran généré n'est relié à aucun autre par défaut. La deuxième façon de procéder consiste à employer les icônes du menu déroulé au survol. Survolez l'écran nommé Screen1, maintenez le bouton gauche de la souris enfoncé sur la première icône, puis déplacez la souris pour générer un deuxième écran. Positionnez-le légèrement à l'extérieur. Relâchez le bouton gauche. Le nouvel écran est définitivement créé et celui-ci est relié à l'écran d'origine par une transition représentée par une courbe directionnelle (voir Figure 10.14).

Image non disponible
Figure 10.14 - transition directionnelle de screen1 vers Screen2

La courbe indique que l'utilisateur pourra naturellement passer de l'écran 1 (Screen1) à l'écran 2 (Screen2). La ligne représente également la transition animée qui aura lieu. Gardez toutefois à l'esprit que cette liaison ne représente pas la fonctionnalité en propre qui permettra de passer d'un écran à l'autre, mais simplement un lien de navigation logique et une transition. D'un seul coup d'œil sur une carte SketchFlow, vous devez être capable de traduire un scénario d'utilisation. Vous remarquez d'ailleurs que la courbe possède une direction, il est donc logique pour l'utilisateur de passer de Screen1 à Screen2, mais pas forcément de Screen2 à Screen1. Une animation de transition sera jouée dans le premier cas, mais pas dans le second. Créez une seconde liaison au survol de Screen2 en cliquant sur la seconde icône et ciblez Screen1 (voir Figure 10.15).

Image non disponible
Figure 10.15 - Transition bidirectionnelle entre Screen1 et Screen2

Screen1 possède une icône représentant une flèche verte, qui indique que Screen1 est l'écran de démarrage du prototype. Vous pouvez décider à tous moment de changer l'écran de démarrage via un clic droit sur celui de votre choix au sein de la carte. Un menu contextuel vous permet de redéfinir cet écran comme écran initial.

Pour définir une transition animée personnalisée, vous pouvez cliquer droit sur la courbe de transition et redéfinir l'option Transition Style. Par défaut, celle-ci est un fondu enchaîné assez efficace. Vous pouvez conserver le fondu actuel. Nous allons créer une première carte de navigation. Comme nous concevons un site Internet, nous partons du principe que l'utilisateur peut naviguer entre chaque page. Nous définirons donc une transition entre chaque écran, mise à part la page Home qui ne sert qu'à atteindre les autres. Ce n'est qu'une page d'accueil ne contenant aucune information importante. Elle pourrait contenir une vidéo ou une animation d'introduction plein écran. L'utilisateur, une fois sur le site, n'aura théoriquement pas besoin de revenir sur la page Home. Nous avons tout de même prévu un bouton à cet effet, dans le pied de page de notre site. Une transition ne serait pas forcément utile. Créez une carte de navigation (voir Figure 10.16). 

Image non disponible
Figure 10.16 - Ébauche de la carte de navigation

Afin de vous aider à mieux visualiser la carte, vous pouvez modifier la couleur de fond de chaque écran. Dans le menu déroulant au survol des écrans (la dernière icône de couleur) ou lors d'un clic droit, vous trouverez l'option Change Visual Tag. Comme nous avions modifié la palette de couleur définie au sein du fichier Sketch.Flow, vous avez à disposition un code couleur correspondant à chaque page du site.

Décompressez le fichier au format zip téléchargé précédemment, à l'emplacement de votre choix sur votre disque dur. Dans le répertoire Assets décompressé, vous trouverez le répertoire Flat-Screens contenant plusieurs images au format jpg. Au sein de Blend, créez un nouveau répertoire nommé Croquis dans le projet CarResellerScreens, puis un deuxième répertoire à l'intérieur nommé EcransSimples. Cliquez droit sur le répertoire EcransSimples et choisissez l'option Add Existing Item… Sélectionnez toutes les images contenues dans le répertoire FlatScreens décompressé, puis cliquez sur OK. Vous venez d'importer toutes les images du répertoire d'un seul coup (voir Figure 10.17).

Image non disponible
Figure 10.17 - Importation des croquis au format jpg

Le fichier nommé ItsTimeToChange.jpg correspond au visuel de la page Home. Les autres fichiers sont explicitement nommés. Dans la carte de navigation, double-cliquez sur la page Eco2-Way. La fenêtre de design affiche la page, celle-ci est vide par défaut. Elle possède exactement la même structure que les pages d'applications Silverlight standard. Dans le panneau Projects, double-cliquez sur Eco2Way.jpg. Vous venez à l'instant de placer l'image dans l'écran Eco2Way. Procédez de même avec tous les autres écrans. Veillez aux dimensions du UserControl racine, celui-ci doit posséder des dimensions de 660 pixels de large par 517 de hauteur. Vous venez de créer un premier prototype simple, Sauvegardez tous les fichiers modifiés via le menu File ou le raccourci Ctrl+Maj+S. Compilez ensuite le projet pour voir le prototype dans le lecteur SketchFlow. Vous découvrirez ses fonctions dans la prochaine section. L'exercice corrigé est dans : chap10/CarReseller_Simple.zip.

9-3. Le lecteur SketchFlow

Le lecteur SketchFlow donne à vos collègues et à toute personne impliquée dans le projet la capacité de communiquer et de commenter le prototype. Lorsque vous avez compilé le projet, il a été embarqué dans le lecteur. Le navigateur affiche donc celui-ci ainsi que le prototype qu'il intègre en son sein. Le lecteur lance par défaut l'écran de démarrage que vous avez défini dans Blend (voir Figure 10.18).

Image non disponible
Figure 10.18 - Chargement de la page de démarrage au sein du lecteur SketchFlow

Nous allons maintenant découvrir et utiliser les fonctionnalités du lecteur.

9-3-1. Navigation

La fenêtre de gauche nommée SketchFlow Player centralise toutes les fonctionnalités. Nous allons les décrire en partant du haut vers le bas. Elle donne tout d'abord accès à un mininavigateur : l'icône représentant une maison (Image non disponible) permet à tout instant de revenir à l'écran de lancement. Les flèches retour et avant permettent de revenir sur le cheminement que vous avez suivi.

Vous remarquez une barre d'adresse indiquant l'écran dans lequel vous vous situez. Il n'est pas possible d'entrer manuellement une adresse. Rafraîchir la page actuelle s'effectue par un clic gauche sur la dernière icône (Image non disponible). À chaque fois que vous naviguez dans un écran, l'onglet Navigate met à jour la liste des pages disponibles à partir de celle en cours. Cette liste est basée sur les transitions définies dans la carte sous Blend. Il est ainsi impossible d'utiliser cet onglet pour atteindre un écran qui ne serait pas lié par une transition. La direction de la transition est prise en compte pour son affichage.

La page Home possède une transition vers l'ensemble des autres écrans de l'application, ceux-ci sont donc tous présents dans la liste. Dans la liste, cliquez sur l'écran Eco2Way. La transition de fondu enchaîné est jouée, la liste est mise à jour, mais la page Home n'y figure pas. C'est tout à fait logique puisqu'aucune transition ne cible cet écran.

Pour finir, vous bénéficiez d'une réglette vous permettant de zoomer l'affichage du prototype en cours. Si vous avez un grand écran, cela devient vite indispensable, car les croquis sont souvent sommaires et donc en faible résolution ; les agrandir est dans ce cas très pratique. Nous étudierons le panneau FEEDBACK à la section 10.3.2 Polices personnalisées. L'onglet MAP autorise une navigation libre de toute transition (voir Figure 10.19).

Image non disponible
Figure 10.19 - L'onglet MAP au sein du lecteur

Lorsque vous double-cliquez sur l'un des écrans, il s'affiche à droite. Si une transition est définie entre l'écran initial et celui d'arrivée, elle est jouée. Dans le cas contraire, la page est simplement affichée de manière brute. Vous avez également la possibilité de zoomer dans la carte de navigation via la réglette située au-dessus. Vous aurez toutefois quelques difficultés à zoomer sur une zone lorsque les cartes de navigation seront complexes, car l'onglet MAP n'est pas redimensionnable. Pour faciliter la navigation, la carte peut également être affichée en surimpression de l'application prototypée (voir Figure 10.20). Il suffit pour cela de cliquer sur l'icône d'agrandissement (Image non disponible).

Image non disponible
Figure 10.20 - La carte de navigation en surimpression

Nous venons de passer en revue les fonctionnalités de navigation. Vous remarquez qu'il n'est nul besoin pour l'instant de créer de réelles interactions utilisateur pour passer d'un écran à l'autre. Cela est très pratique et permet de se concentrer sur l'enchaînement des écrans. Nous aborderons les interactions utilisateur à la section 10.4 Styles et modèles de composants.

9-3-2. Collaboration transverse avec SketchFlow

L'un des enjeux les plus importants de ces dernières années est certainement la communication et la collaboration intermétiers. C'est une problématique qui a toujours existé. Celle-ci passe aujourd'hui au premier plan du fait de la nécessité grandissante d'échanger et de partager les idées et contraintes de production entre créatifs et techniques. Si le XAML est l'un des moyens techniques permettant cette communication, SketchFlow en est bien l'héritier et l'un des plus puissants leviers œuvrant en ce sens. Les onglets Feedback présents à la fois dans le lecteur et dans Blend permettent à l'ensemble des acteurs de communiquer autour du prototype. Même si ces derniers ne fourmillent pas de milliers d'options, celles qui y sont présentes ajoutent une dimension supplémentaire et engendrent ainsi de nouvelles manières de produire.

9-3-2-1. Annotation versus retour utilisateur

Dans tout projet, vous trouverez quatre types de feedback. Le premier vient toujours du client, puisque vous répondez avant tout à une problématique client et sans ce dernier, pas de projet. Ses critiques sont donc prépondérantes. Le deuxième est fourni par les chefs de projet, le directeur technique ou artistique, les managers, qui ont une vision globale de la situation. L'utilisateur final a aussi son mot à dire. Il n'y a pas pire qu'une application qui n'est pas avant tout pensée pour l'utilisateur final et testée par ce type de public. Le diable est dans les détails, l'utilisateur final est souvent le critique le plus difficile sur un moyen terme. Pour finir, vous pouvez émettre des commentaires en tant que concepteur du prototype, que vous soyez développeur ou designer.

Les communications s'effectuent dans deux sens différents. D'une part, les ressentis utilisateur peuvent être adressés aux concepteurs, d'autre part les concepteurs peuvent faire remonter des informations et des remarques, ou encore exposer des problématiques imprévues (voir Figure 10.21).

Image non disponible
Figure 10.21 - Principe d 'échanges de feedback utilisateur et d'annonciation concepteur

Les trois premiers types de feedback concernent les acteurs qui émettront des commentaires d'un point de vue utilisation. Ils navigueront dans le prototype via le lecteur SketchFlow et pourront partager leurs ressentis sous forme de fichiers XML. Ces fichiers sont importés et affichables directement au sein de Blend. À un autre niveau, les concepteurs du prototype peuvent créer des annotations au sein d'Expression Blend qui seront ensuite lisibles par le lecteur SketchFlow, lors de la navigation test.

Nous allons endosser le rôle du concepteur et créer des annotations. Au sein de Blend, sélectionnez l'écran MyCar. Vérifiez ensuite que l'affichage des annotations est correctement activé. L'icône de la bulle d'information (Image non disponible) en bas de la fenêtre de création vous permet d'activer ou de désactiver l'affichage des annotations. Lorsque vous annotez un projet, vous utilisez un identifiant sous forme d'initiales. C'est exactement le même principe que sous Word ou Excel. Pour paramétrer l'identifiant, vous devez ouvrir le menu Tools (outils), sélectionner Options… puis choisir l'onglet Annotations (voir Figure 10.22).

Image non disponible
Figure 10.22 - Paramétrage de l'onglet Annotations

Pour créer un nouveau commentaire, utilisez le raccourci Ctrl+Maj+T. Vous pouvez également ouvrir le menu Tools, puis cliquer sur Create Annotation (voir Figure 10.23).

Image non disponible
Figure 10.23 - Une annotation dans l'écran MyCar

Compilez la solution au sein du lecteur SketchFlow, sélectionnez la page MyCar, puis cliquez sur l'onglet FEEDBACK. Vous y trouverez une icône permettant l'affichage des annotations (Image non disponible). Lorsque vous la survolez, les annotations, s'il y en a, sont affichées temporairement. Cliquez-les pour activer leur affichage tout au long de la navigation (voir Figure 10.24).

Image non disponible
Figure 10.24 - Afficher les annotations

Comme vous le constatez, les annotations sont assez simples à créer et à consulter. Laissez le navigateur ouvert, nous allons utiliser le lecteur SketchFlow afin de créer quelques notes du point de vue responsable de production, client ou utilisateur final. Il existe deux manières de générer un commentaire dans le lecteur. Soit vous créez une ou plusieurs notes associées pour chaque page, soit vous dessinez directement sur la page via les outils proposés par SketchFlow. Le panneau FEEDBACK contient à cet effet trois outils : un stylo et un surligneur dont vous pouvez régler l'épaisseur et la couleur ainsi qu'un correcteur. Pour ajouter une note de page, il suffit de cliquer sur le texte grisé Type your feedback here (voir Figure 10.25).

Il suffit maintenant d'exporter les croquis et commentaires sous forme d'un fichier .feedback au format XML. Cliquez sur l'icône représentant un répertoire. Deux options s'offrent à vous : vous pouvez supprimer tous vos commentaires par une réinitialisation ou les exporter. Choisissez l'option Export Feedback… Vous devez fournir votre nom ainsi que vos initiales. Nous nous mettrons dans la peau de notre chef des projets spéciaux, Nicolas. Il décide d'exporter ses commentaires pour nous les envoyer ensuite par e-mail, il crée donc un fichier nommé Nicolas.feedback (voir Figure 10.26).

Image non disponible
Figure 10.25 - Ajouter un feedback utilisateur
Image non disponible
Figure 10.26 - un exemple de fichiers de feedback

Le contenu de ce fichier est récupérable à tout instant. Pour cela, vous devez cliquer sur l'icône d'ajout depuis le panneau Feedback. Puis, sélectionnez le fichier .feedback. Vous pouvez importer plusieurs fichiers de ce type, mais n'en visionner qu'un seul à la fois (voir Figure 10.27). Ces fichiers sont automatiquement recopiés dans votre projet dans le répertoire Feedback Files. Si ceux-ci ont été générés alors que vous avez modifié le projet entre temps, Blend vous signale qu'ils peuvent être obsolètes. Dès cet instant, il vous appartiendra de déterminer la fiabilité de ces remarques. Lorsque des annotations ou des remontées utilisateur existent dans un écran, des icônes indiquent leur présence au-dessus de cet écran dans la carte de navigation (voir Figure 10.27). Vous pouvez activer ou désactiver l'affichage des commentaires utilisateur en cliquant sur l'icône en haut à droite du panneau Feedback (Image non disponible).

Vous connaissez maintenant les outils facilitant la collaboration et le travail de prototypage en équipe. Il reste toutefois quelques zones d'ombres concernant l'accès au prototype lui-même.

Image non disponible
Figure 10.27 - Visualiser les remontées utilisateur dans Expression Blend
9-3-2-2. Partager un projet SketchFlow

Notre problématique à ce stade est assez simple : nous pouvons créer des retours utilisateur parce que nous sommes détenteur du projet Blend, nous pouvons compiler le projet à tout instant et avoir accès au lecteur ainsi qu'à la sauvegarde des commentaires. Toutefois, le partage des retours ne concerne pas réellement le concepteur, mais plutôt les directeurs de production, le client et l'utilisateur final. Il vous faut donc partager le projet SketchFlow sur un serveur Web ou un espace disque accessible. Blend vous fournit une manière simple de gérer ce type de problématique et crée pour vous le site Web complet contenant le prototype et le lecteur SketchFlow qui l'embarque. Il vous suffit simplement d'ouvrir le menu File, puis de cliquer sur l'option Package SketchFlow Project… (voir Figure 10.28).

Choisissez le dossier qui recevra le projet compilé. Il vous suffira ensuite de télécharger ces fichiers sur le serveur Web de votre choix via une connexion FTP ou SFTP. Vous n'avez rien d'autre à faire qu'attendre la réception des fichiers feedback par e-mail ou par tout autre moyen. Vous avez également la possibilité de créer un fichier au format Word contenant une description complète du prototype. Choisissez alors le menu Export to Microsoft Word… Vous obtiendrez un document comme montré à la Figure 10.29.

Image non disponible
Figure 10.28 - Mettre en forme un projet SketchFlow pour le partage
Image non disponible
Figure 10.29 - Résultat d'une exportation au format Word

Ce type de document est surtout avantageux lorsque les diverses parties se sont accordées sur un prototype. Vous pouvez le considérer comme un cahier des charges macroscopique que vous pouvez à tout instant consulter et modifier. Il pourra même vous être utile pour rédiger un cahier de recettes validant l'application livrable.

Un cahier de recettes est souvent rédigé lorsque les projets atteignent une certaine dimension en tout début de production. Il concentre toutes les informations propres à l'application livrable et à ses fonctionnalités, dans sa version finale. Ainsi, lors de la remise du projet au client, celui-ci peut recenser l'ensemble des fonctionnalités initialement prévues dans le cahier de recettes afin d'éviter tout oubli. Le cahier des charges est quant à lui dédié aux équipes techniques, il découle des impératifs et du livrable. Son contenu est utile tout au long de la production et guide les équipes techniques.

Dans les prochaines sections, nous allons grandement améliorer notre prototype et lui donner vie. Vous n'êtes pas obligé de connaître entièrement le logiciel Blend pour créer des prototypes. C'est là l'une des grandes forces de SketchFlow.

Il est possible de convertir un projet SketchFlow en projet de production et ainsi de le détacher du lecteur Silverlight. Les étapes sont différentes selon le langage et la plate-forme utilisée, Silverlight ou WPF. Elles sont décrites dans la documentation accessible via l'interface d'Expression Blend.

9-4. Interactivité

Jusqu'à maintenant, l'utilisateur ne peut naviguer dans le prototype qu'à travers l'utilisation des fonctionnalités fournies par le lecteur SketchFlow. Cela est vraiment utile et évite de compliquer inutilement la conception du prototype dans la première phase de réflexion. Toutefois, lorsque vous aurez trouvé un consensus sur ses grandes lignes, il est possible de donner au client un aperçu de l'expérience utilisateur finale en lui appliquant une couche d'interactivité. Au sein de SketchFlow, l'interactivité utilisateur se traduit de deux manières différentes. Vous pouvez, comme dans toutes les autres applications, créer du code logique ou glisser des comportements interactifs sur les instances d'UI-Element. Leur nombre est plus important dans ce type de projet et ils sont plus simples d'utilisation. Le prototype acquiert ainsi plus de profondeur et concrétise une partie de la conception. D'une manière complètement différente, vous pouvez simuler des interactions utilisateurs via le panneau SketchFlow Animation. Cette fonctionnalité complètement nouvelle peut être appréhendée de différentes manières. Nous étudierons son fonctionnement à la section 10.4.3 Organiser et nommer les ressources.

9-4-1. Importer des fichiers PSD

Depuis sa version 3, Expression Blend rend possible l'importation de fichiers Photoshop (.psd). La répartition et la composition de visuels par le biais de calques confèrent aux fichiers .psd de nombreux avantages. Bien qu'il ne soit pas le seul dans ce cas, Photoshop est ainsi utilisé depuis des années dans le Web afin de maquetter de nombreux sites XHTML ou Flash. Désormais, les applications Silverlight peuvent être entièrement conçues graphiquement, puis être intégrées par les designers interactifs ou les développeurs dans un deuxième temps. Contrairement à ce principe, et à juste titre, Microsoft place la phase de croquis avant celle de la conception graphique pure et dure. L'utilisation de Photoshop de ce point de vue reste d'actualité. Il est très simple de scanner vos croquis, puis de les retoucher et de les organiser en calques dans un format .psd. Nous allons importer ce type de fichiers pour donner de la profondeur à notre application. Nous pourrons de cette manière séparer et gérer les éléments interactifs individuellement.

L'importation de fichiers Illustrator est également possible depuis la version 3 de Blend. Illustrator est avant tout utilisé pour concevoir le visuel finalisé, ainsi son emploi concerne moins le maquettage et le prototypage. Nous aborderons ce type d'importation au Chapitre 10 Ressources graphiques.

Supprimez tous les composants Image au sein des écrans de la carte de navigation ainsi que le dossier EcransSimples. Dans le répertoire Assets décompressé, vous trouverez le répertoire PSD contenant les mêmes écrans, mais au format Photoshop et organisés en plusieurs calques. Dans la carte de navigation SketchFlow, double-cliquez sur la page Home. Dans le panneau Project, sélectionnez le répertoire Croquis. Ouvrez le menu File, puis l'option Import Adobe Photoshop File... Accédez au répertoire PSD et choisissez le fichier ItsTimeToChange.psd. Une fenêtre d'importation est affichée au premier plan, elle vous permet de gérer l'importation de ce type de fichiers (voir Figure 10.30).

Image non disponible
Figure 10.30 - Fenêtre d'importation des fichiers Photoshop

La structure du fichier est entièrement affichée. Il faut veiller à cocher l'option Check all layers to import. Lorsque le contenu d'un calque est caché dans le fichier .psd, il ne sera importé que si cette option ou la case à cocher adjacente sont cochées. Validez l'importation du fichier. Comme vous avez sélectionné l'écran Home, le contenu du fichier est directement intégré à la page. Si l'agencement et les marges sont sauvegardés, la transparence subira parfois quelques modifications changeant légèrement le visuel importé. Cela n'a que peu d'importance pour nous, car nous travaillons sur un prototype. Lors de l'importation, chaque calque est transformé sous forme d'image au format png. Pour le vérifier, vous pouvez déplier le répertoire ItsTimeToChange_Images créé lors de l'importation. Vous y trouverez les images png du visuel final.

Pour obtenir un résultat visuel fidèle au document d'origine, il faut éviter toutes les spécificités propres au format psd. Rastérisez chaque calque, la plupart des effets ne sont pas conservés (seuls les projections d'ombre et le flou le sont). Il est également plus efficace de recréer les textes sous Blend ou de pixeliser ces derniers s'ils ne sont pas interactifs. Toutes les options de fusion propres aux calques Photoshop sont également à proscrire. Suivez cette logique et vous n'aurez pas de difficultés lors de l'importation.

Suivez les étapes décrites ci-dessus pour chaque écran du prototype. Veillez bien à sélectionner le dossier Croquis afin d'organiser proprement le projet (voir Figure 10.31).

Image non disponible
Figure 10.31 - Le répertoire généré ItsTimeToChange_Images

Dans l'arbre visuel de chaque écran, vous remarquez qu'un conteneur de type Canvas a été généré. Son nom correspond à celui de l'image importée. En son sein, plusieurs contrôles de type Image ont été créés et font référence à une image png. Ils représentent les calques d'origine et possèdent en conséquence un nom en correspondance avec chacun d'eux (voir Figure 10.32).

Comme vous le constatez, le nommage des calques et des objets est crucial. C'est un moyen simple et efficace pour faciliter la communication entre chaque pôle métier. Il n'y a qu'à lire pour comprendre l'utilité de chaque calque. Nous allons maintenant profiter du découpage des calques pour générer une interactivité utilisateur propre à chaque écran.

Image non disponible
Figure 10.32 - L'arbre visuel de l'écran Innovation

9-4-2. Navigation utilisateur

Afin de permettre à l'utilisateur de naviguer, il nous faut d'abord reconstruire le menu principal et le pied de page. Nous pouvons pour cela utiliser de simples instances de Button ayant le style Sketch et regroupées au sein d'un conteneur. Sélectionnez l'écran Home et supprimez l'instance d'Image correspondant au menu. Créez ensuite un conteneur StackPanel dans la grille principale et nommez-le Menu. Disposez-le de manière à l'aligner en bas à droite de la grille et définissez l'empilement de ses enfants en mode horizontal. Glissez quatre boutons à l'intérieur de manière à recréer le menu. Choisissez une couleur conforme au code couleur de chaque écran. Procédez de façon identique pour le pied de page, vous pouvez utiliser un dégradé sur le bord pour créer un effet correspondant au croquis original (voir Figure 10.33).

Image non disponible
Figure 10.33 - Le menu et le pied de page recréés avec des instances de Button et le style Sketch

Nous allons maintenant ajouter un peu de logique au visuel. Rien de plus simple au sein d'un projet SketchFlow : faites un clic droit sur le bouton Eco2Way_menu et sélectionnez l'option Navigate To. La liste des écrans accessibles apparaît, choisissez l'écran Eco2Way (voir Figure 10.34).

Répétez l'opération pour chaque bouton du menu. Ce que nous avons fait est non seulement traduit en XAML, mais consiste simplement à ajouter un comportement à chaque bouton sur lequel vous avez défini une navigation. Le comportement est donc accessible dans l'arbre visuel. Dans les projets SketchFlow, il n'est pas nécessaire de savoir ce fait pour utiliser le comportement. Une majorité d'interactions peut être réalisées par un simple clic droit. Si vous êtes designer interactif, cela peut toutefois être utile dans les cas complexes d'interaction.

Image non disponible
Figure 10,34 - Définir une navigation vers l'écran Boo2Way

Par défaut, l'accès aux écrans se déclenchera sur un clic de l'utilisateur, vous pourriez exécuter cette action lorsqu'un autre événement est diffusé. Une autre problématique se pose si vous souhaitez créer cette navigation pour le bouton Home_footer contenu dans le pied de page. Comme vous êtes sur la page Home, Blend ne vous propose pas de naviguer vers celle-ci. Si vous connaissez un peu les comportements, vous pouvez biaiser en choisissant un autre écran dans la liste afin de créer le comportement sur Home_footer. Vous n'avez plus qu'à modifier son paramètre Target-Screen (voir Figure 10.35).

Image non disponible
Figure 10,35 - Paramétrage du comportement affecté à Home_footer

La page Home est maintenant presque aboutie. Il est très facile de dupliquer les deux StackPanel dans les autres écrans. Faites un copier-coller de ces derniers dans l'écran Eco2Way, replacez le menu en haut de la page pour recouvrir l'image du menu déjà présente, vous n'avez ensuite qu'à supprimer cette dernière ainsi que l'image du pied de page dans l'arbre visuel. Les comportements sont automatiquement copiés avec. Vous venez de créer une navigation utilisateur en moins de cinq minutes.

9-4-3. Simuler un flux d'utilisation

Nous allons maintenant simuler les actions de l'utilisateur via le panneau SketchFlow Animation. Ce dernier n'existe que dans ce type de projet, il permet de créer des animations temporisées et détectées par le lecteur SketchFlow lors de la navigation utilisateur. La personne testant le prototype pourra ainsi avoir un aperçu de l'interactivité tout en évitant un code fastidieux. Chaque écran du prototype peut posséder son propre jeu de simulations utilisateur. Vous allez simuler le clic de l'utilisateur sur un bouton afin de déplier une zone de texte cachée par défaut. Dans la carte de navigation (SketchFlow Map), double-cliquez sur l'écran OurServices. Vous constatez que le panneau SketchFlow Animation est vide (voir Figure 10.36).

Image non disponible
Figure 10,36 - Panneau SketchFlow Animation vide

Il contient un premier écran nommé Base, qui indique l'état par défaut de l'écran. Vous devez modifier quelques propriétés avant de créer la simulation utilisateur. Sélectionnez, dans le Canvas nommé Service, les composants Image MoreOver et ActivateMore. Passez les valeurs de leur propriété Opacity à 0. Ajouter une animation SketchFlow ou créer des animations standard sont des actions similaires. Cliquez sur l'icône représentant le signe plus, puis nommez l'animation AccessMoreInformation. Il suffit pour cela de cliquer sur le nom affiché par défaut et de le modifier. Sélectionnez le premier état nouvellement généré à droite de l'état de base. Il est représenté par une vignette affichant le visuel de l'écran. Vous passez en mode enregistrement d'état visuel. Au sein de SketchFlow, ces états sont appelés Frame ou image-clé (à ne pas confondre avec clé d'animation). Cliquez sur MoreOver, passez la valeur de la propriété Opacity à 100, puis cliquez sur l'icône du caractère " + " situé en haut à droite de la vignette (voir Figure 10.37).

Image non disponible
Figure 10,37 - Création d'un nouvel état d'animation

Vous créez ainsi un nouvel état à partir de l'état modifié. Cliquez sur ActivateMore, passez la valeur de la propriété Opacity à 100. Vous remarquez que lorsque vous survolez un écran, une valeur exprimée en secondes apparaît. Il s'agit du temps de pause durant lequel cet écran est affiché lors de la lecture de l'animation globale. Entre chaque écran, vous pouvez également spécifier une durée ainsi qu'un type de transition. Vous pouvez activer définitivement l'affichage des tempos en cliquant sur l'icône de l'horloge.

Image non disponible
Figure 10,38 - Les durées de pause affichées pour chaque écran

Lorsque vous modifiez des propriétés dont les valeurs ne sont pas interpolables (donc différentes des valeurs de type Point, Double ou Color), vous pouvez activer le système d'agencement fluide qui assure une transition animée de ces propriétés (voir section 7.4 Propagation événementielle). Testez l'animation SketchFlow directement au sein de Blend via le bouton de lecture. N'hésitez pas à revoir les tempos, si besoin est, afin de simuler l'enchaînement des images-clés de manière réaliste. Compilez votre projet et naviguez jusqu'à l'écran OurServices pour accéder à l'animation du point de vue de l'utilisateur du prototype (voir Figure 10.39).

Image non disponible
Figure 10,39 - L'animation SketchFlow accessible

Cliquez sur le lien pour jouer l'animation que vous venez de définir. Ce type d'animation est en fait basé sur le gestionnaire d'états visuels étudié au Chapitre 5 L'arbre visuel et logique. Nous allons maintenant aborder son fonctionnement au sein des projets SketchFlow.

9-4-4. États visuels

Chaque écran peut se comparer à une page de notre application. Les différentes pages peuvent donc posséder leurs propres états visuels. Au sein de la carte de navigation, double-cliquez sur l'écran nommé Innovation. Il permet à l'internaute de visualiser les tous derniers modèles de voiture disponibles sous forme de galerie d'images ou via un simple lecteur vidéo. La galerie ou la vidéo seront affichées à tour de rôle. Pour ce faire, vous allez créer deux états visuels au sein de l'écran Innovation via le panneau States. Créez un groupe d'états nommé DisplayStates, puis deux états nommés respectivement Galery et Video. Par défaut la vidéo est affichée. Sélectionnez l'état Galery, puis passez l'opacité des objets videoSelected et PlayerVideo à 0. Passez ensuite à false la valeur de la propriété IsHitTestVisible de l'objet videoSelected. Dans cet état, l'objet ne reçoit plus les interactions en provenance de la souris. Revenez dans l'état de base puis faites un clic droit sur l'objet videoSelected, sélectionnez l'option Activate State puis Innovation / Galery (voir Figure 10.40).

Vous venez de créer un comportement interactif sur cet objet. Lors du clic de la souris, l'utilisateur affichera l'état Galery. Vous pouvez créer un rectangle transparent, sous l'objet videoSelected, ayant pour objectif d'afficher l'état Video. Spécifiez une durée de transition, pour le groupe d'états, qui n'excède pas une seconde. Lorsque vous testez le prototype, vous avez la possibilité d'accéder aux états visuels de chaque écran (voir Figure 10.41).

Si vous êtes graphiste et que vous n'êtes pas un familier de Blend, l'accès simplifié aux états visuels est une vraie valeur ajoutée en termes de temps. L'utilisation de comportements est l'une des clés de cette réussite, nous les emploierons à nouveau de manières différentes dans le chapitre suivant. Le prototype interactif est dans le dossier : chap10/CarReseller_Interactive.zip.

Image non disponible
Figure 10.40 - Activer un état visuel via un clic droit
Image non disponible
Figure 10,41 - Accès aux états visuels via le lecteur sketchFlow

9-5. Interface riche

Jusqu'à présent l'arborescence de notre prototype est exclusivement constituée d'écrans simples, privilégiant ainsi une approche de conception page par page, très proche des sites XHTML classiques nombreux sur Internet. Cette approche possède certains défauts que nous allons identifier.

9-5-1. Écran versus composant

Comme vous le constatez, l'ensemble des pages du prototype partage deux éléments identiques, le menu principal et le pied de page. Le fait de conserver un design équivalent pour ces deux éléments facilite grandement la navigation et l'interprétation des pages pour l'internaute. D'un point de vue technique, cela pose toutefois problème, car si vous souhaitez changer la disposition de ce menu, vous devrez le faire dans chaque page de votre projet. Ces deux éléments peuvent être considérés comme des instances de composants autonomes assurant leur propre logique. De cette manière, modifier le composant revient à changer toutes ses occurrences dans le projet. Au sein de SketchFlow, ce type de module est appelé composant d'écran. Il s'agit dans les faits d'un UserControl au même titre que notre application principale (voir Chapitre 11 Composants personnalisés). Pour créer ce type d'objet, vous pouvez soit utiliser la carte de navigation, soit sélectionner un (ou plusieurs) contrôle dans l'un des écrans existants, pour le transformer par la suite en composant d'écran. Nous allons utiliser cette méthodologie dans un premier temps, puis nous utiliserons le panneau SketchFlow Map pour finaliser notre approche. Dans l'écran Home, faites un clic droit sur le contrôle StackPanel nommé Menu, puis choisissez l'option Make Into Component Screen… Une boîte de dialogue apparaît vous demandant de nommer le composant d'écran (voir Figure 10.42).

Image non disponible
Figure 10,42 - Fenêtre Make Into Component Screen

Le nom MenuComponent proposé par défaut est éloquent, vous pouvez donc le conserver. Cliquez sur OK, la carte de navigation est automatiquement mise à jour. Vous remarquez qu'un nouveau type de connexion est généré. Celle-ci est en pointillés de couleur verte, apparence attribuée par défaut aux connexions de composants. Un couple de fichiers XAML C# est également créé, Blend ouvre par défaut le fichier XAML correspondant à l'arbre visuel de notre menu. Il est possible d'instancier ce composant en le reliant (MenuComponent) à chaque écran. De cette manière, lorsque vous modifierez le composant source, chacune de ses instances sera mise à jour au sein des écrans du projet. Pour créer de nouvelles connexions à partir du composant, il suffit d'utiliser la dernière icône apparaissant lors du survol du composant au sein de la carte de navigation (voir Figure 10.43).

Image non disponible
Figure 10,43 - Connexion de composants

Il est nécessaire de repositionner correctement le composant pour chaque écran, non seulement dans l'espace, mais également dans l'arbre visuel afin de remplacer parfaitement l'espace occupé par les menus déjà présents. Il ne vous reste plus qu'à supprimer chaque ancien menu StackPanel. Vous pouvez procéder de manière identique pour le pied de page. Si vous souhaitez avoir une meilleure visibilité de l'un des deux types de connexions (composant ou écran), vous pouvez diminuer la luminosité de l'un ou de l'autre. Les composants d'écran possédant par nature de nombreuses connexions, vous pourriez également éclaircir leur couleur afin de les mettre en valeur. Un vert lumineux est idéal dans ce cas. Utilisez l'option SketchFlow Project Settings… (menu Project) pour modifier la couleur actuelle (voir Figure 10.44).

Les composants d'écran sont en fait de simples instances de UserControl. Si d'un point de vue technique SketchFlow n'est pas une révolution et repose sur de solides bases existantes, la facilité de conception qu'il apporte est réellement novatrice et pertinente. Le modèle technique XAML / C#, proposé dès le départ par .Net 3, est un terreau propice à ce genre d'avancées. Maintenant que les fondements sont posés, vous pouvez vous attendre à l'éclosion de ce type d'innovations de manière régulière dans l'avenir. Nous allons maintenant utiliser les composants d'écran d'une manière différente afin de créer un prototype simple d'interface riche.

Image non disponible
Figure 10,44 - Connexions de composants mises en valeur

9-5-2. L'exemple du configurateur riche

La notion de configurateur riche est apparue dans le Web avec les technologies asynchrones comme AJAX. L'idée générale est de faciliter un processus à travers une interface enrichie. Cela peut aller de la réservation hôtelière à la configuration d'une cuisine comme le proposent certaines applications. Au sein de cette section, nous allons prototyper une interface permettant à un internaute de sélectionner et de configurer une voiture. L'objectif final est de faciliter la prise de rendez-vous pour un test de conduite.

9-5-2-1. Le flux d'utilisation

Comme nous l'avons précisé au début de cette section, la navigation page par page possède quelques défauts. Elle force notamment l'utilisateur à relire la totalité de chaque nouvelle page affichée. Cette contrainte peut évidemment être très utile si vous affichez un contenu radicalement différent des précédents. C'est toutefois rarement le cas et, dans90 % des situations, le flux d'utilisation est complètement cassé par ce que l'on appelle l'aveuglement d'informations causé par le rafraîchissement.

Lorsque vous utilisez une application, vous entrez parfois dans le flux, un état idéal d'utilisation dans lequel tout vous paraît logique et évident. Vous enchaînez ainsi rapidement et simplement les actions et accédez facilement aux informations sans vous poser de questions existentielles. Dans ce cas, votre état de concentration est optimal. L'aveuglement d'informations peut vous forcer à quitter cet état. C'est un peu comme si un animateur décidait de faire bouger tous les personnages d'un dessin animé en même temps avec exactement la même amplitude pour chacun d'eux. Le traitement des informations par l'œil est tellement important qu'il n'est pas possible de tout analyser en même temps. On pourrait également percevoir cela comme une forme de censure ou de bruit visuel, tous deux engendrés par un trop plein d'informations impossibles à traiter. Dès lors, vous quittez le flux d'utilisation afin de conserver une distance suffisante et garder une vision globale de la situation. Cette situation est finalement assez désagréable, car elle vous force à prendre du recul et à réfléchir là où tout devrait être simple et compréhensible.

Le flux d'utilisation peut être déstabilisé par d'autres facteurs comme une mauvaise compréhension ou formalisation de l'interface ou des ralentissements qui obligent à prendre du recul du point de vue utilisation. Finalement, vous pouvez très rapidement décourager l'internaute ou l'utilisateur de l'application, si vous n'y prenez pas garde.

L'une des solutions permettant la mise en place de flux d'utilisation consiste à permettre une navigation au sein d'une seule et même page. Vous ne rafraîchissez ainsi qu'une portion de la page et attirez l'attention de l'utilisateur au bon endroit et au bon moment. Les composants d'écran permettent de réaliser ce type d'expérience assez simplement, car vous ne rafraîchirez pas toute la page, mais juste une portion de celle-ci. L'utilisateur est beaucoup moins désorienté dans ce cas et peut même prévoir intuitivement un certain nombre de comportements de l'interface. Ce qui est prévisible le confortera dans son expérience et dans ses choix d'utilisation.

Nous allons maintenant illustrer ce principe à travers le prototypage d'un configurateur riche. Ce dernier est une petite application. Elle sera contenue dans l'écran MyCar et permettra à l'utilisateur de sélectionner un véhicule à partir d'une liste de critères. L'écran est constitué de trois panneaux représentant chacun une étape dans le processus d'utilisation (voir Figure 10.45).

Image non disponible
Figure 10,45 - Les trois panneaux du configurateur riche
9-5-2-2. Créer les composants

Afin de se concentrer sur notre problématique, dézippez le projet : chap10/CarReseller_Flux.zip. Au sein du projet, l'écran MyCar contient une arborescence qui a été générée à partir de dessins scannés et retouchés sous Photoshop et de composants ayant le style Sketch. Les écrans vus précédemment sont tous trois répartis au sein de grilles. Le premier permet de sélectionner les critères et affiche une liste mise à jour dynamiquement. Le deuxième écran donne un aperçu de la voiture et permet de personnaliser ses options. Le dernier écran est un formulaire renseigné par le client qui souhaite tester la voiture sélectionnée sur circuit ou sur route. Sélectionnez la grille Design,
puis faites-en un composant d'écran. Dans le composant nouvellement créé, définissez des dimensions en mode automatique pour le composant de type UserControl racine, ainsi que pour la grille nommée Design. Pour finir, simplifiez l'imbrication en dégroupant la grille LayoutRoot. Vous pouvez réaliser cela via un clic droit et sélectionner l'option Ungroup. Revenez dans l'écran MyCar et répétez l'opération pour les grilles Perso et test.

Dans l'écran MyCar, vous constatez la présence d'encadrés jaunes autour des composants nouvellement générés ainsi qu'un point d'exclamation en haut à gauche. Cela indique que vous devez compiler le projet pour voir le résultat visuel final de ces composants. Pour compiler sans tester le projet, utilisez le raccourci Ctrl+Maj+B. Lorsque la compilation est terminée, les bordures jaunes et le point d'exclamation disparaissent.

Vous obtenez trois composants d'écran liés à l'écran MyCar dans la carte de navigation, ainsi qu'un résultat visuel similaire à celui d'origine pour cet écran (voir Figure 10.46).

Image non disponible
Figure 10,46 - Les trois composants du configurateur riche et l'arbre visuel

Comme vous le constatez, les deux premiers composants sont partagés en deux zones distinctes. Lorsque l'un ou l'autre de ces composants perdra le focus utilisateur, seule l'une des deux parties de ce composant sera visible, l'autre sera simplement rétractée avec une petite animation de fondu. Lorsque l'un des panneaux recevra le focus, il sera mis au premier plan avec un déplacement sur l'axe 3D en z. Ce comportement permet à l'utilisateur de ne pas être submergé d'informations inutiles.

Pour créer ce type de transition, il faut créer des états correspondant au focus utilisateur pour chaque composant, ainsi que pour l'écran MyCar. Dans l'écran MyCar, inversez l'ordre des composants de manière à ce que le panneau DesignComponent soit au premier plan et que le composant TestComponent soit au dernier. Créez ensuite un nouveau groupe d'états visuels nommé Focused-Component, ainsi que trois états nommés en son sein, ayant pour noms respectifs, Design-Focus, PersoFocus et TestFocus. Définissez une transition globale pour le groupe d'états de 6/10e de seconde environ avec une courbe de décélération de votre choix - Cubic Out semble une bonne option (voir Figure 10.47).

Image non disponible
Figure 10,47 - Les états visuels de l'écran MyCar

Dans l'état DesignFocus, modifiez la profondeur sur l'axe 3D z global (GlobalOffsetZ) des composants PersoComponent et TestComponent avec des valeurs respectives de -100 et -200 pixels. Ceux-ci se trouvent à l'arrière-plan, automatiquement l'un derrière l'autre. Sélectionnez l'état PersoFocus, puis modifiez cette fois la position sur l'axe z global de DesignComponent et de TestComponent de -50 et de -200 pixels. Pour finir, dans l'état TestFocus, passez la position sur l'axe z global de DesignComponent à -200 et celle de PersoComponent à -100. Dans cet état, le panneau de TestComponent sera à l'avant plan, puis viendra le panneau PersoComponent au second plan et au dernier plan DesignComponent.

Lorsque l'un des panneaux possède le focus, il ne subit aucune projection 3D. Cela est assez positif pour votre visuel final, car lorsqu'un objet possède une projection, il est rendu sous forme de bitmap (voir Chapitre 8 Les bases de la projection 3D). Cela occasionne un lissage des pixels et peut engendrer des textes ou des vecteurs floutés. Les conséquences de ce rendu bitmap sont moins gênantes pour les panneaux situés à l'arrière-plan, car ils n'ont pas l'intérêt utilisateur. Vous pouvez également vous permettre de repositionner en x les panneaux afin de mettre celui qui a le focus utilisateur en valeur. Utilisez à cette fin les RenderTransform afin d'éviter de rastériser les vecteurs en bitmap. Le mélange des deux types de transformations est donc non seulement possible mais conseillé pour les panneaux ayant le focus qui ne sont pas affectés de projections 3D.

La problématique, à ce stade, est de toujours faire apparaître une portion des composants en arrière-
plan de celui qui possède le focus. Allouer les marges de manière précise n'est pas forcément aisé à cet instant, car vous devez anticiper le fait que les composants seront eux-mêmes dans des états visuels modifiant leur largeur. Vous pourrez également modifier l'agencement des composants à la suite d'une série de tests de l'application et procéder ainsi de manière empirique.

9-5-2-3. Transitions et états visuels de composants

Nous avons réalisé la première partie de notre prototype. Il nous reste à créer les états visuels inhérents à chaque composant d'écran. Dans la carte de navigation, double-cliquez sur l'écran DesignComponent et créez un groupe d'états visuels nommé FocusStates ainsi que des états, la transition et une durée globale (voir Figure 10.48).

Image non disponible
Figure 10,48 - Les états visuels de DesignComponent

Sélectionnez l'état List, puis passez la largeur (Width) ainsi que l'opacité de l'objet Options-Design à 0. Modifiez ensuite la largeur de l'objet ColorDesign à 150. Cliquez sur l'état Unfocus et définissez l'opacité et la largeur de l'objet CurrentSelectionDesign à 0. Passez ensuite la largeur de l'objet ColorDesign à 200. Dans la carte de navigation, double-cliquez sur le composant PersoComponent. Nous allons répéter les mêmes étapes. Créez deux états ayant les mêmes propriétés de transition, nommés respectivement Focus et Unfocus. Cliquez sur l'état Unfocus, puis sélectionnez l'objet nommé PersoRight. Passez la valeur de ses propriétés d'opacité et de largeur à 0. Définissez ensuite la largeur du composant Image ColorPerso à 220 pixels. Pour finir, nous allons modifier le composant TestComponent. Créez deux groupes d'états nommés FocusStates et FillStates, le premier nous permettra d'afficher le visuel en fonction du focus utilisateur, le second gérera le visuel selon que l'utilisateur aura rempli ou non le formulaire. Dans l'état de base, passez la propriété Visibility du composant Message à Collapsed. De cette manière, le texte de confirmation de l'envoi du formulaire n'est pas visible au chargement du configurateur. Créez les états Focus et Unfocus au sein du groupe d'état adéquat, puis les états Filled et NotFilled pour le groupe FillStates (voir Figure 10.49).

Image non disponible
Figure 10,49 - Les états visuels de TestComponent

L'objectif est maintenant de ne laisser afficher que le titre et l'arrière-plan pour l'état Unfocus. Sélectionnez les objets ContentTestDrive, Message et ColorTestDrive pour modifier leur opacité à 0. Fixez également la largeur de ContentTestDrive et de ColorTestDrive à 170 pixels. Afin de gérer les états visuels concernant la saisie du formulaire, sélectionnez l'état Filled, puis modifiez la valeur de la propriété Visibility à Collapsed pour ContentTestDrive ainsi que pour le bouton. À l'opposé, changez la valeur de cette propriété à Visible pour le composant Message.

Nous venons d'éviter un conflit de propriété entre les états visuels du groupe FillStates et Focus-States. En effet, nous n'aurions pas pu utiliser la propriété Opacity dans les deux groupes d'états sans engendrer de bogues à l'exécution (voir Chapitre 6 Boutons personnalisés). Il ne nous reste plus qu'à créer le code logique et les comportements interactifs nécessaires pour donner vie à l'interface. Le projet contenant tous les états visuels est accessible dans : chap10/CarReseller_Etats.zip.

9-5-2-4. Interactivité avancée

Vous trouverez un résumé de tous les états créés pour chaque écran ainsi que l'activation de ceux-ci en fonction de chaque étape d'utilisation au Tableau 10.1.

Ce type de récapitulatif est assez pratique pour concevoir des interfaces riches. C'est une sorte de feuille de route vous permettant de vous y retrouver. Par exemple, pour la première phase d'utilisation, l'écran MyCar affiche l'état DesignFocus, les composants DesignComponent, Perso-Component et TestComponent affichent respectivement les états Focus, Unfocus et Unfocus. Une fois le véhicule choisi dans la liste, l'écran MyCar active l'état PersoFocus, le panneau DesignComponent affiche l'état List, etc. Vous remarquez que pour la dernière étape, TestComponent est Focus, mais qu'il peut également être dans l'état Filled ou NotFilled. Celui-ci bénéficie en effet de deux groupes d'états visuels distincts et affiche donc deux états. Nous allons commencer par initialiser la première étape.

Tableau 10.1 - États visuels en fonction de l'étape

États visuels

Étape 1

Étape 2

Étape 3

Écran MyCar

DesignFocus

PersoFocus

TestFocus

Composant DesignComponent

Focus

List

Unfocus

Composant PersoComponent

Unfocus

Focus

Unfocus

Composant TestComponent

Unfocus

Unfocus

Focus / Filled /NotFilled

À cette fin, vous allez ajouter de la logique via C#. Vous pouvez vous référer au Tableau 10.1 pour vous faciliter ainsi la tâche. Ouvrez le fichier C# Screen_1_3.cs dans Blend ou Visual Studio et utilisez la classe statique VisualStateManager, comme montré ci-dessous :

 
Sélectionnez
public Screen_1_3()
{
   InitializeComponent();
   Loaded +=new System.Windows.RoutedEventHandler(Screen_1_3_Loaded);
}

private void Screen_1_3_Loaded(object sender, RoutedEventArgs e)
{
   VisualStateManager.GoToState(this,"DesignFocus",false);
   VisualStateManager.GoToState(designComponent,"Focus",false);
   VisualStateManager.GoToState(persoComponent,"Unfocus",false);
   VisualStateManager.GoToState(testComponent,"Unfocus",false);
}

Le troisième paramètre indique si une transition animée est utilisée. Dans notre cas, la transition n'a pas besoin d'être jouée, ce paramètre est donc à false. Au sein du composant d'écran Design-Component, cliquez droit sur le composant CurrentSelectionDesign, puis dans le menu Activate State, sélectionnez l'état List. L'utilisateur passe automatiquement dans la seconde étape d'utilisation lorsqu'il cliquera sur le composant. Si nous nous référons au Tableau 10.1, nous devrions également activer trois autres états : PersoFocus pour l'écran MyCar, Focus pour le composant PersoComponent et Unfocus pour le composant TestComponent. À ce stade, nous ne pouvons pas piloter ces états visuels de manière identique pour deux bonnes raisons. La première est que l'interface de Blend ne nous autorise pas à y accéder via la liste des états affichés dans le menu Activate State par le clic droit. La seconde est assez ennuyeuse : même si nous y avions accès, l'état ainsi sélectionné remplacerait celui que nous avons déjà spécifié dans l'arbre visuel. Autrement dit, vous ne pouvez pas spécifier plus d'un comportement de même type sur un composant en utilisant le menu contextuel par clic droit. En réalité, cela est possible, mais uniquement en XAML ou par glisser-déposer du comportement. Nous allons simplement ajouter ces comportements en modifiant le code XAML. Pour cela, passez en mode mixte afin que le code apparaisse, puis sélectionnez CurrentSelectionDesign. Voici la déclaration XAML du comportement :

 
Sélectionnez
<i:Interaction.Triggers>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.DesignComponent" TargetState="List"/>
   </i:EventTrigger>
</i:Interaction.Triggers>

Ajoutez une balise EventTrigger (déclencheur d'événements) au sein de la liste des déclencheurs et modifiez le code comme ci-dessous :

 
Sélectionnez
<i:Interaction.Triggers>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.DesignComponent" TargetState="List"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.Screen_1_3" TargetState="PersoFocus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen=" CarResellerScreens.PersoComponent" TargetState="Focus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.TestComponent" TargetState="Unfocus"/>
   </i:EventTrigger>
</i:Interaction.Triggers>

Testez le prototype dans le navigateur, lorsque vous cliquez sur la liste, trois transitions sont jouées simultanément. La première concerne le panneau design lui-même lorsqu'il passe dans l'état List. La deuxième est jouée au sein de l'écran MyCar et affiche l'état PersoFocus. La troisième affiche le composant PersoComponent en mode Focus. Il faudrait maintenant jouer la transition inverse lorsque l'utilisateur souhaite modifier ses critères de sélection. Cela consiste à réafficher l'état DesignFocus de l'écran MyCar lorsque l'utilisateur clique sur le titre du panneau TitleDesign dans DesignComponent. Comme le titre du panneau est situé en dessous de l'objet ColorDesign, le clic de l'utilisateur ne sera pas diffusé. Pour régler ce problème, il suffit de passer la propriété IsHitTestVisible de l'objet ColorDesign à false. De la même manière que précédemment, ajoutez les balises XAML comme montré ci-dessous :

 
Sélectionnez
<Image x:Name="TitleDesign" Height="44" HorizontalAlignment="Left" 
      Margin="6,3,0,0" VerticalAlignment="Top" Width="99" 
      Source="DesignPanel_Images/TitleDesign.png">
   <i:Interaction.Triggers>
      <i:EventTrigger EventName="MouseLeftButtonDown">
         <pb:ActivateStateAction TargetScreen="CarResellerScreens.DesignComponent" TargetState="Focus"/>
      </i:EventTrigger>
      <i:EventTrigger EventName="MouseLeftButtonDown">
         <pb:ActivateStateAction TargetScreen="CarResellerScreens.Screen_1_3" TargetState="DesignFocus"/>
      </i:EventTrigger>
      <i:EventTrigger EventName="MouseLeftButtonDown">
         <pb:ActivateStateAction TargetScreen="CarResellerScreens.PersoComponent" TargetState="Unfocus"/>
      </i:EventTrigger>
      <i:EventTrigger EventName="MouseLeftButtonDown">
         <pb:ActivateStateAction TargetScreen="CarResellerScreens.TestComponent" TargetState="Unfocus"/>
      </i:EventTrigger>
   </i:Interaction.Triggers>
</Image>

Vous avez la possibilité de consulter le résumé des états pour visualiser les transitions. Testez le prototype ; vous constatez que cette fois, vous pouvez passer d'un état à l'autre à tout moment en cliquant alternativement sur la liste et le titre.

Notre application met en valeur une erreur de conception classique. Lorsque l'utilisateur a sélectionné sa voiture dans la liste, le composant PersoComponent est mis au premier plan et seule la liste des voitures sélectionnables est visible. Si cette voiture ne lui correspond pas et qu'il souhaite changer ses critères, aucun indice visuel ne lui indique que le titre lui permet de réafficher la totalité du panneau DesignComponent. Il n'aura pas forcément l'idée de le cliquer. Le mieux, dans ce cas, est d'afficher une icône sous forme de flèche de retour afin que l'internaute puisse aisément revenir sur ses pas.

Si besoin est, n'hésitez pas à modifier les marges de chaque élément, pour obtenir un alignement des objets sans effets de bords. Une fois que l'utilisateur a personnalisé sa voiture (dans le deuxième panneau), il est temps pour lui de demander un rendez-vous pour la tester en condition réelle. Au sein du composant TestComponent, cliquez droit sur le composant ColorTestDrive, puis au sein du menu Activate State, sélectionnez l'état Focus. Lorsque l'utilisateur cliquera sur ce panneau, celui-ci se dépliera et affichera le formulaire. Vous devez également gérer les quatre autres transitions que nous avons déjà évoquées auparavant. Vous pouvez une fois de plus modifier le code XAML :

 
Sélectionnez
<i:Interaction.Triggers>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.Screen_1_3" TargetState="TestFocus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.TestComponent" TargetState="Focus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.DesignComponent" TargetState="Unfocus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.PersoComponent" TargetState="Unfocus"/>
   </i:EventTrigger>
</i:Interaction.Triggers>

Dans un premier temps, il vous suffit de gérer cette navigation en ajoutant les comportements nécessaires sur le composant PersoComponent, directement au sein de l'écran MyCar :

 
Sélectionnez
<i:Interaction.Triggers>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.Screen_1_3" TargetState="PersoFocus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.TestComponent" TargetState="Unfocus"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.DesignComponent" TargetState="List"/>
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonDown">
      <pb:ActivateStateAction TargetScreen="CarResellerScreens.PersoComponent" TargetState="Focus"/>
   </i:EventTrigger>
</i:Interaction.Triggers>

Dans la carte de navigation, double-cliquez sur le composant d'écran TestComponent. Vous devez encore activer l'état Filled affiché lorsque l'utilisateur soumet correctement le formulaire de rendez-vous. Cliquez droit sur le bouton de soumission et choisissez l'état Filled, le projet est terminé. Vous avez peut-être remarqué plusieurs messages d'alerte dans l'interface de Blend tout au long de l'exercice (voir Figure 10.50).

Image non disponible
Figure 10.50 - Messages d'alerte

Ces messages sont assez clairs, ils vous signalent que le mot Focus est déjà utilisé et hérité en interne par les contrôles utilisateur standard. Vous risquez de rendre cette propriété inaccessible, voir de l'écraser. Ce n'est pas grave dans la mesure où vous créez un prototype. Par contre, en mode de production, nommer les états avec des noms identiques à certains de ceux du framework Silverlight, peut engendrer des conflits et des bogues difficiles à prévoir. Pour éviter ce type de message, vous pouvez modifier le nom des états visuels liés au focus en les francisant. Le prototype finalisé est accessible dans : chap10/CarReseller.zip.

Au prochain chapitre, nous approfondirons nos connaissances des ressources graphiques utilisables dans les projets Silverlight. Nous aborderons également les bonnes pratiques de production concernant leur organisation et leur partage.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Eric Ambrosi et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.