Пользовательские элементы управления в WinRT
137WinRT --- Пользовательские элементы управления
Создавая пользовательский элемент управления в библиотеке Windows Runtime, вероятно, вы хотите сделать его доступным для разных приложений и даже продавать его другим программистам. В этом случае следует задать для элемента управления определение Style по умолчанию, включающее шаблон ControlTemplate по умолчанию.
Библиотека, содержащая классы пользовательских элементов управления, также должна содержать файл с именем generic.xaml в папке с именем Themes. Как и файл generic.xaml, который вы уже видели, этот файл generic.xaml имеет корневой элемент ResourceDictionary и содержит определение Style со свойством TargetType, задающим имя пользовательского элемента управления. Это определение Style должно включать шаблон ControlTemplate по умолчанию.
Visual Studio генерирует основу файла generic.xaml за вас. В библиотеке классов, которая использовалась в предыдущих статьях, я вызвал диалоговое окно Add New Item, выбрал пункт Templated Control и ввел имя NewToggle. Visual Studio генерирует файл NewToggle.cs с набором директив using и следующим определением класса:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
// The Templated Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234235
namespace WinRTTestApp
{
public sealed class NewToggle : Control
{
public NewToggle()
{
this.DefaultStyleKey = typeof(NewToggle);
}
}
}
Это не частичное определение класса! Соответствующего файла NewToggle.xaml не существует, и конструктор не содержит вызова InitializeComponent. Свойство DefaultStyleKey обозначает тип, который должен использоваться при поиске неявных стилей. Visual Studio также генерирует папку Themes и файл generic.xaml, содержащий этот неявный стиль:
<ResourceDictionary ...>
<Style TargetType="local:NewToggle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NewToggle">
<Border
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Если библиотека содержит несколько пользовательских элементов управления, этот же файл будет содержать все определения Style по умолчанию. Для выбора конкретного имени и местонахождения файла есть веские причины: он навсегда будет связан с пользовательским элементом управления, определенным в библиотеке, и ссылаться на него для других целей не придется.
Элемент управления NewToggle предназначен для реализации функциональности выключателя. Одновременно отображаются два разных блока содержимого: один активен, другой неактивен. Касание одного из этих блоков содержимого изменяет состояние активности. За изменение визуального оформления, отражающее изменение состояния, отвечает шаблон.
Я сделал класс NewToggle производным от ContentControl, чтобы он наследовал свойства Content и ContentTemplate. Класс определяет два свойства зависимости - CheckedContent и IsChecked:
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace WinRTTestApp
{
public class NewToggle : ContentControl
{
public event EventHandler CheckedChanged;
Button uncheckButton, checkButton;
static NewToggle()
{
// Зарегистрировать оба свойства зависимости
IsCheckedProperty = DependencyProperty.Register("IsChecked",
typeof(bool),
typeof(NewToggle),
new PropertyMetadata(false, OnCheckedChanged));
CheckedContentProperty = DependencyProperty.Register("CheckedContent",
typeof(object),
typeof(NewToggle),
new PropertyMetadata(null));
}
public NewToggle()
{
DefaultStyleKey = typeof(NewToggle);
}
public static DependencyProperty IsCheckedProperty { private set; get; }
public static DependencyProperty CheckedContentProperty { private set; get; }
public bool IsChecked
{
set { SetValue(IsCheckedProperty, value); }
get { return (bool)GetValue(IsCheckedProperty); }
}
public object CheckedContent
{
set { SetValue(CheckedContentProperty, value); }
get { return GetValue(CheckedContentProperty); }
}
protected override void OnApplyTemplate()
{
if (uncheckButton != null)
uncheckButton.Click -= OnButtonClick;
if (checkButton != null)
checkButton.Click -= OnButtonClick;
uncheckButton = GetTemplateChild("UncheckButton") as Button;
checkButton = GetTemplateChild("CheckButton") as Button;
if (uncheckButton != null)
uncheckButton.Click += OnButtonClick;
if (checkButton != null)
checkButton.Click += OnButtonClick;
base.OnApplyTemplate();
}
private void OnButtonClick(object sender, RoutedEventArgs args)
{
IsChecked = sender == checkButton;
}
static void OnCheckedChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as NewToggle).OnCheckedChanged(EventArgs.Empty);
}
protected virtual void OnCheckedChanged(EventArgs args)
{
VisualStateManager.GoToState(this, this.IsChecked ? "Checked" : "Unchecked", true);
if (CheckedChanged != null)
CheckedChanged(this, args);
}
}
}
Переопределение OnApplyTemplate предполагает, что шаблон содержит два элемента управления Button с именами «UncheckButton» и «CheckButton». Если это условие выполняется, элементы управления сохраняются в полях, и им назначаются обработчики Click. Если после этого будет сделан щелчок на одной из кнопок, свойство IsChecked изменяется, инициируется событие CheckedChanged и вызывается статический метод VisualStateManager.GoToState с состояниями «Checked» или «Unchecked».
Шаблон в generic.xaml содержит две кнопки с этими именами, а также объекты Storyboard, определенные для двух состояний:
<ResourceDictionary ...>
<Style TargetType="local:NewToggle">
<Setter Property="BorderBrush" Value="#aaa" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NewToggle">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="UncheckButton"
Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0"
Value="0" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="CheckButton"
Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0"
Value="8" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<local:UniformGrid Rows="1">
<Button Name="UncheckButton"
FontSize="{TemplateBinding FontSize}"
BorderBrush="Red"
BorderThickness="8"
HorizontalAlignment="Stretch"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<Button Name="CheckButton"
FontSize="{TemplateBinding FontSize}"
BorderBrush="Green"
BorderThickness="0"
HorizontalAlignment="Stretch"
Content="{TemplateBinding CheckedContent}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</local:UniformGrid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Следует учесть, что в более крупных шаблонах две кнопки сами по себе могут иметь шаблоны. В данном случае они содержат шаблонные привязки к свойствам Content и CheckedContent и совместно используют один шаблон ContentTemplate элемента управления. Активный элемент выделяется утолщенной рамкой - красной для левой кнопки, зеленой для правой.
Использование NewToggle продемонстрировано в проекте NewToggleDemo:
<Page ...>
<Page.Resources>
<Style TargetType="local:NewToggle">
<Setter Property="VerticalAlignment" Value="Center"></Setter>
<Setter Property="HorizontalAlignment" Value="Center"></Setter>
</Style>
</Page.Resources>
<Grid Background="#FF1D1D1D">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:NewToggle Content="ВЫКЛ"
CheckedContent="ВКЛ"
Grid.Column="0"
FontSize="24" />
<local:NewToggle Grid.Column="1">
<local:NewToggle.Content>
<Image Source="Images/img1.jpg" />
</local:NewToggle.Content>
<local:NewToggle.CheckedContent>
<Image Source="Images/img2.jpg" />
</local:NewToggle.CheckedContent>
</local:NewToggle>
</Grid>
</Page>
Содержимое первого экземпляра NewToggle состоит из двух текстовых строк, а элемент управления на рисунке ниже находится в неактивном состоянии. Второй элемент NewToggle использует для представления двух состояний - две знаменитых картины; на рисунке он находится в активном состоянии.
Позже будет приводится другой пример пользовательского элемента управления с именем XYSlider.
Если вы используете пользовательский элемент управления в одном приложении, определите его прямо в проекте приложения. В этом случае шаблон по умолчанию можно разместить в файле XAML.