Поделиться через


Создание элемента управления с настраиваемым внешним видом

Windows Presentation Foundation (WPF) позволяет создать элемент управления, внешний вид которого можно настроить. Например, вы можете изменить внешний вид CheckBox, превзойдя возможности настройки свойств, создав новый ControlTemplate. На следующем рисунке показан CheckBox, использующий ControlTemplate по умолчанию и CheckBox, использующий настраиваемый ControlTemplate.

флажок с шаблоном элемента управления по умолчанию. Флажок, использующий шаблон элемента управления по умолчанию

флажок с пользовательским шаблоном элемента управления. Флажок, использующий пользовательский шаблон элемента управления

Если при создании элемента управления следовать модели частей и состояний, внешний вид элемента управления будет настраиваемым. Средства конструктора, такие как Blend для Visual Studio, поддерживают модель частей и состояний, поэтому при выполнении этой модели элемент управления будет настраиваться в этих типах приложений. В этом разделе рассматривается модель с частями и состояниями и как следовать ей при создании собственного элемента управления. В этом разделе используется пример пользовательского элемента управления NumericUpDownдля иллюстрации философии этой модели. Элемент управления NumericUpDown отображает числовое значение, которое пользователь может увеличить или уменьшить, щелкнув кнопки элемента управления. На следующем рисунке показан элемент управления NumericUpDown, который рассматривается в этом разделе.

пользовательского элемента управления NumericUpDown. настраиваемого элемента управления NumericUpDown

В этом разделе содержатся следующие разделы:

Необходимые условия

В этом разделе предполагается, что вы знаете, как создать новый ControlTemplate для существующего элемента управления, знакомы с тем, что представляют собой элементы контракта элемента управления, и понимаете понятия, обсуждаемые в разделе Создание шаблона для элемента управления.

Заметка

Чтобы создать элемент управления, который может быть настроен по внешнему виду, необходимо создать элемент управления, наследующийся от класса Control или от одного из его подклассов, за исключением UserControl. Элемент управления, наследующий от UserControl, — это элемент управления, который можно быстро создать, но он не использует ControlTemplate и не может настроить его внешний вид.

Модель частей и состояний

Модель частей и состояний указывает, как определить визуальную структуру и визуальное поведение элемента управления. Чтобы следовать модели частей и состояний, выполните следующие действия:

  • Определите визуальную структуру и визуальное поведение в ControlTemplate элемента управления.

  • Следуйте определенным рекомендациям, когда логика элемента управления взаимодействует с частями шаблона элемента управления.

  • Предоставьте контракт управления для указания того, что должно быть включено в ControlTemplate.

При определении визуальной структуры и визуального поведения в ControlTemplate элемента управления авторы приложений могут изменить визуальную структуру и визуальное поведение элемента управления, создав новый ControlTemplate вместо написания кода. Необходимо предоставить контракт управления, который сообщает авторам приложений, какие объекты и состояния FrameworkElement должны быть определены в ControlTemplate. При взаимодействии с частями в ControlTemplate следует соблюдать некоторые рекомендации, чтобы элемент управления правильно обрабатывал неполный ControlTemplate. Если вы будете следовать этим трем принципам, авторы приложений смогут создавать ControlTemplate для вашего элемента управления так же легко, как и для элементов управления, которые поставляются с WPF. В следующем разделе подробно описывается каждая из этих рекомендаций.

Определение визуальной структуры и визуального поведения элемента управления в controlTemplate

При создании пользовательского элемента управления с помощью модели частей и состояний вы определяете визуальную структуру элемента управления и визуальное поведение в его ControlTemplate вместо логики. Визуальная структура элемента управления является составной частью объектов FrameworkElement, составляющих элемент управления. Визуальное поведение — это способ отображения элемента управления, когда он находится в определенном состоянии. Дополнительные сведения о создании ControlTemplate, определяющей визуальную структуру и визуальное поведение элемента управления, см. в статье Создание шаблона для элемента управления.

В примере элемента управления NumericUpDown визуальная структура включает два элемента управления RepeatButton и один элемент управления TextBlock. Если вы добавите эти элементы управления в код элемента управления NumericUpDown, например в его конструкторе, позиции этих элементов управления будут неизменяемыми. Вместо определения визуальной структуры и поведения элемента управления в его коде, их следует указать в ControlTemplate. Затем разработчик приложения, чтобы настроить положение кнопок и TextBlock и указать, какое поведение происходит при Value отрицательно, так как ControlTemplate можно заменить.

В следующем примере показана визуальная структура элемента управления NumericUpDown, которая включает RepeatButton для увеличения Value, RepeatButton для уменьшения Valueи TextBlock для отображения 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>

Визуальное поведение элемента управления NumericUpDown заключается в том, что значение находится в красном шрифте, если оно отрицательное. Если изменить ForegroundTextBlock в коде, когда Value отрицателен, NumericUpDown всегда будет показывать красное отрицательное значение. Визуальное поведение элемента управления в ControlTemplate можно указать путем добавления объектов VisualState в ControlTemplate. В следующем примере показаны объекты VisualState для состояний Positive и Negative. Positive и Negative являются взаимоисключающими (управление всегда находится либо в одном, либо в другом), поэтому в примере объекты VisualState помещаются в одну VisualStateGroup. Когда элемент управления переходит в состояние Negative, ForegroundTextBlock становится красным. Когда элемент управления находится в состоянии Positive, Foreground возвращается к исходному значению. Определение объектов VisualState в ControlTemplate рассматривается далее в Создание шаблона для элемента управления.

Заметка

Не забудьте задать присоединенное свойство VisualStateManager.VisualStateGroups на корне FrameworkElement для 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>

Использование частей controlTemplate в коде

Автор ControlTemplate может опустить FrameworkElement или VisualState объекты либо намеренно, либо по ошибке, но логика элемента управления может потребовать правильной работы этих частей. Модель частей и состояний указывает, что элемент управления должен быть устойчивым к ControlTemplate, отсутствующим FrameworkElement или объектам VisualState. Элемент управления не должен вызывать исключение или сообщать об ошибке, если отсутствует FrameworkElement, VisualStateили VisualStateGroup из ControlTemplate. В этом разделе описаны рекомендации по взаимодействию с объектами FrameworkElement и управлению состояниями.

Предвидение отсутствующих объектов FrameworkElement

При определении объектов FrameworkElement в ControlTemplateлогика элемента управления может потребоваться взаимодействовать с некоторыми из них. Например, элемент управления NumericUpDown подписывается на событие Click у кнопок для увеличения или уменьшения Value и устанавливает свойство Text объекта TextBlock в Value. Если пользовательская ControlTemplate пропускает TextBlock или кнопки, то это допустимо, что элемент управления теряет некоторые функции, но вы должны быть уверены, что элемент управления не вызывает ошибки. Например, если ControlTemplate не содержит кнопки для изменения Value, NumericUpDown теряет эту функциональность, но приложение, использующее ControlTemplate, продолжит работать.

Следующие практики обеспечат, что элемент управления корректно реагирует на отсутствие объектов FrameworkElement:

  1. Задайте атрибут x:Name для каждого FrameworkElement, на который необходимо ссылаться в коде.

  2. Определите частные свойства для каждого FrameworkElement, с которыми необходимо взаимодействовать.

  3. Подпишитесь и отмените подписку на любые события, которые ваш элемент управления обрабатывает в наборе доступа свойства FrameworkElement.

  4. Задайте свойства FrameworkElement, определенные на шаге 2 в методе OnApplyTemplate. Это самое раннее время, когда FrameworkElement в ControlTemplate становится доступным для контроля. Используйте x:Name из FrameworkElement, чтобы получить его из ControlTemplate.

  5. Убедитесь, что FrameworkElement не null перед доступом к его членам. Если это null, не сообщайте об ошибке.

В следующих примерах показано, как элемент управления NumericUpDown взаимодействует с объектами FrameworkElement в соответствии с рекомендациями в предыдущем списке.

В примере, который определяет визуальную структуру элемента управления NumericUpDown в ControlTemplate, у RepeatButton, которая увеличивает Value, атрибут x:Name установлен на UpButton. В следующем примере объявляется свойство с именем UpButtonElement, представляющее RepeatButton, объявленное в ControlTemplate. Метод доступа set сначала отменяет подписку на событие Click кнопки, если UpDownElement не null, затем задает свойство, а затем подписывается на событие Click. Также существует свойство, определенное для другого RepeatButton, которое здесь не показано, называемое 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

В следующем примере показана OnApplyTemplate элемента управления NumericUpDown. В примере используется метод GetTemplateChild для получения объектов FrameworkElement из ControlTemplate. Обратите внимание, что пример защищает от случаев, когда GetTemplateChild находит FrameworkElement с указанным именем, которое не соответствует ожидаемому типу. Рекомендуется также игнорировать элементы с указанным x:Name, если они имеют неправильный тип.

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

Следуя рекомендациям, приведенным в предыдущих примерах, вы убедитесь, что элемент управления продолжит работать, когда ControlTemplate отсутствует FrameworkElement.

Использование VisualStateManager для управления состояниями

VisualStateManager отслеживает состояния элемента управления и выполняет логику, необходимую для перехода между состояниями. При добавлении объектов VisualState в ControlTemplateдобавьте их в VisualStateGroup и добавьте VisualStateGroup в присоединенное свойство VisualStateManager.VisualStateGroups, чтобы VisualStateManager получил к ним доступ.

В следующем примере повторяется предыдущий пример, показывающий объекты VisualState, соответствующие Positive и Negative состояния элемента управления. Storyboard в NegativeVisualState делает ForegroundTextBlock красным. Когда элемент управления NumericUpDown находится в состоянии Negative, начинается раскадровка в состоянии Negative. Затем Storyboard в состоянии Negative останавливается, когда элемент управления возвращается в состояние Positive. Positive VisualState не требуется содержать Storyboard, так как, когда Storyboard для Negative останавливается, Foreground возвращается к исходному цвету.

<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>

Обратите внимание, что TextBlock присваивается имя, но TextBlock не находится в контракте элемента управления для NumericUpDown поскольку логика элемента управления никогда не ссылается на TextBlock. Элементы, на которые ссылаются в ControlTemplate, имеют имена, но не должны быть частью контракта управления, так как для нового ControlTemplate возможно не потребуется ссылка на этот элемент. Например, кто создает новый ControlTemplate для NumericUpDown, может решить не указывать, что Value отрицательно, изменив Foreground. В этом случае ни код, ни ControlTemplate не ссылается на TextBlock по имени.

Логика элемента управления отвечает за изменение состояния элемента управления. В следующем примере показано, что элемент управления NumericUpDown вызывает метод GoToState для перехода в состояние Positive, если Value равно 0 или больше, а состояние Negative при Value меньше 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

Метод GoToState выполняет логику, необходимую для надлежащего запуска и остановки сторибордов. Когда элемент управления вызывает GoToState изменить его состояние, VisualStateManager выполняет следующие действия:

  • Если у VisualState, к которому идет элемент управления, есть Storyboard, начинается раскадровка. Затем, если элемент управления VisualState имеет Storyboard, раскадровка заканчивается.

  • Если элемент управления уже находится в указанном состоянии, GoToState не выполняет никаких действий и возвращает true.

  • Если указанное состояние не существует в ControlTemplate из control, GoToState не выполняет никаких действий и возвращает false.

Рекомендации по работе с VisualStateManager

Рекомендуется выполнить следующие действия для поддержания состояний элемента управления:

  • Используйте свойства для отслеживания состояния.

  • Создайте вспомогательный метод для перехода между состояниями.

Элемент управления NumericUpDown использует свойство Value для отслеживания того, находится ли он в состоянии Positive или Negative. Элемент управления NumericUpDown также задает состояния Focused и UnFocused, которые отслеживают параметр IsFocused. Если вы используете состояния, которые не соответствуют свойству элемента управления, можно определить частное свойство для отслеживания состояния.

Один метод, который обновляет все состояния, централизует вызовы к VisualStateManager и поддерживает ваш код в удобочитаемом состоянии. В следующем примере показан вспомогательный метод элемента управления NumericUpDown: UpdateStates. Если Value больше или равно 0, Control находится в состоянии Positive. Если Value меньше 0, элемент управления находится в состоянии Negative. Если IsFocusedtrue, элемент управления находится в состоянии Focused; в противном случае он находится в состоянии Unfocused. Элемент управления может вызывать UpdateStates в любое время, когда требуется изменить его состояние, независимо от изменений состояния.

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

Если вы передаете имя состояния в GoToState, когда элемент управления уже находится в этом состоянии, GoToState ничего не делает, поэтому не нужно проверять текущее состояние элемента управления. Например, если Value изменяется с одного отрицательного числа на другое отрицательное число, раскадровка для состояния Negative не прерывается, и пользователь не увидит изменения в элементе управления.

VisualStateManager использует объекты VisualStateGroup для определения того, из какого состояния выйти при вызове GoToState. Элемент управления всегда находится в одном состоянии для каждого VisualStateGroup, который определяется в ControlTemplate, и покидает это состояние лишь при переходе в другое состояние из той же VisualStateGroup. Например, ControlTemplate элемента управления NumericUpDown определяет объекты Positive и NegativeVisualState в одном VisualStateGroup, а также объекты Focused и UnfocusedVisualState в другом. (Вы можете увидеть Focused и UnfocusedVisualState, определенные в секции Полный пример в этой теме. Когда элемент управления переходит из состояния Positive в состояние Negative или наоборот, он остается в состоянии Focused или Unfocused.

Существует три типичных места, в которых может измениться состояние элемента управления:

  • При применении ControlTemplate к Control.

  • При изменении свойства.

  • При возникновении события.

В следующих примерах показано обновление состояния элемента управления NumericUpDown в этих случаях.

Необходимо обновить состояние элемента управления в методе OnApplyTemplate, чтобы элемент управления отображался в правильном состоянии при применении ControlTemplate. В следующем примере вызывается UpdateStates в OnApplyTemplate, чтобы убедиться, что элемент управления находится в соответствующих состояниях. Например, предположим, что вы создаете элемент управления NumericUpDown, а затем установите для его Foreground зеленый цвет и Value значение -5. Если вы не вызываете UpdateStates при применении ControlTemplate к элементу управления NumericUpDown, элемент управления не находится в состоянии Negative, а значение зеленым, а не красным. Чтобы поместить элемент управления в состояние Negative, необходимо вызвать UpdateStates.

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

Часто необходимо обновить состояния элемента управления при изменении свойства. В следующем примере показан весь метод ValueChangedCallback. Так как ValueChangedCallback вызывается при изменении Value, метод вызывает UpdateStates в случае, если Value изменился с положительного на отрицательный или наоборот. Можно вызывать UpdateStates, если 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));
}
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

При возникновении события также может потребоваться обновить статусы. В следующем примере показано, что NumericUpDown вызывает UpdateStates на Control для обработки события GotFocus.

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

VisualStateManager помогает управлять состояниями элемента управления. Используя VisualStateManager, вы гарантируете правильность перехода элемента управления между состояниями. Если вы следуйте рекомендациям, описанным в этом разделе для работы с VisualStateManager, код элемента управления останется читаемым и поддерживаемым.

Предоставление контракта управления

Вы предоставляете контракт управления, чтобы авторы ControlTemplate знали, что следует поместить в шаблон. Контрольный контракт состоит из трех элементов:

  • Визуальные элементы, которые использует логика элемента управления.

  • Состояния контроля и группы, к которой принадлежит каждое из состояний.

  • Общедоступные свойства, которые визуально влияют на элемент управления.

Кто-то, создающий новый ControlTemplate, должен знать, какие объекты FrameworkElement использует логика элемента управления, какой тип является каждый объект, и что такое его имя. Автор ControlTemplate также должен знать название каждого возможного состояния, в котором может находиться элемент управления, и в каком VisualStateGroup находится элемент управления.

Возвращаясь к примеру NumericUpDown, элемент управления ожидает, что ControlTemplate будет иметь следующие объекты FrameworkElement:

Элемент управления может находиться в следующих состояниях:

Чтобы указать, какие объекты FrameworkElement ожидает контрол, используйте TemplatePartAttribute, который указывает имя и тип ожидаемых элементов. Чтобы указать возможные состояния элемента управления, используйте TemplateVisualStateAttribute, указывающую имя состояния и VisualStateGroup которой он принадлежит. Поместите TemplatePartAttribute и TemplateVisualStateAttribute в определение класса элемента управления.

Любое публичное свойство, влияющее на внешний вид элемента управления, также входит в контракт элемента управления.

В следующем примере указывается объект FrameworkElement и состояния для элемента управления NumericUpDown.

[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

Полный пример

В следующем примере используется весь ControlTemplate для элемента управления 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>

В следующем примере показана логика для 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

См. также