Compartir a través de


Crear un control que tiene una apariencia personalizable

Windows Presentation Foundation (WPF) ofrece la posibilidad de crear un control cuya apariencia se puede personalizar. Por ejemplo, puede cambiar la apariencia de un control CheckBox más de lo que las propiedades de configuración permiten hacer mediante la creación de un nuevo objeto ControlTemplate. En la ilustración siguiente se muestra un control CheckBox que usa un ControlTemplate predeterminado y un control CheckBox que usa un ControlTemplatepersonalizado.

Control CheckBox que usa la plantilla de control predeterminada

Casilla con la plantilla de control predeterminada.

Control CheckBox que usa una plantilla de control personalizada

Casilla con una plantilla de control personalizada.

Si sigue el modelo de estados y elementos al crear un control, la apariencia del control será personalizable. Existen herramientas de diseño como Microsoft Expression Blend que admiten el modelo de estados y elementos, por lo que si sigue dicho modelo, podrá personalizar el control en ese tipo de aplicaciones. En este tema se trata el modelo de estados y elementos, y cómo seguirlo al crear su propio control. En este tema se emplea un ejemplo de un control personalizado, NumericUpDown, con el fin de ilustrar la filosofía de este modelo. El control NumericUpDown muestra un valor numérico, que los usuarios pueden aumentar o disminuir haciendo clic en los botones del control. En la ilustración siguiente se muestra el control NumericUpDown que se explica en este tema.

Control NumericUpDown personalizado

Control personalizado NumericUpDown.

Este tema contiene las siguientes secciones:

  • Requisitos previos

  • Modelo de estados y elementos

  • Definir la estructura y el comportamiento visuales de un control en ControlTemplate

  • Usar elementos de ControlTemplate en el código

  • Proporcionar el contrato de un control

  • Ejemplo completo

Requisitos previos

En este tema se da por supuesto que sabe crear un ControlTemplate nuevo para un control existente, que está familiarizado con cuáles son los elementos del contrato de un control y que entiende los conceptos tratados en Personalizar la apariencia de un control existente creando una clase ControlTemplate.

NotaNota

Para crear un control cuya apariencia se pueda personalizar, debe crear un control que herede de la clase Control o alguna de sus subclases que no sea UserControl.Un control que hereda de UserControl se puede crear rápidamente, pero al no usar ControlTemplate no se puede personalizar su apariencia.

Modelo de estados y elementos

El modelo de estados y elementos especifica cómo definir la estructura y el comportamiento visuales de un control. Para seguir el modelo de estados y elementos, haga lo siguiente:

  • Defina la estructura y el comportamiento visuales en el ControlTemplate de un control.

  • Siga algunos procedimientos recomendados cuando la lógica de su control interactúe con elementos de la plantilla de control.

  • Proporcione un contrato del control para especificar lo que se debe incluir en ControlTemplate.

Al definir la estructura y el comportamiento visuales en el ControlTemplate de un control, los autores de la aplicación pueden cambiar la estructura y el comportamiento visuales del mismo creando un nuevo ControlTemplate en lugar de escribir código. Debe proporcionar un contrato del control que indique a los autores de la aplicación qué objetos y estados de FrameworkElement se deben definir en ControlTemplate. Debe seguir algunos procedimientos recomendados al interactuar con los elementos de ControlTemplate para que el control administre correctamente un ControlTemplate incompleto. Si sigue estos tres principios, los autores de la aplicación podrán crear un ControlTemplate para el control con la misma facilidad que para los controles que se incluyen en WPF. En la próxima sección se explica en detalle cada una de estas recomendaciones.

Definir la estructura y el comportamiento visuales de un control en ControlTemplate

Al crear un control personalizado usando el modelo de estados y elementos, se define la estructura y el comportamiento visuales del control en su ControlTemplate en lugar de en su lógica. La estructura visual de un control es la composición de los objetos FrameworkElement que constituyen el control. El comportamiento visual es el modo en que aparece el control cuando está en determinado estado. Para obtener más información sobre cómo crear un ControlTemplate que especifique la estructura y el comportamiento visuales de un control, vea Personalizar la apariencia de un control existente creando una clase ControlTemplate.

En el ejemplo del control NumericUpDown, la estructura visual incluye dos controles RepeatButton y un 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 y el comportamiento visuales del control en el código, debe hacerlo en el ControlTemplate. A continuación, un desarrollador de aplicaciones personaliza la posición de los botones y de TextBlock, y especifica qué comportamiento se produce cuando Value es negativo porque el ControlTemplate se puede reemplazar.

En el ejemplo siguiente se muestra la estructura visual del control NumericUpDown, que incluye un RepeatButton para aumentar Value, un RepeatButton para disminuir Value y un 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 aparece en una fuente de color rojo si es negativo. Si cambia la propiedad Foreground del TextBlock en el código cuando Value es negativo, NumericUpDown siempre mostrará un valor negativo en rojo. Se especifica el comportamiento visual del control en ControlTemplate agregando objetos VisualState al ControlTemplate. En el ejemplo siguiente se muestran los objetos VisualState para los estados Positive y Negative. Positive y Negative son mutuamente excluyentes (el control siempre está en uno de los dos), por lo que en el ejemplo se colocan los objetos VisualState en un mismo VisualStateGroup. Cuando el control entra en el estado Negative, la propiedad Foreground de TextBlock cambia a rojo. Cuando el control está en el estado Positive, la propiedad Foreground vuelve a su valor original. La definición de objetos VisualState en ControlTemplate se explica más detalladamente en Personalizar la apariencia de un control existente creando una clase ControlTemplate.

NotaNota

Asegúrese de establecer la propiedad adjunta VisualStateManager.VisualStateGroups del FrameworkElement raíz 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>

Usar elementos de ControlTemplate en el código

Un autor de ControlTemplate podría omitir los objetos VisualState o FrameworkElement a propósito o por error, pero la lógica del control podría necesitar esos elementos para funcionar correctamente. El modelo de estados y elementos especifica que el control debe adaptarse a un ControlTemplate al que le falten objetos VisualState o FrameworkElement. El control no debe producir una excepción ni notificar un error si falta un FrameworkElement, VisualState o VisualStateGroup en ControlTemplate. En esta sección se describen los procedimientos recomendados para interactuar con objetos FrameworkElement y administrar estados.

Prever los objetos FrameworkElement que puedan faltar

Al definir objetos FrameworkElement en ControlTemplate, la lógica del control podría necesitar 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 el TextBlock o los botones, 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, el control NumericUpDown pierde esa funcionalidad, pero una aplicación que use el ControlTemplate continuará ejecutándose.

Con los procedimientos siguientes se asegurará de que el control responda correctamente a los objetos FrameworkElement que falten:

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

  2. Defina las propiedades privadas para cada FrameworkElement con el que necesite interactuar.

  3. Suscriba y cancele la suscripción de cualquier evento que el control administre en el descriptor de acceso set de la propiedad del objeto FrameworkElement.

  4. Establezca las propiedades del objeto FrameworkElement que definió en el paso 2 del método OnApplyTemplate. Esto es lo antes que el FrameworkElement está disponible para el control en el ControlTemplate. Use el atributo x:Name del FrameworkElement para obtenerlo de ControlTemplate.

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

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

En el ejemplo que define la estructura visual del control NumericUpDown en ControlTemplate, el RepeatButton que aumenta Value tiene el atributo x:Name establecido en UpButton. En el ejemplo siguiente se declara una propiedad denominada UpButtonElement que representa el RepeatButton que se declara en ControlTemplate. El descriptor de acceso set cancela primero la suscripción al evento Click del botón si UpDownElement no es null; a continuación, establece la propiedad y después se suscribe al evento Click. Aunque no se muestra aquí, también se define una propiedad del otro RepeatButton, denominado DownButtonElement.

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 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);
        }
    }
}

En el ejemplo siguiente se muestra el método OnApplyTemplate para el control NumericUpDown. Se usa el método GetTemplateChild para obtener los objetos FrameworkElement de ControlTemplate. Observe que en el ejemplo se evitan los casos donde GetTemplateChild busca un FrameworkElement con el nombre especificado que no es del tipo esperado. También es un procedimiento recomendado omitir los elementos que tienen el atributo x:Name especificado pero son del tipo equivocado.

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

Siguiendo los procedimientos que se muestran en los ejemplos anteriores, se asegura de que el control continuará ejecutándose cuando a ControlTemplate le falte un FrameworkElement.

Usar VisualStateManager para administrar estados

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

En el ejemplo siguiente se repite el ejemplo anterior donde se muestran los objetos VisualState correspondientes a los estados Positive y Negative del control. El Storyboard de Negative VisualState cambia la propiedad Foreground de TextBlock a rojo. Cuando el control NumericUpDown está en estado Negative, comienza el guión gráfico en el estado Negative. A continuación, el Storyboard del estado Negative se detiene cuando el control vuelve al estado Positive. El VisualState con estado Positive no necesita contener un Storyboard porque cuando el Storyboard para el estado Negative se detiene, la propiedad 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>

Observe que se asigna un nombre a TextBlock, pero TextBlock no es en el contrato del control para NumericUpDown porque la lógica del control nunca hace referencia a TextBlock. Los elementos a los que se hacen referencia en ControlTemplate tienen nombres, pero no necesitan formar parte del contrato del control porque un nuevo ControlTemplate para el control quizás no necesite 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 la propiedad 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 mayor, y en el estado Negative cuando Value es menor que 0.

If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}

El método GoToState ejecuta 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 VisualState hacia el 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, el guión gráfico se detiene.

  • Si el control ya está en el estado que se especifica, el método GoToState no realiza ninguna acción y devuelve true.

  • Si el estado que se especifica no existe en el ControlTemplate de control, el método GoToState no realiza ninguna acción y devuelve false.

Procedimientos recomendados para trabajar con VisualStateManager

Se recomienda que haga lo siguiente para mantener los estados del control:

  • Use propiedades para realizar un seguimiento del estado.

  • Cree un método auxiliar para la transición entre estados.

El control NumericUpDown usa su propiedad Value para comprobar si está en estado Positive o Negative. El control NumericUpDown también define los estados Focused y UnFocused, lo que hace un seguimiento de la propiedad IsFocused. Si emplea 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 se pueda controlar. En el ejemplo siguiente se muestra el método auxiliar UpdateStates del control NumericUpDown. Cuando Value es mayor o igual a 0, el Control está en estado Positive. Cuando Value es menor que 0, el control está en estado Negative. Cuando la propiedad IsFocused es true, el control está en estado Focused; de lo contrario, está en estado Unfocused. El control puede llamar a UpdateStates siempre que necesite cambiar su estado, independientemente del estado que cambie.

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
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);
    }

}

Si pasa el nombre de un estado a GoToState cuando el control ya está en dicho estado, GoToState no hace nada, por lo que no necesita comprobar el estado actual del control. Por ejemplo, si Value cambia de un número negativo a otro número negativo, no se interrumpe el guión gráfico para el estado Negative y el usuario no verá ningún cambio en el control.

VisualStateManager usa objetos de VisualStateGroup para determinar de qué estado salir cuando se llama al método GoToState. El control siempre está en un estado para cada VisualStateGroup que se define en su ControlTemplate correspondiente y únicamente abandona un estado cuando entra en otro estado del mismo VisualStateGroup. Por ejemplo, el control ControlTemplate del NumericUpDown define los objetos Positive y Negative VisualState de un VisualStateGroup, y los objetos Focused y Unfocused VisualState de otro. (Puede ver la definición de VisualState en los estados Focused y Unfocused 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 casos típicos en los que el estado de un control podría cambiar:

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 de forma que el control aparezca en el estado correcto cuando se aplique ControlTemplate. En el ejemplo siguiente se llama a UpdateStates en el método OnApplyTemplate para garantizar que el control esté en los estados adecuados. Por ejemplo, suponga que crea un control NumericUpDown, y que establece su propiedad Foreground en verde y Value en -5. Si no llama a UpdateStates cuando el objeto 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 el control en el estado Negative.

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

Con frecuencia necesita actualizar los estados de un control cuando una propiedad cambia. En el ejemplo siguiente se muestra todo el método ValueChangedCallback. Puesto que se llama a ValueChangedCallback cuando Value cambia, el método llama a UpdateStates si Value cambió de positivo a negativo o viceversa. Es aceptable llamar a UpdateStates cuando Value cambia pero sigue siendo positivo o negativo, ya que en ese caso el control no cambiará de estado.

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
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));
}

También podría necesitar actualizar los estados cuando se produce un evento. En el ejemplo siguiente se muestra que NumericUpDown llama a UpdateStates en el Control para controlar el evento GotFocus.

Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}

VisualStateManager ayuda a administrar los estados del control. Usando VisualStateManager, se asegura de que el control realiza las transiciones entre estados correctamente. Si sigue las recomendaciones descritas en esta sección para trabajar con VisualStateManager, el código del control seguirá siendo legible y podrá mantenerse.

Proporcionar el contrato de un control

Se proporciona el contrato de un control para que los autores de ControlTemplate sepan qué deben incluir en la plantilla. El contrato de un 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 cree un nuevo ControlTemplate debe saber qué objetos FrameworkElement usa la lógica del control, el tipo de objeto que es cada uno y cómo se denomina. Un autor de ControlTemplate también necesita conocer el nombre de los posibles estados en los que se puede hallar el control y en qué VisualStateGroup se encuentra el estado.

Volviendo al ejemplo de NumericUpDown, el control espera que ControlTemplate tenga los siguientes objetos FrameworkElement:

El control puede estar en los estados siguientes:

Para especificar qué objetos FrameworkElement espera el control, se utiliza la clase TemplatePartAttribute, que especifica el nombre y tipo de los elementos esperados. Para especificar los posibles estados de un control, se usa la clase TemplateVisualStateAttribute, que especifica el nombre del estado y a qué VisualStateGroup pertenece. Coloque las clases 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 un control.

En el ejemplo siguiente se especifica el objeto FrameworkElement y los estados del control NumericUpDown.

<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 BackgroundProperty As DependencyProperty
    Public Shared ReadOnly BorderBrushProperty As DependencyProperty
    Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
    Public Shared ReadOnly FontFamilyProperty As DependencyProperty
    Public Shared ReadOnly FontSizeProperty As DependencyProperty
    Public Shared ReadOnly FontStretchProperty As DependencyProperty
    Public Shared ReadOnly FontStyleProperty As DependencyProperty
    Public Shared ReadOnly FontWeightProperty As DependencyProperty
    Public Shared ReadOnly ForegroundProperty As DependencyProperty
    Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
    Public Shared ReadOnly PaddingProperty As DependencyProperty
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty
    Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty


    Private _Background As Brush
    Public Property Background() As Brush
        Get
            Return _Background
        End Get
        Set(ByVal value As Brush)
            _Background = value
        End Set
    End Property

    Private _BorderBrush As Brush
    Public Property BorderBrush() As Brush
        Get
            Return _BorderBrush
        End Get
        Set(ByVal value As Brush)
            _BorderBrush = value
        End Set
    End Property

    Private _BorderThickness As Thickness
    Public Property BorderThickness() As Thickness
        Get
            Return _BorderThickness
        End Get
        Set(ByVal value As Thickness)
            _BorderThickness = value
        End Set
    End Property

    Private _FontFamily As FontFamily
    Public Property FontFamily() As FontFamily
        Get
            Return _FontFamily
        End Get
        Set(ByVal value As FontFamily)
            _FontFamily = value
        End Set
    End Property

    Private _FontSize As Double
    Public Property FontSize() As Double
        Get
            Return _FontSize
        End Get
        Set(ByVal value As Double)
            _FontSize = value
        End Set
    End Property

    Private _FontStretch As FontStretch
    Public Property FontStretch() As FontStretch
        Get
            Return _FontStretch
        End Get
        Set(ByVal value As FontStretch)
            _FontStretch = value
        End Set
    End Property

    Private _FontStyle As FontStyle
    Public Property FontStyle() As FontStyle
        Get
            Return _FontStyle
        End Get
        Set(ByVal value As FontStyle)
            _FontStyle = value
        End Set
    End Property

    Private _FontWeight As FontWeight
    Public Property FontWeight() As FontWeight
        Get
            Return _FontWeight
        End Get
        Set(ByVal value As FontWeight)
            _FontWeight = value
        End Set
    End Property

    Private _Foreground As Brush
    Public Property Foreground() As Brush
        Get
            Return _Foreground
        End Get
        Set(ByVal value As Brush)
            _Foreground = value
        End Set
    End Property

    Private _HorizontalContentAlignment As HorizontalAlignment
    Public Property HorizontalContentAlignment() As HorizontalAlignment
        Get
            Return _HorizontalContentAlignment
        End Get
        Set(ByVal value As HorizontalAlignment)
            _HorizontalContentAlignment = value
        End Set
    End Property

    Private _Padding As Thickness
    Public Property Padding() As Thickness
        Get
            Return _Padding
        End Get
        Set(ByVal value As Thickness)
            _Padding = value
        End Set
    End Property

    Private _TextAlignment As TextAlignment
    Public Property TextAlignment() As TextAlignment
        Get
            Return _TextAlignment
        End Get
        Set(ByVal value As TextAlignment)
            _TextAlignment = value
        End Set
    End Property

    Private _TextDecorations As TextDecorationCollection
    Public Property TextDecorations() As TextDecorationCollection
        Get
            Return _TextDecorations
        End Get
        Set(ByVal value As TextDecorationCollection)
            _TextDecorations = value
        End Set
    End Property

    Private _TextWrapping As TextWrapping
    Public Property TextWrapping() As TextWrapping
        Get
            Return _TextWrapping
        End Get
        Set(ByVal value As TextWrapping)
            _TextWrapping = value
        End Set
    End Property

    Private _VerticalContentAlignment As VerticalAlignment
    Public Property VerticalContentAlignment() As VerticalAlignment
        Get
            Return _VerticalContentAlignment
        End Get
        Set(ByVal value As VerticalAlignment)
            _VerticalContentAlignment = value
        End Set
    End Property
End Class
[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; }
}

Ejemplo completo

El ejemplo siguiente es todo el ControlTemplate para el control NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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.

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
    Private _value As Integer

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        _value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
        Get
            Return _value
        End Get
    End Property
End Class
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; }
        }
    }
}

Vea también

Conceptos

Personalizar la apariencia de un control existente creando una clase ControlTemplate

Otros recursos

Personalización de controles