Части шаблона в WinRT
142WinRT --- Части шаблона
Вероятно, пока я описывал процесс конструирования шаблона для Button, у вас возник вопрос - как эта концепция работает с более сложными элементами управления? Возьмем хотя бы Slider. У этого элемента управления имеются подвижные части. Как элемент управления должен ссылаться на эти шаблоны?
Программный код элемента управления - такого, как Slider - предполагает, что некоторые элементы, образующие шаблон, имеют определенные имена. В процессе инициализации код элемента управления получает ссылки на эти элементы в переопределении метода OnApplyTemplate, вызывая метод GetTemplateChild с этими именами. Код элемента управления может сохранить эти объекты в полях, назначить для них обработчики событий и изменять их свойства в то время, когда пользователь выполняет операции с элементом управления.
К сожалению, эти именованные части не обозначены ни в одной виденной мной документации Windows Runtime. Чтобы получить информацию о них, вам придется изучать шаблоны по умолчанию в generic.xaml. Во многих случаях знать все до последней мелочи не обязательно. Считается, что элемент управления не должен выдавать исключения, если некоторые части шаблона отсутствуют. Для обеспечения минимальной функциональности шаблон Slider должен содержать шаблоны для горизонтальной и вертикальной ориентации. Эти раздельные шаблоны обычно относятся к типу Grid, и им присваиваются имена HorizontalTemplate и VerticalTemplate.
В каждой панели Grid должен находиться объект Rectangle, охватывающий всю площадь Slider, с именем HorizontalTrackRect или VerticalTrackRect, объект Thumb с именем HorizontalThumb или VerticalThumb и второй объект Rectangle, который отображается слева от Thumb, с именем HorizontalDecreaseRect или VerticalDecreaseRect. Когда пользователь перемещает ползунок Thumb или щелкает/прикасается к шкале Slider, элемент управления изменяет размер второго прямоугольника в соответствии со значением Slider.
Рассмотрим простейший шаблон Slider, который содержит несколько явно заданных свойств и игнорирует элементы TickBar, образующие необязательные деления шкалы. Я назвал этот проект BareBonesSlider:
<Page ...>
<Page.Resources>
<ControlTemplate x:Key="sliderTmpl" TargetType="Slider">
<Grid>
<Grid Name="HorizontalTemplate" Background="Transparent" Height="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Name="HorizontalTrackRect"
Grid.ColumnSpan="3"
Fill="DarkOrange"
Margin="0 12" />
<Thumb Name="HorizontalThumb"
DataContext="{TemplateBinding Value}"
Grid.Column="1"
Width="24" />
<Rectangle Name="HorizontalDecreaseRect"
Fill="LimeGreen"
Margin="0 12" />
</Grid>
<Grid Name="VerticalTemplate"
Visibility="Collapsed"
Background="Transparent"
Width="40">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Name="VerticalTrackRect"
Grid.RowSpan="3"
Fill="DarkOrange"
Margin="12 0" />
<Thumb Name="VerticalThumb"
Grid.Row="1"
DataContext="{TemplateBinding Value}"
Height="24" />
<Rectangle Name="VerticalDecreaseRect"
Grid.Row="2"
Fill="LimeGreen"
Margin="12 0" />
</Grid>
</Grid>
</ControlTemplate>
</Page.Resources>
<Grid Background="#FF1D1D1D">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition/>
</Grid.RowDefinitions>
<Slider Template="{StaticResource sliderTmpl}"
Margin="40" />
<Slider Grid.Row="1"
Template="{StaticResource sliderTmpl}"
Orientation="Vertical"
Margin="40" />
</Grid>
</Page>
В конце файла XAML определяются два элемента управления Slider, горизонтальный и вертикальный, ссылающиеся на эти шаблоны.
Я опишу шаблон горизонтальной версии Slider; вертикальный шаблон имеет аналогичную структуру. Общая ширина панели Grid с именем «HorizontalTemplate» равна ширине элемента управления Slider в макете. Панель Grid содержит три столбца. Объект Rectangle с именем «HorizontalTrackRect» простирается на все три столбца, так что ширина Rectangle всегда совпадает с шириной самого элемента управления Slider. Объект Rectangle с именем «HorizontalDecreaseRect» занимает первый столбец Grid, которому назначена ширина Auto; это приводит к сокращению Rectangle до нулевой ширины. Thumb занимает центральный столбец Grid, который также имеет ширину Auto; это означает, что ширина центрального столбца соответствует размеру Thumb.
Код реализации позволяет Thumb перемещаться только по горизонтали и без выхода за границы Slider. Когда пользователь манипулирует с Thumb или прикасается/щелкает на шкале Slider, код соответствующим образом изменяет свойство Width элемента «HorizontalDecreaseRect». Для минимального значения Slider свойство Width задается равным нулю, а для максимального - ширине элемента «HorizontalTrackRect», уменьшенной на ширину Thumb. Я задал размеры и поля компонентов так, чтобы объект Thumb был чуть крупнее прямоугольников.
Обратите внимание: шаблон содержит одну привязку TemplateBinding, которая связывает свойство DataContext объекта Thumb со свойством Value элемента управления Slider. Это необходимо для того, чтобы во всплывающей подсказке Slider отображалось правильное значение.
Манипулируя с ползунком Thumb в программе BareBonesSlider, можно заметить, что при нажатии он становится почти черным. Класс Thumb является производным от Control, а следовательно, ему можно назначить собственный шаблон. Это делается в шаблоне Slider по умолчанию - в секции Resources, присоединенной к внешней панели Grid шаблона.
Совсем небольшое изменение программы BareBonesSlider позволяет добиться интересного результата - «Slider на пружине»:
<Page ...>
<Page.Resources>
<ControlTemplate x:Key="sliderTemplate" TargetType="Slider">
<Grid>
<Grid.Resources>
<Style TargetType="Path">
<Setter Property="StrokeLineJoin" Value="Round" />
<Setter Property="StrokeThickness" Value="6" />
<Setter Property="Stretch" Value="Fill" />
</Style>
</Grid.Resources>
<Grid Name="HorizontalTemplate"
Background="Transparent" Height="54">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Name="HorizontalTrackRect"
Grid.ColumnSpan="3" />
<Thumb Name="HorizontalThumb"
Grid.Column="1"
DataContext="{TemplateBinding Value}"
Width="12" />
<Rectangle Name="HorizontalDecreaseRect" Fill="Transparent" />
<Path Stroke="DarkOrange"
Width="{Binding ElementName=HorizontalDecreaseRect,
Path=Width}"
Data="M 0 0 L 10 10, 20 0, 30 10, 40 0,
40 10, 30 0, 20 10, 10 0, 0 10 Z" />
<Path Stroke="LimeGreen"
Grid.Column="2"
Data="M 0 0 L 10 10, 20 0, 30 10, 40 0,
40 10, 30 0, 20 10, 10 0, 0 10 Z" />
</Grid>
<Grid Name="VerticalTemplate"
Visibility="Collapsed" Width="54">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Name="VerticalTrackRect" Grid.RowSpan="3" />
<Thumb Name="VerticalThumb"
Grid.Row="1"
DataContext="{TemplateBinding Value}"
Height="12" />
<Rectangle Name="VerticalDecreaseRect"
Grid.Row="2" />
<Path Stroke="DarkOrange"
Grid.Row="2"
Height="{Binding ElementName=VerticalDecreaseRect,
Path=Height}"
Data="M 0 0 L 10 10, 0 20, 10 30, 0 40,
10 40, 0 30, 10 20, 0 10, 10 0 Z" />
<Path Stroke="LimeGreen"
Grid.Row="0"
Data="M 0 0 L 10 10, 0 20, 10 30, 0 40,
10 40, 0 30, 10 20, 0 10, 10 0 Z" />
</Grid>
</Grid>
</ControlTemplate>
</Page.Resources>
<Grid Background="#FF1D1D1D">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Slider Grid.Row="0"
Template="{StaticResource sliderTemplate}"
Margin="50" />
<Slider Grid.Row="1"
Template="{StaticResource sliderTemplate}"
Orientation="Vertical"
Margin="50" />
</Grid>
</Page>
Два шаблона имеют одинаковую структуру, не считая того, что у всех элементов Rectangle свойству Fill задано значение Transparent. Кроме того, в каждый шаблон добавлены два элемента Path. Первый элемент Path находится в первом столбце (для горизонтальной версии Slider) и окрашен в зеленый цвет. Его ширина (Width) привязана к свойству Width элемента с именем «HorizontalDecreaseRect». Второй элемент Path окрашен в оранжевый цвет и занимает третий столбец. Все они имеют одинаковую геометрию (сетка из перекрещивающихся линий) со свойством Stretch в режиме Fill. Это означает, что узор будет заполнять все выделенное ему пространство.
В результате индикатор выглядит так, словно ползунок закреплен на пружинах:
Шаблон по умолчанию для ProgressBar достаточно сложен, потому что в нем должны быть представлены как определенные, так и неопределенные состояния. Но если ограничиться только определенными состояниями ProgressBar, шаблон становится очень простым: код реализации изменяет ширину элемента с именем «ProgressBarIndicator» в диапазоне от 0 до ширины элемента с именем «DeterminateRoot». В шаблоне по умолчанию «DeterminateRoot» представляет собой элемент Border, содержащий выровненный по левому краю элемент Rectangle с именем «ProgressBarIndicator».
В приложении SpeedometerProgressBar ни «DeterminateRoot», ни «ProgressBarIndicator» не видны, но свойство Width элемента «DeterminateRoot» жестко запрограммировано равным 180. Это означает, что свойство Width элемента «ProgressBarIndicator» будет изменяться в диапазоне от 0 до 180. Привязка от свойства Width элемента «ProgressBarIndicator» использует целевое свойство Angle объекта RotateTransfonn, что приводит к вращению стрелки-индикатора в диапазоне от 0 до 180 градусов:
<Page ...>
<Page.Resources>
<ControlTemplate x:Key="progressBarTemplate" TargetType="ProgressBar">
<Grid>
<Grid.Resources>
<Style TargetType="Line">
<Setter Property="Stroke" Value="#000" />
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="X1" Value="-85" />
<Setter Property="X2" Value="-95" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="16" />
<Setter Property="Foreground" Value="#000" />
</Style>
</Grid.Resources>
<Border Width="400" Height="220"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="White">
<!-- Панель Canvas для размещения графики-->
<Canvas Width="0" Height="0"
RenderTransform="1 0 0 1 0 50" >
<!-- Обязательные части шаблона ProgressBar -->
<Border Name="DeterminateRoot" Width="180">
<Rectangle Name="ProgressBarIndicator"
HorizontalAlignment="Left" />
</Border>
<Line RenderTransform=" 1.00 0.00 -0.00 1.00 0 0" />
<Line RenderTransform=" 0.95 0.31 -0.31 0.95 0 0" />
<Line RenderTransform=" 0.81 0.59 -0.59 0.81 0 0" />
<Line RenderTransform=" 0.59 0.81 -0.81 0.59 0 0" />
<Line RenderTransform=" 0.31 0.95 -0.95 0.31 0 0" />
<Line RenderTransform=" 0.00 1.00 -1.00 0.00 0 0" />
<Line RenderTransform="-0.31 0.95 0.95 0.31 0 0" />
<Line RenderTransform="-0.59 0.81 0.81 0.59 0 0" />
<Line RenderTransform="-0.81 0.59 0.59 0.81 0 0" />
<Line RenderTransform="-0.95 0.31 0.31 0.95 0 0" />
<Line RenderTransform="-1.00 0.00 0.00 1.00 0 0" />
<TextBlock Text="0%" Canvas.Left="-118" Canvas.Top="-8" />
<TextBlock Text="20%" Canvas.Left="-110" Canvas.Top="-70" />
<TextBlock Text="40%" Canvas.Left="-50" Canvas.Top="-110" />
<TextBlock Text="60%" Canvas.Left="26" Canvas.Top="-110" />
<TextBlock Text="80%" Canvas.Left="82" Canvas.Top="-69" />
<TextBlock Text="100%" Canvas.Left="100" Canvas.Top="-8" />
<!-- Стрелка-указатель -->
<Polygon Points="5 5 5 -5 -75 0" Stroke="#000"
Fill="DarkMagenta">
<Polygon.RenderTransform>
<RotateTransform Angle="{Binding ElementName=ProgressBarIndicator,
Path=Width}" />
</Polygon.RenderTransform>
</Polygon>
</Canvas>
</Border>
</Grid>
</ControlTemplate>
</Page.Resources>
<Grid Background="#FF1D1D1D">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ProgressBar Template="{StaticResource progressBarTemplate}"
Margin="54" Value="{Binding ElementName=slider, Path=Value}" />
<Slider Name="slider"
Grid.Row="1" Margin="54"
VerticalAlignment="Center" />
</Grid>
</Page>
Завершающая часть файла XAML создает экземпляр ProgressBar с этим шаблоном и привязывает его к Slider для тестирования.
Приложения SpringLoadedSlider и SpeedometerProgressBar созданы на базе файлов XAML, которые изначально были написаны для WPF для статьи в выпуске «MSDN Magazine» за январь 2007 года. Хотя мне пришлось слегка изменить шаблоны, чтобы компенсировать различия между WPF и Windows Runtime, в целом они похожи. И хотя полная портируемость между всеми средами на базе XAML пока недостижима, нельзя отрицать, что работа, выполненная шесть лет назад, достаточно легко адаптируется для новых платформ.