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’un CheckBox paramètre au-delà de ce que feront les propriétés en créant un nouveau ControlTemplate. L’illustration suivante montre un CheckBox qui utilise une valeur par défaut ControlTemplate et un CheckBox qui utilise un personnalisé ControlTemplate.

A checkbox with the default control template. CheckBox qui utilise le modèle de contrôle par défaut

A checkbox with a custom control template. CheckBox 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é, NumericUpDownpour illustrer la philosophie de ce modèle. Le NumericUpDown contrôle 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 NumericUpDown contrôle abordé dans cette rubrique.

NumericUpDown custom control. Un contrôle NumericUpDown personnalisé

Cette rubrique contient les sections suivantes :

Prérequis

Cette rubrique suppose que vous savez comment créer un ControlTemplate 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.

Remarque

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 Control classe ou de l’une de ses sous-classes autres que UserControl. Un contrôle qui hérite UserControl est un contrôle qui peut être créé rapidement, mais il n’utilise pas un ControlTemplate contrôle et vous ne pouvez pas personnaliser son apparence.

Modèles de parties et d’é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 les parties et le modèle d’états, vous devez effectuer les opérations suivantes :

  • Définissez la structure visuelle et le comportement visuel dans le ControlTemplate 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 contrôle, les auteurs d’applications peuvent modifier la structure visuelle et le comportement visuel de votre contrôle en créant un nouveau ControlTemplate au lieu d’écrire du code. Vous devez fournir un contrat de contrôle qui indique aux auteurs d’applications quels FrameworkElement objets et états doivent être définis dans le ControlTemplate. Vous devez suivre certaines bonnes pratiques lorsque vous interagissez avec les parties dans le ControlTemplate cas où votre contrôle gère correctement un élément incomplet ControlTemplate. Si vous suivez ces trois principes, les auteurs d’applications pourront créer un ControlTemplate contrôle aussi facilement que possible 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 du modèle de parties et d’états, vous définissez la structure visuelle et le comportement visuel du contrôle au ControlTemplate lieu de sa logique. La structure visuelle d’un contrôle est le composite des FrameworkElement objets 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 élément 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 NumericUpDown contrôle, la structure visuelle comprend deux RepeatButton contrôles et un TextBlock. Si vous ajoutez ces contrôles dans le code de son NumericUpDown constructeur, par exemple, les positions de ces contrôles ne sont pas modifiables. 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 spécifier le comportement qui se produit lorsqu’il Value est négatif, car il ControlTemplate peut être remplacé.

L’exemple suivant montre la structure visuelle du NumericUpDown contrôle, qui inclut une RepeatButton augmentation, une RepeatButton pour Valuediminuer Valueet une 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 NumericUpDown contrôle est que la valeur se trouve dans une police rouge si elle est négative. Si vous modifiez le ForegroundTextBlock code lorsque le Value code est négatif, il NumericUpDown affiche toujours une valeur négative rouge. Vous spécifiez le comportement visuel du contrôle dans le ControlTemplate contrôle en ajoutant des VisualState objets au ControlTemplate. L’exemple suivant montre les VisualState objets pour les états et Negative les Positive états. Positive et Negative sont mutuellement exclusifs (le contrôle est toujours dans exactement l’un des deux), de sorte que l’exemple place les VisualState objets dans un seul VisualStateGroup. Lorsque le contrôle passe à l’état Negative , le ForegroundTextBlock rouge devient rouge. Lorsque le contrôle est dans l’état Positive , le Foreground retour à sa valeur d’origine. La définition d’objets VisualState dans un objet ControlTemplate est décrite plus loin dans Créer un modèle pour un contrôle.

Remarque

Veillez à définir la VisualStateManager.VisualStateGroups propriété jointe à 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 ControlTemplate auteur peut omettre FrameworkElement ou VisualState objets, de manière délibérée ou par erreur, mais la logique de votre contrôle peut nécessiter que ces parties fonctionnent correctement. Le modèle de parties et d’états spécifie que votre contrôle doit être résilient à un ControlTemplate objet manquant ou VisualState manquantFrameworkElement. Votre contrôle ne doit pas lever d’exception ou signaler une erreur si un FrameworkElement, VisualStateou VisualStateGroup est manquant dans le ControlTemplatefichier . Cette section décrit les pratiques recommandées pour interagir avec FrameworkElement les objets et gérer les états.

Anticiper les objets FrameworkElement manquants

Lorsque vous définissez des FrameworkElement objets dans la ControlTemplatelogique de votre contrôle, vous devrez peut-être interagir avec certains d’entre eux. Par exemple, le NumericUpDown contrôle s’abonne à l’événement des Click boutons pour augmenter ou diminuer Value et définir la Text propriété sur ValueTextBlock . Si un bouton personnalisé ControlTemplate omet les boutons ou les TextBlock boutons, il est acceptable que le contrôle perde certaines de ses fonctionnalités, mais vous devez être sûr que votre contrôle n’entraîne pas d’erreur. Par exemple, si un ControlTemplate bouton ne contient pas les boutons à modifier Value, la NumericUpDown perte de cette fonctionnalité, mais une application qui utilise le ControlTemplate fichier continuera à s’exécuter.

Les pratiques suivantes garantissent que votre contrôle répond correctement aux objets manquants FrameworkElement :

  1. Définissez l’attribut x:Name pour chaque FrameworkElement référence dans le code.

  2. Définissez des propriétés privées pour chacune FrameworkElement dont vous avez besoin pour interagir.

  3. Abonnez-vous à tous les événements gérés par votre contrôle dans l’accesseur défini de la FrameworkElement propriété.

  4. Définissez les propriétés que vous avez définies à l’étape FrameworkElement 2 de la OnApplyTemplate méthode. Il s’agit du plus tôt que le FrameworkElementControlTemplate contrôle soit disponible. Utilisez le x:Name fichier FrameworkElement pour l’obtenir à partir du ControlTemplate.

  5. Vérifiez que le n’est FrameworkElement pas null avant d’accéder à ses membres. Si c’est nullle cas, ne signalez pas d’erreur.

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

Dans l’exemple qui définit la structure visuelle du NumericUpDown contrôle dans le ControlTemplate, l’attribut RepeatButton augmente Value a la x:Name valeur UpButton. L’exemple suivant déclare une propriété appelée UpButtonElement qui représente celle RepeatButton déclarée dans le ControlTemplate. L’accesseur set se désinscrit d’abord de l’événement du Click bouton si UpDownElement ce n’est pas nullle cas, il définit 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 NumericUpDown suivant montre le OnApplyTemplate contrôle. L’exemple utilise la GetTemplateChild méthode pour obtenir les FrameworkElement objets de l’objet ControlTemplate. Notez que l’exemple protège contre les cas où GetTemplateChild trouve un FrameworkElement nom spécifié qui n’est pas du type attendu. Il est également recommandé d’ignorer les éléments qui ont le type spécifié x:Name , 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 affichées dans les exemples précédents, vous vous assurez que votre contrôle continuera à s’exécuter quand il ControlTemplate manque un FrameworkElement.

Utiliser VisualStateManager pour gérer les états

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

L’exemple suivant répète l’exemple précédent qui montre les VisualState objets qui correspondent aux états et Negative aux Positive états du contrôle. Les Storyboard tournures ForegroundNegativeVisualState du TextBlock rouge. Lorsque le NumericUpDown contrôle est dans l’état Negative , le storyboard dans l’état Negative commence. Ensuite, l’état StoryboardNegative s’arrête lorsque le contrôle revient à l’état Positive . Il PositiveVisualState n’est pas nécessaire de contenir une Storyboard valeur, car lorsque l’arrêt StoryboardNegative est arrêté, le Foreground retour à 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 nom est donné, mais qu’il TextBlock n’est pas dans le contrat de contrôle car NumericUpDown la logique du contrôle ne fait jamais référence à l’élément TextBlock. Les éléments référencés dans les ControlTemplate noms ont des noms, mais n’ont pas besoin de faire partie du contrat de contrôle, car un nouveau ControlTemplate pour le contrôle n’a peut-être pas besoin de 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 qu’il Value est négatif en modifiant le Foreground. Dans ce cas, ni le code ni les ControlTemplate références par TextBlock nom.

La logique du contrôle est responsable de la modification de l’état du contrôle. L’exemple suivant montre que le NumericUpDown contrôle appelle la GoToState méthode pour entrer dans l’état Positive lorsque Value la valeur est 0 ou supérieure, et que l’état Negative est Value 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 GoToState méthode 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, procédez VisualStateManager comme suit :

  • VisualState Si le contrôle va avoir un Storyboard, le storyboard commence. Ensuite, si le VisualState contrôle provient d’un Storyboard, le storyboard 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 le ControlTemplatecontrolfichier , 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 NumericUpDown contrôle utilise sa Value propriété pour suivre s’il est dans l’état ou Negative dans l’étatPositive. Le NumericUpDown contrôle définit également les états et UnFocused les Focused états, qui effectue le suivi de la IsFocused propriété. 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 vers le VisualStateManager code et conserve votre code gérable. L’exemple suivant montre la NumericUpDown méthode d’assistance du contrôle. UpdateStates Lorsqu’il Value est supérieur ou égal à 0, il Control est dans l’état Positive . Quand Value la valeur est inférieure à 0, le contrôle est dans l’état Negative . Quand IsFocused c’est truele cas, 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 lorsque GoToState le contrôle est déjà dans cet état, ne fait rien, GoToState vous n’avez donc pas besoin de case activée pour l’état actuel du contrôle. Par exemple, si Value les modifications d’un nombre négatif à un autre nombre négatif, le storyboard pour l’état Negative n’est pas interrompu et l’utilisateur ne voit pas de modification dans le contrôle.

Les VisualStateManager objets utilisent VisualStateGroup des objets pour déterminer l’état à quitter lorsque vous appelez GoToState. Le contrôle est toujours dans un état pour chaque VisualStateGroup qui est défini dans son ControlTemplate et laisse uniquement un état lorsqu’il passe dans un autre état du même VisualStateGroup. Par exemple, le ControlTemplateNumericUpDown contrôle définit les objets et NegativeVisualState les Positive objets dans un etVisualStateUnfocusedFocused les objets d’un VisualStateGroup autre. (Vous pouvez voir la section Exemple complet de cette rubrique etVisualStateUnfocusedla Focused définir lorsque le contrôle passe de l’état Positive à l’étatNegative, ou inversement, le contrôle reste dans l’état ou Unfocused dans l’étatFocused.

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

  • Lorsque l’objet ControlTemplate est appliqué à l’objet Control.

  • Lorsqu’une propriété change.

  • Lorsqu’un événement se produit.

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

Vous devez mettre à jour l’état du contrôle dans la OnApplyTemplate méthode afin que le contrôle apparaisse dans l’état correct lorsque celui-ci ControlTemplate est appliqué. L’exemple suivant appelle UpdateStatesOnApplyTemplate pour s’assurer que le contrôle se trouve dans les états appropriés. Par exemple, supposons que vous créez un NumericUpDown contrôle, puis définissez-le Foreground sur vert et Value sur -5. Si vous n’appelez UpdateStates pas lorsque le ControlTemplate contrôle est appliqué 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 la méthode entière ValueChangedCallback . Étant donné qu’elle ValueChangedCallback est appelée lorsque Value des modifications sont apportées, la méthode appelle UpdateStates en cas Value de changement positif à négatif ou inversement. Il est acceptable d’appeler UpdateStates lorsque Value les modifications restent positives ou négatives, car dans ce cas, le contrôle ne change 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 les NumericUpDown appels sur le Control serveur de gestion de l’événement GotFocusUpdateStates.

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

Vous VisualStateManager pouvez ainsi 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 le VisualStateManagercode de votre contrôle, le code de votre contrôle reste lisible et gérable.

Fourniture du contrat de contrôle

Vous fournissez un contrat de contrôle afin que ControlTemplate les auteurs sachent quoi placer 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 nouveau ControlTemplate doit savoir quels FrameworkElement objets la logique du contrôle utilise, le type de chaque objet et son nom. Un ControlTemplate auteur doit également connaître le nom de chaque état possible dans lequel le contrôle peut se trouver et dans lequel VisualStateGroup il se trouve.

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

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

Pour spécifier les FrameworkElement objets attendus par le contrôle, vous utilisez le TemplatePartAttribute, qui spécifie le nom et le 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 auquel VisualStateGroup il appartient. Placez et placez la TemplatePartAttributeTemplateVisualStateAttribute 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 FrameworkElement états du NumericUpDown contrôle.

[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 est l’intégralité ControlTemplate du NumericUpDown contrôle.

<!--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 pour le 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