[WPF] ContentPresenter란?

WPF의 큰 장점이 XAML을 이용하여 시각적으로 표현이 유연한 Windows Desktop App 개발을 할 수 있다는 점인데요, 이 시각적으로 표현이 유연한 레벨이 되려면 XAML의 러닝커브가 좀 있는 거 같습니다. 이제 갓 WPF를 1년 남짓 한 저로서는 ContentPresenter는 좀 어려운 개념인데요.

WPF의 컨트롤은 무조건 ContentControl, ItemsControl를 상속받아 구현되었다고 알고 있습니다.
ContentControl은 하위 컨트롤을 딱 1개 받을 수 있는 컨트롤이고, ItemsControl은 하위 컨트롤로 Collection형태의 다수의 Item을 받을 수 있는 것으로 알고 있으며, 이 때 하위 컨트롤에 대한 표현 형식을 DataTemplate으로 여러 Control들을 조합하여 표현하는 것으로 알고 있습니다.

그래서 저같은 초보들이 이런 궁금증을 갖는 듯 합니다. DataTemplete으로 컨트롤들을 조합하여 자유롭게 표현이 가능한데 도대체 ControlTemplate은 언제쓰는거지? 실제로 저같은 초보를 많이보지만 저도 몰라서 답변을 못하고 있습니다.

찾아보니 Winform에서 OnPaint같은 메서드로 컨트롤을 다시 그리듯, 컨트롤의 원형을 뭉개버리고 완전히 새로운 형태의 시각적 컨트롤로 탈바꿈 시킬 때 ControlTemplate을 쓰는 것 같았습니다. 그래서 아직 초보수준이라 컨트롤의 형태를 바꿔 버릴 만큼의 시각적 업무는 못하는데다 산업용 프로그램이다보니 그냥 이런거다만 하고 넘어갔는데요.

DevExpress를 사용하면서 예제로 받은 샘플이 간단하게 빨간 테두리만 치는 것인데 ControlTemplate을 사용해서 정의를 하고, ContentPresenter를 쓰고 있었습니다. ContentPresenter라는 것을 처음 본 것은 아니지만, 이해하기 어려워서 일단 넘어가고 있었는데, 막상 예제로 받아보니 이제는 알아야겠다는 생각이 들어서 이것저것 자료조사를 해봤습니다. 하지만 아직 이해가 좀 힘들었습니다.

여러 예제들을 보니 본 예제중에서는 ControlTemplate은 ContentPresenter랑 1:1로 꼭 붙어 다니면서 ControlTemplate을 사용해서 재정의한 것을 그냥 <ContentPresenter /> 이거 한 줄로 표현해주는 느낌이었습니다. 사용법이 좀 이해가 안 갔던게…재정의한 XAML의 가장 안쪽에 <ContentPresenter/>이거만 쓰는거 같던데 XAML의 어느부분에서 저 코드를 써야하는지도 모르겠습니다.

혹시 ControlTemplate과 ContentPresenter에 대해 이거다! 라고 설명이 가능한 아티클이 있을까요? 있으면 링크 부탁드리고…설명도 부가적으로 해주신다면 감사드리겠습니다.

1개의 좋아요

DataTemplete는 데이터를 어떻게 표현할것이냐 이고, ControlTemplete는 컨트롤을 어떻게 표현할것이냐의 차이가 있습니다. 자신이 직접 컨트롤을 만들거나, 아니면 기존의 컨트롤의 모양을 수정하지 않으면 ControlTemplete는 실제로 실무에 그닥 사용하지 않아도 됩니다. (기존의 컨트롤을 잘 이용하면 됩니다.)

ControlTemplete의 훌륭한 예제는 WPF에서 기본 제공하는 컨트롤들의 소스코드입니다. 우리가 자주 사용하는 TextBox, Label, Button 등은 내부적으로 ControlTemplete으로 어떻게 보여지는지를 Style로 기본 지정되어 있습니다. ModernWpfUI 등의 WPF UI 라이브러리의 소스코드를 살펴보는 것도 좋습니다. 이런 라이브러리는 기본적으로 기본 컨트롤 뿐만 아니라 자체 제공하는 컨트롤의 ControlTemplete를 반드시 지정해야 하기 때문입니다.

ContentPresenter는 간단히 접근하셔도 되세요. 외부의 Content가 그 자리에 놓여진다고 생각하면 됩니다. 가장 대표적인 예가 Button의 Content인데, MS 문서의 예제 코드를 잠시 빌려오겠습니다.

<Style TargetType="Button">
  <!--Set to true to not get any properties from the themes.-->
  <Setter Property="OverridesDefaultStyle" Value="True"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Ellipse Fill="{TemplateBinding Background}"/>
          <ContentPresenter HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

TargetType을 기본 타입인 Button으로 지정했으므로 적용되는 리소스의 위치(단계)에 따라 그 단계 이후엔 전역 적용이 되게 됩니다.

<Button>xxx</Button>

xxx의 트리가 그대로 ContentPresenter의 위치에 들어간다고 생각하면 됩니다.

Content가 여러개인 컨트롤도 있을 수 있습니다. 이런 경우 다음 처럼 ContentPresenter에 Content속성에 TempleteBinding 해주면 됩니다.

<ControlTemplate>  
    <Grid>
        <ContentPresenter Content="{TemplateBinding ContentLeft}" HorizontalAlignment="Left"/>
        <ContentPresenter Content="{TemplateBinding ContentRight}" HorizontalAlignment="Right"/>
    </Grid>
</ControlTemplate> 

여기서 ContentLeft, ContentRight는 이 컨트롤에서 템플릿 바인딩 되도록 노출해줘야 합니다.

7개의 좋아요

WPF 컨트롤 XAML위치는 아마 여기 일꺼에요.

wpf/src/Microsoft.DotNet.Wpf/src/Themes/XAML at main · dotnet/wpf (github.com)

3개의 좋아요

@Vincent 두서 없이 설명드려볼게요.

아시는 부분도 많으시겠지만 시작하실 분들을 위해서도 상세하게 설명드립니다.

우선 Control 클래스에서부터 시작해볼게요.

Control 클래스는 Template (ControlTemplate) 속성을 포함하고 있습니다.

public class Control
{
    public ControlTemplate Template { get; set; }
}

그리고 ItemsControlContentControl은 Control 클래스를 상속받고 있죠. 그래서 이 둘은 Template을 사용할 수 있어요.

Template (ControlTemplate)은 컨트롤 모양이라고 생각하면 편해요.

그리고,
기본적으로 제공되는 Button은 실제로 ControlTemplate이 아래처럼 구현되어 있을겁니다.

<Style TargetType="{x:Type Button}">
    <Setter Property="Template">
        <ControlTemplate TargetType="{x:Type Button}">
            <Border BorderThickness="1"
                    BorderBrush="#CCCCCC"
                    Background="{TemplateParent Background}"
                    Padding="8 4 8 4"
                    CornerRadius="2 2 2 2">
                <ContentPresenter ContentSource="Content"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#DDDDDD"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Setter>
</Style>

버튼의 모양, 트리거 등은 ControlTemplate에서 정의하고, 내부에 더 커스터마이징 가능한 영역을 지정하기 위해 ContentPresenter를 사용합니다.

그래서 Button을 사용할 때에는 Content 내용만 넣으면 되죠.

<Button Content="버튼"/>

Content를 입력하면 내부적으로 ContentPresenter를 대체하게 됩니다.

그리고 이와 동일한 개념으로 Window 클래스 또한 동일한 구조입니다.
윈도우도 버튼과 동일하게 ContentControl을 상속받는데요.

이미 윈도우도 ControlTemplate아래처럼 제공하고 있습니다.

<Style TargetType="{x:Type Window}">
    <Setter Property="Template">
        <ControlTemplate TargetType="{x:Type Window}">
            <Border>
                <Grid>
                    <Grid.RowDefinition>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinition>
                    <Button x:Name="PART_MinButton"/>
                    <Button x:Name="PART_MaxButton"/>
                    <Button x:Name="PART_CloseButton"/>
                    <ContentPresenter ContentSource="Content"/>
                </Grid>
            </Border>
        </ContentTemplate>
    </Setter>
</Style>

윈도우 클래스도 이미 ControlTemplate에서 모든 모양이 구현되어 제공되고 있기 때문에 ContentPresenter 영역에 해당하는 Content 부분만 채워 넣으면 되는데요.

소스코드에서는 아래처럼 Content 영역만 사용할 수도 있습니다.

Button btn = new();
btn.Content "확인";

Window win = new();
win.Content = new Grid();

또한 이미 ControlTemplate이 정의되어 있기 때문에,
Xaml에서도 마찬가지로 Content 영역만 신경쓰면 됩니다.

<Window x:Class="Exam.MainWindow.cs" ...>
    <Window.Content>
        <Grid>
            <!-- 윈도우 안 영역 -->
        </Grid>
    </Window.Content>
</Window>

ContentControl, ItemsControl을 상속받는 모든 컨트롤은 사실 ControlTemplate의 모양(기반)을 시작으로 이루어져 있는 셈이죠.

그리고 Content는 생략할 수 도 있어서 우리가 Window나 UserControl을 생성하면 아래처럼 바로 Grid 등의 UI 컨트롤을 배치시킬 수 있습니다.

(이런 생략 규칙이 Content 개념을 이해하는데 독이 되지 않았나 생각해요…)

<Window x:Class="Exam.MainWindow.cs" ...>
    <Grid>
        <views:ProfileView/>
    </Grid>
</Window>
<Button>
    확인
</Button>

그래서 평범해 보이는 Content 속성이 WPF에서는 실로 엄청 중요한 역할을 하고 있죠.

Control 입장에서 봤을 때 ControlTemplate는 필수이고 ContentPresenter는 선택사항입니다. 또 ContentPresenter는 얼마든지 개수를 늘릴 수도 있어요.

가령 Expander는 2개의 ContentPresenter를 가지고 있죠.

<Style TargetType="{x:Type TabItem}">
    <Setter Property="Template">
        <ControlTemplate TargetType="{x:Type TabItem}">
            <StackPanel>
                <ContentPresenter ContentSource="Header"/>
                <ContentPresenter ContentSource="Content"/>
            </StackPanel>
        </ContentTemplate>
    </Setter>
</Style>

때문에 ContentPresenter를 아래처럼 다양하게 이를 다양하게 사용할 수 있습니다.

<Expander Header="접기">
    <!-- 내용 -->
</Expander>

<Expander Header="접기" Content="내용"/>

<Expander>
    <Expander.Header>
        <CheckBox Content="접기"/>
    </Expander.Header>
    <Expander.Content>
        <DataGrid/>
    </Expander.Content>
</Expander>

<Expander HeaderTemplate="{StaticResource MyHeaderStyle}"/>
    <DataGrid/>
</Expander>

이처럼 Expander 뿐만 아니라 Window, UserControl, GroupBox, TabControl, TabItem, ComboBox 등등 다양한 컨트롤을 ControlTemplate 을 통해 설계하고 ContentPresenter 또는 ItemsPresenter 영역을 활용해서 가변적인 UI를 만들도록 설계되어 있습니다.

끝으로…

ControlTemplate을 잘 활용한다면 DevExpress 같은 컨트롤을 원하는 방향으로 직접 만들 수 있기 때문에 제 개인적으로는 이 개념이 WPF에서가장 중요한 내용이라고 생각해요.

그리고 ControlTemplate에 익숙해진다면 뭐든 다 만들 수 있고요. :smile:

읽어주셔서 감사합니다.

11개의 좋아요

단계적으로 설명 잘 주셔서 관련 필요한 모든 분들에게 유익한 글인것 같습니다 :slight_smile:

2개의 좋아요

음… 예전에 WPF에 대해서 회사 초짜들 교육할때 좀 했었는데…
많은 분들이 답을 달아주셨지만, 제가 생각하는 접근법도 이해하시는데에 도움이 되실거 같아요!
(이제 그 초짜들이 알아서 좀 해서 제가 UI를 안만지다 보니 기억이 잘 나진 않지만…)

이거 사실 처음 하시는 분들은 진짜 복잡하죠…
하지만 이해하면 MS의 어마어마한 아키텍처링을 흐느낄 수 있습니다.

우선 제가 답하고자 하는 질문은 아래 내용입니다.

ControlTemplate과 ContentPresenter에 대해 이거다!

0, 사전 지식
MS는 UI Control에서 Logic과 Visual 을 완벽하게 분리하고 싶었습니다.
그리고 그것을 처음으로 도입한것이 WPF입니다.
바로 FrameworkElementFrameworkTemplate입니다.

  • FrameworkElement : UI Logic 담당 (이를 위한 마우스 이벤트 및 UI로써 사용자와 상호작용하는데에 필요한 것들의 책임을 가집니다.)
  • FrameworkTemplate : Visual 담당 (찍어낼 수 있는 Visual의 모습을 담습니다.)

이렇게 분리된 2개가 어디서 만나냐면…
바로 Control입니다.

즉, 이제 우리는 MS의 마법(?)에 의해 로직을 그대로 재사용하면서 Visual만 바꿀 수 있습니다.

Visual과 상호작용을 위한 Logic의 분리.

위 문장을 꼭 기억하세요!
그래야지 WPF의 컨트롤 스트럭처를 이해할 수 있습니다.

1. ContentPresenter
ContentPresenter는 Content라는 그 특정(?)의 무언가를 표시 하는데에 사용합니다.
그럼 여기서 이 Content란 무엇일까요?

무엇이든 상관없습니다.

우리가 코드레벨에서 작성하는 모든 코드에 Visual을 씌울 수 있습니다.

아래와 같은 개체가 있습니다.

class Person
{
   public string Name { get; set; }
}

이것은 코드 레벨에서의 단순 Class입니다.
이것은 일반인도 인식할 수 있는 Graphic으로 표현된 Visual 이 없습니다.

이러한 놈들에게 Visual을 입혀서 표현하는것, 그것이 ContentPresenter입니다.
그리고 Visual을 정의하는것이 DataTemplate(FrameworkTemplate을 상속)입니다.

<ContentPresenter Content="{binding person}" ContentTemplate="{StaticResource Person.DefaultVisual}">

// (문법 이거 맞나요? 오랜만이라;;;)

위 구문을 이렇게 해석할 수 있습니다.

코드 레벨에서만 존재하는 Person이라는 개체를 Person.DefaultVisual의 모습으로 Visual을 입혔다.

그리고 여기서도 MS가 이루고자 했던 Logic 과 Visual의 분리를 이루어낸것입니다…

이해를 돕기 위해 class를 사용했지만, 언급했듯 Content는 무엇이든 상관없습니다. 심지어 enum이라도 상관없습니다. 중요한건 Code level에서만 존재하는 것에 Visual(Graphic)을 입히는것 입니다.

다시 한번 한마디로 정리하자면, ContentPresenter는 무엇이든 상관없는 그 Content라는 놈을 위한 표시기!

2. ControlTemplate

ControlTemplate은 사실 쉽습니다.
한마디로 이야기하자면 Control 클래스를 위한 전용 FrameworkTemplate입니다

FrameworkTemplate은 추상화입니다.
구현체로 두가지가 존재하는데, 그것은 ControlTemplateDataTemplate입니다.

각각 용도는 아래와 같습니다.

  • ControlTemplate
    Control 클래스 전용 Visual 담당.
    즉, UI Control로써 사용자와 상호작용하는 Logic을 위한 Visual을 정의 하기 위해 사용합니다.

  • DataTemplate
    ContentPresenter 클래스의 전용 Visual 담당
    즉, 무엇이든 상관없는 그 Content 라는 놈을 위한 Visual을 정의하기 위해 사용합니다.

다시 한번 한마디로 정리하자면, ControlTemplate은 Control을 위한 전용 Visual 담당!


여담으로… 사실 이러한 Logic과 View의 분리가 있었기에, MVVM이라는 패턴이 나올 수 있었습니다.
이 말을 다른말로 풀자면… MVVM은 Framework의 전폭적인 지원이 중요한 요소입니다.
해서… android와 같은곳에서의 MVVM은 … 너무 별로입니다…

8개의 좋아요

xxx의 트리가 그대로 ContentPresenter의 위치에 들어간다고 생각하면 됩니다.
Content가 여러개인 컨트롤도 있을 수 있습니다. 이런 경우 다음 처럼 ContentPresenter에 Content속성에 TempleteBinding 해주면 됩니다.

이 설명이 딱 꽂히네요 감사합니다 ㅎㅎ 템플릿 바인딩은 이것만봐서는 아직 잘 모르겠는데 이렇게 고급진 답변이 올라올 걸 예상하여 다음에 탬플릿 바인딩도 질문을 한 번 올려봐야겠습니다 ㅎㅎ

2개의 좋아요

와 너무나 정성스럽고 길게 잘 써주셔서 너무나 감사합니다 ㅎㅎ

(이런 생략 규칙이 Content 개념을 이해하는데 독이 되지 않았나 생각해요…)

이 분이 어떤 말인지 알겠습니다…ㅎㅎ 요컨대 ControlTemplate으로 내가 어느정도 정의해줄테니 ContentPresenter 부터는 네가 알아서 정의하렴 요런 느낌이군요 ㅎㅎ 단지 Content자리를 생략해버려서 거기에 사실 프레젠터가 오는지 몰랐던 점이 아쉽네요 ㅎㅎ

다시 한 번 감사드립니다!!

2개의 좋아요

오오 FrameworkElement와 FrameworkTemplate에 대한 개념은 또 처음 접해보네요… 내일 설명하실 부분도 기대하게 됩니다 ㅎㅎ 감사합니다!!

1개의 좋아요

아 ControlTemplate과 DataTemplate 정의 부분이 인상적이네요. 감사합니다 ㅎㅎ

2개의 좋아요

WPF 쓰면서 그동안 궁금했던 점이 시원하게 해결되었습니다! 감사합니다 : )

3개의 좋아요

정말 감사합니다!!
wpf하면서 궁금했던점이 해소되는거 같습니다!

3개의 좋아요

대박입니다!
여기 답변들을 모아서 책으로 냈으면 싶을 정도로 쉽게 찾아보기 힘든, 현업 개발자의 노하우와 공부의 결과가 잘 정리된 댓글들이네요!

더불어 제가 추천하는 VisualTree 에 대한 이해 방법 하나 살포시 얹어 봅니다.

요거 강추요. 지금은 비슷한 기능들이 VS에 포함되어 있어서 중요가 낮아지긴 했지만
포커스 문제라든지 사용성 같은 것 때문에 저는 아직 snoop을 씁니다.(오히려 VS가 더 불편할 지경…ㅋㅋ)

snoop만 잘 써도 xaml과 visual tree에 관한 이해가 반은 먹고 들어갑니다요. ㅋㅅㅋ

4개의 좋아요

=> WPF의 컨트롤은 무조건 ContentControl, ItemsControl를 상속받아 구현되었다고 알고 있습니다.
이 부분* 때문에 저는 다른 각도에서 조심스럽게 이야기해볼게요.
단순하게 클래스 계보도를 찾아가 보면 각각의 역할이 무엇인지 알 수가 있는 것 같아요.
WPF 컨트롤은 무조건 ItemsControl 상속받아 구현 => 무조건은 아닌 것 같아요.

상속받은 상위 클래스의 역할이 무엇인지만 알면 위의 글들은 쉽게 이해가 되었던 것 같습니다. ( 클래스 계보도 )

2개의 좋아요