VisualStateManager в WinRT

92

Возможно, в ходе экспериментов с определением нового визуального оформления Button вы заметили, что кнопка всегда остается функциональной - при щелчках или касаниях исправно инициируются события Click. С визуальной обратной связью для пользователя дело обстоит сложнее. Внешний вид обычных кнопок изменяется при блокировке, получении фокуса с клавиатуры, в процессе щелчка или наведения на них указателя мыши.

Эти разные варианты оформления называются визуальными состояниями (visual states). Они встраиваются прямо в шаблон при помощи классов, являющихся частью системы управления визуальными состояниями (Visual State Manager).

Класс Button имеет семь визуальных состояний, разделенных на две группы:

Состояния каждой группы являются взаимоисключающими. Например, не существует визуального состояния для заблокированной и одновременно нажатой кнопки. Код реализации переводит элемент управления в эти состояния вызовами VisualStateManager.GoToState. Эти состояния всегда обозначаются текстовыми именами.

Часто визуальные состояния реализуются дополнительными элементами визуального дерева шаблона; в нормальном состоянии такие элементы остаются невидимыми. Невидимость может быть обусловлена использованием цвета, совпадающего с цветом фона, заданием значения Collapsed свойству Visibility или нулевым уровнем Opacity. Чтобы элемент стал видимым, к свойству применяется анимация. Часто такие анимации имеют нулевую продолжительность, то есть происходят мгновенно, но при желании их можно растянуть по времени.

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

В стандартном элементе управления Button фокус ввода с клавиатуры обозначается пунктирной линией, окружающей содержимое кнопки; это означает, что содержимое заключается в элемент Border с ContentPresenter, а следовательно, и пунктирная линия, и ContentPresenter должны находиться в панели Grid с одной ячейкой. Ниже приведена пунктирная линия, реализованная элементом Rectangle с именем «focusRect»:

<Grid Background="#FF1D1D1D">
        <Button Content="Щелкни по мне!"
                Click="Button_Click"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Background="White"
                Foreground="#111"
                BorderBrush="LimeGreen"
                BorderThickness="2"
                Padding="24">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            CornerRadius="10">
                        <Grid>
                            <ContentPresenter Margin="{TemplateBinding Padding}"/>
                            <Rectangle Name="focusRect"
                                       StrokeThickness="1"
                                       StrokeDashArray="1 2"
                                       Stroke="{TemplateBinding Foreground}"
                                       RadiusX="10" RadiusY="10"
                                       Margin="5" />
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Button.Template>
        </Button>
</Grid>

Результат выглядит так:

Настройка шаблона под визуальное состояние

Конечно, объект Rectangle не должен отображаться постоянно. Один из способов сделать его невидимым - назначить нулевой уровень Opacity:

<Rectangle Name="focusRect"
           Opacity="0"
           StrokeThickness="1"
           StrokeDashArray="1 2"
           Stroke="{TemplateBinding Foreground}"
           RadiusX="10" RadiusY="10"
           Margin="5" />

Затем, обычно в корневом элементе визуального дерева, образующего ControlTemplate - в нашем примере сразу же после начального тега Border - размещается секция VisualStateManager.VisualStateGroups. В ней находятся теги VisualStateGroup для каждой группы, а в них - теги VisualState для каждого состояния из группы. Все состояния идентифицируются атрибутами x:Name:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Normal">
            
        </VisualState>
        
        <VisualState x:Name="Pressed">
            
        </VisualState>
        
        <VisualState x:Name="PointerOver">
            
        </VisualState>
        
        <VisualState x:Name="Disabled">
            
        </VisualState>
    </VisualStateGroup>
    
    <VisualStateGroup x:Name="FocusedStates">
        <VisualState x:Name="Unfocused">
            
        </VisualState>

        <VisualState x:Name="Focused">

        </VisualState>

        <VisualState x:Name="PointerFocused">

        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Если визуальная часть базового шаблона спроектирована для состояний Normal и Unfocused, эти теги можно сделать пустыми. А если вы не хотите обрабатывать какие-либо состояния, эти теги тоже можно оставить пустыми:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Normal" />
        <VisualState x:Name="PointerOver" />
        
        <VisualState x:Name="Pressed">
            
        </VisualState>
        
        <VisualState x:Name="Disabled">
            
        </VisualState>
    </VisualStateGroup>
    
    <VisualStateGroup x:Name="FocusedStates">
        <VisualState x:Name="Unfocused" />
        <VisualState x:Name="PointerFocused" />

        <VisualState x:Name="Focused">

        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Только не удаляйте их! В группе должны содержаться теги для всех состояний. Стоит убрать хотя бы одно, и переход в это состояние не произойдет. Для состояний, которые вы намерены обрабатывать, поместите между тегами VisualState тег Storyboard с анимациями, применяемыми к элементам, предоставленным для этой цели. Пример:

<VisualStateGroup x:Name="FocusedStates">
    <VisualState x:Name="Unfocused" />
    <VisualState x:Name="PointerFocused" />

    <VisualState x:Name="Focused">
        <Storyboard>
            <DoubleAnimation Storyboard.TargetName="focusRect"
                             Storyboard.TargetProperty="Opacity"
                             To="1" Duration="0" />
        </Storyboard>
    </VisualState>
</VisualStateGroup>

Обратите внимание на отсутствие свойства From. Указывается только конечное значение свойства - без начального. После того как это будет сделано, при получении фокуса ввода элементом управления вызывается его метод OnGotFocus. Элемент управления отвечает вызовом VisualStateManager.GoToState с состоянием «Focused». Это инициирует выполнение Storyboard, в результате чего целевое свойство Opacity становится равно 1. Когда элемент управления теряет фокус ввода, он вызывает метод VisualStateManager.GoToState с состоянием «Unfocused», что приводит к отмене анимации.

Для заблокированного состояния весь элемент управления должен окрашиваться в серый цвет. Хороший способ достижения такого эффекта - наложение на элемент полупрозрачного черного прямоугольника, у которого свойство Visibility имеет значение Collapsed. Итак, давайте поместим Border в другую панель Grid и добавим в Grid именованный объект Rectangle, который визуально располагается поверх Border. При этом я вынес разметку VisualStateManager во внешнюю панель Grid:

<Button.Template>
    <ControlTemplate TargetType="Button">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            ...
        </VisualStateManager.VisualStateGroups>
        <Border Name="border" ...>
            <Grid>
                <ContentPresenter Name="contentPresenter" ... />
                <Rectangle Name="focusRect" ... />
            </Grid>
        </Border>
    </Grid>
    </ControlTemplate>
</Button.Template>

Я также присвоил имена элементам Border и ContentPresenter, чтобы ссылаться на них в анимациях. Для состояния Disabled я определил анимацию, которая делает прямоугольник disabledRect видимым, а для состояния Pressed - две анимации, задающих фоновый и основной цвет элемента управления.

Окончательная версия стиля и шаблона представлена в проекте CustomButtonTemplate. Прежде всего, для того чтобы избежать слишком длинных строк, я определил ControlTemplate как отдельный объект в словаре Resources и включил ссылку на него в Style:

<Page ...>
    
    <Page.Resources>
        <ControlTemplate x:Key="btnTemplate" TargetType="Button">
            <Grid>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="PointerOver" />
                        <VisualState x:Name="Normal" />

                        <VisualState x:Name="Pressed">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                               Storyboard.TargetProperty="Background">
                                    <DiscreteObjectKeyFrame Value="LightGray" KeyTime="0" />
                                </ObjectAnimationUsingKeyFrames>

                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentPresenter"
                                                               Storyboard.TargetProperty="Foreground">
                                    <DiscreteObjectKeyFrame Value="#000" KeyTime="0" />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>

                        <VisualState x:Name="Disabled">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
                                                               Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame Value="Visible" KeyTime="0"  />
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>

                    <VisualStateGroup x:Name="FocusedStates">
                        <VisualState x:Name="Unfocused" />
                        <VisualState x:Name="PointerFocused" />

                        <VisualState x:Name="Focused">
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetName="focusRect"
                                                 Storyboard.TargetProperty="Opacity"
                                                 To="1" Duration="0" />
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>

                <Border Name="border"
                        Background="{TemplateBinding Background}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        CornerRadius="10">

                    <Grid>
                        <ContentPresenter Name="contentPresenter"
                                          Margin="{TemplateBinding Padding}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          ContentTransitions="{TemplateBinding ContentTransitions}" />

                        <Rectangle Name="focusRect"
                                   Stroke="{TemplateBinding Foreground}"
                                   Opacity="0"
                                   StrokeThickness="2"
                                   StrokeDashArray="1 2"
                                   RadiusX="10" RadiusY="10"
                                   Margin="5" />
                    </Grid>
                </Border>

                <Rectangle Name="disabledRect" Visibility="Collapsed"
                           Fill="#000" Opacity=".6" />
            </Grid>
        </ControlTemplate>

        <Style x:Key="btnStyle" TargetType="Button">
            <Setter Property="Background" Value="White" />
            <Setter Property="Foreground" Value="LightCoral" />
            <Setter Property="BorderBrush" Value="LimeGreen" />
            <Setter Property="BorderThickness" Value="4" />
            <Setter Property="FontSize" Value="32" />
            <Setter Property="Padding" Value="10" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="Template" Value="{StaticResource btnTemplate}" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Button Content="Отключить кнопку по центру"
                FontSize="22"
                Style="{StaticResource btnStyle}"
                Click="Button_Click_1" />

        <Button Name="centerButton"
                Content="Кнопка по центру"
                Grid.Column="1"
                Style="{StaticResource btnStyle}"
                FontSize="42"
                Background="#aaa"
                Foreground="PowderBlue" />

        <Button Content="Включить кнопку по центру"
                FontSize="22"
                Grid.Column="2"
                Style="{StaticResource btnStyle}"
                Click="Button_Click_2" />
    </Grid>
</Page>

Файл XAML завершается определениями трех кнопок. Средняя кнопка получает локальные значения свойств, заменяющие значения их Style. Две внешних кнопки устанавливают и снимают блокировку средней кнопки:

private void Button_Click_1(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = false;
}

private void Button_Click_2(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = true;
}

Ha следующем снимке экрана средняя кнопка заблокирована, а третья кнопка имеет фокус ввода:

Три кнопки с различными визуальными состояниями

Использование generic.xaml

Найдите следующий каталог на машине, на которой установлен пакет Visual Studio:

C:\Program Files (x86)\Windows Kits\8.1\Include\winrt\xaml\design

В нем находятся два файла. Более короткий файл, themeresources.xaml, содержит в основном определения SolidColorBrush для стандартных цветов, доступных для приложений Windows Runtime, включая хорошо известные цвета ApplicationPageBackgroundThemeBrush и ApplicationForegroundThemeBrush. Полные наборы этих цветов разделены на три секции: Default (темная тема), Light и HighContrast. Пользователь может выбрать высококонтрастную схему в разделе Специальные возможности программы --> Параметры компьютера, вызываемой чудо-кнопкой Параметры.

Большой файл generic.xaml содержит те же определения, что и themeresources.xaml, а также все определения Style и ControlTemplate по умолчанию для всех стандартных элементов. Если вы захотите действительно хорошо освоить проектирование пользовательских шаблонов, очень важно изучить шаблоны по умолчанию в generic.xaml. В этих шаблонах также содержится (по-видимому) единственная документация по визуальным состояниям, связанным с каждым элементом управления, и именованные части, которые будут рассматриваться в следующей статье.

Чтобы найти стиль по умолчанию для конкретного элемента управления, выполните поиск строки TargetType, за которой следует имя элемента управления.

Часто в шаблонах содержатся ссылки на кисти, определенные ранее в файле generic.xaml, с определением специальных кистей для различных визуальных состояний. Например, анимации визуальных состояний в шаблоне Button по умолчанию ссылаются на кисти с такими именами, как ButtonPressedBackgroundThemeBrush и ButtonPressedForegroundThemeBrush. Фактические цвета этих кистей изменяются в зависимости от схемы оформления Light или Dark, выбранной приложением, или схемы HighContrast, которая могла быть выбрана пользователем.

Определения стилей всех стандартных элементов управления не имеют ключей. По сути это неявные стили, применяемые к элементу управления при создании его экземпляра. Все, что предоставляет приложение, становится дополнением для этого неявного стиля.

Хороший способ разработки нового шаблона для элемента управления заключается в простом копировании всего готового определения Style из generic.xaml в ваш собственный файл XAML для последующей модификации.

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