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.
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:
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.
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>
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.
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.
Pasos siguientes
.NET Desktop feedback