Partager via


Création d'un contrôle avec une apparence personnalisable

Windows Presentation Foundation (WPF) vous permet de créer un contrôle dont l'apparence peut être personnalisée. Par exemple, vous pouvez modifier l'apparence d'une CheckBox au-delà de ce que permet la définition des propriétés en créant un ControlTemplate. L'illustration suivante montre une CheckBox qui utilise un ControlTemplate par défaut et une CheckBox qui utilise un ControlTemplatepersonnalisé.

Case à cocher qui utilise le modèle de contrôle par défaut

Case à cocher avec le modèle de contrôle par défaut.

Case à cocher qui utilise un modèle de contrôle personnalisé

Case à cocher avec un modèle de contrôle personnalisé.

Si vous suivez le modèle de composants et d'états lorsque vous créez un contrôle, l'apparence de ce dernier est personnalisable. Les outils du concepteur, tels que Microsoft Expression Blend, prennent en charge le modèle de composants et d'états, de sorte que lorsque vous suivez ce modèle votre contrôle est personnalisable dans ces types d'applications. Cette rubrique décrit le modèle de composants et d'états et explique comment le suivre lorsque vous créez votre propre contrôle. Elle utilise un exemple de contrôle personnalisé, NumericUpDown, pour illustrer la philosophie de ce modèle. Le contrôle NumericUpDown affiche une valeur numérique qu'un utilisateur peut augmenter ou diminuer en cliquant sur les boutons du contrôle. L'illustration suivante montre les contrôles NumericUpDown qui sont discutés dans cette rubrique.

Contrôle NumericUpDown personnalisé

Contrôle personnalisé NumericUpDown.

Cette rubrique contient les sections suivantes :

  • Composants requis

  • Modèle de composants et d'états

  • Définition de la structure visuelle et du comportement visuel d'un contrôle dans un ControlTemplate

  • Utilisation de composants du ControlTemplate dans le code

  • Fourniture du contrat de contrôle

  • Exemple complet

Composants requis

Cette rubrique suppose que vous savez comment créer un ControlTemplate pour un contrôle existant, connaissez les éléments d'un contrat de contrôle et comprenez les concepts présentés dans Personnalisation de l'apparence d'un contrôle existant en créant un ControlTemplate.

RemarqueRemarque

Pour créer un contrôle dont l'apparence peut être personnalisée, vous devez créer un contrôle qui hérite de la classe Control ou de l'une de ses sous-classes autres que UserControl.Un contrôle qui hérite de UserControl est un contrôle qui peut être créé rapidement. Cependant, il n'utilise pas de ControlTemplate et vous ne pouvez pas personnaliser son apparence.

Modèle de composants et d'états

Le modèle de composants et d'états spécifie comment définir la structure visuelle et le comportement visuel d'un contrôle. Pour suivre le modèle de composants et d'états, vous devez procéder comme suit :

  • Définissez la structure visuelle et le comportement visuel dans le ControlTemplate d'un contrôle.

  • Suivez certaines meilleures pratiques lorsque la logique du contrôle interagit avec des composants du modèle de contrôle.

  • Fournissez un contrat de contrôle pour spécifier les éléments à inclure dans le ControlTemplate.

Lorsque vous définissez la structure visuelle et le comportement visuel dans le ControlTemplate d'un contrôle, les auteurs d'application peuvent modifier la structure visuelle et le comportement visuel de votre contrôle en créant un ControlTemplate au lieu d'écrire du code. Vous devez fournir un contrat de contrôle qui indique aux auteurs d'application les objets et les états FrameworkElement qui doivent être définis dans le ControlTemplate. Vous devez appliquer certaines meilleures pratiques lorsque vous interagissez avec les composants du ControlTemplate afin que votre contrôle gère correctement un ControlTemplate incomplet. Si vous appliquez ces trois principes, les auteurs d'application seront en mesure de créer un ControlTemplate pour votre contrôle tout aussi facilement qu'ils le font pour les contrôles fournis avec WPF. La section suivante explique en détail chacune de ces recommandations.

Définition de la structure visuelle et du comportement visuel d'un contrôle dans un ControlTemplate

Lorsque vous créez votre contrôle personnalisé à l'aide du modèle de composants et d'états, vous définissez la structure visuelle et le comportement visuel du contrôle dans son ControlTemplate plutôt que dans sa logique. La structure visuelle d'un contrôle est le composite d'objets FrameworkElement qui constituent le contrôle. Le comportement visuel désigne l'apparence du contrôle dans un état donné. Pour plus d'informations sur la création d'un ControlTemplate qui spécifie la structure visuelle et le comportement visuel d'un contrôle, consultez Personnalisation de l'apparence d'un contrôle existant en créant un ControlTemplate.

Dans l'exemple du contrôle NumericUpDown, la structure visuelle inclut deux contrôles RepeatButton et un TextBlock. Si vous ajoutez ces contrôles au code du contrôle NumericUpDown, dans son constructeur par exemple, les positions de ces contrôles ne peuvent pas être modifiées. Au lieu de définir la structure visuelle et le comportement visuel du contrôle dans son code, vous devez les définir dans le ControlTemplate. Un développeur d'applications peut alors personnaliser la position des boutons et du TextBlock, et spécifier le comportement qui se produit lorsque Value est négative parce que le ControlTemplate peut être remplacé.

L'exemple suivant montre la structure visuelle du contrôle NumericUpDown, qui inclut un RepeatButton pour augmenter Value, un RepeatButton pour diminuer Value et un TextBlock pour afficher Value.

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
              Margin="7,2,2,2" Grid.RowSpan="2" 
              Background="#E0FFFFFF"
              VerticalAlignment="Center" 
              HorizontalAlignment="Stretch">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     Path=Value}"/>
      </Border>

      <RepeatButton Content="Up" Margin="2,5,5,0"
        Name="UpButton"
        Grid.Column="1" Grid.Row="0"/>
      <RepeatButton Content="Down" Margin="2,0,5,5"
        Name="DownButton"
        Grid.Column="1" Grid.Row="1"/>

      <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
        Stroke="Black" StrokeThickness="1"  
        Visibility="Collapsed"/>
    </Grid>

  </Grid>
</ControlTemplate>

Le fait que la valeur apparaisse en rouge lorsqu'elle est négative constitue un comportement visuel du contrôle NumericUpDown. Si vous modifiez le Foreground du TextBlock dans le code lorsque la Value est négative, le NumericUpDown affichera toujours une valeur négative rouge. Vous spécifiez le comportement visuel du contrôle dans le ControlTemplate en ajoutant des objets VisualState au ControlTemplate. L'exemple suivant montre les objets VisualState pour les états Positive et Negative. Positive et Negative s'excluent mutuellement (le contrôle est toujours exactement dans l'un des deux), ainsi l'exemple place les objets VisualState dans un seul VisualStateGroup. Lorsque le contrôle bascule dans l'état Negative, le Foreground du TextBlock devient rouge. Lorsque le contrôle est dans l'état Positive, le Foreground retrouve sa valeur d'origine. La définition des objets VisualState dans un ControlTemplate est examinée plus en détail dans Personnalisation de l'apparence d'un contrôle existant en créant un ControlTemplate.

RemarqueRemarque

Veillez à définir la propriété jointe VisualStateManager.VisualStateGroups sur le FrameworkElement racine de ControlTemplate.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Utilisation de composants du ControlTemplate dans le code

Un auteur ControlTemplate peut omettre les objets FrameworkElement ou VisualState, délibérément ou par erreur, mais la logique de votre contrôle a peut-être besoin de ces composants pour fonctionner correctement. Le modèle de composants et d'états spécifie que votre contrôle doit être résistant à un ControlTemplate auquel il manque les objets FrameworkElement ou VisualState. Votre contrôle ne doit pas lever d'exception ou signaler une erreur s'il manque un FrameworkElement, un VisualState ou un VisualStateGroup dans le ControlTemplate. Cette section décrit les méthodes recommandées pour interagir avec les objets FrameworkElement et gérer des états.

Anticiper des objets FrameworkElement manquants

Lorsque vous définissez des objets FrameworkElement dans le ControlTemplate, la logique de votre contrôle devra peut-être interagir avec certains d'entre eux. Par exemple, le contrôle NumericUpDown s'abonne à l'événement Click des boutons pour augmenter ou diminuer Value et affecte à la propriété Text du TextBlock la valeur Value. Si un ControlTemplate personnalisé omet le TextBlock ou des boutons, il est acceptable que le contrôle perde une partie de ses fonctionnalités, mais vous devez vous assurer que votre contrôle ne provoque pas d'erreur. Par exemple, si un ControlTemplate ne contient pas les boutons pour modifier Value, le NumericUpDown perd cette fonctionnalité, mais une application qui utilise le ControlTemplate continuera à s'exécuter.

Les méthodes suivantes garantissent que votre contrôle répond de manière appropriée à des objets FrameworkElement manquants :

  1. Définissez l'attribut x:Name de chaque FrameworkElement que vous devez référencer dans le code.

  2. Définissez des propriétés privées pour chaque FrameworkElement avec lequel vous devez interagir.

  3. Abonnez-vous et annulez votre abonnement à tous les événements que votre contrôle gère dans l'accesseur set de la propriété FrameworkElement.

  4. Définissez les propriétés FrameworkElement que vous avez définies à l'étape 2 dans la méthode OnApplyTemplate. C'est la première fois que le FrameworkElement dans le ControlTemplate est accessible au contrôle. Utilisez le x:Name du FrameworkElement pour l'obtenir à partir du ControlTemplate.

  5. Vérifiez que le FrameworkElement n'est pas null avant d'accéder à ses membres. S'il est null, ne signalez pas d'erreur.

Les exemples suivants montrent comment le contrôle NumericUpDown interagit avec les objets FrameworkElement conformément aux recommandations dans la liste précédente.

Dans l'exemple qui définit la structure visuelle du contrôle NumericUpDown dans le ControlTemplate, l'attribut x:Name du RepeatButton qui augmente Value a la valeur UpButton. L'exemple suivant déclare une propriété appelée UpButtonElement qui représente le RepeatButton déclaré dans le ControlTemplate. L'accesseur set annule d'abord l'abonnement à l'événement Click du bouton si UpDownElement n'est pas null. Il définit ensuite la propriété, puis s'abonne à l'événement Click. Il existe aussi une propriété, ne figurant pas ici, qui est également définie pour l'autre RepeatButton, nommée DownButtonElement.

Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property
private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}

L'exemple suivant montre le OnApplyTemplate pour le contrôle NumericUpDown. L'exemple utilise la méthode GetTemplateChild pour obtenir les objets FrameworkElement à partir du ControlTemplate. Remarquez que l'exemple évite les cas où GetTemplateChild trouve un FrameworkElement dont le nom spécifié n'est pas du type attendu. Il est également recommandé d'ignorer les éléments qui ont le x:Name spécifié mais dont le type est incorrect.

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

En suivant les méthodes présentées dans les exemples précédents, vous êtes assuré que votre contrôle continuera de s'exécuter s'il manque un FrameworkElement dans le ControlTemplate.

Utiliser le VisualStateManager pour gérer des états

Le VisualStateManager assure le suivi des états d'un contrôle et exécute la logique nécessaire à la transition entre les états. Lorsque vous ajoutez des objets VisualState au ControlTemplate, vous les ajoutez à un VisualStateGroup et vous ajoutez le VisualStateGroup à la propriété jointe VisualStateManager.VisualStateGroups afin que le VisualStateManager puisse y accéder.

L'exemple suivant est similaire à l'exemple précédent qui montre les objets VisualState qui correspondent aux états Positive et Negative du contrôle. Le Storyboard dans le VisualState Negative fait apparaître en rouge le Foreground du TextBlock. Lorsque le contrôle NumericUpDown est dans l'état Negative, le storyboard dans l'état Negative démarre. Puis, le Storyboard dans l'état Negative s'arrête lorsque le contrôle retourne à l'état Positive. Le VisualState Positive n'a pas besoin de contenir de Storyboard car lorsque le Storyboard pour le Negative s'arrête, le Foreground reprend sa couleur d'origine.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Notez qu'un nom est attribué au TextBlock, mais que le TextBlock n'est pas dans le contrat de contrôle pour NumericUpDown parce que la logique du contrôle ne référence jamais le TextBlock. Les éléments référencés dans le ControlTemplate ont des noms, mais ne doivent pas faire partie du contrat de contrôle car un nouveau ControlTemplate pour le contrôle ne doit pas nécessairement référencer cet élément. Par exemple, quelqu'un qui crée un nouveau ControlTemplate pour NumericUpDown peut décider de ne pas indiquer que Value est négative en modifiant le Foreground. Dans ce cas, ni le code ni le ControlTemplate ne référence le TextBlock par nom.

La logique du contrôle est chargée de modifier l'état du contrôle. L'exemple suivant montre que le contrôle NumericUpDown appelle la méthode GoToState pour passer à l'état Positive lorsque Value est supérieure ou égale à 0, et à l'état Negative lorsque Value est inférieure à 0.

If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}

La méthode GoToState exécute la logique nécessaire au démarrage et à l'arrêt appropriés des storyboards. Lorsqu'un contrôle appelle GoToState pour modifier son état, le VisualStateManager effectue les opérations suivantes :

  • Si le VisualState dans lequel bascule le contrôle possède un Storyboard, le storyboard démarre. Puis, si le VisualState d'où provient le contrôle possède un Storyboard, le storyboard s'arrête.

  • Si le contrôle est déjà dans l'état spécifié, GoToState n'effectue aucune action et retourne true.

  • Si l'état spécifié n'existe pas dans le ControlTemplate de control, GoToState n'effectue aucune action et retourne false.

Meilleures pratiques pour utiliser le VisualStateManager

Il est recommandé d'effectuer les opérations suivantes pour gérer les états de votre contrôle :

  • utiliser des propriétés pour suivre son état ;

  • créer une méthode d'assistance pour effectuer la transition entre les états.

Le contrôle NumericUpDown utilise sa propriété Value pour déterminer s'il se trouve dans l'état Positive ou Negative. Le contrôle NumericUpDown définit aussi les états Focused et UnFocused, qui suivent la propriété IsFocused. Si vous utilisez des états qui ne correspondent pas naturellement à une propriété du contrôle, vous pouvez définir une propriété privée pour suivre l'état.

Une méthode unique qui met à jour tous les états centralise les appels au niveau du VisualStateManager et permet de conserver le code gérable. L'exemple suivant présente UpdateStates, la méthode d'assistance du contrôle NumericUpDown. Lorsque Value est supérieure ou égale à 0, le Control est dans l'état Positive. Lorsque Value est inférieure à 0, le contrôle est dans l'état Negative. Lorsque IsFocused est true, le contrôle est dans l'état Focused ; sinon, il est dans l'état Unfocused. Le contrôle peut appeler UpdateStates chaque fois qu'il doit modifier son état, indépendamment des modifications d'états.

Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub
private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (IsFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }

}

Si vous passez un nom d'état à GoToState lorsque le contrôle est déjà dans cet état, GoToState n'a aucun effet, vous n'avez donc pas besoin de vérifier l'état actuel du contrôle. Par exemple, si Value passe d'un nombre négatif à un autre nombre négatif, le storyboard de l'état Negative n'est pas interrompu et l'utilisateur ne verra aucune modification au niveau du contrôle.

Le VisualStateManager utilise des objets VisualStateGroup pour déterminer l'état qu'il doit quitter lorsque vous appelez GoToState. Le contrôle est toujours dans un état donné pour chaque VisualStateGroup défini dans son ControlTemplate et il ne le quitte que lorsqu'il passe à un autre état à partir du même VisualStateGroup. Par exemple, le ControlTemplate du contrôle NumericUpDown définit les objets VisualState Positive et Negative dans un VisualStateGroup et les objets VisualState Focused et Unfocused dans un autre. (Vous pouvez voir le VisualState Focused et Unfocused défini dans la section Exemple complet de cette rubrique. Lorsque le contrôle passe de l'état Positive à l'état Negative, ou vice versa, l'état du contrôle demeure Focused ou Unfocused).

L'état d'un contrôle peut changer dans trois cas typiques :

  • lorsque le ControlTemplate est appliqué au Control ;

  • lorsqu'une propriété est modifiée ;

  • lorsqu'un événement se produit.

Les exemples suivants montrent la mise à jour de l'état du contrôle NumericUpDown dans ces cas-là.

Vous devez mettre à jour l'état du contrôle dans la méthode OnApplyTemplate afin que le contrôle apparaisse dans l'état approprié quand le ControlTemplate est appliqué. L'exemple suivant appelle UpdateStates dans OnApplyTemplate pour garantir que le contrôle est dans les états appropriés. Par exemple, supposez que vous créez un contrôle NumericUpDown, puis que vous affectez la couleur verte à Foreground et la valeur -5 à Value. Si vous n'appelez pas UpdateStates lorsque le ControlTemplate est appliqué au contrôle NumericUpDown, le contrôle n'est pas dans l'état Negative et la valeur est verte au lieu de rouge. Vous devez appeler UpdateStates pour faire passer le contrôle dans l'état Negative.

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

Vous devez souvent mettre à jour les états d'un contrôle lorsqu'une propriété change. L'exemple suivant présente la méthode ValueChangedCallback complète. Comme ValueChangedCallback est appelé lorsque Value est modifiée, la méthode appelle UpdateStates si Value passe d'une valeur positive à une valeur négative, ou vice versa. Il est acceptable d'appeler UpdateStates lorsque Value change mais reste positive ou négative, car, dans ce cas, le contrôle ne change pas d'état.

Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
private static void ValueChangedCallback(DependencyObject obj, 
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, 
            newValue));
}

Vous pourriez également devoir mettre à jour les états lorsqu'un événement se produit. L'exemple suivant montre que le NumericUpDown appelle UpdateStates sur le Control pour gérer l'événement GotFocus.

Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}

Le VisualStateManager vous aide à gérer les états de votre contrôle. En utilisant le VisualStateManager, vous garantissez que votre contrôle effectue les transitions entre états de manière correcte. Si vous suivez les recommandations décrites dans cette section pour utiliser le VisualStateManager, le code de votre contrôle restera lisible et gérable.

Fourniture du contrat de contrôle

Vous fournissez un contrat de contrôle pour indiquer aux auteurs de ControlTemplate les éléments à inclure dans le modèle. Un contrat de contrôle comporte trois éléments :

  • les éléments visuels utilisés par la logique du contrôle ;

  • les états du contrôle et le groupe auquel appartient chaque état ;

  • les propriétés publiques qui affectent visuellement le contrôle.

Quelqu'un qui crée un ControlTemplate doit savoir quels objets FrameworkElement sont utilisés par la logique du contrôle, ainsi que le type et le nom de chaque objet. Un auteur de ControlTemplate doit également savoir le nom de chaque état possible dans lequel le contrôle peut être et le VisualStateGroup dans lequel l'état se trouve.

Si nous reprenons l'exemple de NumericUpDown, le contrôle s'attend à ce que le ControlTemplate possède les objets FrameworkElement suivants :

Le contrôle peut être dans les états suivants :

Pour spécifier les objets FrameworkElement attendus par le contrôle, vous utilisez le TemplatePartAttribute, qui spécifie le nom et type des éléments attendus. Pour spécifier les états possibles d'un contrôle, vous utilisez le TemplateVisualStateAttribute, qui spécifie le nom de l'état et le VisualStateGroup auquel il appartient. Placez le TemplatePartAttribute et le TemplateVisualStateAttribute dans la définition de classe du contrôle.

Toute propriété publique qui affecte l'apparence de votre contrôle fait également partie du contrat de contrôle.

L'exemple suivant spécifie l'objet FrameworkElement et les états d'un contrôle NumericUpDown.

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly BackgroundProperty As DependencyProperty
    Public Shared ReadOnly BorderBrushProperty As DependencyProperty
    Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
    Public Shared ReadOnly FontFamilyProperty As DependencyProperty
    Public Shared ReadOnly FontSizeProperty As DependencyProperty
    Public Shared ReadOnly FontStretchProperty As DependencyProperty
    Public Shared ReadOnly FontStyleProperty As DependencyProperty
    Public Shared ReadOnly FontWeightProperty As DependencyProperty
    Public Shared ReadOnly ForegroundProperty As DependencyProperty
    Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
    Public Shared ReadOnly PaddingProperty As DependencyProperty
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty
    Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty


    Private _Background As Brush
    Public Property Background() As Brush
        Get
            Return _Background
        End Get
        Set(ByVal value As Brush)
            _Background = value
        End Set
    End Property

    Private _BorderBrush As Brush
    Public Property BorderBrush() As Brush
        Get
            Return _BorderBrush
        End Get
        Set(ByVal value As Brush)
            _BorderBrush = value
        End Set
    End Property

    Private _BorderThickness As Thickness
    Public Property BorderThickness() As Thickness
        Get
            Return _BorderThickness
        End Get
        Set(ByVal value As Thickness)
            _BorderThickness = value
        End Set
    End Property

    Private _FontFamily As FontFamily
    Public Property FontFamily() As FontFamily
        Get
            Return _FontFamily
        End Get
        Set(ByVal value As FontFamily)
            _FontFamily = value
        End Set
    End Property

    Private _FontSize As Double
    Public Property FontSize() As Double
        Get
            Return _FontSize
        End Get
        Set(ByVal value As Double)
            _FontSize = value
        End Set
    End Property

    Private _FontStretch As FontStretch
    Public Property FontStretch() As FontStretch
        Get
            Return _FontStretch
        End Get
        Set(ByVal value As FontStretch)
            _FontStretch = value
        End Set
    End Property

    Private _FontStyle As FontStyle
    Public Property FontStyle() As FontStyle
        Get
            Return _FontStyle
        End Get
        Set(ByVal value As FontStyle)
            _FontStyle = value
        End Set
    End Property

    Private _FontWeight As FontWeight
    Public Property FontWeight() As FontWeight
        Get
            Return _FontWeight
        End Get
        Set(ByVal value As FontWeight)
            _FontWeight = value
        End Set
    End Property

    Private _Foreground As Brush
    Public Property Foreground() As Brush
        Get
            Return _Foreground
        End Get
        Set(ByVal value As Brush)
            _Foreground = value
        End Set
    End Property

    Private _HorizontalContentAlignment As HorizontalAlignment
    Public Property HorizontalContentAlignment() As HorizontalAlignment
        Get
            Return _HorizontalContentAlignment
        End Get
        Set(ByVal value As HorizontalAlignment)
            _HorizontalContentAlignment = value
        End Set
    End Property

    Private _Padding As Thickness
    Public Property Padding() As Thickness
        Get
            Return _Padding
        End Get
        Set(ByVal value As Thickness)
            _Padding = value
        End Set
    End Property

    Private _TextAlignment As TextAlignment
    Public Property TextAlignment() As TextAlignment
        Get
            Return _TextAlignment
        End Get
        Set(ByVal value As TextAlignment)
            _TextAlignment = value
        End Set
    End Property

    Private _TextDecorations As TextDecorationCollection
    Public Property TextDecorations() As TextDecorationCollection
        Get
            Return _TextDecorations
        End Get
        Set(ByVal value As TextDecorationCollection)
            _TextDecorations = value
        End Set
    End Property

    Private _TextWrapping As TextWrapping
    Public Property TextWrapping() As TextWrapping
        Get
            Return _TextWrapping
        End Get
        Set(ByVal value As TextWrapping)
            _TextWrapping = value
        End Set
    End Property

    Private _VerticalContentAlignment As VerticalAlignment
    Public Property VerticalContentAlignment() As VerticalAlignment
        Get
            Return _VerticalContentAlignment
        End Get
        Set(ByVal value As VerticalAlignment)
            _VerticalContentAlignment = value
        End Set
    End Property
End Class
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}

Exemple complet

L'exemple suivant illustre le ControlTemplate complet pour le NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NumericUpDown">
          <Grid  Margin="3" 
                Background="{TemplateBinding Background}">


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

                <!--Make the Value property red when it is negative.-->
                <VisualState Name="Negative">
                  <Storyboard>
                    <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <VisualState Name="Positive"/>
              </VisualStateGroup>

              <VisualStateGroup Name="FocusStates">

                <!--Add a focus rectangle to highlight the entire control
                    when it has focus.-->
                <VisualState Name="Focused">
                  <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                   Storyboard.TargetProperty="Visibility" Duration="0">
                      <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                          <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                      </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>

                <!--Return the control to its initial state by
                    hiding the focus rectangle.-->
                <VisualState Name="Unfocused"/>
              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid>
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" 
                Margin="7,2,2,2" Grid.RowSpan="2" 
                Background="#E0FFFFFF"
                VerticalAlignment="Center" 
                HorizontalAlignment="Stretch">
                <!--Bind the TextBlock to the Value property-->
                <TextBlock Name="TextBlock"
                  Width="60" TextAlignment="Right" Padding="5"
                  Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type local:NumericUpDown}}, 
                                 Path=Value}"/>
              </Border>

              <RepeatButton Content="Up" Margin="2,5,5,0"
                Name="UpButton"
                Grid.Column="1" Grid.Row="0"/>
              <RepeatButton Content="Down" Margin="2,0,5,5"
                Name="DownButton"
                Grid.Column="1" Grid.Row="1"/>

              <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                Stroke="Black" StrokeThickness="1"  
                Visibility="Collapsed"/>
            </Grid>

          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

L'exemple suivant illustre la logique pour le NumericUpDown.

Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs
    Private _value As Integer

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        _value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
        Get
            Return _value
        End Get
    End Property
End Class
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);

            }
        }

        private static void ValueChangedCallback(DependencyObject obj, 
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, 
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }


        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }


        private void UpdateStates(bool useTransitions)
        {
            if (Value >= 0)
            {
                VisualStateManager.GoToState(this, "Positive", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Negative", useTransitions);
            }

            if (IsFocused)
            {
                VisualStateManager.GoToState(this, "Focused", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Unfocused", useTransitions);
            }

        }

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

            set
            {
                if (downButtonElement != null)
                {
                    downButtonElement.Click -=
                        new RoutedEventHandler(downButtonElement_Click);
                }
                downButtonElement = value;

                if (downButtonElement != null)
                {
                    downButtonElement.Click +=
                        new RoutedEventHandler(downButtonElement_Click);
                }
            }
        }

        void downButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value--;
        }

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

            set
            {
                if (upButtonElement != null)
                {
                    upButtonElement.Click -=
                        new RoutedEventHandler(upButtonElement_Click);
                }
                upButtonElement = value;

                if (upButtonElement != null)
                {
                    upButtonElement.Click +=
                        new RoutedEventHandler(upButtonElement_Click);
                }
            }
        }

        void upButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value++;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Focus();
        }


        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            UpdateStates(true);
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);
            UpdateStates(true);
        }
    }


    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}

Voir aussi

Concepts

Personnalisation de l'apparence d'un contrôle existant en créant un ControlTemplate

Autres ressources

Personnalisation des contrôles