Стили и шаблоны (WPF .NET)

Стилизация и использование шаблонов Windows Presentation Foundation (WPF) относятся к набору возможностей, которые позволяют разработчикам и дизайнерам создавать визуально привлекательные эффекты и согласованный внешний вид своих продуктов. При настройке внешнего вида приложения необходима строгая модель стилизации и шаблонов, обеспечивающая обслуживание и совместное использование внешнего вида в приложениях и между ними. WPF предоставляет такую модель.

Еще одной возможностью модели стилизации WPF является разделение представления и логики. Дизайнеры могут создавать внешний вид приложения только с помощью XAML в то же самое время, когда разработчики работают над логикой программы, используя языки C# или Visual Basic.

В этом обзоре основное внимание уделяется аспектам стилизации и использования шаблонов приложения и не рассматриваются концепции привязки данных. Подробнее о привязке данных см. в разделе Общие сведения о привязке данных.

Важно иметь представление о ресурсах, которые позволяют повторно использовать стили и шаблоны. Дополнительные сведения о ресурсах см. в разделе Общие сведения о ресурсах XAML.

Важно!

Документация по рабочему столу для .NET 7 и .NET 6 находится в стадии разработки.

Пример

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

Styled ListView

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

Полный пример см. в разделе Вводная часть примера стилизации и использования шаблонов.

Стили

Элемент Style можно рассматривать как удобный способ применения набора значений свойств к нескольким элементам. Стиль можно использовать для любого элемента, производного от FrameworkElement или FrameworkContentElement, например Window или Button.

Чаще всего стиль объявляется как ресурс в разделе Resources файла XAML. Так как стили являются ресурсами, для них действуют те же правила определения области, что и для всех других ресурсов. Проще говоря, то, где вы объявляете стиль, влияет на то, где этот стиль может быть применен. Например, если объявить стиль в корневом элементе файла XAML определения приложения, стиль может использоваться в любом месте приложения.

Например, в следующем коде XAML объявляются два стиля для TextBlock, один из которых автоматически применяется ко всем элементам TextBlock, а для другого необходимы явные ссылки.

<Window.Resources>
    <!-- .... other resources .... -->

    <!--A Style that affects all TextBlocks-->
    <Style TargetType="TextBlock">
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="FontFamily" Value="Comic Sans MS"/>
        <Setter Property="FontSize" Value="14"/>
    </Style>
    
    <!--A Style that extends the previous TextBlock Style with an x:Key of TitleText-->
    <Style BasedOn="{StaticResource {x:Type TextBlock}}"
           TargetType="TextBlock"
           x:Key="TitleText">
        <Setter Property="FontSize" Value="26"/>
        <Setter Property="Foreground">
            <Setter.Value>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <LinearGradientBrush.GradientStops>
                        <GradientStop Offset="0.0" Color="#90DDDD" />
                        <GradientStop Offset="1.0" Color="#5BFFFF" />
                    </LinearGradientBrush.GradientStops>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

Ниже приведен пример стилей, объявленных выше.

<StackPanel>
    <TextBlock Style="{StaticResource TitleText}" Name="textblock1">My Pictures</TextBlock>
    <TextBlock>Check out my new pictures!</TextBlock>
</StackPanel>

Styled textblocks

Дополнительные сведения см. в статье Создание стиля элемента управления в WPF.

ControlTemplates

В WPF ControlTemplate элемента управления определяет внешний вид этого элемента управления. Структуру и внешний вид элемента управления можно изменить, определив и назначив для него новый объект ControlTemplate. Во многих случаях шаблоны являются достаточно гибким средством, и вам не придется писать собственные пользовательские элементы управления.

Каждый элемент управления имеет шаблон по умолчанию, назначенный свойству Control.Template. Шаблон соединяет визуальное представление элемента управления с его возможностями. Путем определения шаблона в XAML можно изменить внешний вид элемента управления без написания какого-либо кода. Каждый шаблон предназначен для конкретного элемента управления, например Button.

Обычно шаблон объявляется как ресурс в разделе Resources файла XAML. Как и для всех ресурсов, применяются правила области.

Шаблоны элементов управления более сложные, чем стиль. Это связано с тем, что шаблон элемента управления перезаписывает весь его внешний вид, в то время как стиль просто применяет изменения свойств к существующему элементу управления. Однако, так как шаблон элемента управления применяется путем установки свойства Control.Template, можно использовать стиль для определения или установки шаблона.

Обычно разработчики позволяют создать копию существующего шаблона и изменить ее. Например, в конструкторе WPF в Visual Studio выберите элемент управления CheckBox, а затем щелкните правой кнопкой мыши и последовательно выберите Изменить шаблон>Создать копию. Эта команда создает стиль, который определяет шаблон.

<Style x:Key="CheckBoxStyle1" TargetType="{x:Type CheckBox}">
    <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual1}"/>
    <Setter Property="Background" Value="{StaticResource OptionMark.Static.Background1}"/>
    <Setter Property="BorderBrush" Value="{StaticResource OptionMark.Static.Border1}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type CheckBox}">
                <Grid x:Name="templateRoot" Background="Transparent" SnapsToDevicePixels="True">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Border x:Name="checkBoxBorder" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="1" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
                        <Grid x:Name="markGrid">
                            <Path x:Name="optionMark" Data="F1 M 9.97498,1.22334L 4.6983,9.09834L 4.52164,9.09834L 0,5.19331L 1.27664,3.52165L 4.255,6.08833L 8.33331,1.52588e-005L 9.97498,1.22334 Z " Fill="{StaticResource OptionMark.Static.Glyph1}" Margin="1" Opacity="0" Stretch="None"/>
                            <Rectangle x:Name="indeterminateMark" Fill="{StaticResource OptionMark.Static.Glyph1}" Margin="2" Opacity="0"/>
                        </Grid>
                    </Border>
                    <ContentPresenter x:Name="contentPresenter" Grid.Column="1" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasContent" Value="true">
                        <Setter Property="FocusVisualStyle" Value="{StaticResource OptionMarkFocusVisual1}"/>
                        <Setter Property="Padding" Value="4,-1,0,0"/>

... content removed to save space ...

Редактирование копии шаблона — отличный способ научиться работать с шаблонами. Вместо создания пустого шаблона проще редактировать копию и изменять некоторые аспекты визуального представления.

Пример см. в статье Создание шаблона элемента управления.

TemplateBinding

Возможно, вы заметили, что ресурс шаблона, определенный в предыдущем разделе, использует расширение разметки TemplateBinding. TemplateBinding является оптимизированной формой привязки для сценариев шаблонов, аналогичных привязке, созданной с помощью {Binding RelativeSource={RelativeSource TemplatedParent}}. TemplateBinding может использоваться для привязки частей шаблона к свойствам элемента управления. Например, каждый элемент управления имеет свойство BorderThickness. Используйте TemplateBinding, чтобы управлять элементом шаблона, затрагиваемым параметром элемента управления.

ContentControl и ItemsControl

Если в элементе ControlTemplateContentControl объявляется ContentPresenter, элемент ContentPresenter будет автоматически привязан к свойствам ContentTemplate и Content. Аналогичным образом элемент ItemsPresenter, который находится в ControlTemplateItemsControl, автоматически привязывается к свойствам ItemTemplate и Items.

DataTemplates

В этом примере приложения имеется элемент управления ListBox, связанный со списком фотографий.

<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}"
         Background="Silver" Width="600" Margin="10" SelectedIndex="0"/>

Теперь ListBox выглядит так.

ListBox before applying template

Большинство элементов управления имеют некое содержимое, и это содержимое часто поступает из данных, к которым осуществляется привязка. В этом примере такими данными является список фотографий. В WPF для определения визуального представления данных можно использовать DataTemplate. По сути, то, что вы поместите в DataTemplate, определяет визуальное представление данных в отображаемом приложении.

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

public class Photo
{
    public Photo(string path)
    {
        Source = path;
    }

    public string Source { get; }

    public override string ToString() => Source;
}
Public Class Photo
    Sub New(ByVal path As String)
        Source = path
    End Sub

    Public ReadOnly Property Source As String

    Public Overrides Function ToString() As String
        Return Source
    End Function
End Class

Чтобы фотографии отображались как изображения, необходимо создать DataTemplate в качестве ресурса.

<Window.Resources>
    <!-- .... other resources .... -->

    <!--DataTemplate to display Photos as images
    instead of text strings of Paths-->
    <DataTemplate DataType="{x:Type local:Photo}">
        <Border Margin="3">
            <Image Source="{Binding Source}"/>
        </Border>
    </DataTemplate>
</Window.Resources>

Обратите внимание, что свойство DataType подобно свойству TargetTypeStyle. Если DataTemplate находится в разделе ресурсов, когда вы указываете свойство DataType для типа и не назначаете ему атрибут x:Key, DataTemplate будет применяться каждый раз при появлении этого типа. Вы всегда можете назначить DataTemplate атрибут x:Key, а затем задать его в качестве StaticResource для свойств, которые принимают типы DataTemplate (например, свойство ItemTemplate или ContentTemplate).

По существу DataTemplate в приведенном выше примере определяет, что при наличии объекта Photo он должен отображаться в качестве элемента Image в рамках Border. С этим шаблоном DataTemplate приложение теперь выглядит так.

Photo image

Модель использования шаблонов данных предоставляет и другие возможности. Например, для отображения данных коллекции, содержащей другие коллекции, с помощью типа HeaderedItemsControl, например Menu или TreeView, имеется шаблон HierarchicalDataTemplate. Другой возможностью использования шаблонов данных является элемент DataTemplateSelector, который позволяет выбрать DataTemplate для использования на основе пользовательской логики. Дополнительные сведения см. в разделе Общие сведения о шаблонах данных, в котором более подробно рассматриваются различные возможности использования шаблонов данных.

Триггеры

Триггер задает значения свойств или активирует различные действия (например, анимацию) при изменении значения свойства или при возникновении какого-либо события. В Style, ControlTemplateи DataTemplate есть свойство Triggers, которое может содержать набор триггеров. Существует несколько типов триггеров.

PropertyTriggers

Объект Trigger, который задает значения свойств или активирует действия на основе значения свойства, называется триггером свойств.

Чтобы продемонстрировать использование триггеров свойств, можно сделать каждый элемент ListBoxItem частично прозрачным, если он не выбран. В следующем стиле для свойства Opacity типа ListBoxItem задается значение 0.5. Если свойство IsSelected имеет значениеtrue, то, несмотря на это, для Opacity присваивается значение 1.0.

<Window.Resources>
    <!-- .... other resources .... -->

    <Style TargetType="ListBoxItem">
        <Setter Property="Opacity" Value="0.5" />
        <Setter Property="MaxHeight" Value="75" />
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Trigger.Setters>
                    <Setter Property="Opacity" Value="1.0" />
                </Trigger.Setters>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

В этом примере объект Trigger используется для установки значения свойства, но обратите внимание, что у класса Trigger также есть свойства EnterActions и ExitActions, которые позволяют триггеру выполнять действия.

Обратите внимание, что свойство MaxHeight типа ListBoxItem имеет значение 75. На следующем рисунке третий элемент является выбранным.

Styled ListView

Объекты EventTrigger и раскадровки

Еще один тип триггера — это объект EventTrigger, который активирует набор действий по возникновению события. Например, следующие объекты EventTrigger определяют, что при наведении указателя мыши на элемент ListBoxItem выполняется анимированный переход свойства MaxHeight к значению 90 в течение 0.2 сек. Когда указатель мыши перемещается за пределы этого элемента, свойство возвращается к исходному значению в течение 1 сек. Обратите внимание, что нет необходимости указывать значение To для анимации MouseLeave. Анимация сама может отслеживать исходное значение.

<Style.Triggers>
    <Trigger Property="IsSelected" Value="True">
        <Trigger.Setters>
            <Setter Property="Opacity" Value="1.0" />
        </Trigger.Setters>
    </Trigger>
    <EventTrigger RoutedEvent="Mouse.MouseEnter">
        <EventTrigger.Actions>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation
                        Duration="0:0:0.2"
                        Storyboard.TargetProperty="MaxHeight"
                        To="90"  />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger.Actions>
    </EventTrigger>
    <EventTrigger RoutedEvent="Mouse.MouseLeave">
        <EventTrigger.Actions>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation
                        Duration="0:0:1"
                        Storyboard.TargetProperty="MaxHeight"  />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger.Actions>
    </EventTrigger>
</Style.Triggers>

Подробнее см. в статье Общие сведения о Storyboard.

На следующем рисунке указатель мыши наведен на третий элемент.

Styling sample screenshot

Объекты MultiTrigger, DataTrigger и MultiDataTrigger

Помимо триггеров Trigger и EventTrigger, существуют и другие типы триггеров. Триггер MultiTrigger позволяет задавать значения свойств на основании нескольких условий. Используйте триггеры DataTrigger и MultiDataTrigger, когда свойство условия имеет привязку к данным.

Визуальные состояния

Элементы управления всегда находятся в определенном состоянии. Например, когда указатель мышь перемещается над элементом управления, то считается, что элемент управления находится в обычном состоянии MouseOver. Элемент управления без определенного состояния рассматривается как элемент управления с обычным состоянием Normal. Состояния разбиваются на группы, а упомянутые выше состояния являются частью группы состояний CommonStates. Большинство элементов управления имеют две группы состояний: CommonStates и FocusStates. Для каждой группы состояний, применяемой к элементу управления, элемент управления всегда находится в одном из состояний каждой группы, например CommonStates.MouseOver и FocusStates.Unfocused. Элемент управления не может находиться в двух разных состояниях в рамках одной группы, например CommonStates.Normal и CommonStates.Disabled. Ниже приведена таблица состояний, которые большинство элементов управления распознают и используют.

Имя VisualState Имя VisualStateGroup Description
Норм. CommonStates Состояние по умолчанию.
MouseOver CommonStates Указатель мыши расположен над элементом управления.
При нажатии CommonStates Элемент управления нажат.
Выключено CommonStates Элемент управления отключен.
Режим фокусировки FocusStates Элемент управления имеет фокус.
Без фокуса ввода FocusStates Элемент управления не имеет фокуса.

Определив System.Windows.VisualStateManager в корневом элементе шаблона элемента управления, вы можете активировать анимации при переходе элемента управления в определенное состояние. VisualStateManager объявляет, какие сочетания VisualStateGroup и VisualState отслеживать. Когда элемент управления переходит в состояние отслеживания, запускается анимация, определенная VisualStateManager.

Например, следующий код XAML отслеживает состояние CommonStates.MouseOver, чтобы анимировать цвет заливки элемента backgroundElement. Когда элемент управления возвращается в состояние CommonStates.Normal, восстанавливается цвет заливки элемента backgroundElement.

<ControlTemplate x:Key="roundbutton" TargetType="Button">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="CommonStates">
                <VisualState Name="Normal">
                    <ColorAnimation Storyboard.TargetName="backgroundElement"
                                    Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                    To="{TemplateBinding Background}"
                                    Duration="0:0:0.3"/>
                </VisualState>
                <VisualState Name="MouseOver">
                    <ColorAnimation Storyboard.TargetName="backgroundElement"
                                    Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                    To="Yellow"
                                    Duration="0:0:0.3"/>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        ...

Дополнительные сведения о раскадровках см. в статье Общие сведения о Storyboard.

Общие ресурсы и темы

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

Темы WPF задаются с помощью механизмов стилизации и использования шаблонов, которые WPF предоставляет для настройки отображения любого элемента.

Ресурсы темы WPF хранятся в словарях внедренных ресурсов. Эти словари ресурсов должны быть внедрены в подписанную сборку и могут быть внедрены либо в ту же сборку, что и сам код, либо в параллельную сборку. Для библиотеки PresentationFramework.dll (сборки, содержащей элементы управления WPF) ресурсы тем находятся в ряде параллельных сборок.

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

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

Для совместного использования набора ресурсов (в том числе стилей и шаблонов) в приложениях можно создать файл XAML и определить объект ResourceDictionary, который содержит ссылку на файл shared.xaml.

<ResourceDictionary.MergedDictionaries>
  <ResourceDictionary Source="Shared.xaml" />
</ResourceDictionary.MergedDictionaries>

Это совместное использование файла shared.xaml, который определяет объект ResourceDictionary, содержащий набор стилей и ресурсов кисти, что позволяет придать согласованный вид элементам управления приложения.

Подробнее см. в статье Объединенные словари ресурсов.

Если вы создаете тему для пользовательского элемента управления, ознакомьтесь с разделом Определение ресурсов на уровне темы в статье Общие сведения о разработке элементов управления.

См. также