WPF 컨트롤 템플릿의 핵심 개념과 활용

로직 트리와 뷰 트리 이해하기

로직 트리는 애플리케이션에 추가된 요소들의 계층 구조를 의미합니다. 이 구조는 속성 상속, 이벤트 전파, 스타일 적용 등의 기능을 지원합니다. 반면 뷰 트리는 로직 트리를 세분화한 형태로, 각 컨트롤은 내부적으로 여러 하위 요소로 구성됩니다. 예를 들어 버튼은 단순한 박스가 아니라 ButtonChrome, ContentPresenter, TextBlock 등으로 분해됩니다.

모든 요소는 FrameworkElement 클래스에서 파생되며, 뷰 트리의 구조는 스타일이나 속성 설정에 따라 달라질 수 있습니다. 예를 들어 버튼에 텍스트를 포함하면 자동으로 TextBlock이 생성됩니다. 하지만 컨텐츠 컨트롤인 버튼은 임의의 요소를 중첩할 수 있어 유연성이 큽니다.

트리 탐색 도구

System.Windows.LogicalTreeHelperVisualTreeHelper는 트리 탐색을 위한 메서드를 제공합니다:

  • GetParent(): 부모 요소를 반환
  • GetChildren(): 자식 요소 목록을 반환 (컨테이너 여부 구분 없음)
  • GetChildrenCount(), GetChild(): 뷰 트리의 하위 요소 접근

뷰 트리 표시 코드 예제

public void ShowVisualTree(DependencyObject root)
{
    treeView.Items.Clear();
    ProcessElement(root, null);
}

private void ProcessElement(DependencyObject element, TreeViewItem parentItem)
{
    var item = new TreeViewItem();
    item.Header = element.GetType().Name;
    item.IsExpanded = true;

    if (parentItem == null)
        treeView.Items.Add(item);
    else
        parentItem.Items.Add(item);

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
    {
        ProcessElement(VisualTreeHelper.GetChild(element, i), item);
    }
}

템플릿의 개념

WPF 컨트롤은 외관이 없는 구조를 가지며, 외형은 컨트롤 템플릿을 통해 정의됩니다. 템플릿은 XAML로 작성되며, 컨트롤의 레이아웃과 시각적 표현을 결정합니다.

템플릿의 종류

  • ControlTemplate: 컨트롤의 전체 레이아웃을 재정의
  • DataTemplateHierarchicalDataTemplate: 데이터를 뷰로 매핑
  • ItemsPanelTemplate: ItemsControl 내부의 항목 배치 방식 정의

데이터 템플릿은 기존 컨트롤 내부에 요소를 추가하는 데 사용되며, 컨트롤의 기본 구조를 변경하지 않습니다. 반면 컨트롤 템플릿은 완전히 새로운 모델을 구성할 수 있어 더 강력한 커스터마이징이 가능합니다.

컨트롤 템플릿 만들기

기본적인 버튼 템플릿은 다음과 같습니다:

<ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type Button}">
  <Border Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2">
    <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}" />
  </Border>
</ControlTemplate>

ContentPresenter는 컨텐츠가 삽입될 위치를 나타내며, 모든 컨텐츠 컨트롤에 필수입니다. 또한 TemplateBinding을 사용하여 컨트롤의 속성 값을 템플릿 내부에서 참조할 수 있습니다.

템플릿 바인딩 활용

<ContentPresenter Margin="{TemplateBinding Padding}" />

이러한 바인딩은 단방향이며, Freezable 타입의 속성에는 적용되지 않으므로 문제가 발생할 경우 일반 데이터 바인딩으로 대체할 수 있습니다.

트리거를 통한 상태 반응

<ControlTemplate.Triggers>
  <Trigger Property="IsMouseOver" Value="True">
    <Setter TargetName="Border" Property="Background" Value="DarkRed" />
  </Trigger>
  <Trigger Property="IsPressed" Value="True">
    <Setter TargetName="Border" Property="Background" Value="IndianRed" />
    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki" />
  </Trigger>
  <Trigger Property="IsKeyboardFocused" Value="True">
    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
  </Trigger>
  <Trigger Property="IsEnabled" Value="False">
    <Setter TargetName="Border" Property="Foreground" Value="Gray" />
    <Setter TargetName="Border" Property="Background" Value="MistyRose" />
  </Trigger>
</ControlTemplate.Triggers>

키보드 포커스 시 보여지는 초점 표시는 Rectangle을 사용해 가시성을 조절하며, Grid 내부에 위치시켜 컨텐츠 위에 겹치도록 합니다.

템플릿 리소스 관리

템플릿을 별도의 리소스 사전(.xaml) 파일로 분리하여 관리할 수 있습니다:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type Button}">
    <Border Background="{TemplateBinding Background}"
           BorderBrush="{TemplateBinding BorderBrush}"
           BorderThickness="2" CornerRadius="2">
      <Grid>
        <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                    StrokeDashArray="1 2" StrokeThickness="1" SnapsToDevicePixels="True" />
        <ContentPresenter Margin="{TemplateBinding Padding}"
                           RecognizesAccessKey="True" />
      </Grid>
    </Border>
    <ControlTemplate.Triggers>
      <Trigger Property="IsKeyboardFocused" Value="True">
        <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
      </Trigger>
    </ControlTemplate.Triggers>
  </ControlTemplate>
</ResourceDictionary>

스타일을 통한 템플릿 적용

<Style x:Key="StyledButton" TargetType="{x:Type Button}">
  <Setter Property="Template" Value="{StaticResource CustomButtonTemplate}" />
  <Setter Property="Background" Value="{StaticResource DefaultColor}" />
  <Setter Property="BorderBrush" Value="{StaticResource BorderColor}" />
  <Setter Property="Foreground" Value="White" />
  <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
      <Setter Property="Background" Value="{StaticResource HoverColor}" />
    </Trigger>
    <Trigger Property="IsPressed" Value="True">
      <Setter Property="Background" Value="{StaticResource PressedColor}" />
    </Trigger>
    <Trigger Property="IsEnabled" Value="False">
      <Setter Property="Background" Value="{StaticResource DisabledColor}" />
    </Trigger>
  </Style.Triggers>
</Style>

템플릿을 적용하려면 Style 속성을 사용해야 하며, Template 속성은 직접 지정하는 것이 아닙니다:

<Button Style="{StaticResource StyledButton}" Margin="10" Padding="5">
  커스텀 템플릿 버튼
</Button>

자동 적용을 위한 타입 스타일

<Style TargetType="{x:Type Button}">
  <Setter Property="Template" Value="{StaticResource CustomButtonTemplate}" />
</Style>

기본 스타일을 해제하려면:

<Button Style="{x:Null}" ... />

태그: WPF ControlTemplate DataTemplate TemplateBinding Style

6월 9일 16:47에 게시됨