Partager via


Création d’un contrôle qui a 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’un CheckBox au-delà de ce que feront les propriétés de définition en créant une nouvelle ControlTemplate. L’illustration suivante montre un CheckBox qui utilise une ControlTemplate par défaut et un CheckBox qui utilise un ControlTemplatepersonnalisé.

Une case à cocher avec le modèle de contrôle par défaut. Une case à cocher qui utilise le modèle de contrôle par défaut

Une case à cocher avec un modèle de contrôle personnalisé. Une case à cocher qui utilise un modèle de contrôle personnalisé

Si vous suivez les parties et le modèle d’états lorsque vous créez un contrôle, l’apparence de votre contrôle est personnalisable. Les outils de concepteur tels que Blend pour Visual Studio prennent en charge les parties et le modèle d’états. Par conséquent, lorsque vous suivez ce modèle, votre contrôle sera personnalisable dans ces types d’applications. Cette rubrique décrit les parties et le modèle d’états et explique comment le suivre lorsque vous créez votre propre contrôle. Cette rubrique 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 le contrôle NumericUpDown décrit dans cette rubrique.

Contrôle personnalisé NumericUpDown. Un contrôle personnalisé NumericUpDown

Cette rubrique contient les sections suivantes :

Conditions préalables

Cette rubrique suppose que vous savez comment créer une nouvelle ControlTemplate pour un contrôle existant, connaître les éléments d’un contrat de contrôle et comprendre les concepts abordés dans Créer un modèle pour un contrôle.

Note

Pour créer un contrôle qui peut avoir son apparence 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, mais il n’utilise pas de ControlTemplate et vous ne pouvez pas personnaliser son apparence.

Modèle des parties et des états

Le modèle de parties et d’états spécifie comment définir la structure visuelle et le comportement visuel d’un contrôle. Pour suivre le modèle des parties et des états, vous devez effectuer les opérations suivantes :

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

  • Suivez certaines bonnes pratiques lorsque la logique de votre contrôle interagit avec des parties du modèle de contrôle.

  • Fournissez un contrat de contrôle pour spécifier ce qui doit être inclus dans le ControlTemplate.

Lorsque vous définissez la structure visuelle et le comportement visuel dans le ControlTemplate d’un contrôle, les auteurs d’applications peuvent modifier la structure visuelle et le comportement visuel de votre contrôle en créant une nouvelle ControlTemplate au lieu d’écrire du code. Vous devez fournir un contrat de contrôle qui indique aux auteurs d’applications quels objets et états FrameworkElement doivent être définis dans le ControlTemplate. Vous devez suivre certaines bonnes pratiques lorsque vous interagissez avec les parties de l'ControlTemplate afin que votre contrôle gère correctement une ControlTemplateincomplète. Si vous suivez ces trois principes, les auteurs d’applications pourront créer un ControlTemplate pour votre contrôle, tout aussi facilement que pour les contrôles fournis avec WPF. La section suivante décrit chacune de ces recommandations en détail.

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 des parties et du modèle d’états, vous définissez la structure visuelle et le comportement visuel du contrôle dans son ControlTemplate au lieu de sa logique. La structure visuelle d’un contrôle est le composite des objets FrameworkElement qui composent le contrôle. Le comportement visuel est la façon dont le contrôle apparaît lorsqu’il est dans un certain état. 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 Créer un modèle pour un contrôle.

Dans l’exemple du contrôle NumericUpDown, la structure visuelle comprend deux contrôles RepeatButton et un TextBlock. Si vous ajoutez ces contrôles dans le code du contrôle NumericUpDown (dans son constructeur, par exemple), les positions de ces contrôles ne pourront pas être modifiées. Au lieu de définir la structure visuelle et le comportement visuel du contrôle dans son code, vous devez le définir dans le ControlTemplate. Ensuite, un développeur d’applications pour personnaliser la position des boutons et TextBlock et spécifier le comportement qui se produit lorsque Value est négatif, car 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 Valueet 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>

Un comportement visuel du contrôle NumericUpDown est que la valeur se trouve dans une police rouge si elle est négative. Si vous modifiez la Foreground du TextBlock dans le code lorsque la Value est négative, la NumericUpDown affiche 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 dans l’un des deux), de sorte que l’exemple place les objets VisualState dans un seul VisualStateGroup. Lorsque le contrôle passe à l’état Negative, le Foreground du TextBlock devient rouge. Lorsque le contrôle est dans l’état Positive, le Foreground retourne à sa valeur d’origine. La définition d’objets VisualState dans un ControlTemplate est décrite plus en détail dans Créer un modèle pour un contrôle.

Note

Veillez à définir la propriété jointe VisualStateManager.VisualStateGroups sur la racine FrameworkElement du 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 parties du ControlTemplate dans le code

Un auteur de ControlTemplate peut omettre des objets FrameworkElement ou VisualState, délibérément ou par erreur, mais il se peut que la logique de votre contrôle nécessite ces parties pour fonctionner correctement. Le modèle de parties et d’états spécifie que votre contrôle doit être résilient dans un contexte où ControlTemplate manque d’objets FrameworkElement ou VisualState. Votre contrôle ne doit pas lever d’exception ou signaler une erreur si un FrameworkElement, VisualState ou VisualStateGroup est manquant dans le ControlTemplate. Cette section décrit les pratiques recommandées pour interagir avec les objets FrameworkElement et la gestion des états.

Anticipez les objets FrameworkElement manquants

Lorsque vous définissez FrameworkElement objets dans le ControlTemplate, la logique de votre contrôle peut avoir besoin d’interagir avec certaines d’entre elles. Par exemple, le contrôle NumericUpDown s’abonne à l’événement Click des boutons pour augmenter ou diminuer Value et définit la propriété Text du TextBlock sur Value. Si une ControlTemplate personnalisée omet les TextBlock ou boutons, il est acceptable que le contrôle perde certaines de ses fonctionnalités, mais vous devez être sûr 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 pratiques suivantes garantissent que votre contrôle répond correctement aux objets FrameworkElement manquants :

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

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

  3. Abonnez-vous et désabonnez-vous de 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 le moment le plus précoce où le FrameworkElement dans le ControlTemplate est disponible pour le contrôle. Utilisez le x:Name du FrameworkElement pour l’obtenir 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 de la liste précédente.

Dans l’exemple qui définit la structure visuelle du contrôle NumericUpDown dans le ControlTemplate, l'RepeatButton qui augmente Value a son attribut x:Name défini sur UpButton. L’exemple suivant déclare une propriété appelée UpButtonElement qui représente la RepeatButton déclarée dans le ControlTemplate. L'accesseur set commence par se désinscrire de l'événement Click du bouton si UpDownElement n'est pas null, définit ensuite la propriété, puis s'abonne à l'événement Click. Il existe également une propriété définie, mais pas affichée ici, pour l’autre RepeatButton, appelée DownButtonElement.

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);
        }
    }
}
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

L’exemple suivant montre l’OnApplyTemplate pour le contrôle NumericUpDown. L’exemple utilise la méthode GetTemplateChild pour obtenir les objets FrameworkElement à partir du ControlTemplate. Notez que l’exemple protège contre les cas où GetTemplateChild trouve un FrameworkElement portant le nom spécifié qui n’est pas du type attendu. Il est également recommandé d’ignorer les éléments qui ont la x:Name spécifiée, mais qui sont de type incorrect.

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

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

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

    UpdateStates(False)
End Sub

En suivant les pratiques présentées dans les exemples précédents, vous vous assurez que votre contrôle continuera de fonctionner lorsque l'ControlTemplate est absent d'un FrameworkElement.

Utiliser VisualStateManager pour gérer les états

Le VisualStateManager effectue le suivi des états d’un contrôle et effectue la logique nécessaire à la transition entre les états. Lorsque vous ajoutez des objets VisualState au ControlTemplate, vous les ajoutez à un VisualStateGroup et ajoutez le VisualStateGroup à la propriété jointe VisualStateManager.VisualStateGroups afin que le VisualStateManager y ait accès.

L’exemple suivant répète l’exemple précédent qui montre les objets VisualState qui correspondent aux états Positive et Negative du contrôle. Le Storyboard dans le NegativeVisualState rend le Foreground du TextBlock rouge. Lorsque le contrôle NumericUpDown est dans l’état Negative, le plan conceptuel dans l’état Negative commence. Ensuite, le Storyboard dans l’état Negative s’arrête lorsque le contrôle revient à l’état Positive. L'PositiveVisualState n’a pas besoin de contenir un Storyboard, car lorsque le Storyboard de l'Negative s’arrête, le Foreground retourne à 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 que le TextBlock a un nom, mais que le TextBlock n’est pas dans le contrat de contrôle pour NumericUpDown, car la logique du contrôle ne fait jamais référence à la 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 nouvel ControlTemplate pour le contrôle n’a peut-être pas besoin de référencer cet élément. Par exemple, une personne qui crée une nouvelle 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 désigne le TextBlock par son nom.

La logique du contrôle est responsable de la modification de l’état du contrôle. L’exemple suivant montre que le contrôle NumericUpDown appelle la méthode GoToState pour entrer dans l’état Positive lorsque Value est égal à 0 ou supérieur, et l’état Negative lorsque Value est inférieur à 0.

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

La méthode GoToState effectue la logique nécessaire pour démarrer et arrêter les storyboards de manière appropriée. Lorsqu’un contrôle appelle GoToState pour modifier son état, l'VisualStateManager effectue les opérations suivantes :

  • Si le VisualState vers lequel le contrôle se dirige possède un Storyboard, le plan conceptuel commence. Ensuite, si le VisualState d’où provient le contrôle a un Storyboard, le plan conceptuel se termine.

  • 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 la ControlTemplate de control, GoToState n’effectue aucune action et retourne false.

Meilleures pratiques pour l’utilisation de VisualStateManager

Il est recommandé d’effectuer les opérations suivantes pour maintenir les états de votre contrôle :

  • Utilisez des propriétés pour suivre son état.

  • Créez une méthode d’assistance pour passer d’un état à l’autre.

Le contrôle NumericUpDown utilise sa propriété Value pour suivre s’il se trouve dans l’état Positive ou Negative. Le contrôle NumericUpDown définit également les états Focused et UnFocused, qui assurent le suivi de 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 afin de suivre l'état.

Une méthode unique qui met à jour tous les états centralise les appels au VisualStateManager et conserve votre code gérable. L’exemple suivant montre la méthode d'aide du contrôle NumericUpDown, UpdateStates. Lorsque Value est supérieur ou égal à 0, la Control est dans l’état Positive. Lorsque Value est inférieur à 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, quel que soit l’état qui change.

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);
    }
}
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

Si vous passez un nom d’état à GoToState lorsque le contrôle est déjà dans cet état, GoToState ne fait rien, 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 voit pas de modification dans le contrôle.

Le VisualStateManager utilise des objets VisualStateGroup pour déterminer quel état quitter lorsque vous appelez GoToState. Le contrôle est toujours dans un seul état pour chaque VisualStateGroup défini dans son ControlTemplate et ne quitte un état que pour passer à un autre état du même VisualStateGroup. Par exemple, le ControlTemplate du contrôle NumericUpDown définit les objets Positive et NegativeVisualState dans un VisualStateGroup et les objets Focused et UnfocusedVisualState dans un autre. (Vous pouvez voir les Focused et les UnfocusedVisualState définis dans la section Exemple complet de cette rubrique Lorsque le contrôle passe de l’état Positive à l’état Negative, ou inversement, le contrôle reste dans l’état Focused ou Unfocused.

Il existe trois endroits typiques où l’état d’un contrôle peut changer :

  • Lorsque le ControlTemplate est appliqué au Control.

  • Lorsqu’une propriété change.

  • Lorsqu’un événement se produit.

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

Vous devez mettre à jour l’état du contrôle dans la méthode OnApplyTemplate afin que le contrôle apparaisse dans l’état correct lorsque le ControlTemplate est appliqué. L’exemple suivant appelle UpdateStates dans OnApplyTemplate pour s’assurer que le contrôle est dans les états appropriés. Par exemple, supposons que vous créez un contrôle NumericUpDown, puis définissez son Foreground sur vert et Value sur -5. 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 placer le contrôle dans l’état Negative.

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

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

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

    UpdateStates(False)
End Sub

Vous devez souvent mettre à jour les états d’un contrôle lorsqu’une propriété change. L’exemple suivant montre l’ensemble de la méthode ValueChangedCallback. Étant donné que ValueChangedCallback est appelé lorsque Value change, la méthode appelle UpdateStates au cas où Value changé de positif à négatif ou inversement. Il est acceptable d’appeler UpdateStates lorsque Value change, mais reste positif ou négatif, car dans ce cas, le contrôle ne changera pas les états.

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));
}
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

Vous devrez peut-être également mettre à jour les états lorsqu’un événement se produit. L’exemple suivant montre que le NumericUpDown appelle le UpdateStates sur le Control pour gérer l’événement GotFocus.

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

Le VisualStateManager vous aide à gérer les états de votre contrôle. En utilisant le VisualStateManager, vous assurez que votre contrôle passe correctement entre les états. Si vous suivez les recommandations décrites dans cette section pour utiliser l'VisualStateManager, le code de votre contrôle reste lisible et gérable.

Fourniture du contrat de contrôle

Vous fournissez un contrat de commande afin que les auteurs ControlTemplate sachent quoi mettre dans le modèle. Un contrat de contrôle comporte trois éléments :

  • Éléments visuels que la logique du contrôle utilise.

  • États du contrôle et du groupe auquel appartient chaque état.

  • Les propriétés publiques qui ont un effet visuel sur le contrôle.

Quelqu’un qui crée une nouvelle ControlTemplate doit savoir quels objets FrameworkElement la logique du contrôle utilise, quel type chaque objet est et quel est son nom. L’auteur ControlTemplate doit également connaître le nom de chaque état possible dans lequel le contrôle peut se trouver, ainsi que l’état VisualStateGroup dans lequel il se trouve.

En retournant à l’exemple NumericUpDown, le contrôle s’attend à ce que le ControlTemplate dispose des objets FrameworkElement suivants :

Le contrôle peut se trouver dans les états suivants :

Pour spécifier quels objets FrameworkElement le contrôle attend, vous utilisez le TemplatePartAttribute, qui détermine le nom et le type des éléments attendus. Pour spécifier les états possibles d’un contrôle, vous utilisez l'TemplateVisualStateAttribute, qui spécifie le nom de l’état et le VisualStateGroup auquel il appartient. Placez les TemplatePartAttribute et TemplateVisualStateAttribute sur 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 et les états FrameworkElement pour le contrôle NumericUpDown.

[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; }
}
<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 TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

Exemple complet

L’exemple suivant présente la totalité de ControlTemplate pour le contrôle NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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 montre la logique de l'NumericUpDown.

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; }
        }
    }
}
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

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

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

Voir aussi