www.bewise.fr

Recherche

Petite promenade au pays de WPF

Par David Catuhe, posté le 10/04/2007

Profil : Développeur | Niveau : Intermédiaire (200)

Tags : WPF | Partager : Partager sur Delicious Partager sur Facebook Partager sur Twitter

1 Introduction

Technologie majeure du Framework .NET 3.0, WPF (Windows Presentation Foundation) constitue un changement important dans la manière que nous avons de développer des interfaces graphiques riches.

Tout au long de cet article, nous allons nous promener au milieu de certaines fonctionnalités disponibles en nous laissant guider par la construction d'un projet simple.

Pour nous servir de véhicule, j'ai pris le parti de n'utiliser que l'approche déclarative de WPF à savoir l'utilisation de XAML (Ce choix est motivé par le fait que WPF est très orienté dans son design sur XAML ce qui se traduit par une utilisation plus verbeuse si l'on fait le choix de coder via un langage tel que C#).

Pour la compilation, il faut installer le Framework .NET 3.0 :

http://www.microsoft.com/downloads/details.aspx?FamilyID=10CC340B-F857-4A14-83F5-25634C3BF043&displaylang=en

Pour faciliter son utilisation, il est intéressant d'installer une extension (Orcas) pour Visual Studio 2005 :

http://www.microsoft.com/downloads/details.aspx?FamilyId=F54F5537-CC86-4BF5-AE44-F5A1E805680D&displaylang=en

2 Mise en place

Nous allons commencer par définir le projet de base qui sera composé uniquement d'une fenêtre dans laquelle nous allons placer un bouton.

   1:  <Window x:Class="BlackButton.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      Title="BlackButton" Height="100" Width="200"
   5:      >
   6:      <Grid>
   7:        <Button>OKButton>
   8:      Grid>
   9:  Window>

Il s'agit ici d'un fichier XAML simplissime dans lequel nous déclarons notre fenêtre, une grille qui délimite la zone cliente et un bouton.

Le tag Window définit les deux namespaces minimaux que sont :

· http://schemas.microsoft.com/winfx/2006/xaml/presentation : namespace concernant l'ensemble des contrôles de bases

· http://schemas.microsoft.com/winfx/2006/xaml : namespace concernant les propriétés et extensions utilisées par le parser xaml (par exemple la propriété Name)

Le résultat est à la hauteur de nos espérances :

image

Le bouton occupe tout l'espace disponible puisqu'aucune notion de taille ni de placement ne lui ont été communiquée. J'ai volontairement désactivé les thèmes XP ou Vista pour nous retrouver face à la rudesse du design de base.

Avant de nous aventurer plus loin nous allons ouvrir une première parenthèse. Il serait en effet intéressant d'avoir notre bouton qui se positionne horizontalement au centre de notre fenêtre et verticalement au bas de cette dernière.

Pour ce faire nous allons utiliser les attributs d'alignement et la notion de taille minimale (MinWidth). Cette dernière est nécessaire sinon notre bouton occupera juste la taille de son texte.

   1:  <Window x:Class="BlackButton.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      Title="BlackButton" Height="100" Width="200"
   5:      >
   6:      <Grid>
   7:        <Button HorizontalAlignment="Center" 
   8:  VerticalAlignment="Bottom" MinWidth="100">OKButton>
   9:      Grid>
  10:  Window>

Ce qui nous donne :

image

Nous allons maintenant mettre en place plusieurs améliorations visuelles afin de rendre notre bouton plus « funky » (Le but étant d'obtenir un bouton dans le style de ceux de la Xbox).

WPF nous permet de définir aussi bien le comportement d'un contrôle (ses événements et ses réactions associées, comme dans un développement standard) que de modifier très simplement son aspect visuel.

Chaque contrôle possède par défaut un template visuel. Ce template définit comment le contrôle doit être dessiné. Nous allons donc nous attacher à modifier le template de notre bouton selon l'architecture suivante :

· Un fond de couleur noire aux bords arrondis

· Un effet de reflet lumineux sur la partie haute de couleur argenté

· Le texte du bouton sera centré sur le contrôle

Cette description va donc se traduire en WPF de la manière suivante :

2.1 Modifier le template et ajouter le fond noir arrondi

   1:        <Button HorizontalAlignment="Center" 
   2:  VerticalAlignment="Bottom" MinWidth="100" MinHeight="30">
   3:  OK
   4:          <Button.Template>
   5:            <ControlTemplate TargetType="{x:Type Button}">
   6:              <Rectangle RadiusX="9" RadiusY="9" Fill="Black">
   7:              Rectangle>              
   8:            ControlTemplate>          
   9:          Button.Template>
  10:        Button>

Via le XAML nous pouvons prendre une propriété du bouton (en l'occurrence la propriété Template) et lui affecter un nouveau contenu. Ce dernier est composé d'un ControlTemplate que l'on initialise pour qu'il travaille sur le type Button (ce qui lui permettra de connaitre les propriétés disponibles par la suite).

Notre rectangle quant à lui est rempli de noir et voit ses bords arrondis via les propriétés RadiusX et RadiusY.

Vous noterez que le bouton s'est vu rajouter la propriété MinHeight pour ne pas disparaître car pour l'instant aucun texte n'est dessiné et donc la hauteur vaut 0 par défaut (hauteur automatique en fonction du contenu).

Le résultat obtenu donne donc :

image

2.2 Rajout du reflet

Pour rajouter notre reflet sur la partie supérieure et pour que ce dernier s'adapte toujours à la taille du bouton, nous allons faire intervenir une grille qui définira deux lignes. La première ligne contiendra le reflet et nous définirons que le rectangle du fond occupera les deux lignes.

Pour arriver à produire l'effet de reflet nous allons utiliser un rectangle dont la couleur variera du blanc plein (opacité proche de 100%) vers du blanc totalement transparent (opacité égale à 0).

Le résultat en WPF :

   1:       

Notre second rectangle posséde une marge qui lui évite d'être aligné sur le rectangle de fond. De plus son remplissage se fait non pas de manière uniforme mais via une brosse linéaire (brosse dont la couleur varie linéairement). Cette dernière part du point 0,0 (le point en haut à gauche du contrôle) et va vers le point 0,1 (le point en bas à gauche du contrôle) en faisant linéairement varier sa couleur entre la valeur #CCFFFFFF (blanc quasiment opaque) vers la valeur #00FFFFFF (blanc transparent).

La grille nous permet de placer le reflet sur la partie haute du contrôle. Comme la taille des lignes est exprimée en valeur proportionnelle (40% et 60%), le reflet sera toujours adapté quelque soit la taille du bouton.

Le résultat à l'écran :

image

L'association des contrôles aux diverses lignes de la grille va nous amener vers une nouvelle parenthèse. En effet, nous pouvons noter que les propriétés Row et Rowspan sont préfixées par le nom de la classe Grid. De plus si l'on regarde les propriétés disponibles sur un rectangle par exemple, Row et Rowspan n'existent pas.

En effet, ces propriétés sont induites par le fait que les rectangles sont des enfants de la grille. Et c'est effectivement la grille qui définit ces propriétés (statiques). On parle alors de propriétés attachées (attached properties). Plutôt que de demander à chaque contrôle de définir les propriétés Row et Rowspan alors que ces propriétés ne sont nécessaires que dans le cadre des grilles, WPF fournit la possibilité d'attacher des propriétés à un contrôle en liaison avec un autre contrôle. Charge au contrôle parent de venir requêter ces propriétés et de les interpréter comme il le souhaite.

2.3 Intégrer le texte du bouton

La partie finale de notre construction va se résumer à intégrer un contrôle capable de dessiner le contenu de notre bouton à savoir son texte.

Pour se faire, nous allons utiliser le contrôle ContentPresenter dont le but est précisément de gérer l'affichage du contenu d'autres contrôles.

Nous allons demander à notre ContentPresenter de se centrer sur la grille qui le contient en recouvrant l'ensemble des lignes de cette dernière (Grid.Rowspan="2").

Le résultat en WPF :

   1:        <Button HorizontalAlignment="Center" 
   2:            VerticalAlignment="Bottom" MinWidth="100" 
   3:  MinHeight="30">
   4:          OK
   5:          <Button.Template>
   6:            <ControlTemplate TargetType="{x:Type Button}">
   7:              <Grid>
   8:                <Grid.RowDefinitions>
   9:                  <RowDefinition Height="0.4*"/>
  10:                  <RowDefinition Height="0.6*"/>
  11:                Grid.RowDefinitions>
  12:                <Rectangle Grid.Row="0" Grid.RowSpan="2" 
  13:  RadiusX="9" RadiusY="9" Fill="Black">
  14:                Rectangle>
  15:                <Rectangle Grid.Row="0" Margin="2,2,2,0" 
  16:  RadiusX="9" RadiusY="9">
  17:                  <Rectangle.Fill>
  18:                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
  19:                      <LinearGradientBrush.GradientStops>
  20:                        <GradientStopCollection>
  21:                          <GradientStop Color="#ccffffff" Offset="0" />
  22:                          <GradientStop Color="#00ffffff" Offset="1" />
  23:                        GradientStopCollection>
  24:                      LinearGradientBrush.GradientStops>
  25:                    LinearGradientBrush>
  26:                  Rectangle.Fill>
  27:                Rectangle>
  28:                <ContentPresenter Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="center" HorizontalAlignment="center"
  29:  TextElement.Foreground="White"/>
  30:              Grid>
  31:            ControlTemplate>
  32:          Button.Template>
  33:        Button>

Notez ici que nous avons spécifié une couleur de Foreground (couleur d'affichage du texte) pour notre ContentPresenter via la propriété attachée TextElement.Foreground. Pourquoi sur TextElement ? Parce que nous savons que pour afficher le texte « OK », le ContentPresenter utilisera au final un TextElement.

Le résultat au final :

image

3 Interactivité

Maintenant que nous disposons d'un contrôle visuellement en accord avec nos besoins, nous allons nous attacher à le rendre un peu plus dynamique.

Premièrement nous allons faire en sorte que lors du survol de la souris le bouton indique bien à l'utilisateur qu'il peut lui cliquer dessus.

Par la suite nous allons modifier notre bouton pour qu'il change d'aspect lors qu'il est cliqué.

3.1 Gestion du survol de la souris

Pour montrer que le bouton est actif lors du survol de la souris, nous allons changer la couleur de fond du bouton et nous amuser à rajouter un léger halo autour de ce dernier.

Nous allons donc avoir besoin de déclencher une action lorsque certaines conditions seront réunies. C'est ici que rentrent en jeu les triggers de WPF.

Un trigger WPF est un déclencheur d'actions en fonction d'événements ou de modifications de valeurs (propriétés ou data binding). Pour faire simple, dans notre cas le déclencheur va attendre que la propriété IsMouseOver de notre bouton passe à vrai. A ce moment là il va déclencher une action qui en l'occurrence sera de changer la manière dont le fond du bouton se dessine.

Le code WPF va donner ceci :

   1:  <Button.Template>
   2:            <ControlTemplate TargetType="{x:Type Button}">
   3:              <Grid>
   4:                <Grid.RowDefinitions>
   5:                  <RowDefinition Height="0.4*"/>
   6:                  <RowDefinition Height="0.6*"/>
   7:                Grid.RowDefinitions>
   8:                <Rectangle x:Name="backGround" Grid.Row="0"  Grid.RowSpan="2" RadiusX="9" RadiusY="9" Fill="Black">
   9:                Rectangle>
  10:                   .
  11:              .
  12:  .
  13:              Grid>
  14:              <ControlTemplate.Triggers>
  15:                <Trigger Property="IsMouseOver" Value="True">                
  16:                  <Setter Property="Fill" TargetName="backGround">
  17:                    <Setter.Value>
  18:                      <LinearGradientBrush EndPoint="0.979,1.069" 
  19:  StartPoint="0.742,-0.931">
  20:                        <GradientStop Color="#FFC331D7" Offset="0.011"/>
  21:                        <GradientStop Color="#FF000000" Offset="1"/>
  22:                      LinearGradientBrush>
  23:                    Setter.Value>
  24:                  Setter>
  25:                Trigger>                
  26:              ControlTemplate.Triggers>
  27:            ControlTemplate>
  28:          Button.Template> GlowSize="0" x:Name="glowEffect"/>
  29:        Button>>

Nous avons défini un nouveau trigger sur notre template. Ce dernier écoute la propriété IsMouseOver du bouton et attend qu'elle passe à vrai. Son action se résume à affecter via un Setter une nouvelle valeur à la propriété Fill de l'objet dont le nom est « backGround » (Notons que ce nom a été affecté à notre premier rectangle via sa propriété  x:Name).

Il est à noter que cette affection cesse dès que le trigger voit que sa condition n'est plus vraie. En effet, pour donner la valeur finale à une propriété (telle que la propriété Fill ici), WPF évalue à la fois la valeur de base (couleur noire) mais intègre éventuellement tous les triggers actifs (donc dégradé de violet ici). Du moment où un trigger se désactive, la propriété retrouve instantanément sa valeur d'origine.

Le résultat donne un superbe bouton rempli avec un dégradé de violet vers du noir :

image

En ce qui concerne l'effet de halo nous allons nous appuyer sur un BitmapEffect.

Un BitmapEffect est un processus graphique qui s'applique sur le rendu final du contrôle juste avant qu'il ne soit dessiné à l'écran. Il existe plusieurs BitmapEffects mais nous allons nous intéresser tout particulièrement à celui qui justement génère des halos autour des contrôles : le OuterGlowBitmapEffect.

Pour appliquer un tel effet à notre contrôle la déclaration suivante suffit :

   1:  
   2:            "{x:Type Button}"
   3:              
   4:                
   5:                  "DarkViolet"
   6:                
   7:  .

Nous demandons donc à WPF de renseigner la propriété BitmapEffect de notre grille avec une instance d'un OuterGlowBitmapEffect se nommant « glowEffect » et de couleur violet foncé. Notons ici que par défaut cet effet est de taille 0 ce qui signifie qu'il n'est pas actif. En effet, c'est via notre trigger que nous allons l'activer.

Toutefois, pour apporter un peu de dynamisme à tout ça nous n'allons pas l'activer brutalement mais plutôt de manière progressive en utilisant une animation.

Le mécanisme des animations consiste en WPF à faire évoluer dans le temps une propriété d'un objet. Pour contrôler cette évolution on utilise des objets issus de la classe Animation. Ces animations sont rassemblées au sein de storyboards qui servent à configurer de manière commune plusieurs animations.

Pour créer un joli effet d'animations aussi bien lorsque le halo apparaîtra que lorsqu'il disparaîtra nous allons faire appel aux EnterActions et ExitActions d'un trigger. Ces deux propriétés définissent les actions à mener lorsque le trigger s'active et lorsqu'il se désactive, nous permettant ainsi de lancer une animation à l'activation et une autre à la désactivation.

Le résultat en WPF donne donc :

   1:  <Button.Template>
   2:            <ControlTemplate TargetType="{x:Type Button}">
   3:              <Grid>
   4:                <Grid.BitmapEffect>
   5:                  <OuterGlowBitmapEffect GlowColor="DarkViolet" GlowSize="0" x:Name="glowEffect"/>
   6:                Grid.BitmapEffect>
   7:                <Grid.RowDefinitions>
   8:                  <RowDefinition Height="0.4*"/>
   9:                  <RowDefinition Height="0.6*"/>
  10:                Grid.RowDefinitions>
  11:  .
  12:              <ControlTemplate.Triggers>
  13:                <Trigger Property="IsMouseOver" Value="True">                
  14:                  <Setter Property="Fill" TargetName="backGround">
  15:                    <Setter.Value>
  16:                      <LinearGradientBrush EndPoint="0.979,1.069" 
  17:  StartPoint="0.742,-0.931">
  18:                        <GradientStop Color="#FFC331D7" Offset="0.011"/>
  19:                        <GradientStop Color="#FF000000" Offset="1"/>
  20:                      LinearGradientBrush>
  21:                    Setter.Value>
  22:                  Setter>
  23:                  <Trigger.EnterActions>
  24:                    <BeginStoryboard>
  25:                      <Storyboard>
  26:                        <DoubleAnimation To="4" Duration="0:0:0.3"
  27:                          Storyboard.TargetName="glowEffect" 
  28:                          Storyboard.TargetProperty="GlowSize" />
  29:                      Storyboard>
  30:                    BeginStoryboard>
  31:                  Trigger.EnterActions>
  32:                  <Trigger.ExitActions>
  33:                    <BeginStoryboard>
  34:                      <Storyboard>
  35:                        <DoubleAnimation To="0" Duration="0:0:0.1" 
  36:                          Storyboard.TargetName="glowEffect" 
  37:                          Storyboard.TargetProperty="GlowSize" />
  38:                      Storyboard>
  39:                    BeginStoryboard>
  40:                  Trigger.ExitActions>                
  41:                Trigger>                
  42:              ControlTemplate.Triggers>
  43:            ControlTemplate>
  44:          Button.Template>
  45:        Button>

Lors de l'entrée de la souris sur le contrôle, le halo va grossir jusqu'à une taille de 4 en 0.3s. Lors de la sortie de la souris, ce même halo verra sa taille diminuer jusqu'à disparaitre en 0.1s.

Le résultat en image :

image

3.2 Gestion du clic

Pour signifier le clic de l'utilisateur sur notre bouton, nous allons simuler son enfoncement via une transformation.

Il existe en WPF deux types de transformations pour les contrôles :

· Les transformations de layouts : elles s'appliquent sur le calcul de la position des contrôles entre eux et à donc un impact sur leur placement les uns par rapport aux autres

· Les transformations de rendu : elles s'appliquent après le calcul de layout et n'ont donc qu'un impact sur le contrôle lui-même.

Dans notre cas nous allons utiliser une transformation de rendu pour éviter que les contrôles environnants ne se déplacent lorsque l'on simulera l'enfoncement (via un zoom du bouton).

La mise en place de notre zoom va se faire via un autre trigger qui s'appuiera sur la propriété IsPressed du bouton. Lorsque le trigger s'activera il affectera la propriété RenderTransform de notre grille sur une transformation de zoom (de réduction).

Le résultat WPF donne donc :

   1:            <ControlTemplate TargetType="{x:Type Button}">
   2:              <Grid x:Name="mainGrid" RenderTransformOrigin="0.5,0.5">
   3:                .
   4:              <ControlTemplate.Triggers>
   5:                <Trigger Property="IsPressed" Value="True">
   6:                  <Setter Property="RenderTransform" TargetName="mainGrid">
   7:                    <Setter.Value>
   8:                      <ScaleTransform ScaleX=".9" ScaleY=".9" />
   9:                    Setter.Value>
  10:                  Setter>
  11:                Trigger>
  12:                  .
  13:              ControlTemplate.Triggers>
  14:            ControlTemplate>
  15:          Button.Template>

Afin de repérer notre grille depuis le trigger, il faut bien entendu la nommer. De plus, par défaut les transformations se font depuis le coin haut gauche des contrôles, or nous souhaitons que le zoom se fasse avec comme axe le centre du bouton. C'est pour cela que la grille redéfinit son origine de transformation au centre de sa zone de rendu (sachant que cette dernière s'étend entre 0,0 et 1.0, 1.0).

4 Partager notre template

Fier que nous sommes de notre superbe bouton, il est temps maintenant de le déployer sur tout notre projet. Pour ce faire, nous pouvons adopter deux solutions :

· Partager notre template au travers d'une ressource d'applications

· Extraire notre bouton et en faire un contrôle utilisateur

4.1 Définir un style dans une ressource

Pour définir une ressource d'application en WPF, il suffit de venir rajouter cette dernière dans la collection Resources de la classe Application. Une ressource est un objet de tout type identifié par une clé.

Nous allons donc copier notre template directement au sein de la déclaration des ressources de notre application (app.xaml) ce qui donne :

   1:  <Application.Resources>
   2:        <ControlTemplate x:Key="BlackButtonTemplate" TargetType="{x:Type Button}">
   3:          <Grid x:Name="mainGrid"  RenderTransformOrigin="0.5,0.5">
   4:            <Grid.BitmapEffect>
   5:              <OuterGlowBitmapEffect GlowColor="DarkViolet" GlowSize="0" x:Name="glowEffect"/>
   6:            Grid.BitmapEffect>
   7:            <Grid.RowDefinitions>
   8:              <RowDefinition Height="0.4*"/>
   9:              <RowDefinition Height="0.6*"/>
  10:            Grid.RowDefinitions>
  11:          .

Pour référencer cette ressource, chaque bouton souhaitant profiter de notre template devra uniquement faire pointer sa propriété Template sur la ressource BlackButtonTemplate. En WPF, cela donne donc :

   1:        <Button HorizontalAlignment="Center" 
   2:  VerticalAlignment="Bottom" 
   3:  MinWidth="100" 
   4:  MinHeight="30" 
   5:  Template="{StaticResource BlackButtonTemplate}">
   6:          OK
   7:        Button>

4.2 Créer un contrôle utilisateur

Pour partager notre contrôle au-delà des limites de notre projet, nous pouvons mettre en place un contrôle utilisateur.

En WPF, un contrôle utilisateur se construit comme une fenêtre hormis le fait que les tags WPF Window sont remplacés par UserControl :

   1:  <UserControl x:Class="BlackButton.BlackButtonControl"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   4:      <Grid>
   5:        <Button HorizontalAlignment="Center" 
   6:  VerticalAlignment="Bottom" MinWidth="100" MinHeight="30">
   7:          OK
   8:          <Button.Template>
   9:            <ControlTemplate TargetType="{x:Type Button}"> 
  10:          .

Leur utilisation au sein d'une fenêtre se fait selon la manière suivante :

   1:  <Window x:Class="BlackButton.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      xmlns:local="clr-namespace:BlackButton"
   5:      Title="BlackButton" Height="100" Width="200"
   6:      >
   7:      <Grid>
   8:        <local:BlackButtonControl 
   9:  VerticalAlignment="Top">
  10:  local:BlackButtonControl>
  11:        <local:BlackButtonControl 
  12:  VerticalAlignment="Bottom">
  13:  local:BlackButtonControl>
  14:      Grid>
  15:  Window>

Pour que le parser WPF puisse reconnaitre le tag BlackButtonControl, nous devons définir le namespace auquel il appartient via l'attribut xmlns. En l'occurrence, le namespace en question ne provient pas d'Internet mais d'un assembly .NET (ce qui est indiqué par la commande clr-namespace).

5 Conclusion

Finalement notre voyage initiatique s'achève ici. Nous avons pu voir ensemble comment, sans jamais écrire une ligne de code C#, mettre en oeuvre un visuel riche et dynamique via la flexibilité et la puissance de XAML et de WPF.

Dans un futur article nous nous attarderons plus longuement sur les propriétés un peu particulières que sont les dependency properties et les attached properties car ces dernières jouent un rôle essentiel dans le mécanisme de trigger et d'animations.

Passionné de développement et geek dans l’âme. David dirige Bewise qu’il a fondé en 1999 avec ses acolytes Frédéric Colin et Yann Faure. Il est également très impliqué dans le développement du moteur 3D Nova pour la société Vertice qu’il a fondé en 2002 avec son ami Michel Rousseau. Les sujets qui lui tiennent à cœur sont autour des technologies WPF, Silverlight, Windows Phone 7 ou encore DirectX.

Voir les autres publications de l'auteur


Commentaires