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

137

Создавая пользовательский элемент управления в библиотеке 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 использует для представления двух состояний - две знаменитых картины; на рисунке он находится в активном состоянии.

Пример пользовательского элемента управления в Win RT

Позже будет приводится другой пример пользовательского элемента управления с именем XYSlider.

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

Пройди тесты
Лучший чат для C# программистов