Compartir a través de


Crear un control que tenga una apariencia personalizable

Windows Presentation Foundation (WPF) le ofrece la posibilidad de crear un control cuya apariencia se pueda personalizar. Por ejemplo, puede cambiar la apariencia de un CheckBox elemento 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 valor predeterminado ControlTemplate y un CheckBox que usa un personalizado ControlTemplate.

Una casilla con la plantilla de control predeterminada. CheckBox que usa la plantilla de control predeterminada

Una casilla con una plantilla de control personalizada. CheckBox que usa una plantilla de control personalizada

Si sigue el modelo de partes y estados al crear un control, la apariencia del control será personalizable. Las herramientas del diseñador como Blend para Visual Studio admiten los elementos y el modelo de estados, por lo que al seguir este modelo, el control se personalizará en esos tipos de aplicaciones. En este tema se describe el modelo de partes y 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 NumericUpDown control 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 NumericUpDown control que se describe en este tema.

Control personalizado NumericUpDown. Un control NumericUpDown personalizado

Este tema contiene las secciones siguientes:

Prerrequisitos

En este tema se da por supuesto 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 para un control.

Nota:

Para crear un control que pueda tener su apariencia personalizada, debe crear un control que herede de la Control clase 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 elemento y no se puede personalizar su apariencia.

Modelo de partes y estados

El modelo de partes y estados especifica cómo definir la estructura visual y el comportamiento visual de un control. Para seguir el modelo de partes y estados, debe hacer lo siguiente:

  • Define 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 partes de la plantilla de control.

  • Proporcione un contrato de control para especificar lo que debe incluirse en el ControlTemplate.

Al definir la estructura visual y el comportamiento visual en de ControlTemplate 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é FrameworkElement objetos y estados deben definirse en ControlTemplate. Debe seguir algunas mejores prácticas cuando interactúe con las partes de ControlTemplate para que su control maneje correctamente un ControlTemplate incompleto. Si sigue estos tres principios, los autores de aplicaciones podrán crear un ControlTemplate para su control tan fácilmente como lo hacen para los controles que vienen con WPF. En la siguiente sección se explica detalladamente cada una de estas recomendaciones.

Definiendo la estructura visual y el comportamiento visual de un control en una Plantilla de Control

Al crear tu control personalizado mediante el modelo de partes y estados, defines 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 FrameworkElement objetos que componen el control. El comportamiento visual es la forma en que aparece el control cuando se encuentra en un estado determinado. Para obtener más información sobre la creación de un ControlTemplate objeto que especifica la estructura visual y el comportamiento visual de un control, vea Crear una plantilla para un control.

En el ejemplo del NumericUpDown control , la estructura visual incluye dos RepeatButton controles y un TextBlock. Si agrega estos controles en el código del NumericUpDown control 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 para personalizar la posición de los botones y TextBlock especificar qué comportamiento se produce cuando Value es negativo porque ControlTemplate se puede reemplazar.

En el ejemplo siguiente se muestra la estructura visual del NumericUpDown control , que incluye para RepeatButton aumentar Value, para RepeatButton reducir Valuey para TextBlock 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 NumericUpDown control es que el valor está en una fuente roja si es negativo. Si cambia el Foreground en el TextBlock en el código cuando Value es negativo, NumericUpDown mostrará siempre un valor negativo rojo. Especifique el comportamiento visual del control en ControlTemplate mediante la adición VisualState de objetos a ControlTemplate. En el ejemplo siguiente se muestran los objetos de los estados VisualState y PositiveNegative. Positive y Negative son mutuamente excluyentes (el control siempre está en exactamente uno de los dos), por lo que el ejemplo coloca los VisualState objetos en un solo VisualStateGroup. Cuando el control entra en el estado Negative, el Foreground de TextBlock se vuelve rojo. Cuando el control está en estado Positive , devuelve Foreground a su valor original. La definición VisualState de objetos en un ControlTemplate elemento se describe más detalladamente en Creación de una plantilla para un control.

Nota:

Asegúrese de establecer la VisualStateManager.VisualStateGroups propiedad adjunta 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

Un ControlTemplate autor puede omitir FrameworkElement u VisualState objetos, ya sea intencionadamente o por error, pero la lógica del control podría necesitar esas partes para funcionar correctamente. El modelo de partes y estados especifica que el control debe ser resistente a un ControlTemplate que faltan FrameworkElement objetos o VisualState . El control no debe producir una excepción ni notificar un error si falta un FrameworkElement, VisualState, o VisualStateGroup del ControlTemplate. En esta sección se describen los procedimientos recomendados para interactuar con FrameworkElement objetos y administrar estados.

Anticipación de los objetos FrameworkElement que faltan

Al definir FrameworkElement objetos en ControlTemplate, es posible que la lógica del control tenga que interactuar con algunos de ellos. Por ejemplo, el NumericUpDown control se suscribe al evento de los botones Click para aumentar o disminuir Value y establece la propiedad Text del TextBlock a Value. Si un control personalizado ControlTemplate omite los botones TextBlock o, es aceptable que el control pierda parte de su funcionalidad, pero debe asegurarse de que el control no produzca un error. Por ejemplo, si un ControlTemplate no contiene los botones para cambiar Value, pierde NumericUpDown esa funcionalidad, pero una aplicación que usa seguirá ControlTemplate ejecutándose.

Los procedimientos siguientes garantizarán que el control responda correctamente a los objetos que faltan FrameworkElement :

  1. Establezca el x:Name atributo para cada FrameworkElement al que necesite referirse en el código.

  2. Defina las propiedades privadas de cada FrameworkElement con la que necesite interactuar.

  3. Suscríbase a y anule la suscripción de cualquier evento que maneje su control en el descriptor de acceso set de la propiedad FrameworkElement.

  4. Establezca las propiedades que definió en el FrameworkElement paso 2 del OnApplyTemplate método . Esta es la primera vez que FrameworkElement en ControlTemplate está disponible para el control. Utiliza el x:Name de FrameworkElement para obtenerlo de ControlTemplate.

  5. Compruebe que FrameworkElement no sea null antes de acceder a sus miembros. Si es null, no notifique un error.

En los ejemplos siguientes se muestra cómo interactúa el NumericUpDown control con FrameworkElement objetos de acuerdo con las recomendaciones de la lista anterior.

En el ejemplo que define la estructura visual del control NumericUpDown en el ControlTemplate, el RepeatButton que incrementa Value tiene su atributo x:Name establecido en UpButton. El siguiente ejemplo declara una propiedad llamada UpButtonElement que representa el RepeatButton declarado en ControlTemplate. El set descriptor de acceso primero anula la suscripción al evento del botón Click si UpDownElement no null es, luego establece la propiedad y luego 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 siguiente ejemplo se muestra el OnApplyTemplate del control NumericUpDown. El ejemplo utiliza el método GetTemplateChild para obtener los objetos FrameworkElement de ControlTemplate. Tenga en cuenta que el ejemplo se asegura contra 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 especificado x:Name , 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, usted asegura que el control seguirá ejecutándose cuando falte un ControlTemplate en FrameworkElement.

Usar VisualStateManager para administrar estados

El VisualStateManager realiza un seguimiento de los estados de un control y ejecuta la lógica necesaria para la transición entre estados. Cuando agrega VisualState objetos a ControlTemplate, los agrega a VisualStateGroup y luego agrega VisualStateGroup a la propiedad adjunta VisualStateManager.VisualStateGroups para que VisualStateManager tenga acceso a ellos.

En el siguiente ejemplo se repite el ejemplo anterior que muestra los objetos VisualState que corresponden a los Positive y Negative estados del control. El Storyboard en el NegativeVisualState convierte el Foreground del TextBlock en rojo. Cuando el NumericUpDown control está en el estado Negative, comienza el guión gráfico del estado Negative. Entonces, el Storyboard en el estado Negative se detiene cuando el control regresa al estado Positive. El PositiveVisualState no necesita contener un Storyboard porque cuando el Storyboard para el Negative se detiene, el 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 con NumericUpDown porque la lógica del control nunca hace referencia a TextBlock. Los elementos a los que se hace referencia en el ControlTemplate tienen nombres, pero no necesitan formar parte del contrato de control porque un nuevo ControlTemplate para el control podría no necesitar 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 las ControlTemplate referencian el TextBlock por nombre.

La lógica del control es responsable de cambiar el estado del control. En el ejemplo siguiente se muestra que el NumericUpDown control llama al GoToState método para entrar en el Positive estado cuando Value es 0 o superior y el Negative estado 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 GoToState método realiza la lógica necesaria para iniciar y detener los guiones gráficos correctamente. Cuando un control llama GoToState a para cambiar su estado, VisualStateManager hace lo siguiente:

  • Si el VisualState al que va el control tiene un Storyboard, comienza el guión gráfico. A continuación, si el VisualState del que procede el control tiene un Storyboard, termina el storyboard.

  • 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 devuelve false.

Procedimientos recomendados 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 NumericUpDown control usa su Value propiedad para verificar si está en el estado Positive o Negative. El NumericUpDown control también define los estados Focused y UnFocused, que sigue 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 mantiene el código manejable. En el ejemplo siguiente se muestra el NumericUpDown método auxiliar del control, UpdateStates. Cuando Value es mayor o igual que 0, Control está en el Positive estado . Cuando Value es menor que 0, el control está en estado Negative . Cuando IsFocused es true, el control está en estado Focused ; de lo contrario, se encuentra en el Unfocused estado . El control puede llamar 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 Negative estado no se interrumpe y el usuario no verá un cambio en el control.

VisualStateManager usa VisualStateGroup objetos para determinar el estado del que se va a salir al llamar a GoToState. El control siempre está en un estado para cada VisualStateGroup que está definido en su ControlTemplate y solo deja un estado cuando entra en otro estado en el mismo VisualStateGroup. Por ejemplo, el control ControlTemplate de NumericUpDown define los objetos Positive y NegativeVisualState en uno, y los objetos VisualStateGroup y FocusedUnfocused en otro. (Puede ver los Focused y UnfocusedVisualState definidos 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 lugares típicos en los que el estado de un control puede cambiar:

En los ejemplos siguientes se muestra cómo actualizar el estado del NumericUpDown control en estos casos.

Debe actualizar el estado del control en el OnApplyTemplate método para que el control aparezca en el estado correcto cuando ControlTemplate se aplique . En el siguiente ejemplo, se llama a UpdateStates en OnApplyTemplate para asegurar que el control esté en los estados adecuados. Por ejemplo, supongamos que crea un NumericUpDown control y, a continuación, establece su Foreground en verde y Value en -5. Si no llama a UpdateStates cuando ControlTemplate se aplica al NumericUpDown control , el control no está en estado Negative y el valor es verde en lugar de rojo. Debe llamar a UpdateStates para colocar el control en el estado de 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 se muestra el método ValueChangedCallback completo. Dado que ValueChangedCallback se llama 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 tal caso, el control no cambiará de estado.

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. El siguiente ejemplo muestra que NumericUpDown llama a UpdateStates en Control para manejar 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

El VisualStateManager ayuda a administrar los estados del control. Al usar VisualStateManager, asegúrese de que el control realiza correctamente la transición 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.

Proporcionar el contrato de control

Proporcione un contrato de control para que ControlTemplate los autores sepan qué colocar en la plantilla. Un contrato de control tiene tres elementos:

  • Elementos visuales que usa la lógica del control.

  • Estados del control y del grupo al que pertenece cada estado.

  • Propiedades públicas que afectan visualmente al control.

Alguien que crea un nuevo ControlTemplate debe saber qué FrameworkElement objetos usa la lógica del control, qué tipo es cada objeto y cuál es su nombre. Un ControlTemplate autor también debe conocer el nombre de cada estado posible en el que puede estar el control y en el que VisualStateGroup se encuentra el estado.

Al volver al NumericUpDown ejemplo, el control espera ControlTemplate que tenga los siguientes FrameworkElement objetos:

El control puede estar en los siguientes estados:

Para especificar qué objetos espera el control FrameworkElement, use TemplatePartAttribute, que especifica el nombre y el tipo de los elementos esperados. Para especificar los estados posibles de un control, usted utiliza TemplateVisualStateAttribute, que especifica el nombre del estado y el estado al que VisualStateGroup pertenece. Coloque los elementos 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 FrameworkElement objeto y los estados del NumericUpDown control .

[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 el ControlTemplate completo para el 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>

En el ejemplo siguiente se 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

Consulte también

  • Crear una plantilla para un control
  • Personalización de controles