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 :
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 :
<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 :
<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
Aucun commentaire pour le moment.