본문 바로가기
카테고리 없음

[WPF] Control Templates (+Logical & Visual Trees)

by 성장의 용 2025. 8. 10.
728x90
반응형

1. Logical & Visual Trees

* Logical Tree

 

  • XAML에서 작성한 UI 요소의 논리적 관계 구조를 나타낸다.
  • “어떤 요소가 어떤 요소의 자식인지”와 같은 구성 및 계층 관계를 반영한다.
  • 데이터 바인딩, 리소스 탐색, 이벤트 라우팅(Logical Routing) 등에 사용된다.

- 특징

  • 컨트롤의 구성 요소만 표시되며, 내부적으로 렌더링되는 세부 그래픽 요소(버튼의 사각형, 테두리 등)는 표시되지 않음
  • FrameworkElement.Parent나 LogicalTreeHelper로 탐색 가능.
  • 리소스(Resource) 탐색 경로는 Logical Tree를 따라 올라간다.
//xaml
<Window>
    <StackPanel>
        <Button Content="OK" />
        <Button Content="Cancel" />
    </StackPanel>
</Window>

// Logical Tree
Window
 └─ StackPanel
     ├─ Button ("OK")
     └─ Button ("Cancel")

 

 

* Visual Tree

 

  • 실제 렌더링에 사용되는 모든 시각적 요소들의 계층 구조를 나타낸다
  • Logical Tree의 각 요소가 렌더링되기 위해 더 세분화된 요소(Visual, Drawing, Border 등) 로 확장됨

 

- 특징

 

  • 컨트롤 템플릿 내부의 구조, 각종 장식 요소(Decorators), 레이아웃 컨테이너 등이 모두 포함된다.
  • 렌더링, Hit Testing(마우스 충돌 판정) 등에 사용된다.
  • VisualTreeHelper를 사용해 탐색 가능

 

Button
 └─ Border
     └─ ContentPresenter
         └─ TextBlock ("OK")

* Logical & Visual Tree 보기 예시

// Logical Tree 보기
foreach (var child in LogicalTreeHelper.GetChildren(myWindow))
{
    Console.WriteLine(child);
}

// Visual Tree 보기
void PrintVisualTree(DependencyObject obj, int indent = 0)
{
    Console.WriteLine(new string(' ', indent) + obj.GetType().Name);
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        PrintVisualTree(VisualTreeHelper.GetChild(obj, i), indent + 2);
    }
}
PrintVisualTree(myButton);

2. Control Template

 

  • 기존 윈도우 컨트롤은 외양이 고정되어 있으며, WPF 이전에는 커스터마이징이 제한적이었음.
  • WPF의 ControlTemplate은 이러한 제약을 깨고, 컨트롤의 Visual Tree를 완전히 새롭게 구성할 수 있는 자유를 제공

* 구조와 정의

 

  • ControlTemplate은 XAML 리소스로 정의되며, Control.Template 속성에 할당해서 컨트롤의 외관을 바꿀 수 있음.
  • 주로 스타일(Style)에 포함시켜 재사용하거나 전역 리소스로 공유 가능

* Trigger와 Visual States

 

 

  • ControlTemplate 내부에서는 Trigger, EventTrigger, DataTrigger 등을 활용해 상태에 따른 시각 효과나 애니메이션을 정의할 수 있음.
  • WPF의 VisualStateManager를 이용하면, 상태 기반(예: Normal, MouseOver 등) 시각 효과 전환도 가능

 

* 예제들

- 버튼 외형 변경

 

  • ContentPresenter가 원래 버튼의 Content를 표시.
  • Logical Tree에는 여전히 Button이지만, Visual Tree는 Border와 ContentPresenter로 구성.

 

<Window.Resources>
    <ControlTemplate x:Key="FlatButtonTemplate" TargetType="Button">
        <Border Background="LightBlue" CornerRadius="5" Padding="5">
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
    </ControlTemplate>
</Window.Resources>

<StackPanel Margin="20">
    <Button Template="{StaticResource FlatButtonTemplate}" Content="Click Me!" Width="100" Height="40"/>
</StackPanel>

 

- 스타일과 결합해서 전역 적용

 

  • Template을 스타일에 넣으면 해당 윈도우의 모든 Button에 적용.
  • 코드 중복 없이 외형 통일 가능.

 

<Window.Resources>
    <Style TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="Orange" CornerRadius="3" Padding="8">
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

<StackPanel>
    <Button Content="OK"/>
    <Button Content="Cancel"/>
</StackPanel>

- Trigger로 상태 변화

 

  • Trigger를 이용해 마우스 오버, 클릭 상태에서 배경색 변경.
  • VisualStateManager를 쓰면 더 정교하게 가능

 

<Window.Resources>
    <ControlTemplate x:Key="HoverButtonTemplate" TargetType="Button">
        <Border x:Name="border" Background="LightGray" CornerRadius="5" Padding="5">
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="border" Property="Background" Value="SkyBlue"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="border" Property="Background" Value="SteelBlue"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Window.Resources>

<Button Template="{StaticResource HoverButtonTemplate}" Content="Hover Me!" Width="120" Height="40"/>

- checkbox의 커스터마이징

 

  • 체크박스의 체크 마크 대신 배경색으로 상태 표현.
  • 기존 테마의 체크박스 모양 완전 제거 가능.

 

<Window.Resources>
    <ControlTemplate x:Key="CustomCheckBoxTemplate" TargetType="CheckBox">
        <StackPanel Orientation="Horizontal">
            <Border x:Name="box" Width="20" Height="20" BorderBrush="Black" BorderThickness="1" Margin="0,0,5,0"/>
            <ContentPresenter VerticalAlignment="Center"/>
        </StackPanel>
        <ControlTemplate.Triggers>
            <Trigger Property="IsChecked" Value="True">
                <Setter TargetName="box" Property="Background" Value="Green"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Window.Resources>

<CheckBox Template="{StaticResource CustomCheckBoxTemplate}" Content="Accept Terms"/>

- ItemsControl 커스터마이징

 

  • ItemsPresenter는 ItemsControl의 아이템 목록을 표시하는 핵심 요소.
  • 템플릿으로 감싸면 외형만 변경.

 

<Window.Resources>
    <ControlTemplate x:Key="CustomItemsControlTemplate" TargetType="ItemsControl">
        <Border BorderBrush="Black" BorderThickness="1" Padding="5">
            <ItemsPresenter/>
        </Border>
    </ControlTemplate>
</Window.Resources>

<ItemsControl Template="{StaticResource CustomItemsControlTemplate}">
    <sys:String>Item 1</sys:String>
    <sys:String>Item 2</sys:String>
    <sys:String>Item 3</sys:String>
</ItemsControl>

- Button + VisualStateManager 예제

 

  • VisualStateManager로 상태 변화에 애니메이션 부여.
  • 복잡한 UI 상태 전환에 유용

 

<Window.Resources>
    <ControlTemplate x:Key="VSMButtonTemplate" TargetType="Button">
        <Border x:Name="border" Background="Gray" CornerRadius="4" Padding="5">
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="MouseOver">
                    <Storyboard>
                        <ColorAnimation Storyboard.TargetName="border"
                                        Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                        To="LightBlue" Duration="0:0:0.2"/>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Pressed">
                    <Storyboard>
                        <ColorAnimation Storyboard.TargetName="border"
                                        Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                        To="DarkBlue" Duration="0:0:0.1"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </ControlTemplate>
</Window.Resources>

<Button Template="{StaticResource VSMButtonTemplate}" Content="VSM Button"/>

 

* Templates vs Style

둘 다 UI 외형을 바꾸지만 적용 범위와 기능에서 차이가 있다.

구분 Style Template
목적 컨트롤 속성 집합을 재사용 가능하게 묶어서 적용 컨트롤의 시각적 구조(Visual Tree) 자체를 변경
적용 대상 거의 모든 DependencyObject
(주로 FrameworkElement)
주로 Control 및 하위 클래스
변경 범위 색상, 크기, 폰트, 여백, 패딩 등 속성 값 변경 버튼의 테두리, 배경, 내부 구조 등 UI 구성 요소 자체 변경
핵심 요소 Setter, Trigger, BasedOn ControlTemplate, DataTemplate, ItemsPanelTemplate
예시 효과 버튼 색을 빨간색으로, 글자 크기를 16으로 변경 버튼을 완전 평면 UI로, 또는 이미지를 배경으로 한 원형 버튼으로 변경