WPF의 스타일 및 템플릿

WPF(Windows Presentation Foundation) 스타일 지정 및 템플릿은 개발자와 디자이너가 제품에 대해 시각적으로 매력적인 효과와 일관된 모양을 만들 수 있는 기능 집합을 나타냅니다. 앱의 모양을 사용자 지정할 때 앱 내에서 또는 앱 간에 모양을 유지 관리하고 공유할 수 있는 강력한 스타일 지정 및 템플릿 모델이 필요합니다. WPF에서 이 모델을 제공합니다.

WPF 스타일 지정 모델의 또 다른 기능은 프레젠테이션과 논리의 분리입니다. 개발자가 C# 또는 Visual Basic을 사용하여 프로그래밍 논리 작업을 수행할 때 동시에 디자이너가 XAML만 사용하여 앱 모양 작업을 수행할 수 있습니다.

이 개요에서는 앱의 스타일 지정 및 템플릿 측면을 집중적으로 살펴보고 데이터 바인딩 개념은 설명하지 않습니다. 데이터 바인딩에 대한 자세한 내용은 데이터 바인딩 개요를 참조하세요.

스타일 및 템플릿을 재사용할 수 있게 해주는 리소스를 이해해야 합니다. 리소스에 대한 자세한 내용은 XAML 리소스를 참조하세요.

예제

이 개요에서 제공하는 샘플 코드는 다음 그림에 표시된 간단한 사진 검색 애플리케이션을 기반으로 합니다.

스타일 지정된 ListView

이 간단한 사진 샘플에서는 스타일 지정 및 템플릿을 사용하여 시각적으로 눈에 띄는 사용자 환경을 만듭니다. 이 샘플에는 두 개의 TextBlock 요소와 이미지 목록에 바인딩된 ListBox 컨트롤이 있습니다.

전체 샘플을 보려면 Introduction to Styling and Templating Sample(스타일 지정 및 템플릿 샘플 소개)을 참조하세요.

스타일

Style를 여러 요소에 속성 값 집합을 적용하는 편리한 방법으로 생각할 수 있습니다. FrameworkElement 또는 FrameworkContentElement에서 파생되는 모든 요소(예: Window 또는 Button)에 스타일을 사용할 수 있습니다.

스타일을 선언하는 가장 일반적인 방법은 XAML 파일의 Resources 섹션에 있는 리소스입니다. 스타일은 리소스이므로 모든 리소스에 적용되는 동일한 범위 지정 규칙을 따릅니다. 간단히 말해서 스타일을 선언하는 경우 스타일을 적용할 수 있는 위치에 영향을 줍니다. 예를 들어 스타일을 앱 정의 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>

스타일 적용된 TextBlock

ControlTemplates

WPF에서 컨트롤의 ControlTemplate은 컨트롤의 모양을 정의합니다. 새 ControlTemplate을 정의하고 컨트롤에 할당하여 컨트롤의 구조와 모양을 변경할 수 있습니다. 대부분의 경우 템플릿을 사용하면 충분하므로 사용자 지정 컨트롤을 작성할 필요가 없습니다.

각 컨트롤에는 Control.Template 속성에 할당된 기본 템플릿이 있습니다. 템플릿은 컨트롤의 시각적 표시를 컨트롤의 기능과 연결합니다. XAML에서 템플릿을 정의하므로 코드를 작성하지 않고도 컨트롤의 모양을 변경할 수 있습니다. 각 템플릿은 Button 같은 특정 컨트롤을 위해 디자인되었습니다.

일반적으로 XAML 파일의 Resources 섹션에서 템플릿을 리소스로 선언합니다. 모든 리소스와 마찬가지로 범위 지정 규칙이 적용됩니다.

컨트롤 템플릿에는 스타일보다 훨씬 더 많은 것이 관련됩니다. 컨트롤 템플릿은 전체 컨트롤의 시각적 모양을 다시 작성하는 반면 스타일은 단순히 기존 컨트롤에 속성 변경 내용을 적용하기 때문입니다. 그러나 Control.Template 속성을 설정하여 컨트롤 템플릿을 적용하므로 템플릿을 정의하거나 설정하는 데 스타일을 사용할 수 있습니다.

일반적으로 디자이너를 사용하면 기존 템플릿의 복사본을 만들고 수정할 수 있습니다. 예를 들어 Visual Studio WPF 디자이너에서 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

ContentPresenterContentControlControlTemplate에 선언된 경우 ContentPresenter는 자동으로 ContentTemplateContent 속성에 바인딩됩니다. 마찬가지로 ItemsControlControlTemplate에 있는 ItemsPresenter는 자동으로 ItemTemplateItems 속성에 바인딩됩니다.

DataTemplates

이 샘플 앱에는 사진 목록에 바인딩된 ListBox 컨트롤이 있습니다.

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

ListBox는 현재 다음과 같이 표시됩니다.

템플릿 적용 전의 ListBox

대부분 컨트롤에는 몇 가지 콘텐츠 형식이 있고 해당 콘텐츠는 보통 바인딩할 데이터에서 나옵니다. 이 샘플에서 데이터는 사진 목록입니다. WPF에서는 DataTemplate를 사용하여 데이터의 시각적 표시를 정의합니다. 기본적으로 DataTemplate에 배치하면 렌더링된 앱에서 데이터가 표시되는 모양이 결정됩니다.

샘플 앱에서 각 사용자 지정 Photo 개체에는 이미지의 파일 경로를 지정하는 string 형식의 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 속성은 StyleTargetType 속성과 비슷합니다. DataTemplate이 리소스 섹션에 있는 경우 DataType 속성을 형식으로 지정하고 x:Key를 생략하면 해당 형식이 나타날 때마다 DataTemplate이 적용됩니다. 언제나 x:Key를 사용하여 DataTemplate을 할당한 다음 DataTemplate 형식을 사용하는 속성(예: ItemTemplate 속성 또는 ContentTemplate 속성)에 대한 StaticResource로 설정할 수 있습니다.

기본적으로 위의 예제에서 DataTemplatePhoto 개체가 있을 때마다 Border 내에 Image로 표시되도록 정의합니다. 이 DataTemplate으로 이제 앱이 다음과 같이 표시됩니다.

사진 이미지

데이터 템플릿 모델은 다른 기능을 제공합니다. 예를 들어 Menu 또는 TreeView와 같은 HeaderedItemsControl 유형을 사용하여 다른 컬렉션을 포함하는 컬렉션 데이터를 표시하는 경우 HierarchicalDataTemplate이 있습니다. 다른 데이터 템플릿 기능은 사용자 지정 논리에 따라 사용할 DataTemplate을 선택할 수 있는 DataTemplateSelector입니다. 자세한 내용은 다양한 데이터 템플릿 기능을 자세히 설명하는 템플릿 개요를 참조하세요.

트리거

트리거는 속성 값이 변경되거나 이벤트가 발생할 때 속성을 설정하거나 애니메이션 등의 작업을 시작합니다. Style, ControlTemplateDataTemplate에는 모두 트리거 집합을 포함할 수 있는 Triggers 속성이 있습니다. 다음과 같은 여러 가지 유형의 트리거가 있습니다.

PropertyTriggers

속성 값을 설정하거나 속성 값에 따라 작업을 시작하는 Trigger를 속성 트리거라고 합니다.

속성 트리거를 사용하는 방법을 보여 주기 위해 선택하지 않는 한 각 ListBoxItem을 부분적으로 투명하게 만들 수 있습니다. 다음 스타일은 ListBoxItemOpacity 값을 0.5로 설정합니다. 그러나 IsSelected 속성이 true인 경우 Opacity1.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 클래스에도 트리거를 사용하여 작업을 수행할 수 있는 EnterActionsExitActions 속성이 있습니다.

ListBoxItemMaxHeight 속성이 75로 설정된 것에 유의합니다. 다음 그림에서 세 번째 항목이 선택된 항목입니다.

스타일 지정된 ListView

EventTrigger 및 and Storyboard

또 다른 트리거 유형은 이벤트 발생에 따라 일련의 작업을 시작하는 EventTrigger입니다. 예를 들어 다음 EventTrigger 개체는 마우스 포인터가 ListBoxItem 안으로 들어가면 MaxHeight 속성이 0.2초 동안 90 값으로 애니메이션 효과를 적용하도록 지정합니다. 마우스가 항목을 떠나면 속성은 1초 기간에 걸쳐 원래 값으로 돌아갑니다. MouseLeave 애니메이션에 To 값을 지정할 필요는 없습니다. 이는 애니메이션은 원래 값을 추적할 수 있기 때문입니다.

<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>

자세한 내용은 스토리보드 개요를 참조하세요.

다음 그림에서 마우스가 세 번째 항목을 가리키고 있습니다.

스타일 지정 샘플 스크린샷

MultiTrigger, DataTrigger 및 MultiDataTrigger

TriggerEventTrigger 외에 다른 유형의 트리거도 있습니다. MultiTrigger를 사용하면 여러 조건에 따라 속성 값을 설정할 수 있습니다. 조건의 속성이 데이터 바인딩된 경우 DataTriggerMultiDataTrigger를 사용합니다.

시각적 상태

컨트롤은 항상 특정 상태 에 있습니다. 예를 들어 마우스를 컨트롤의 노출 영역 위로 움직이면 컨트롤이 일반 MouseOver 상태에 있는 것으로 간주됩니다. 특정 상태가 없는 컨트롤은 일반 Normal 상태에 있는 것으로 간주됩니다. 상태는 그룹으로 구분되며 앞서 언급한 상태는 CommonStates 상태 그룹의 일부입니다. 대부분의 컨트롤에는 두 개의 상태 그룹 CommonStatesFocusStates가 있습니다. 컨트롤에 적용되는 각 상태 그룹의 컨트롤은 항상 각 그룹의 한 상태(예: CommonStates.MouseOverFocusStates.Unfocused)에 있습니다. 그러나 컨트롤은 동일한 그룹 내에서 서로 다른 두 가지 상태(예: CommonStates.NormalCommonStates.Disabled)에 있을 수 없습니다. 다음 표는 대부분의 컨트롤이 인식하고 사용하는 상태입니다.

VisualState 이름 VisualStateGroup 이름 설명
보통 CommonStates 기본 상태입니다.
MouseOver CommonStates 마우스 포인터가 컨트롤 위에 있습니다.
누름 CommonStates 컨트롤을 눌렀습니다.
사용 안 함 CommonStates 컨트롤이 비활성화되었습니다.
포커스 있음 FocusStates 컨트롤에 포커스가 있습니다.
포커스 없음 FocusStates 컨트롤에 포커스가 없습니다.

컨트롤 템플릿의 루트 요소에 System.Windows.VisualStateManager을 정의하여 컨트롤이 특정 상태로 전환될 때 애니메이션을 트리거할 수 있습니다. VisualStateManager는 감시할 VisualStateGroupVisualState의 조합을 선언합니다. 컨트롤이 감시되는 상태로 들어가면 VisaulStateManager에서 정의한 애니메이션이 시작됩니다.

예를 들어 다음 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>

        ...

스토리보드에 대한 자세한 내용은 스토리보드 개요를 참조하세요.

공유 리소스 및 테마

일반적인 WPF 앱에는 앱 전체에 적용되는 여러 UI 리소스가 있을 수 있습니다. 전체적으로 이 리소스 집합을 앱에 대한 테마로 간주할 수 있습니다. WPF는 ResourceDictionary 클래스로 캡슐화된 리소스 사전을 사용하여 UI 리소스를 테마로 패키징할 수 있도록 지원합니다.

WPF 테마는 WPF가 요소의 시각 효과를 사용자 지정하기 위해 표시하는 스타일 지정 및 템플릿 메커니즘을 통해 정의됩니다.

WPF 테마 리소스는 포함된 리소스 사전에 저장됩니다. 이러한 리소스 사전은 서명된 어셈블리에 포함되어야 하고 같은 어셈블리에 코드 자체로 포함되거나 side-by-side 어셈블리에 포함될 수 있습니다. WPF 컨트롤이 포함된 어셈블리인 PresentationFramework.dll의 경우 테마 리소스는 일련의 side-by-side 어셈블리에 포함됩니다.

테마는 요소의 스타일을 검색할 때 보이는 마지막 위치가 됩니다. 일반적으로 적절한 리소스를 검색할 때 요소 트리에서 위로 이동하면 검색이 시작되고 앱 리소스 컬렉션을 확인하고 마지막으로 시스템을 쿼리합니다. 이를 통해 앱 개발자는 테마에 도달하기 전에 트리 또는 앱 수준에서 개체에 대한 스타일을 다시 정의할 수 있습니다.

리소스 사전을 여러 앱에 걸쳐 테마를 다시 사용할 수 있는 개별 파일로 정의할 수 있습니다. 또한 같은 형식의 리소스를 제공하지만 값이 서로 다른 여러 리소스 사전을 정의하여 전환 가능한 테마를 만들 수 있습니다. 앱에 스킨을 지정하려면 앱 수준에서 이러한 스타일이나 다른 리소스를 다시 정의하는 것이 좋습니다.

앱 전체에서 스타일 및 템플릿을 비롯한 리소스 집합을 공유하려면 XAML 파일을 만들고 shared.xaml 파일에 대한 참조를 포함하는 ResourceDictionary를 정의할 수 있습니다.

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

shared.xaml 공유 자체가 앱의 컨트롤이 일관된 모양을 갖도록 하는 스타일 및 브러시 리소스 집합을 포함하는 ResourceDictionary를 정의합니다.

자세한 내용은 병합된 리소스 사전을 참조하세요.

사용자 지정 컨트롤에 대한 테마를 만드는 경우 컨트롤 제작 개요테마 수준에서 리소스 정의 섹션을 참조하세요.

참조