Volver al inicio

Controles WPF personalizados: Estilos y Propiedades adjuntas

El artículo describe métodos para crear controles WPF personalizados sin herencia y UserControl. Cubre propiedades adjuntas para parametrizar estilos, implementación de conmutadores animados mediante VisualStateManager, y la arquitectura del sistema de temas.

Estilizado de controles WPF: Botones y Conmutadores sin Code-Behind
Advertisement 728x90

Creando controles personalizados de WPF: Estilizado de botones e interruptores sin herencia

Al desarrollar aplicaciones WPF, a menudo surge la necesidad de adaptar controles estándar a un diseño corporativo sin comprar bibliotecas comerciales. En lugar de crear UserControls personalizados o heredar de clases base, es más eficiente usar estilos y propiedades adjuntas: esto preserva la compatibilidad con el framework y simplifica el mantenimiento.

Arquitectura del sistema de temas en WPF

Para gestionar la apariencia de la aplicación, se recomienda centralizar todos los recursos en una carpeta Style. Cada tema (por ejemplo, claro y oscuro) se configura como un directorio separado que contiene un archivo de paleta de colores y el diccionario de recursos principal. Esto permite cambiar de tema fácilmente en tiempo de ejecución reemplazando un solo ResourceDictionary en Application.Resources.

La implementación del cambio de temas se ve de la siguiente manera:

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

Este enfoque garantiza que todos los elementos de la interfaz de usuario actualicen su apariencia de inmediato sin necesidad de recargar la ventana.

Estilizado de botones con propiedades adjuntas

En lugar de crear una clase RoundedButton personalizada, es mejor definir una propiedad adjunta CornerRadius en una clase estática. Esto permite establecer el radio de las esquinas directamente en XAML sin violar el principio de composición:

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

En la plantilla de estilo del botón, esta propiedad se usa mediante enlace al TemplatedParent:

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

Ventajas de este enfoque:

  • No es necesario escribir código behind adicional
  • Se preserva la compatibilidad completa con Button
  • Posibilidad de sobrescribir el radio tanto en el estilo como a nivel de instancia
  • Soporte para animaciones y disparadores de estado

Implementación de ToggleButton con un interruptor animado

Un interruptor personalizado que imita el estilo de iOS se puede implementar completamente mediante ControlTemplate sin una sola línea de C#. Componentes principales de la plantilla:

  • Canvas para posicionamiento preciso de elementos
  • DrawingImage para iconos (marca de verificación y círculo)
  • Border con RenderTransform para movimiento animado
  • VisualStateManager para responder a Checked/Unchecked

La animación de desplazamiento se implementa mediante DoubleAnimation dentro 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)"/>

Es importante no especificar el valor inicial (From) para que la animación siempre comience desde la posición actual: esto evita saltos durante alternancias frecuentes.

Recomendaciones para organizar estilos

Para la escalabilidad del proyecto, sigue estas reglas:

  • El estilo base (sin x:Key) debe contener solo Template y Setters comunes
  • Las variantes específicas (PrimaryButton, IconButton, etc.) se heredan mediante BasedOn
  • Todos los colores se mueven a un archivo separado (por ejemplo, DarkColors.xaml)
  • Las propiedades adjuntas se agrupan por funcionalidad en clases estáticas

Esto simplifica la refactorización y permite crear rápidamente nuevos tipos de controles basados en plantillas existentes.

Puntos clave

  • Evita heredar de Button/ToggleButton si la lógica de comportamiento no cambia
  • Las propiedades adjuntas son la forma preferida de extender la funcionalidad de los estilos
  • VisualStateManager proporciona gestión declarativa de estados sin código
  • El sistema de temas centralizado simplifica el soporte para múltiples esquemas de colores
  • Las animaciones deben usar valores relativos (sin From) para evitar artefactos visuales

— Editorial Team

Advertisement 728x90

Leer después