posté @ Monday, July 23, 2007 4:42 PM

 Lors de la création d'une interface graphique pour une application riche, il arrive souvent que l'on se trouve dans une situation où il éxiste un contrôle qui a presque la fonctionnalité que l'on recherche, mais dont il manque un morceau.

Avec WPF, la plupart du temps, on peut résoudre celà par une édition de template ou de style, afin de combiner les fonctionnalités de différents contrôles entre eux, effectuer du DataBinding etc.

Mais dans certains cas, ca ne suffit pas, ou bien ca ne parrait pas assez réutilisable.

Nous allons donc voir comment créer un Custom Control WPF basé sur un contrôle existant : Nous allons customiser le contrôle Expander pour créer un AnimatedExpander.

Création du projet et anatomie des fichiers de base

Pour commencer, nous allons créer dans Visual Studio (Orcas, ou 2005 avec le designer WPF) un projet de type WPF Custom Control Library :

 

new custom control library

Par défaut, le designer nous crée une classe C# et un dictionnaire de ressources Xaml. Nous allons supprimer le fichier C#, et supprimer les styles du fichier Xaml (qui se trouve dans le répertoire Themes du projet). Vous devez obtenir un fichier Xaml qui ressemble à ca:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyWPFLibrary">    
</ResourceDictionary>

Nous allons maintenant pouvoir créer notre Custom Control : clic droit sur le projet -> New Item -> WPF Custom Control (attention à ne pas sélectionner User Control, il ne s'agit pas du tout de la même chose) et rentrer le nom "AnimatedExpander".

Le Designer nous a alors créé un fichier .cs (la classe représentant notre contrôle) et un style de base associé (se trouvant dans le fichier Themes/Generic.xaml).

Le premier point important à signaler, est la ligne de code située dans le constructeur statique de notre contrôle:

public class AnimatedExpander : Expander
{
    static AnimatedExpander()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(AnimatedExpander), 
new FrameworkPropertyMetadata(typeof(AnimatedExpander))); } }

Cette ligne permet de spécifier que le style par défaut de notre contrôle n'est plus celui de la classe de base (Control), mais celui définit pour le type AnimatedExtender (celui définit dans Generic.xaml). C'est ceci qui fait le lien entre les styles / templates définis dans le Xaml et notre classe C#.

 

Préparation du contrôle

Le designer nous a créé un contrôle vide dérivant directement de la classe Control. Hors, nous ne voulons pas redévelopper toute la logique de l'Expander, mais seulement la modifier. Nous allons donc devoir effectuer 3 choses:

  • Faire dériver notre classe d'Expander (plutôt que de Control)
  • Remplacer les styles / templates créés par le designer par ceux de l'Expander de base
  • Customiser tout ca !

Le premier point est le plus simple, une seule chose à faire, dans la classe C#, remplacer public class AnimatedExpander : Control  par public class AnimatedExpander : Expander.

 La deuxième est un petit peu plus complexe. Pour nous aider, nous allons utiliser Blend, créer un projet Application WPF, ajouter un Expander, et utiliser les fonctionnalité de Blend pour récupérer les styles et templates par défaut au format Xaml:

  1. Dans Blend, créer un projet application WPF
  2. Ajoutez à la fenêtre un contrôle Expander
  3. Sélectionnez l'expander, allez dans le menu Objet -> Edit Style -> Edit a copy
  4. Dans la boîte de dialogue, sélectionnez "Apply to all" et créez un nouveau fichier de resource:
    new style
  5. Ouvrez le fichier de ressources : il contient une série de styles / templates només (qui sont en fait des ressources utilisés par le style de l'Expander) ainsi qu'un style non nomé appliqué aux contrôles Expander. Copiez l'intégralité de ces styles dans le fichier Generic.xaml de votre librarie de contrôle (supprimez au préalable le style créé par Visual Studio).
  6. La dernière chose à faire avant de customiser notre contrôle est de modifier la déclaration du style de l'Expander (le style non nommé) pour qu'il s'applique à notre AnimatedExpander:
<Style TargetType="{x:Type local:AnimatedExpander}">
        <Setter Property="Foreground" 
Value
="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="VerticalContentAlignment" Value="Stretch"/> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:AnimatedExpander}"> <Border SnapsToDevicePixels="true"
Background
="{TemplateBinding Background}"
                            BorderBrush
="{TemplateBinding BorderBrush}"
                            BorderThickness
="{TemplateBinding BorderThickness}"
                            CornerRadius
="3"> <DockPanel> <ToggleButton FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}"
                            Margin
="1"
                                          MinHeight
="0"
                                          MinWidth
="0"
                                          x
:Name="HeaderSite"
                                          Style
="{StaticResource ExpanderDownHeaderStyle}"
                                          ...
/> <ContentPresenter Focusable="false"
                                          Visibility
="Collapsed"
                                              HorizontalAlignment
="{TemplateBinding HorizontalContentAlignment}"
                                              Margin
="{TemplateBinding Padding}"
                                              x
:Name="ExpandSite"
.../> </DockPanel> </Border> <ControlTemplate.Triggers> <Trigger Property="IsExpanded" Value="true"> <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/> </Trigger> <Trigger Property="ExpandDirection" Value="Right"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderRightHeaderStyle}"/> </Trigger> <Trigger Property="ExpandDirection" Value="Up"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderUpHeaderStyle}"/> </Trigger> <Trigger Property="ExpandDirection" Value="Left"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderLeftHeaderStyle}"/> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Foreground"
Value
="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>

Nous avons maintenant un contrôle AnimatedExpander qui a éxactement le même comportement que l'Expander de base, avec un template par défaut que nous pouvons manipuler... Il est temps de le customiser!

 

Les mains dans le cambouis 

Si l'on observe le template de l'expander de base, on peut voir que tout est fait via DataBinding et triggers : par défaut le ContentPresenter "ExpandSite" est "Collapsed" et un trigger sur la propriété "IsExpanded" permet de le faire passer Visible.

Pour rendre notre contrôle animable, nous allons devoir supprimer ce comportement à 2 états : dans le template, supprimez le Trigger et la définition de la propriété Visibility sur le controle ExpandSite.

Une fois ceci fait, nous allons devoir modifier la façon dont sont traiter les évènements Expanded et Collapsed.

Pour celà, retournez dans le code C# de notre contrôle. A l'intérieur de celui-ci, nous allons créer une DependencyProperty pour paramétrer la durée de l'animation, et une autre que nous n'exposerons pas dans une propriété publique, pour contrôler la taille du contenu :

    public class AnimatedExpander : Expander
    {
        static AnimatedExpander()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(AnimatedExpander), 
new FrameworkPropertyMetadata(typeof(AnimatedExpander))); } public Duration AnimationDuration { get { return (Duration)GetValue(AnimationDurationProperty); } set { SetValue(AnimationDurationProperty, value); } } // Using a DependencyProperty as the backing store for AnimationDuration.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register("AnimationDuration", typeof(Duration),
typeof(AnimatedExpander),
new UIPropertyMetadata(new Duration(TimeSpan.FromMilliseconds(300)))); // Using a DependencyProperty as the backing store for ContentSize.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentSizeProperty = DependencyProperty.Register("ContentSize", typeof(double),
typeof(AnimatedExpander ), new UIPropertyMetadata(0.0)); }

Nous pouvons retourner dans le template, pour effectuer le binding sur notre DependencyProperty "ContentSize" :

...

<ControlTemplate.Triggers>
  <Trigger Property="ExpandDirection" Value="Down">
     <Setter TargetName="ExpandSite" Property="Height" 
Value
="{Binding ContentSize, RelativeSource={RelativeSource TemplatedParent}}"/> </Trigger> <Trigger Property="ExpandDirection" Value="Right"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderRightHeaderStyle}"/> <Setter TargetName="ExpandSite" Property="Width"
Value
="{Binding ContentSize, RelativeSource={RelativeSource TemplatedParent}}"/> </Trigger> <Trigger Property="ExpandDirection" Value="Up"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderUpHeaderStyle}"/> <Setter TargetName="ExpandSite" Property="Height"
Value
="{Binding ContentSize, RelativeSource={RelativeSource TemplatedParent}}"/> </Trigger> <Trigger Property="ExpandDirection" Value="Left"> <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/> <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/> <Setter Property="Style" TargetName="HeaderSite"
Value
="{StaticResource ExpanderLeftHeaderStyle}"/> <Setter TargetName="ExpandSite" Property="Width"
Value
="{Binding ContentSize, RelativeSource={RelativeSource TemplatedParent}}"/> </Trigger>

...

En fonction de la valeur de "ExpandDirection" nous affectons un binding différent (soit à la hauteur, soit à la largeur de L'ExpandSite). Nous allons maintenant animer la propriété ContentSize en surchargeant les méthodes OnCollapsed et OnExpanded de l'Expander :

protected override void OnCollapsed()
        {
            UIElement uiContent = Content as UIElement;
            if (uiContent != null)
            {
                switch (ExpandDirection)
                {
                    case ExpandDirection.Up:
                    case ExpandDirection.Down:

                        DoubleAnimation animHeight = new DoubleAnimation(0.0
, AnimationDuration); animHeight.AccelerationRatio = 0.5; animHeight.DecelerationRatio = 0.5; this.BeginAnimation(ContentSizeProperty, animHeight); break; case ExpandDirection.Left: case ExpandDirection.Right: DoubleAnimation animWidth = new DoubleAnimation(0.0
, AnimationDuration); animWidth.AccelerationRatio = 0.5; animWidth.DecelerationRatio = 0.5; this.BeginAnimation(ContentSizeProperty, animWidth); break; } } base.OnCollapsed(); } protected override void OnExpanded() { UIElement uiContent = Content as UIElement; if (uiContent != null) { // This updates the "DesiredSize" property of the uiContent // in order to get the optimal size depending on the Maximum size of the Expander // This should be improved taking care of the size of the header. uiContent.Measure(new Size(MaxWidth, MaxHeight)); switch (ExpandDirection) { case ExpandDirection.Up: case ExpandDirection.Down: DoubleAnimation animHeight = new DoubleAnimation(uiContent.DesiredSize.Height
, AnimationDuration); animHeight.AccelerationRatio = 0.5; animHeight.DecelerationRatio = 0.5; this.BeginAnimation(ContentSizeProperty, animHeight); break; case ExpandDirection.Left: case ExpandDirection.Right: DoubleAnimation animWidth = new DoubleAnimation(uiContent.DesiredSize.Width
, AnimationDuration); animWidth.AccelerationRatio = 0.5; animWidth.DecelerationRatio = 0.5; this.BeginAnimation(ContentSizeProperty, animWidth); break; } } base.OnExpanded(); }

Nous avons maintenant un Expander animé fonctionnel !

Pour télécharger les sources, cliquez ici.

 

Mots clés Technorati : , , ,

Commentaires :

# re: [WPF] Etendre les possibilité d'un contrôle WPF
Ecrit par Matthieu MEZIL le 7/24/2007 2:34 PM
Très bon article. Bon boulot ! :-)
# property sales in fuerteventura
Ecrit par property sales in fuerteventura le 2/23/2008 6:22 PM
great prices for property sales
# Property sales in Fuerteventura
Ecrit par Property sales in Fuerteventura le 2/28/2008 6:42 PM
Occasionally, you'll become overwhelmed by the monstrous sum of golf property data at your disposal.
# sundial
Ecrit par sundial le 6/14/2008 7:13 PM
great site for sundials
# divorce books
Ecrit par divorce books le 6/26/2008 5:55 AM
some info re divorce guides for under $40.00
# re: [WPF] Etendre les possibilité d'un contrôle WPF
Ecrit par Christian FINEL le 6/14/2010 1:39 PM
Bon article ^^

Content de retrouver un membre de la promo ;)
# re: [WPF] Etendre les possibilité d'un contrôle WPF
Ecrit par Jonathan ANTOINE le 9/21/2010 3:02 PM
Hello Simon,

Merci pour ton article ! Par contre j'ai remarqué que la valeur de IsExpanded n'est pas respectée au chargement...

Voici une version de mon cru qui s'inspire de la tienne (en ne redéfinissant que le template par contron) qui la prend en compte : http://blog.lexique-du-net.com/index.php?post/2010/09/21/Create-an-animated-expander

PS: bonne vacances :p
# re: [WPF] Etendre les possibilité d'un contrôle WPF
Ecrit par les meilleurs casino en ligne le 12/22/2010 1:25 PM
This article gives the light in which we can observe the reality. this is very nice one and gives in depth information. thanks for this nice article Good post..
# wing Shoes
Ecrit par wing Shoes le 8/19/2011 5:58 AM
Adidas Wing Shoes Genuine, who done fifteenth inside La Liga previous period, appointed Frenchman Philippe Montanier because discipline earlier that summertime, yet Vela is a first brand-new participant to be able to become a member of in front of the brand-new advertising campaign.jeremy scott wings shoes outlet
.Jeremy Scott Wings
# re: [WPF] Etendre les possibilité d'un contrôle WPF
Ecrit par Leo Labat le 1/13/2012 4:37 PM
Hello Simon

Ecrire un commentaire :

Titre :*
Nom *
Email
Url
Commentaire : *  


Please add 4 and 5 and type the answer here: