Retour à l'accueil

Contrôles WPF personnalisés : Styles et propriétés attachées

L'article décrit des méthodes pour créer des contrôles WPF personnalisés sans héritage et UserControl. Il couvre les propriétés attachées pour paramétrer les styles, l'implémentation de bascules animées via VisualStateManager, et l'architecture du système de thèmes.

Mise en forme des contrôles WPF : Boutons et bascules sans code-behind
Advertisement 728x90

Création de contrôles WPF personnalisés : stylisation de boutons et interrupteurs sans héritage

Lors du développement d'applications WPF, il est souvent nécessaire d'adapter les contrôles standard à une charte graphique d'entreprise sans acheter de bibliothèques commerciales. Au lieu de créer des UserControls personnalisés ou d'hériter des classes de base, il est plus efficace d'utiliser des styles et des propriétés attachées — cela préserve la compatibilité avec le framework et simplifie la maintenance.

Architecture du système de thèmes dans WPF

Pour gérer l'apparence de l'application, il est recommandé de centraliser toutes les ressources dans un dossier Style. Chaque thème (par ex., clair et sombre) est configuré comme un répertoire séparé contenant un fichier de palette de couleurs et le dictionnaire de ressources principal. Cela permet un changement de thème facile à l'exécution en remplaçant un seul ResourceDictionary dans Application.Resources.

L'implémentation du changement de thème ressemble à ceci :

Google AdInline article slot
public void ChangeTheme(Theme theme)
{
    if (CurrentTheme == theme) return;

    CurrentTheme = theme;
    Uri themeSource = theme switch
    {
        Theme.Dark => new Uri("pack://application:,,,/CustomWpfControls;component/Style/DarkTheme/DarkTheme.xaml", UriKind.Absolute),
        Theme.Light => new Uri("pack://application:,,,/CustomWpfControls;component/Style/LightTheme/LightTheme.xaml", UriKind.Absolute),
        _ => throw new ArgumentOutOfRangeException(nameof(theme))
    };

    var dictionary = Resources.MergedDictionaries[0];
    dictionary.Clear();
    dictionary.Source = themeSource;
}

Cette approche garantit que tous les éléments d'interface utilisateur mettent à jour instantanément leur apparence sans recharger la fenêtre.

Stylisation des boutons avec des propriétés attachées

Au lieu de créer une classe RoundedButton personnalisée, il est préférable de définir une propriété attachée CornerRadius dans une classe statique. Cela permet de définir le rayon des coins directement en XAML sans violer le principe de composition :

public static class RoundedButton
{
    public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.RegisterAttached(
        "CornerRadius",
        typeof(CornerRadius),
        typeof(RoundedButton),
        new FrameworkPropertyMetadata(new CornerRadius(0d), 
            FrameworkPropertyMetadataOptions.AffectsMeasure | 
            FrameworkPropertyMetadataOptions.AffectsRender));

    public static void SetCornerRadius(UIElement element, CornerRadius value) =>
        element.SetValue(CornerRadiusProperty, value);

    public static CornerRadius GetCornerRadius(UIElement element) =>
        (CornerRadius)element.GetValue(CornerRadiusProperty);
}

Dans le modèle de style du bouton, cette propriété est utilisée via une liaison au TemplatedParent :

Google AdInline article slot
<Border CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(customWpfControls:RoundedButton.CornerRadius)}" ... />

Avantages de cette approche :

  • Pas besoin d'écrire du code-behind supplémentaire
  • Compatibilité totale avec Button préservée
  • Possibilité de surcharger le rayon à la fois dans le style et au niveau de l'instance
  • Support des animations et des déclencheurs d'état

Implémentation de ToggleButton avec un interrupteur animé

Un interrupteur personnalisé imitant le style iOS peut être entièrement implémenté via ControlTemplate sans une seule ligne de C#. Composants principaux du modèle :

  • Canvas pour le positionnement précis des éléments
  • DrawingImage pour les icônes (checkmark et cercle)
  • Border avec RenderTransform pour le mouvement animé
  • VisualStateManager pour répondre à Checked/Unchecked

L'animation de déplacement est implémentée via DoubleAnimation à l'intérieur de Storyboard :

Google AdInline article slot
<DoubleAnimation Duration="0:0:0.1"
                 To="20"
                 AccelerationRatio="0.2"
                 DecelerationRatio="0.7"
                 Storyboard.TargetName="Switcher"
                 Storyboard.TargetProperty="(RenderTransform).(TranslateTransform.X)"/>

Important : ne pas spécifier la valeur initiale (From) afin que l'animation démarre toujours de la position actuelle — cela évite les sauts lors des basculements fréquents.

Recommandations pour l'organisation des styles

Pour la scalabilité du projet, suivez ces règles :

  • Le style de base (sans x:Key) ne doit contenir que Template et Setters communs
  • Les variantes spécifiques (PrimaryButton, IconButton, etc.) sont héritées via BasedOn
  • Toutes les couleurs sont déplacées dans un fichier séparé (par ex., DarkColors.xaml)
  • Les propriétés attachées sont regroupées par fonctionnalité dans des classes statiques

Cela simplifie le refactoring et permet la création rapide de nouveaux types de contrôles basés sur les modèles existants.

Points clés

  • Éviter d'hériter de Button/ToggleButton si la logique de comportement ne change pas
  • Les propriétés attachées sont la façon préférée d'étendre la fonctionnalité des styles
  • VisualStateManager fournit une gestion déclarative des états sans code
  • Le système de thèmes centralisé simplifie le support de plusieurs schémas de couleurs
  • Les animations doivent utiliser des valeurs relatives (sans From) pour éviter les artefacts visuels

— Editorial Team

Advertisement 728x90

Lire ensuite