Guía de desarrollo para controles personalizados en WPF

Fundamentos de los Custom Controls

En el ecosistema de Windows Presentation Foundation (WPF), un Custom Control representa la máxima expresión de extensibilidad. A diferencia de un UserControl, que es una composición estáticca de elementos existantes, un Custom Control separa estrictamente el comportamiento (definido en C#) de la apariencia (definida en un archivo de recursos XAML, generalmente Generic.xaml).

Requerimiento Solución recomendada Justificación
Combinar controles para una vista rápida UserControl Desarrollo ágil, lógica y vista acopladas.
Crear una biblioteca de UI reutilizable CustomControl Soporta plantillas (Templating) y temas.
Modificar el comportamiento base de un control CustomControl Permite la herencia de clases y sobreescritura de metadatos.

Escenario 1: Botón con bordes suavizados

El objetivo es extender el botón estándar para permitir la configuración directa del radio de las esquinas sin necesidad de redefinir el estilo global en cada instancia.


// BotonSuave.cs
using System.Windows;
using System.Windows.Controls;

namespace MiLibreriaUI
{
    public class BotonSuave : Button
    {
        static BotonSuave()
        {
            // Vinculamos el control con su estilo definido en Themes/Generic.xaml
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(BotonSuave), 
                new FrameworkPropertyMetadata(typeof(BotonSuave)));
        }

        // Definición de la propiedad de dependencia para el radio de curvatura
        public static readonly DependencyProperty RadioCurvaturaProperty =
            DependencyProperty.Register("RadioCurvatura", typeof(CornerRadius), 
            typeof(BotonSuave), new PropertyMetadata(new CornerRadius(0)));

        public CornerRadius RadioCurvatura
        {
            get => (CornerRadius)GetValue(RadioCurvaturaProperty);
            set => SetValue(RadioCurvaturaProperty, value);
        }
    }
}

En el archivo Themes/Generic.xaml, definimos la estructura visual mediante un ControlTemplate:


<Style TargetType="{x:Type local:BotonSuave}">
    <Setter Property="Background" Value="#2ecc71"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Padding" Value="10,5"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:BotonSuave}">
                <Border x:Name="CapaExterna"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="{TemplateBinding RadioCurvatura}">
                    <ContentPresenter HorizontalAlignment="Center" 
                                      VerticalAlignment="Center" 
                                      Margin="{TemplateBinding Padding}"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="CapaExterna" Property="Background" Value="#27ae60"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Escenario 2: Control de acción con icono y texto

Este control permite integrar una imagen y una etiqueta descriptiva, soportando eventos de clic personalizados y orientación flexible.


// AccionVisual.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace MiLibreriaUI
{
    public class AccionVisual : Control
    {
        static AccionVisual()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(AccionVisual), 
                new FrameworkPropertyMetadata(typeof(AccionVisual)));
        }

        public static readonly DependencyProperty IconoProperty =
            DependencyProperty.Register("Icono", typeof(ImageSource), typeof(AccionVisual));

        public static readonly DependencyProperty TituloProperty =
            DependencyProperty.Register("Titulo", typeof(string), typeof(AccionVisual), new PropertyMetadata("Click aquí"));

        public ImageSource Icono
        {
            get => (ImageSource)GetValue(IconoProperty);
            set => SetValue(IconoProperty, value);
        }

        public string Titulo
        {
            get => (string)GetValue(TituloProperty);
            set => SetValue(TituloProperty, value);
        }

        // Evento Enrutado para el Clic
        public static readonly RoutedEvent ClicEvent = 
            EventManager.RegisterRoutedEvent("Clic", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AccionVisual));

        public event RoutedEventHandler Clic
        {
            add => AddHandler(ClicEvent, value);
            remove => RemoveHandler(ClicEvent, value);
        }

        protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            RaiseEvent(new RoutedEventArgs(ClicEvent));
        }
    }
}

XAML para el diseño de AccionVisual:


<Style TargetType="{x:Type local:AccionVisual}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:AccionVisual}">
                <Border Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="1" Padding="8">
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{TemplateBinding Icono}" Width="16" Height="16" Margin="0,0,8,0"/>
                        <TextBlock Text="{TemplateBinding Titulo}" VerticalAlignment="Center"/>
                    </StackPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Escenario 3: Selector de calificación profesional

Este componente permite al usuario seleccionar una puntuación mediante estrellas, manejando lógica de redondeo y estados visuales complejos.


// SelectorCalificacion.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace MiLibreriaUI
{
    public class SelectorCalificacion : Control
    {
        static SelectorCalificacion()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectorCalificacion), 
                new FrameworkPropertyMetadata(typeof(SelectorCalificacion)));
        }

        public static readonly DependencyProperty PuntajeProperty =
            DependencyProperty.Register("Puntaje", typeof(int), typeof(SelectorCalificacion), 
                new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public int Puntaje
        {
            get => (int)GetValue(PuntajeProperty);
            set => SetValue(PuntajeProperty, value);
        }

        public static readonly DependencyProperty MaximoProperty =
            DependencyProperty.Register("Maximo", typeof(int), typeof(SelectorCalificacion), new PropertyMetadata(5));

        public int Maximo
        {
            get => (int)GetValue(MaximoProperty);
            set => SetValue(MaximoProperty, value);
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Point pos = e.GetPosition(this);
            double proporcion = pos.X / ActualWidth;
            Puntaje = (int)Math.Ceiling(proporcion * Maximo);
        }
    }
}

Resumen de implementación

Para construir componentes robustos en WPF, es vital seguir estos principios técnicos:

  • Registro de Metadatos: Siempre sobrescriba DefaultStyleKey en el constructor estático para asegurar que el motor de renderizado localice la plantilla correcta.
  • Propiedades de Dependencia: Use DependencyProperty para permitir que sus controles participen en el sistema de Binding, Animaciones y Estilos de WPF.
  • Localización de Recursos: Los estilos por defecto deben residir en /Themes/Generic.xaml dentro del ensamblado del control; de lo contrario, el sistema no los encontrará automáticamente.
  • TemplateBinding: Utilice esta marca de extensión dentro del XAML para conectar las propiedades de la clase C# con los elementos visuales de la plantilla de forma eficiente.

Etiquetas: WPF C# XAML CustomControls DesktopDevelopment

Publicado el 10-26 18:39