Итак, мы снова рассказываем об элементах управления на платформе UWP.
В предыдущей части мы познакомились со средствами расширения существующих элементов управления без вмешательства в их внутреннее устройство. Однако не всегда требуемого результата можно достичь малой кровью посредством присоединенных свойств (Attached Properties) или поведений (Behaviors).
Часть 2. Изменение существующих элементов управления
Для лучшего понимания того, в каком векторе будет развиваться дальнейшая дискуссия, приведем рисунок, иллюстрирующий видение того, какие «уровни» воздействия на элементы управления можно выделить.
Уровни воздействия на элементы управления
Из него видно, что предыдущая часть касалась самого внешнего уровня. На нем мы оказываем влияние на элемент управления через вышеуказанные средства расширений, а также через переопределение стилей. Для полноты картины к этому уровню можно также отнести и указание значений свойств элементов управления, которые они предоставляют внешнему миру.
Следующим уровнем воздействия является вмешательство в разметку шаблона существующего элемента управления.
Для начала работы с разметкой нам, очевидно, необходимо её получить. С этим нам помогут два источника:
• Официальная документация Microsoft. На данной странице представлен список элементов управления, поставляемых с UWP по каждому из которых можно получить шаблон разметки
• Приложение Blend For Visual Studio, умеющее предоставлять шаблоны элементов управления
Создание копии шаблона элемента управления в Blend
Создание копии шаблона Button
Копия шаблона Button
Оба способа обладают своими преимуществами.
• Документация:
— более простой и быстрый доступ к шаблону,
— явное обращение внимания на используемые им ThemeResources и состояния VisualStateManager
• Blend:
— можно использовать как песочницу для работы с нужным шаблоном,
— легкий доступ к шаблонам дочерних объектов,
— дополнительные функции IDE облегчающие работу при создании анимации
Располагая шаблонами, мы можем приступать к работе с ними. По началу они могут вызывать легкую неразбериху объемами разметки, но в ней становится намного легче ориентироваться, если помнить, что шаблоны элементов управления UWP и большинства сторонних разработчиков придерживаются определенной негласной конвенции.
Общее строение шаблона элемента управление
Ознакомимся с ним на примере шаблона элемента управления CheckBox.
<Style TargetType="CheckBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}"/>
<Setter Property="Padding" Value="8,5,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Grid Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<!--<VisualStateManager.VisualStateGroups>
...
</VisualStateManager.VisualStateGroups>-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid VerticalAlignment="Top" Height="32">
<Rectangle x:Name="NormalRectangle"
Fill="Transparent"
Stroke="{ThemeResource SystemControlForegroundBaseMediumHighBrush}"
StrokeThickness="{ThemeResource CheckBoxBorderThemeThickness}"
UseLayoutRounding="False"
Height="20"
Width="20" />
<FontIcon x:Name="CheckGlyph"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Glyph=""
FontSize="20"
Foreground="{ThemeResource SystemControlHighlightAltChromeWhiteBrush}"
Opacity="0" />
</Grid>
<ContentPresenter x:Name="ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Grid.Column="1"
AutomationProperties.AccessibilityView="Raw"
TextWrapping="Wrap" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Копия шаблона CheckBox
В данном шаблоне можем выделить несколько составляющих его частей:
• Набор простых сеттеров вида <Setter Property="Property_Name" Value="Property_Value" />. Они выполняют функцию указания значений по умолчанию свойств элемента управления. В частности, например, по умолчанию CheckBox не может иметь ширину меньшую, чем 120. На этом примере видно, что некоторые проблемы в процессе верстки могут идти из шаблона по умолчанию, который может «мешать» достижению требуемого результата.
• Список простых сеттеров заканчивается сеттером <Setter Property="Template">…</Setter> в котором и определяется каркас строения элемента управления. Этот каркас можно разбить на две основных части:
- Непосредственно разметка шаблона элемента управления состоящая из других элементов управления c указанием значений свойств по умолчанию
- Коллекция VisualStateGroups определяющая набор визуальных состояний элементов управления, в одном из которых он может находиться в конкретный момент
Список визуальных состояний CheckBox
Ознакомившись со списком визуальных состояний элемента управления CheckBox, видим, что в нем определенно по 4 визуальных состояния для каждого из 3 значений свойства IsChecked: true, false, null.
Так, например, переход в визуальное состояние UncheckedPointerOver устанавливает цвет обводки элемента NormalRectangle в значение SystemControlHighlightBaseHighBrush, берущееся из темы приложения.
<VisualState x:Name="UncheckedPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="NormalRectangle"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame
KeyTime="0"
Value="{ThemeResource SystemControlHighlightBaseHighBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="UncheckedPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="NormalRectangle"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame
KeyTime="0"
Value="{ThemeResource SystemControlBackgroundBaseMediumBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="NormalRectangle"
Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame
KeyTime="0"
Value="{ThemeResource SystemControlHighlightTransparentBrush}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation
Storyboard.TargetName="NormalRectangle"
Storyboard.TargetProperty="StrokeThickness"
To="{ThemeResource CheckBoxCheckedStrokeThickness}"
Duration="0" />
</Storyboard>
</VisualState>
Пара визуальных состояний CheckBox
Также обратим внимание, что визуальное состояние <VisualState x:Name="UncheckedNormal" />, указанного в коллекции, пусто. Причина этого в том, что механика работы менеджера визуальных состояний такова, что при переходе из состояния A в состояние B, те свойства, на которые оказывало влияние состояние A, но не оказывает состояние B, берут значения определенные как по умолчанию. Данная механика повторяет механику триггеров реализованную в WPF, но не вошедшую в UWP.
Ознакомившись с общим строением шаблонов элементов управления, приступим к работе с ними для получения требуемых результатов.
С шаблоном существующего элемента управления можно совершить следующие действия:
• Удаление «лишних» элементов
• Изменение требуемых элементов, уже определенных в нем
• Добавление новых элементов.
Модификация шаблона посредством удаления составляющих его элементов
Частой задачей, с которой мы сталкивались — убрать у элемента управления TextBox кнопку очистки введенного текста. Представим одно из возможных решений данной задачи. Получив шаблон, находим в нем элемент который необходимо удалить – <Button x:Name="DeleteButton" … />
<Button x:Name="DeleteButton"
Grid.Row="1"
Style="{StaticResource DeleteButtonStyle}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="{ThemeResource HelperButtonThemePadding}"
IsTabStop="False"
Grid.Column="1"
Visibility="Collapsed"
FontSize="{TemplateBinding FontSize}"
MinWidth="34"
VerticalAlignment="Stretch"/>
Кнопка на удаление
Также важно не забыть удалить все остальные места в разметке, которые обращаются тем или иным образом к данному элементу. Так удалению подвергаются: <Style x:Name="DeleteButtonStyle"/> и <VisualState x:Name="ButtonVisible"/>. Удаление последнего в общем позволяет нам и вовсе удалить весь <VisualStateGroup x:Name="ButtonStates"/>.
Определим получившемуся стилю x:Key — <Style x:Key="articleTextBox" TargetType="TextBox"> и применим к требуемому полю ввода текста <TextBox Style="{StaticResource articleTextBox}"/>
TextBox без кнопки очистки
Стоит отметить, что данный способ модификации шаблона не всегда срабатывает. В рамках следующей статьи будут рассмотрены внутренности элементов управления на уровне Control.cs и мы увидим, что порой не только в верстке определяются зависимости на элементы составляющие шаблон, но и в коде. Бывают случаи, когда модификация шаблона посредством удаления приводит к некорректной работе элемента управления либо и вовсе исключениям (exceptions).
Модификация шаблона посредством изменений составляющих его элементов
В нашей практике была ситуация, когда нам потребовалось внести следующие изменения в элемент управления CalendarDatePicker:
• Выделить хедеры выходных дней красным цветом
• Увеличить размер шрифта хедеров выходных дней
• Поменять вертикальные стрелки навигации на горизонтальные
• Поместить стрелку «влево» в левую часть панели, стрелку «вправо» в правую часть, а заголовок «год месяц декада» расположить посередине между этими стрелками
Внешний вид календаря CalendarDatePicker по умолчанию
Новый вид календаря CalendarDatePicker
• Получаем шаблон элемента управления CalendarDatePicker.
В процессе анализа шаблона обнаруживаем, что в него входит другой элемент управления, содержащий то, что мы ищем – CalendarView.
CalendarView входящий в шаблон CalendarDatePicker
• Получаем шаблон элемента управления CalendarView. В нем делаем следующие изменения:
• У кнопок <Button x:Name="PreviousButton"/> и <Button x:Name="NextButton"/> устанавливаем значения свойств Content в "" и "" соответственно
• У полей <TextBlock x:Name="WeekDay6"/> и <TextBlock x:Name="WeekDay7"/> устанавливаем значение свойства Foreground в Red
• В стиль <Style x:Key="WeekDayNameStyle"/> добавляем сеттер />
• В контейнере Grid содержащем <Button x:Name="PreviousButton"/> и <Button x:Name="NextButton"/> делаем очевидные изменения в верстке для выполнения поставленной задачи.
Готово. Таким образом, мы взяли уже готовый шаблон и привели его внешний вид в соответствие с требованиями.
Модификация шаблона посредством добавления в него новых элементов
Очевидно, что данный способ без доступа к Control.cs очень ограничен. Максимум что от него можно получить – добавление какой-то визуальной составляющей, которую можно разнообразить посредством состояний VisualStateManager. Это относится к третьему способу расширения существующих элементов управления.
К трем вышеуказанным способам можно было бы добавить ещё один, наиболее интересный – создание нового элемента управления наследующегося от существующего с использованием его шаблона. Но данный способ тяготеет больше к следующей части нашего рассказа, которая и будет посвящена теме создания новых элементов управления.
Автор: MobileDimension