Cómo crear una plantilla para un control (WPF.NET)

Con Windows Presentation Foundation (WPF), puede usar su propia plantilla reutilizable para personalizar la estructura visual y el comportamiento de un control existente. Las plantillas se pueden aplicar de forma global a la aplicación, las ventanas y las páginas, o bien directamente a los controles. La mayoría de los escenarios en los que es necesario crear un control se pueden solventar creando en su lugar una plantilla para un control existente.

Importante

La documentación de la guía de escritorio para .NET 7 y .NET 6 está en proceso de elaboración.

En este artículo, veremos cómo crear un objeto ControlTemplate para el control Button.

Cuándo crear un objeto ControlTemplate

Los controles tienen muchas propiedades, como Background, Foreground y FontFamily. Estas propiedades se encargan de distintos aspectos relacionados con la apariencia del control, pero los cambios que se pueden realizar configurando estas propiedades son limitados. Por ejemplo, la propiedad Foreground se puede establecer en azul y FontStyle en cursiva en un objeto CheckBox. Si queremos personalizar la apariencia del control de forma diferente a como lo hace la configuración de las demás propiedades del control, hay que crear un objeto ControlTemplate.

En la mayoría de las interfaces de usuario, los botones tienen la misma apariencia en general: un rectángulo con texto. Si quisiéramos crear un botón redondeado, podríamos crear un control que herede del botón o que vuelva a crear la funcionalidad del botón y que, además, aporte el elemento visual circular.

Para no tener que crear más controles, se puede personalizar el diseño visual de un control existente. En el caso del botón redondeado, hay que crear un objeto ControlTemplate con el diseño visual que buscamos.

Por otra parte, si necesitamos un control con nueva funcionalidad, con propiedades diferentes y con nuevas configuraciones, crearíamos un objeto UserControl.

Requisitos previos

Cree una aplicación de WPF y, en MainWindow.xaml (o en otra ventana de su elección), configure las siguientes propiedades en el elemento <Window>:

Propiedad Value
Title Template Intro Sample
SizeToContent WidthAndHeight
MinWidth 250

Configure el contenido del elemento <Window> en el siguiente código XAML:

<StackPanel Margin="10">
    <Label>Unstyled Button</Label>
    <Button>Button 1</Button>
    <Label>Rounded Button</Label>
    <Button>Button 2</Button>
</StackPanel>

Al final del proceso, el archivo MainWindow.xaml debe tener un aspecto similar al siguiente:

<Window x:Class="IntroToStylingAndTemplating.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:IntroToStylingAndTemplating"
        mc:Ignorable="d"
        Title="Template Intro Sample" SizeToContent="WidthAndHeight" MinWidth="250">
    <StackPanel Margin="10">
        <Label>Unstyled Button</Label>
        <Button>Button 1</Button>
        <Label>Rounded Button</Label>
        <Button>Button 2</Button>
    </StackPanel>
</Window>

Si la aplicación se ejecuta, tendrá el siguiente aspecto:

Ventana de WPF con dos botones sin estilo

Creación de una clase ControlTemplate

La forma más común de declarar un objeto ControlTemplate es como un recurso en la sección Resources de un archivo XAML. Dado que las plantillas son recursos, siguen las mismas reglas de ámbito que se aplican a todos los recursos. Simplemente, la ubicación de la declaración de una plantilla afecta a dónde se puede aplicar la plantilla. Por ejemplo, si declara una plantilla en el elemento raíz del archivo XAML de definición de la aplicación, puede usar en cualquier parte de esta. Si la plantilla se define en una ventana, solo los controles de esa ventana podrán usarla.

Para empezar, agregue un elemento Window.Resources al archivo MainWindow.xaml:

<Window x:Class="IntroToStylingAndTemplating.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:IntroToStylingAndTemplating"
        mc:Ignorable="d"
        Title="Template Intro Sample" SizeToContent="WidthAndHeight" MinWidth="250">
    <Window.Resources>
        
    </Window.Resources>
    <StackPanel Margin="10">
        <Label>Unstyled Button</Label>
        <Button>Button 1</Button>
        <Label>Rounded Button</Label>
        <Button>Button 2</Button>
    </StackPanel>
</Window>

Cree un elemento <ControlTemplate> con las siguientes propiedades configuradas:

Propiedad Value
x:Key roundbutton
TargetType Button

Esta plantilla de control será sencilla:

  • Un elemento raíz del control, un elemento Grid
  • Un elemento Ellipse para trazar el aspecto redondeado del botón
  • Un elemento ContentPresenter para mostrar el contenido del botón especificado por el usuario
<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <Ellipse Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</ControlTemplate>

TemplateBinding

Al crear un elemento ControlTemplate, puede que quiera seguir usando las propiedades públicas para cambiar la apariencia del control. La extensión de marcado TemplateBinding enlaza una propiedad de un elemento que está en el elemento ControlTemplate a una propiedad pública que está definida por el control. Cuando se usa TemplateBinding, se habilitan propiedades en el control que actúan como parámetros de la plantilla. Es decir, cuando se establece una propiedad de un control, ese valor se pasa al elemento que tiene la extensión TemplateBinding.

Ellipse

Tenga en cuenta que las propiedades Fill y Stroke del elemento <Ellipse> están enlazadas a las propiedades Foreground y Background del control.

ContentPresenter

También se agrega a la plantilla un elemento <ContentPresenter>. Dado que esta plantilla está diseñada para un botón, tenga en cuenta que el botón hereda de ContentControl. El botón presenta el contenido del elemento. Se puede establecer cualquier elemento dentro del botón, como texto sin formato o incluso otro control. Los dos botones siguientes son válidos:

<Button>My Text</Button>

<!-- and -->

<Button>
    <CheckBox>Checkbox in a button</CheckBox>
</Button>

En estos dos ejemplos, el texto y la casilla se configuran como la propiedad Button.Content. Lo que se establezca como contenido se puede mostrar a través de un elemento <ContentPresenter>, que es lo que hace la plantilla.

Si el elemento ControlTemplate se aplica a un tipo ContentControl, como un elemento Button, se busca un elemento ContentPresenter en el árbol de elementos. Si se halla el elemento ContentPresenter, la plantilla enlaza automáticamente la propiedad Content del control a ContentPresenter.

Uso de la plantilla

Busque los botones que se declararon al principio de este artículo.

<StackPanel Margin="10">
    <Label>Unstyled Button</Label>
    <Button>Button 1</Button>
    <Label>Rounded Button</Label>
    <Button>Button 2</Button>
</StackPanel>

Establezca la propiedad Template del segundo botón en el recurso roundbutton:

<StackPanel Margin="10">
    <Label>Unstyled Button</Label>
    <Button>Button 1</Button>
    <Label>Rounded Button</Label>
    <Button Template="{StaticResource roundbutton}">Button 2</Button>
</StackPanel>

Si ejecuta el proyecto y observa el resultado, verá que el botón tiene un fondo redondeado.

Ventana de WPF con un botón ovalado de plantilla

Posiblemente se haya dado cuenta de que el botón no es un círculo perfecto, sino que está distorsionado. El elemento <Ellipse> funciona de forma que se expande para rellenar el espacio disponible. Para que el círculo sea uniforme, cambie las propiedades width y height del botón al mismo valor:

<StackPanel Margin="10">
    <Label>Unstyled Button</Label>
    <Button>Button 1</Button>
    <Label>Rounded Button</Label>
    <Button Template="{StaticResource roundbutton}" Width="65" Height="65">Button 2</Button>
</StackPanel>

Ventana de WPF con un botón circular de plantilla

Incorporación de un desencadenador

Aunque un botón que tiene una plantilla aplicada tiene un aspecto diferente, se comporta igual que cualquier otro botón. Si se presiona, se activa el evento Click. Pese a ello, posiblemente se haya dado cuenta de que, al mover el ratón por el botón, sus elementos visuales no cambian. Todas estas interacciones visuales están definidas por la plantilla.

Gracias a los sistemas de propiedades y eventos dinámicos que WPF proporciona, se puede ver una propiedad específica de un valor y, después, volver a aplicar el estilo de la plantilla cuando sea necesario. En este ejemplo, veremos la propiedad IsMouseOver del botón. Cuando el ratón esté sobre el control, aplique el estilo <Ellipse> con un nuevo color. Este tipo de desencadenador se conoce como PropertyTrigger.

Para que funcione, hay que agregar un nombre al elemento <Ellipse> al que poder hacer referencia. Asígnele el nombre backgroundElement.

<Ellipse x:Name="backgroundElement" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />

Después, agregue un nuevo elemento Trigger a la colección ControlTemplate.Triggers. El desencadenador inspeccionará el evento IsMouseOver en busca del valor true.

<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <Ellipse x:Name="backgroundElement" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="true">

        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Después, agregue un elemento <Setter> al elemento <Trigger>, que cambia la propiedad Fill de <Ellipse> a un nuevo color.

<Trigger Property="IsMouseOver" Value="true">
    <Setter Property="Fill" TargetName="backgroundElement" Value="AliceBlue"/>
</Trigger>

Ejecute el proyecto. Tenga en cuenta que, al mover el ratón sobre el botón, el color de <Ellipse> cambia.

El ratón se mueve por el botón de WPF para cambiar el color de relleno

Uso de un elemento VisualState

Los estados visuales se definen y desencadenan a través de un control. Por ejemplo, cuando el ratón se mueve por el control, se desencadena el estado CommonStates.MouseOver. Los cambios de propiedad se pueden animar en función del estado actual del control. En la sección anterior, usamos un elemento <PropertyTrigger> para cambiar el segundo plano del botón a AliceBlue cuando la propiedad IsMouseOver era true. Esta vez, cree un estado visual que anime el cambio de este color a través de una transición suave. Para más información sobre los elementos VisualState, vea Estilos y plantillas en WPF.

Para convertir <PropertyTrigger> a un estado visual animado, quite en primer lugar el elemento <ControlTemplate.Triggers> de la plantilla.

<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <Ellipse x:Name="backgroundElement" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</ControlTemplate>

Después, en el elemento raíz <Grid> de la plantilla de control, agregue el elemento <VisualStateManager.VisualStateGroups> con un elemento <VisualStateGroup> para CommonStates. Defina dos estados, Normal y MouseOver.

<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="CommonStates">
                <VisualState Name="Normal">
                </VisualState>
                <VisualState Name="MouseOver">
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Ellipse x:Name="backgroundElement" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</ControlTemplate>

Cuando ese estado se desencadene, se aplicarán las animaciones definidas en el elemento <VisualState>. Cree animaciones para cada estado. Las animaciones se colocan en un elemento <Storyboard>. Para obtener más información sobre los guiones gráficos, vea Información general sobre guiones gráficos.

  • Normal

    Este estado anima el relleno de la elipse y lo restaura al color Background del control.

    <Storyboard>
        <ColorAnimation Storyboard.TargetName="backgroundElement" 
            Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
            To="{TemplateBinding Background}"
            Duration="0:0:0.3"/>
    </Storyboard>
    
  • MouseOver

    Este estado anima el color Background de la elipse hacia un nuevo color: Yellow.

    <Storyboard>
        <ColorAnimation Storyboard.TargetName="backgroundElement" 
            Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" 
            To="Yellow" 
            Duration="0:0:0.3"/>
    </Storyboard>
    

Ahora, <ControlTemplate> debería tener el siguiente aspecto.

<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="CommonStates">
                <VisualState Name="Normal">
                    <Storyboard>
                        <ColorAnimation Storyboard.TargetName="backgroundElement" 
                            Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                            To="{TemplateBinding Background}"
                            Duration="0:0:0.3"/>
                    </Storyboard>
                </VisualState>
                <VisualState Name="MouseOver">
                    <Storyboard>
                        <ColorAnimation Storyboard.TargetName="backgroundElement" 
                            Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" 
                            To="Yellow" 
                            Duration="0:0:0.3"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Ellipse Name="backgroundElement" Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" />
        <ContentPresenter x:Name="contentPresenter" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</ControlTemplate>

Ejecute el proyecto. Tenga en cuenta que, al mover el ratón por el botón, el color de <Ellipse> se anima.

el mouse se mueve por el botón de WPF para cambiar el color de relleno con un estado visual

Pasos siguientes