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

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

Элемент управления CheckBox, использующий шаблон элемента управления по умолчанию

Флажок с шаблоном элемента управления по умолчанию.

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

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

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

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

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

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

  • Предварительные требования

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

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

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

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

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

Предварительные требования

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

ПримечаниеПримечание

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

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

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

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

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

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

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

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

При создании пользовательского элемента управления с помощью модели частей и состояний визуальная структура и визуальное поведение элемента управления задается не в его логике, а в его шаблоне ControlTemplate. Визуальная структура элемента управления состоит из объектов FrameworkElement, которые составляют элемент управления. Визуальное поведение – это способ отображения элемента управления в определенных состояниях. Дополнительные сведения о создании шаблона ControlTemplate, задающего визуальную структуру и визуальное поведение элемента управления см. в разделе Настройка внешнего вида существующего элемента управления путем создания объекта 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 состоит в том, что отрицательное значение отображается красным цветом. При изменении свойства Foreground элемента TextBlock в коде в случае, если значение Value отрицательное, NumericUpDown все равно будет показывать красное отрицательное значение. Визуальное поведение элемента управления в шаблоне ControlTemplate задается путем добавления объектов VisualState в шаблон ControlTemplate. В следующем примере показаны объекты VisualState для состояний Positive и Negative. Состояния Positive и Negative являются взаимоисключающими (элемент управления всегда находится в одном из этих двух состояний), поэтому в примере эти объекты VisualState помещаются в одну группу VisualStateGroup. Когда элемент управления переходит в состояние Negative, свойство Foreground элемента TextBlock включает красный цвет. Если элемент управления переходит в состояние Positive, свойство Foreground возвращается в первоначальное значение. Определение объектов VisualState в шаблоне ControlTemplate подробнее рассматривается в разделе Настройка внешнего вида существующего элемента управления путем создания объекта 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 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);
        }
    }
}

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

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

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

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

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

В следующем примере повторяется предыдущий пример, в котором показаны объекты VisualState, соответствующие состояниям Positive и Negative элемента управления. Объект Storyboard в состоянии VisualState, имеющем значение Negative, переключает свойство Foreground объекта TextBlock на красный цвет. Когда элемент управления NumericUpDown переходит в состояние Negative, начинается раскадровка в состоянии Negative. Затем, когда элемент управления возвращается в состояние Positive раскадровка Storyboard в состоянии Negative останавливается. Объекту 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 или больше 0, и в состояние Negative, когда значение Value меньше 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);
}

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

  • Если объект VisualState, в который переходит элемент управления, имеет объект Storyboard, то начинается раскадровка. Далее, если объект VisualState, из которого переходит элемент управления, имеет объект Storyboard, то раскадровка останавливается.

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

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

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

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

  • воспользоваться свойствами для отслеживания состояния элемента управления;

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

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

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

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

}

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

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

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

  • когда шаблон ControlTemplate применяется к элементу управления Control;

  • когда изменяется свойство;

  • когда возникает событие.

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

Следует обновить состояние элемента управления в методе OnApplyTemplate, чтобы этот элемент управления отображался в правильном состоянии, когда применяется шаблон ControlTemplate. В следующем примере для проверки правильности состояния элемента управления вызывается вспомогательный метод UpdateStates из метода OnApplyTemplate. В качестве примера предположим, что создается элемент управления NumericUpDown, а затем его свойство Foreground устанавливается в зеленый цвет, а Value – в значение -5. Если при применении шаблона ControlTemplate к элементу управления NumericUpDown не вызывается UpdateStates, то элемент управления не находится в состоянии Negative, и значение отображается зеленым, а не красным цветом. Необходимо вызвать метод UpdateStates, чтобы перевести элемент управления в состояние 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);
}

Часто бывает необходимо обновлять состояния элементов управления при изменении свойств. В следующем примере показан весь метод ValueChangedCallback. Поскольку метод ValueChangedCallback вызывается при изменении значения Value, этот метод вызывает UpdateStates в том случае, когда Value изменяется с положительного на отрицательное значение или наоборот. Допустимо вызвать метод UpdateStates, когда Value изменяется, но остается положительным или отрицательным, поскольку в этом случае элемент управления не изменяет состояние.

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

Может также потребоваться обновить состояния при возникновении события. В следующем примере показывается, что NumericUpDown вызывает метод UpdateStates в элементе управления Control для обработки события 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 помогает управлять состояниями элементов управления. С помощью VisualStateManager можно гарантировать правильные переходы элемента управления между состояниями. При выполнении рекомендаций, приведенных в этом разделе для работы с VisualStateManager, код элемента управления будет сохраняться понятным и обслуживаемым.

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

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

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

  • состояния элемента управления и группа, к которой принадлежит каждое состояние;

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

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

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

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

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

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

В следующем примере задаются объект FrameworkElement и состояния для элемента управления 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; }
}

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

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

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

См. также

Основные понятия

Настройка внешнего вида существующего элемента управления путем создания объекта ControlTemplate

Другие ресурсы

Настройка элементов управления