Crear un control que tiene una apariencia personalizable
Windows Presentation Foundation (WPF) le ofrece la posibilidad de crear un control cuyo aspecto se puede personalizar. Por ejemplo, puede cambiar la apariencia de un elemento CheckBox más allá de lo que harán las propiedades de configuración mediante la creación de un nuevo ControlTemplate. En la ilustración siguiente se muestra un CheckBox que usa un ControlTemplate predeterminado y un CheckBox que usa un ControlTemplate personalizado.
Casilla que usa la plantilla de control predeterminado
Casilla que usa una plantilla de control personalizado
Si sigue el modelo de elementos y estados al crear un control, la apariencia del control será personalizable. Las herramientas del diseñador, como Blend para Visual Studio admiten el modelo de elementos y estados, por lo que al seguir este modelo, el control se personalizará en esos tipos de aplicaciones. En este tema se describen los elementos y el modelo de estados y cómo seguirlo al crear su propio control. En este tema se usa un ejemplo de un control personalizado, NumericUpDown
, para ilustrar la filosofía de este modelo. El control NumericUpDown
muestra un valor numérico, que un usuario puede aumentar o disminuir haciendo clic en los botones del control. En la ilustración siguiente se muestra el control NumericUpDown
que se describe en este tema.
Control NumericUpDown personalizado
Este tema contiene las siguientes secciones:
Prerrequisitos
En este tema se supone que sabe cómo crear un nuevo ControlTemplate para un control existente, está familiarizado con los elementos de un contrato de control y comprende los conceptos descritos en Creación de una plantilla de un control.
Nota
Para crear un control que pueda tener su apariencia personalizada, debe crear un control que herede de la clase Control o de una de sus subclases distintas de UserControl. Un control que hereda de UserControl es un control que se puede crear rápidamente, pero no usa un ControlTemplate y no se puede personalizar su apariencia.
Modelo de elementos y estados
El modelo de elementos y estados especifica cómo definir la estructura visual y el comportamiento visual de un control. Para seguir el modelo de elementos y estados, debe hacer lo siguiente:
Defina la estructura visual y el comportamiento visual en el ControlTemplate de un control.
Siga ciertos procedimientos recomendados cuando la lógica del control interactúe con elementos de la plantilla de control.
Proporcione un contrato de control para especificar qué debe incluirse en ControlTemplate.
Al definir la estructura visual y el comportamiento visual en el ControlTemplate de un control, los autores de aplicaciones pueden cambiar la estructura visual y el comportamiento visual del control mediante la creación de un nuevo ControlTemplate en lugar de escribir código. Debe proporcionar un contrato de control que indique a los autores de la aplicación qué objetos FrameworkElement y estados deben definirse en ControlTemplate. Debe seguir algunos procedimientos recomendados cuando interactúe con las partes de ControlTemplate para que el control controle correctamente un elemento ControlTemplate incompleto. Si sigue estos tres principios, los autores de aplicaciones podrán crear un ControlTemplate para el control de la forma más sencilla posible para los controles que se suministran con WPF. En la sección siguiente se explica cada una de estas recomendaciones con detalle.
Definición de la estructura visual y el comportamiento visual de un control en un ControlTemplate
Al crear el control personalizado mediante el modelo de elementos y estados, se define la estructura visual y el comportamiento visual del control en su ControlTemplate en lugar de en su lógica. La estructura visual de un control es la composición de objetos FrameworkElement que componen el control. El comportamiento visual es la forma en que aparece el control cuando está en un estado determinado. Para obtener más información sobre cómo crear un objeto ControlTemplate que especifica la estructura visual y el comportamiento visual de un control, consulte Creación de una plantilla de un control.
En el ejemplo del control NumericUpDown
, la estructura visual incluye dos controles RepeatButton y TextBlock. Si agrega estos controles en el código del control NumericUpDown
(en su constructor), por ejemplo, las posiciones de esos controles serían inalterables. En lugar de definir la estructura visual del control y el comportamiento visual en su código, debe definirlo en ControlTemplate. A continuación, un desarrollador de aplicaciones puede personalizar la posición de los botones y TextBlock y especificar qué comportamiento se produce cuando Value
es negativo porque ControlTemplate se puede reemplazar.
En el ejemplo siguiente se muestra la estructura visual del control NumericUpDown
, que incluye un objeto RepeatButton para aumentar Value
, un objeto RepeatButton para reducir Value
y un objeto TextBlock para mostrar Value
.
<ControlTemplate TargetType="src:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type src:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
Un comportamiento visual del control NumericUpDown
es que el valor está en una fuente roja si es negativo. Si cambia Foreground de TextBlock en el código cuando Value
es negativo, NumericUpDown
siempre mostrará un valor negativo rojo. Especifique el comportamiento visual del control en ControlTemplate mediante la adición de objetos VisualState a ControlTemplate. En el ejemplo siguiente se muestran los objetos VisualState de los estados Positive
y Negative
. Positive
y Negative
son mutuamente excluyentes (el control siempre está exactamente en uno de los dos), por lo que el ejemplo coloca los objetos VisualState en un solo VisualStateGroup. Cuando el control entra en el estado Negative
, el Foreground de TextBlock se vuelve rojo. Cuando el control está en el estado Positive
, Foreground vuelve a su valor original. La definición de objetos VisualState en ControlTemplate se describe más detalladamente en Creación de una plantilla de un control.
Nota
Asegúrese de establecer la propiedad adjunta VisualStateManager.VisualStateGroups en la raíz FrameworkElement de ControlTemplate.
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
Uso de partes de ControlTemplate en código
Es posible que un autor de ControlTemplate omita los objetos FrameworkElement o VisualState, ya sea intencionadamente o por error, pero la lógica del control podría necesitar esos elementos para funcionar correctamente. El modelo de partes y estados especifica que el control debe ser resistente a un ControlTemplate al que le faltan los objetos FrameworkElement o VisualState. El control no debe producir una excepción o notificar un error si falta FrameworkElement, VisualState o VisualStateGroup de ControlTemplate. En esta sección se describen los procedimientos recomendados para interactuar con objetos FrameworkElement y administrar estados.
Anticipación de los objetos FrameworkElement que faltan
Al definir objetos FrameworkElement en ControlTemplate, es posible que la lógica del control tenga que interactuar con algunos de ellos. Por ejemplo, el control NumericUpDown
se suscribe al evento Click de los botones para aumentar o disminuir Value
y establece la propiedad Text de TextBlock en Value
. Si un ControlTemplate personalizado omite TextBlock o los botones, es aceptable que el control pierda parte de su funcionalidad, pero debe asegurarse de que el control no provoca un error. Por ejemplo, si un ControlTemplate no contiene los botones para cambiar Value
, NumericUpDown
pierde esa funcionalidad, pero una aplicación que usa ControlTemplate seguirá ejecutándose.
Los procedimientos siguientes garantizarán que el control responda correctamente a los objetos FrameworkElement que faltan:
Establezca el atributo
x:Name
para cada uno de los objeto FrameworkElement a los que necesite hacer referencia en el código.Defina las propiedades privadas para cada uno de los objetos FrameworkElement con los que necesite interactuar.
Suscríbase y cancele la suscripción a eventos que controle el control en el descriptor de acceso set de la propiedad FrameworkElement.
Establezca las propiedades FrameworkElement que definió en el paso 2 del método OnApplyTemplate. Esta es la primera vez que FrameworkElement en ControlTemplate está disponible para el control. Use el
x:Name
de FrameworkElement para obtenerlo de ControlTemplate.Compruebe que FrameworkElement no es
null
antes de acceder a sus miembros. Si esnull
, no notifique un error.
En los ejemplos siguientes se muestra cómo interactúa el control NumericUpDown
con los objetos FrameworkElement de acuerdo con las recomendaciones de la lista anterior.
En el ejemplo que define la estructura visual del control NumericUpDown
en ControlTemplate, RepeatButton que aumenta Value
tiene su atributo x:Name
establecido en UpButton
. En el ejemplo siguiente se declara una propiedad denominada UpButtonElement
que representa el objeto RepeatButton declarado en ControlTemplate. El descriptor de acceso set
primero cancela la suscripción al evento Click del botón si UpDownElement
no es null
, establece la propiedad y, a continuación, se suscribe al evento Click. También hay una propiedad definida, pero no se muestra aquí, para el otro RepeatButton, denominado DownButtonElement
.
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
En el ejemplo siguiente se muestra OnApplyTemplate del control NumericUpDown
. En el ejemplo se usa el método GetTemplateChild para obtener los objetos FrameworkElement de ControlTemplate. Observe que el ejemplo protege los casos en los que GetTemplateChild encuentra un FrameworkElement con el nombre especificado que no es del tipo esperado. También es un procedimiento recomendado omitir los elementos que tienen el x:Name
especificado, pero que son del tipo incorrecto.
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
Al seguir las prácticas que se muestran en los ejemplos anteriores, asegúrese de que el control seguirá ejecutándose cuando a ControlTemplate le falte FrameworkElement.
Uso de VisualStateManager para administrar estados
VisualStateManager realiza un seguimiento de los estados de un control y realiza la lógica necesaria para realizar la transición entre estados. Cuando se agregan objetos VisualState a ControlTemplate, se agregan a VisualStateGroup y se agrega VisualStateGroup a la propiedad adjunta VisualStateManager.VisualStateGroups para que VisualStateManager tenga acceso a ellos.
En el ejemplo siguiente se repite el ejemplo anterior que muestra los objetos VisualState que corresponden a los estados Positive
y Negative
del control. El Storyboard en el Negative
VisualState vuelve a Foreground del TextBlock de color rojo. Cuando el control NumericUpDown
está en el estado Negative
, comienza el guión gráfico en el estado Negative
. A continuación, el objeto Storyboard en el estado Negative
se detiene cuando el control vuelve al estado Positive
. No es necesario que Positive
VisualState contenga un elemento Storyboard porque cuando el Storyboard de Negative
se detiene, Foreground vuelve a su color original.
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
Tenga en cuenta que a TextBlock se le asigna un nombre, pero TextBlock no está en el contrato de control de NumericUpDown
porque la lógica del control nunca hace referencia a TextBlock. Los elementos a los que se hace referencia en ControlTemplate tienen nombres, pero no necesitan formar parte del contrato de control porque un nuevo ControlTemplate del control es posible que no tenga que hacer referencia a ese elemento. Por ejemplo, alguien que crea un nuevo ControlTemplate para NumericUpDown
podría decidir no indicar que Value
es negativo cambiando Foreground. En ese caso, ni el código ni ControlTemplate hace referencia a TextBlock por nombre.
La lógica del control es responsable de cambiar el estado del control. En el ejemplo siguiente se muestra que el control NumericUpDown
llama al método GoToState para entrar en el estado Positive
cuando Value
es 0 o superior, y el estado Negative
cuando Value
es menor que 0.
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
El método GoToState realiza la lógica necesaria para iniciar y detener los guiones gráficos correctamente. Cuando un control llama a GoToState para cambiar su estado, VisualStateManager hace lo siguiente:
Si el objeto VisualState al que va el control tiene Storyboard, comienza el guión gráfico. A continuación, si el objeto VisualState del que procede el control tiene Storyboard, termina el guión gráfico.
Si el control ya está en el estado especificado, GoToState no realiza ninguna acción y devuelve
true
.Si el estado especificado no existe en el ControlTemplate de
control
, GoToState no realiza ninguna acción y devuelvefalse
.
Prácticas recomendadas para trabajar con VisualStateManager
Se recomienda hacer lo siguiente para mantener los estados del control:
Use propiedades para realizar un seguimiento de su estado.
Cree un método auxiliar para realizar la transición entre estados.
El control NumericUpDown
usa su propiedad Value
para realizar un seguimiento de si está en el estado Positive
o Negative
. El control NumericUpDown
también define los estados Focused
y UnFocused
, que realiza un seguimiento de la propiedad IsFocused. Si usa estados que no corresponden naturalmente a una propiedad del control, puede definir una propiedad privada para realizar un seguimiento del estado.
Un único método que actualiza todos los estados centraliza las llamadas a VisualStateManager y hace que el código sea administrable. En el ejemplo siguiente se muestra el método del asistente, UpdateStates
, del control NumericUpDown
. Cuando Value
es mayor o igual que 0, Control está en el estado Positive
. Cuando Value
es menor que 0, el control está en el estado Negative
. Cuando IsFocused es true
, el control está en estado Focused
; de lo contrario, está en el estado Unfocused
. El control puede llamar a UpdateStates
cada vez que necesite cambiar su estado, independientemente del estado que cambie.
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
Si pasa un nombre de estado a GoToState cuando el control ya está en ese estado, GoToState no hace nada, por lo que no es necesario comprobar el estado actual del control. Por ejemplo, si Value
cambia de un número negativo a otro número negativo, el guión gráfico del estado Negative
no se interrumpe y el usuario no verá un cambio en el control.
VisualStateManager usa objetos VisualStateGroup para determinar de qué estado se va a salir al llamar a GoToState. El control siempre está en un estado para cada VisualStateGroup definido en su ControlTemplate y solo deja un estado cuando entra en otro estado del mismo VisualStateGroup. Por ejemplo, el ControlTemplate del control NumericUpDown
define los objetos Positive
yNegative
VisualState en un VisualStateGroup y los objetos Focused
y Unfocused
VisualState en otro. (Puede ver la definición de Focused
y Unfocused
VisualState en la sección Ejemplo completo de este tema. Cuando el control pasa del estado Positive
al estado Negative
, o viceversa, el control permanece en el estado Focused
o Unfocused
.
Hay tres situaciones típicas en los que el estado de un control podría cambiar:
Cuando ControlTemplate se aplica a Control.
Cuando cambia una propiedad.
Cuando se produce un evento.
En los ejemplos siguientes se muestra cómo actualizar el estado del control NumericUpDown
en estos casos.
Debe actualizar el estado del control en el método OnApplyTemplate para que el control aparezca en el estado correcto cuando se aplique ControlTemplate. En el ejemplo siguiente se llama a UpdateStates
en OnApplyTemplate para asegurarse de que el control está en los estados adecuados. Por ejemplo, supongamos que se crea un control NumericUpDown
y, a continuación, Foreground se establece en verde y Value
en -5. Si no llama a UpdateStates
cuando ControlTemplate se aplica al control NumericUpDown
, el control no está en el estado Negative
y el valor es verde en lugar de rojo. Debe llamar a UpdateStates
para poner al control en el estado Negative
.
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
A menudo es necesario actualizar los estados de un control cuando cambia una propiedad. En el ejemplo siguiente muestra el método ValueChangedCallback
completo. Dado que se llama a ValueChangedCallback
cuando Value
cambia, el método llama a UpdateStates
en caso de que Value
cambie de positivo a negativo o viceversa. Es aceptable llamar a UpdateStates
cuando Value
cambia, pero permanece positivo o negativo porque, en ese caso, el control no cambiará los estados.
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
También es posible que tenga que actualizar los estados cuando se produce un evento. En el ejemplo siguiente se muestra que NumericUpDown
llama a UpdateStates
en Control para controlar el evento GotFocus.
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
VisualStateManager ayuda a administrar los estados del control. Al usar VisualStateManager, asegúrese de que el control realiza correctamente las transiciones entre estados. Si sigue las recomendaciones descritas en esta sección para trabajar con VisualStateManager, el código del control seguirá siendo legible y fácil de mantener.
Provisión del contrato de control
Proporcione un contrato de control para que los autores de ControlTemplate sepan qué colocar en la plantilla. Un contrato de control tiene tres elementos:
Los elementos visuales que usa la lógica del control.
Los estados del control y el grupo al que pertenece cada estado.
Las propiedades públicas que afectan visualmente al control.
Alguien que crea un nuevo ControlTemplate debe saber qué objetos FrameworkElement usa la lógica del control, qué tipo es cada objeto y cuál es su nombre. Un autor de ControlTemplate también debe conocer el nombre de cada estado posible en el que puede estar el control y en qué VisualStateGroup está el estado.
Volviendo al ejemplo de NumericUpDown
, el control espera que ControlTemplate tenga los siguientes objetos FrameworkElement:
Un objeto RepeatButton denominado
UpButton
.Un objeto RepeatButton denominado
DownButton.
El control puede estar en uno de los siguientes estados:
En
ValueStates
VisualStateGroupPositive
Negative
En
FocusStates
VisualStateGroupFocused
Unfocused
Para especificar qué objetos FrameworkElement espera el control, use TemplatePartAttribute, que especifica el nombre y el tipo de los elementos esperados. Para especificar los posibles estados de un control, use TemplateVisualStateAttribute, que especifica el nombre del estado y a qué VisualStateGroup pertenece. Coloque TemplatePartAttribute y TemplateVisualStateAttribute en la definición de clase del control.
Cualquier propiedad pública que afecte a la apariencia del control también forma parte del contrato de control.
En el ejemplo siguiente se especifica el objeto FrameworkElement y los estados del control NumericUpDown
.
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public static readonly DependencyProperty BackgroundProperty;
public static readonly DependencyProperty BorderBrushProperty;
public static readonly DependencyProperty BorderThicknessProperty;
public static readonly DependencyProperty FontFamilyProperty;
public static readonly DependencyProperty FontSizeProperty;
public static readonly DependencyProperty FontStretchProperty;
public static readonly DependencyProperty FontStyleProperty;
public static readonly DependencyProperty FontWeightProperty;
public static readonly DependencyProperty ForegroundProperty;
public static readonly DependencyProperty HorizontalContentAlignmentProperty;
public static readonly DependencyProperty PaddingProperty;
public static readonly DependencyProperty TextAlignmentProperty;
public static readonly DependencyProperty TextDecorationsProperty;
public static readonly DependencyProperty TextWrappingProperty;
public static readonly DependencyProperty VerticalContentAlignmentProperty;
public Brush Background { get; set; }
public Brush BorderBrush { get; set; }
public Thickness BorderThickness { get; set; }
public FontFamily FontFamily { get; set; }
public double FontSize { get; set; }
public FontStretch FontStretch { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
public Brush Foreground { get; set; }
public HorizontalAlignment HorizontalContentAlignment { get; set; }
public Thickness Padding { get; set; }
public TextAlignment TextAlignment { get; set; }
public TextDecorationCollection TextDecorations { get; set; }
public TextWrapping TextWrapping { get; set; }
public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
Inherits Control
Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
Public Shared ReadOnly TextWrappingProperty As DependencyProperty
Public Property TextAlignment() As TextAlignment
Public Property TextDecorations() As TextDecorationCollection
Public Property TextWrapping() As TextWrapping
End Class
Ejemplo completo
El ejemplo siguiente es todo el ControlTemplate del control NumericUpDown
.
<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VSMCustomControl">
<Style TargetType="{x:Type local:NumericUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
<VisualStateGroup Name="FocusStates">
<!--Add a focus rectangle to highlight the entire control
when it has focus.-->
<VisualState Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
hiding the focus rectangle.-->
<VisualState Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
El ejemplo siguiente muestra la lógica de NumericUpDown
.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace VSMCustomControl
{
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public NumericUpDown()
{
DefaultStyleKey = typeof(NumericUpDown);
this.IsTabStop = true;
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(int), typeof(NumericUpDown),
new PropertyMetadata(
new PropertyChangedCallback(ValueChangedCallback)));
public int Value
{
get
{
return (int)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
typeof(ValueChangedEventHandler), typeof(NumericUpDown));
public event ValueChangedEventHandler ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
protected virtual void OnValueChanged(ValueChangedEventArgs e)
{
// Raise the ValueChanged event so applications can be alerted
// when Value changes.
RaiseEvent(e);
}
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
private RepeatButton downButtonElement;
private RepeatButton DownButtonElement
{
get
{
return downButtonElement;
}
set
{
if (downButtonElement != null)
{
downButtonElement.Click -=
new RoutedEventHandler(downButtonElement_Click);
}
downButtonElement = value;
if (downButtonElement != null)
{
downButtonElement.Click +=
new RoutedEventHandler(downButtonElement_Click);
}
}
}
void downButtonElement_Click(object sender, RoutedEventArgs e)
{
Value--;
}
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
void upButtonElement_Click(object sender, RoutedEventArgs e)
{
Value++;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Focus();
}
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
UpdateStates(true);
}
}
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
public class ValueChangedEventArgs : RoutedEventArgs
{
private int _value;
public ValueChangedEventArgs(RoutedEvent id, int num)
{
_value = num;
RoutedEvent = id;
}
public int Value
{
get { return _value; }
}
}
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
Inherits Control
Public Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
Me.IsTabStop = True
End Sub
Public Shared ReadOnly ValueProperty As DependencyProperty =
DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))
Public Property Value() As Integer
Get
Return CInt(GetValue(ValueProperty))
End Get
Set(ByVal value As Integer)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
GetType(ValueChangedEventHandler), GetType(NumericUpDown))
Public Custom Event ValueChanged As ValueChangedEventHandler
AddHandler(ByVal value As ValueChangedEventHandler)
Me.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As ValueChangedEventHandler)
Me.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
' Raise the ValueChanged event so applications can be alerted
' when Value changes.
MyBase.RaiseEvent(e)
End Sub
#Region "NUDCode"
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
Private m_downButtonElement As RepeatButton
Private Property DownButtonElement() As RepeatButton
Get
Return m_downButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_downButtonElement IsNot Nothing Then
RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
m_downButtonElement = value
If m_downButtonElement IsNot Nothing Then
AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
End Set
End Property
Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value -= 1
End Sub
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value += 1
End Sub
Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
MyBase.OnMouseLeftButtonDown(e)
Focus()
End Sub
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
MyBase.OnLostFocus(e)
UpdateStates(True)
End Sub
#End Region
End Class
Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
ByVal e As ValueChangedEventArgs)
Public Class ValueChangedEventArgs
Inherits RoutedEventArgs
Public Sub New(ByVal id As RoutedEvent,
ByVal num As Integer)
Value = num
RoutedEvent = id
End Sub
Public ReadOnly Property Value() As Integer
End Class
Vea también
.NET Desktop feedback