ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Foundations: 특수한 컨트롤을 위한 템플릿
    .NET/WPF 2010. 5. 25. 10:55
    반응형
    http://msdn.microsoft.com/ko-kr/magazine/cc135986.aspx








    Foundations: 특수한 컨트롤을 위한 템플릿
    Foundations
    특수한 컨트롤을 위한 템플릿
    Charles Petzold

    코드 다운로드 위치: Foundations2008_01.exe (784 KB) 
    Browse the Code Online
    일반적인 개체를 특수한 시각적 개체로 바꾸기를 즐기는 프로그래머를 위해 WPF(Windows®Presentation Foundation)에서는 템플릿이라는 흥미로운 기능을 제공합니다. 과거에는 컨트롤의 기능과 시각적 모양이 컨트롤의 코드 내에서 밀접하게 연결되었습니다. WPF에서 컨트롤의 기능은 여전히 코드에서 구현되지만 시각적인 부분은 코드에서 분리되어 XAML에 정의되는 템플릿의 형식을 취합니다. 프로그래머와 디자이너는 새로운 템플릿(대부분 코드가 전혀 없는 XAML)을 만들어 컨트롤의 코드를 수정하지 않고도 컨트롤의 시각적인 모양을 완전히 바꿀 수 있습니다.
    필자는 작년 첫 칼럼에서 ScrollBar, ProgressBar 및 Slider 컨트롤을 위한 템플릿을 디자인하는 방법을 설명했습니다. 템플릿 기능과 관련하여 그 칼럼에서 설명하지 않은 측면이 있는데 바로 새 사용자 지정 컨트롤을 디자인할 때는 컨트롤의 시각적 모양을 정의하는 기본 템플릿을 제공해야 할뿐만 아니라, 이 컨트롤을 사용하는 프로그래머가 해당 템플릿을 대체할 수 있도록 해야 한다는 점입니다. 물론 컨트롤을 꼭 이렇게 작성해야 한다는 법은 없고, 실제로 필자의 책 Applications = Code + Markup(Microsoft Press®, 2006)을 보면 대체 가능한 템플릿을 정의하는 사용자 지정 컨트롤은 없습니다. 그러나 템플릿을 대체할 수 있도록 허용하면 이 컨트롤을 사용해야 하는 다른 개발자(여러분도 포함)에게 훨씬 더 편리합니다.
    이 칼럼에서는 올바르게 작동하는 보기 좋은 컨트롤을 만드는 데서 그치지 않고 동적 연결 라이브러리를 통해 배포되는 컨트롤의 기본 대체 가능 템플릿을 정의하는 메커니즘까지 설명하겠습니다. 여기에서 설명할 템플릿 기술의 상당수는 필자가 기존 WPF 컨트롤의 템플릿을 통해 배운 것입니다. 여러분도 원한다면 Applications = Code + Markup의 25장에 있는 DumpControlTemplate 프로그램을 사용하여 모든 표준 WPF 컨트롤의 기본 템플릿을 편리한 XAML 형식으로 추출하여 살펴볼 수 있습니다.

    요소 및 컨트롤
    이전 Windows 클라이언트 프로그래밍 환경에 대한 경험이 있는 프로그래머는 WPF 클래스 계층 구조를 보면 금방 궁금증을 느낍니다. 네이티브 Windows API를 예로 들면, 화면에 표시되는 시각적 모양이 있는 모든 것이 "창"으로 분류되고 Windows Forms에서는 모든 것이 "컨트롤"입니다. 그러나 WPF에서 Control 클래스는 TextBlock, Image, Decorator, Panel 등의 다른 많은 시각적 개체와 마찬가지로 FrameworkElement에서 파생됩니다. 그렇다면 요소와 컨트롤의 차이점은 정확히 무엇일까요?
    첫째, Control 클래스는 Foreground, Background 및 5개의 글꼴 관련 속성을 포함하여 매우 유용한 속성 모음을 FrameworkElement 클래스에 추가합니다. Control에서 이러한 속성을 직접 사용하지는 않습니다. 이러한 속성은 Control에서 파생되는 클래스의 편의를 위한 것입니다.
    둘째, Control 클래스는 IsTabStop 및 TabIndex 속성을 추가합니다. 이는 컨트롤의 경우 요소와는 달리 대부분 Tab 키 탐색 체인을 거쳐야 함을 의미합니다. 간단히 말해 요소는 보이기 위한 것이지만 컨트롤은 상호 작용을 위한 것입니다. 물론 요소도 포커스를 받고 키보드, 마우스 및 스타일러스 입력에 반응할 수 있습니다.
    셋째, Control 클래스는 ControlTemplate 형식의 Template 속성을 정의합니다. 이 템플릿은 대부분 컨트롤의 시각적 모양을 구성하는 요소 및 다른 컨트롤의 시각적 트리이며, 속성 변경 및 이벤트에 따라 이 시각적 모양을 변경하는 트리거를 포함하는 경우가 많습니다.
    이 세 번째 특성은 Control에서 파생된 클래스는 시각적 모양을 사용자 지정할 수 있지만 FrameworkElement에서 파생된 다른 클래스는 그렇지 않다는 점을 의미합니다. 물론 TextBlock과 Image는 시각적 모양을 갖지만 이러한 요소가 표시하는 서식이 지정된 텍스트나 비트맵에는 아무것도 추가되지 않기 때문에 이를 사용자 지정한다는 것은 의미가 없습니다. 반면 ScrollBar는 같은 기능을 수행하면서도 다양한 모양을 가질 수 있습니다. 템플릿은 바로 이를 위한 것입니다.
    프로그래머 입장에서 요소와 컨트롤의 가장 큰 차이점은 FrameworkElement에서 파생시키는 경우에는 요소의 시각적 요소와 그 자식을 화면에 렌더링하기 위해 대개 MeasureOverride, ArrangeOverride 및 OnRender를 재정의해야 하며 Control에서 파생하는 경우에는 Template 속성의 ControlTemplate 개체에 있는 시각적 트리에서 컨트롤의 모양이 정의되므로 일반적으로 이러한 메서드를 재정의할 필요가 없다는 점입니다.
    WPF에는 ContentControl을 통해 Control에서 파생되는 UserControl이라는 클래스가 포함되어 있습니다. 이 UserControl은 간단한 사용자 지정 컨트롤을 위한 기본 클래스로 권장되며, 다양한 용도에서 훌륭히 역할을 수행합니다. 예를 들어 필자의 책 25장에 나오는 DatePicker 컨트롤은 UserControl에서 파생됩니다. 그러나 Control과 UserControl 간의 중요한 차이점을 유념해야 합니다. 즉, UserControl에서 파생할 때 XAML에 시각적 트리를 정의할 수 있지만 이 시각적 트리는 UserControl의 Content 속성의 자식입니다. UserControl은 간단한 자체 기본 템플릿이 있으며 이 템플릿은 Border 내에 ContentPresenter를 중첩할 뿐이므로 대체할 일이 별로 없습니다.
    UserControl에서 파생된 클래스의 시각적 트리는 대체하기 위한 것이 아니므로 이 클래스의 코드와 시각적 트리는 더 밀접하게 결합될 수 있습니다. 이와 반대로 Control에서 파생하고 대체 가능한 기본 템플릿을 제공할 계획이라면 코드와 시각적 트리 간의 상호 작용을 단순하게 유지하고 잘 문서화해야 합니다.

    기본 템플릿 및 DLL
    이번 칼럼에서는 다시 달력 컨트롤을 사용해 보기로 결심했습니다. 이 칼럼의 소스 코드는 CalendarTemplateDemo라는 단일 Visual Studio® 솔루션입니다. 여기에는 CalendarControls라는 DLL을 만드는 라이브러리 프로젝트와 이러한 컨트롤을 사용하는 4개의 데모 프로그램이 포함되어 있습니다.
    CalendarControls 라이브러리에는 CalendarControls 네임스페이스에 있는 3개의 컨트롤(CalendarMonth, CalendarDay 및 CalendarDayNotes)을 위한 코드와 기본 템플릿이 포함되어 있습니다. 이러한 각 클래스는 자체 C# 파일에 정의되어 있습니다. 앞의 두 클래스는 Control에서 파생되며, CalendarDayNotes는 CalendarDay에서 파생됩니다.
    컨트롤을 사용하는 응용 프로그램에서 템플릿을 대체할 수 있도록 DLL에 기본 템플릿을 정의하려면 매우 엄격한 규칙을 따라야 합니다. 이러한 규칙을 따르지 않으면, 기본 템플릿이 없는 컨트롤 또는 템플릿은 있지만 응용 프로그램에서 이러한 템플릿을 대체할 수 없는 컨트롤이 만들어집니다. DLL 프로젝트의 Themes 디렉터리에는 루트 요소가 ResourceDictionary인 generic.xaml이라는 파일이 있어야 합니다. 리소스는 DLL 내의 컨트롤을 가리키는 Style 요소입니다. CalendarControls 라이브러리의 generic.xaml 파일은 그림 1과 비슷합니다.
    <ResourceDictionary 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:cc="clr-namespace:CalendarControls">
        <Style TargetType="cc:CalendarMonth" />
            ...
        </Style>
    
        <Style TargetType="cc:CalendarDay" />
            ...
        </Style>
    
        <Style TargetType="cc:CalendarDayNotes" />
            ...
        </Style>
    </ResourceDictionary>
    
    
    이 파일의 각 Style 요소에는 컨트롤의 기본 템플릿을 설정하기 위한 한 개 이상의 Setter 요소가 있습니다.
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="cc:CalendarMonth">
                ...
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    
    ControlTemplate에는 컨트롤에 대한 시각적 트리가 포함되며, 템플릿 내에서 사용되는 리소스를 정의하기 위한 Resources 섹션이 필요에 따라 포함될 수 있습니다. 많은 경우 다른 속성 또는 이벤트의 변경에 대한 템플릿 반응을 정의하는 Triggers 섹션이 포함됩니다.
    또는 필자와 같이 이러한 기본 템플릿을 별도의 파일로 분리할 수 있습니다. CalendarControls 프로젝트의 Themes 디렉터리에는 CalendarMonthStyle.xaml, CalendarDayStyle.xaml, CalendarDayNotesStyle.xaml이라는 3개의 파일이 포함되어 있습니다. 각 파일에는 ResourceDictionary 루트 요소와 특정 컨트롤을 가리키는 Style 형식의 자식 하나가 있습니다. generic.xaml 파일은 그림 2에서 볼 수 있듯이 이러한 3개의 리소스 파일을 참조합니다.
    <ResourceDictionary 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:cc="clr-namespace:CalendarControls">
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary 
                Source="CalendarControls;Component/themes/
                    CalendarMonthStyle.xaml" />
            <ResourceDictionary 
                Source="CalendarControls;Component/themes/
                     CalendarDayStyle.xaml" />
            <ResourceDictionary 
                Source="CalendarControls;Component/themes/CalendarDayNotesStyle.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
    
    
    이 스타일을 CalendarMonth, CalendarDay 및 CalendarDayNotes 형식의 개체와 성공적으로 결합하려면 이러한 클래스의 DefaultStyleKey 속성에서 올바른 값이 반환되어야 합니다. 이러한 이유로, 이 클래스의 정적 생성자는 클래스의 형식을 반환하도록 해당 속성의 기본값을 변경합니다. 이를 위해 DefaultStyleKey 종속성 속성의 메타데이터를 다음과 같이 다시 정의합니다.
    static CalendarMonth()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarMonth),
            new FrameworkPropertyMetadata(typeof(CalendarMonth)));
        ...
    }
    
    OverrideMetadata에 대한 첫 번째 인수는 메타데이터를 수정하는 클래스의 형식이며, FrameworkPropertyMetadata 생성자에 대한 인수는 클래스의 형식이기도 한 DefaultStyleKey 속성의 새 기본값을 나타냅니다.
    DLL에 대한 어셈블리 정보 파일(일반적으로 이름이 AssemblyInfo.cs인 파일)은 다음과 같은 어셈블리 특성을 넣기에 적합한 장소입니다.
    [assembly: ThemeInfo(ResourceDictionaryLocation.None,
                         ResourceDictionaryLocation.SourceAssembly)]
    
    이는 테마별 템플릿 집합은 없지만 컨트롤의 일반 템플릿은 컨트롤 자체와 같은 어셈블리(DLL)에 있음을 의미합니다.

    컨트롤 파생의 기본 사항
    Control에서 파생할 때는 일반적으로 몇 가지 새로운 공용 속성을 정의해야 합니다. 대부분의 경우 데이터 바인딩 및 애니메이션의 대상이 될 수 있도록 이러한 속성을 종속성 속성으로 지원해야 합니다. 속성이 가져오기 전용 속성이고 바인딩 대상이 될 수 없는 경우라도 종속성 속성을 사용하면 이러한 속성이 변경될 때 다른 개체에서 이를 알 수 있도록 하는 알림 메커니즘이 제공됩니다.
    다른 클래스에서 이미 정의한 속성을 정의해야 하지만 Control에서 상속하지 않는 경우에는 속성을 전체적으로 다시 정의하지 마십시오. 대신 기존 종속성 속성에 대해 AddOwner를 호출합니다. AddOwner의 반환 값을 공용 정적 읽기 전용 필드로 저장한 다음 이 값을 CLR 속성에서 사용하면 됩니다.
    상속된 속성의 기본값을 변경해야 하는 경우에는 두 가지 방법이 있습니다. 단순히 클래스의 생성자에서 속성 값을 설정하는 것은 좋은 생각이 아닙니다. 이러한 지역 설정은 우선 순위가 매우 높기 때문에 스타일 또는 속성으로 다시 정의할 수 없습니다. 우선 순위가 낮은 방식은 앞서 설명한 DefaultStyleKey 속성에서와 마찬가지로 종속성 속성에 대해 OverrideMetadata를 호출하는 것입니다. Control 클래스는 이러한 방법으로 IsTabStop의 기본값을 True로 설정합니다.
    우선 순위가 낮은 다른 방식은 컨트롤의 기본 템플릿과 동일한 Style 요소에서 Setter를 사용하여 속성을 설정하는 것입니다. 많은 WPF 컨트롤이 이 기술을 사용하여 SnapsToDevicePixels, MinWidth 또는 MinHeight와 같은 컨트롤의 시각적 모양과 관련된 속성을 설정합니다. 상속된 속성의 값이 변경될 때마다 클래스가 알림을 받아야 하는 경우 OverrideMetadata를 사용하여 추가 콜백 메서드를 설치할 수 있습니다.
    Control에서 파생하는 데 따르는 가장 큰 과제는 필요한 부분을 모두 충족하도록 코드와 템플릿 간의 상호 작용을 정의하면서도 대체 템플릿을 작성하려는 다른 개발자의 작업을 어렵게 하지 않는 것입니다. 일반적으로 컨트롤 코드에서 템플릿을 수용하는 데에는 다음과 같은 여러 가지 방법 있습니다.
    • 템플릿이 TemplateBinding 태그 확장을 통해 액세스할 수 있는 속성을 코드에 정의합니다.
    • 템플릿이 트리거로 사용할 수 있는 속성 및 이벤트를 코드에 정의합니다.
    • 템플릿 내의 단추로 시작할 수 있는 RoutedCommand 속성 또는 필드를 코드에 정의합니다.
    • 코드에서 템플릿에 특정 도우미 요소가 있다고 가정하고, 경우에 따라 미리 정의된 이름을 통해 이러한 요소를 참조합니다.
    이러한 모든 기술에 대한 예를 살펴보겠습니다.
    일부 경우에는 템플릿이 대체될 때 Control 파생 클래스가 이를 인지하여 새 템플릿에 액세스하고 연결할 수 있도록 해야 합니다. Control 클래스는 Template 속성이 변경될 때 알림을 받도록 다시 정의할 수 있는 가상 OnTemplateChanged 메서드를 정의합니다. 그러나 필자의 경험으로는 템플릿이 아직 적용되지 않았기 때문에 OnTemplateChanged 메서드는 필요한 연결 작업을 수행하기에 썩 좋은 대상은 아닙니다. 이보다는 FrameworkElement에 의해 정의되지만 기본 구현이 없는 OnApplyTemplate 메서드를 다시 정의하는 편이 훨씬 더 낫습니다.

    요소 및 컨트롤의 계층
    CalendarControls 라이브러리에 있는 MonthCalendar 컨트롤은 한 달을 표시합니다. TwoCalendars 프로그램은 그림 3에서 볼 수 있듯이 각기 영어와 프랑스어로 된 두 개의 MonthCalendar 인스턴스를 표시합니다.
    그림 3 MonthCalendar의 두 인스턴스 (더 크게 보려면 이미지를 클릭하십시오.)
    기본 템플릿이 Win32®의 대응 항목과 비슷하게 보인다는 점은 WPF 디자인 원리와 일치합니다. 이러한 대응 항목은 대체로 평범한데, 여기에서 볼 수 있는 달력도 확실히 그렇습니다. 그러나 이 프로젝트를 시작할 때부터 염두에 둔 목표 중 하나는 Sunday(프랑스어의 경우 dimanche)로 시작하지 않아도 되는 달력을 올바르게 구현하는 것이었습니다. 하지만 일반 달력의 범위를 벗어나는 모험은 하지 않았습니다.
    예를 비교적 단순하게 유지하기 위해 날짜 선택 개념은 구현하지 않기로 했습니다. 그러나 특정 날짜를 강조 표시하는 IsToday라는 속성이 있습니다.
    필자가 WPF에서 배운 중요한 교훈 중 하나는 간단한 컨트롤과 요소를 사용하여 복잡한 컨트롤을 만들 수 있다는 것입니다. 그림 3에는 CalendarMonth의 코드 부분에서 정의된 것은 아무것도 없습니다. 모두 기본 XAML 템플릿의 일부입니다. 테두리가 전체를 감싸고 배경을 제공합니다. 맨 위에 있는 단추들은 RepeatButton 컨트롤로, 달력을 한 달씩 또는 한 해씩 앞뒤로 이동합니다. 단추 사이에는 TextBlock이 있습니다. 요일은 UniformGrid를 ItemsPanel로 사용하는 StatusBar의 StatusBarItem 개체입니다. 날짜 역시 UniformGrid에 표시됩니다.
    필자는 각 날짜를 CalendarDay라는 별도의 컨트롤로 만들었는데, 그 이유는 곧 드러납니다. CalendarDay 역시 Control에서 파생되며 기본 템플릿은 TextBlock을 포함하는 Border입니다. CalendarMonth 컨트롤은 한 달을 표시할 때 28, 29, 30 또는 31개의 CalendarDay 개체를 생성합니다. 이 두 달력을 표시하는 TwoCalendars.xaml 파일은 그림 4에서 볼 수 있습니다.
    <!-- TwoCalendars.xaml(Charles Petzold 작성, 2007년 9월) -->
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:cc="clr-namespace:CalendarControls;assembly=CalendarControls" 
      x:Class="Petzold.TwoCalendars.TwoCalendars"
      Title="Two Calendars Demonstration">
    
      <Window.Resources></Window.Resources>
        
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
    
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
    
        <cc:CalendarMonth Grid.Column="0" Margin="24" />
            
        <cc:CalendarMonth Grid.Column="1" Margin="24"
          Culture="fr-FR" />
      </Grid>
    </Window>
    (참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)
    

    코드와 XAML
    CalendarDay는 CalendarMonth보다 훨씬 단순하고 전체를 보여 줄 수 있을 정도로 짧기 때문에 이 프로젝트에서는 먼저 여기에 초점을 맞추어 보겠습니다. 그림 5는 CalendarDay.cs를 보여 줍니다. 이는 partial 클래스가 아니라는 점에 유의하십시오. 기본 템플릿은 CalendarDay 클래스에 속하지 않고, CalendarDay 형식의 개체에 자동으로 적용됩니다.
    // CalendarDay.cs(Charles Petzold 작성, 2007년 9월)
    using System;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace CalendarControls
    {
        public class CalendarDay : Control
        {
            // 정적 생성자: 기본값 변경
            static CalendarDay()
            {
                DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarDay),
                    new FrameworkPropertyMetadata(typeof(CalendarDay)));
    
                IsTabStopProperty.OverrideMetadata(typeof(CalendarDay),
                    new FrameworkPropertyMetadata(false));
            }
    
            // 날짜 의존 관계 속성 및 속성
            public static readonly DependencyProperty DateProperty =
                DependencyProperty.Register("Date",
                    typeof(DateTime),
                    typeof(CalendarDay),
                    new PropertyMetadata(DateChangedCallback));
    
            public DateTime Date
            {
                set { SetValue(DateProperty, value); }
                get { return (DateTime)GetValue(DateProperty); }
            }
    
            // IsToday 의존 관계 속성 및 속성
            static readonly DependencyProperty IsTodayProperty =
                DependencyProperty.Register("IsToday",
                    typeof(bool),
                    typeof(CalendarDay), 
                    new PropertyMetadata(false));
    
            public bool IsToday
            {
                set { SetValue(IsTodayProperty, value); }
                get { return (bool)GetValue(IsTodayProperty); }
            }
    
            // 읽기 전용 Day 의존 관계 속성 및 속성
            static readonly DependencyPropertyKey DayKey =
                DependencyProperty.RegisterReadOnly("Day",
                    typeof(string),
                    typeof(CalendarDay),
                    new PropertyMetadata());
    
            public static readonly DependencyProperty DayProperty =
                DayKey.DependencyProperty;
    
            public string Day
            {
                protected set { SetValue(DayKey, value); }
                get { return (string)GetValue(DayProperty); }
            }
    
            // DateChangedCallback 메서드
            static void DateChangedCallback(DependencyObject obj,
               DependencyPropertyChangedEventArgs args)
            {
                CalendarDay calday = obj as CalendarDay;
                calday.Day = calday.Date.Day.ToString();
            }
        }
    }
    
    
    이 클래스는 3개의 새로운 속성으로 DateTime 형식의 Date, bool 형식의 IsToday, 그리고 string 형식의 가져오기 전용 속성인 Day를 정의하며, 이러한 속성은 모두 종속성 속성에 의해 지원됩니다. Day 속성은 전적으로 템플릿을 위한 것입니다. Date 속성이 변경될 때마다 클래스는 DateTime 개체의 Day 속성을 문자열로 변환하고 이를 자체 Day 속성으로 설정합니다.
    CalendarDayStyle.xaml 파일에는 CalendarDay 컨트롤용 템플릿이 포함되어 있습니다. 템플릿은 많은 경우 Border 요소로 시작되며 이 요소의 속성은 TemplateBinding 확장을 통해 Control 클래스에서 정의된 것과 같은 이름의 속성으로 설정됩니다. 이러한 속성은 템플릿에서 명시적으로 참조되지 않으면 컨트롤의 시각적 모양에 영향을 미치지 않습니다.
    시각적 트리를 통해 상속된 Control 속성은 템플릿에서 참조할 필요가 없습니다. 이러한 상속된 속성에는 Foreground와 5개의 글꼴 관련 속성이 있습니다. 이러한 속성에 대한 모든 변경은 CalendarDay 클래스에 정의된 Day 속성을 표시하는 TextBlock에 의해 자동으로 상속됩니다. HorizontalContentAlignment 및 VerticalContentAlignment 속성은 텍스트가 셀에서 정렬되는 방식을 결정합니다. 템플릿은 다음과 같이 Triggers 섹션으로 완료됩니다.
    <Trigger Property="IsToday" Value="True">
      <Setter Property="Background"
        Value="{DynamicResource 
        {x:Static SystemColors.HighlightBrushKey}}" />
      <Setter Property="Foreground"
        Value="{DynamicResource 
        {x:Static SystemColors.HighlightTextBrushKey}}" />
    </Trigger> 
    
    CalendarDay에 정의된 IsToday 속성이 True이면 강조 표시된 항목에 대해 Background 및 Foreground 속성이 시스템 브러시로 설정됩니다. 이 Triggers 섹션에는 MultiTrigger 요소 및 EventTrigger 요소도 포함될 수 있으며, EventTrigger 요소는 애니메이션을 트리거할 수 있습니다.
    CalendarDay에 Border 요소가 포함되더라도 테두리는 보이지 않습니다. CalendarMonth에 대한 템플릿도 비슷하게 정의되며 월 전체를 감싸는 테두리도 보이지 않습니다. 테두리가 보이지 않는 것은 컨트롤이 BorderBrush의 기본값을 null로 정의하고 BorderThickness 기본값을 Border 요소 자체와 마찬가지로 0으로 정의하기 때문입니다. 각 날짜를 표시되는 사각형 안에 넣고 싶은 생각이 들 것입니다. 템플릿의 속성을 좀더 눈에 띄는 값으로 변경할 수 있습니다.
    <Border BorderThickness="1"
            BorderBrush="Black" ...
    
    이 방법의 문제는 나중에 이 컨트롤을 사용하는 프로그래머가 이러한 값을 변경하려면 기본 템플릿을 변경해야 한다는 점입니다. 따라서 템플릿에서 하드 코딩되는 속성 값은 최소화해야 합니다. 컨트롤과 컨트롤 기본 템플릿의 디자이너라면 리소스 파일에 있는 Style 요소를 사용하여 이러한 속성을 좀더 적절한 기본값으로 설정할 수 있습니다. 컨트롤을 사용하는 개발자라면 컨트롤을 사용할 때 이러한 속성을 설정할 수 있습니다.
    예를 들어 TwoCalendars.xaml 파일에서 첫 번째 달력을 다음과 같이 변경할 수 있습니다.
    <cc:CalendarMonth BorderThickness="1"
                      BorderBrush="Black" ...
    
    이렇게 하면 전체 달력 주위에 테두리가 표시됩니다. 그렇지만 CalendarDay 테두리는 어떻게 설정해야 할까요? CalendarMonth는 모든 CalendarDay 개체를 내부적으로 생성하기 때문에 TwoCalendars.xaml 파일에 CalendarDay 요소는 아예 없습니다.
    해결책은 CalendarDay를 가리키는 스타일을 사용하는 것입니다. TwoCalendars.xaml의 Resources 섹션에 다음을 추가해 보십시오.
    <Style TargetType="cc:CalendarDay">
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="BorderBrush"
                Value="{DynamicResource 
                    {x:Static SystemColors.ControlTextBrushKey}}" />
        <Setter Property="HorizontalContentAlignment"
                Value="Center" />
        <Setter Property="VerticalContentAlignment"
                Value="Center" />
        <Setter Property="FontStyle" Value="Italic" />
    </Style>
    
    이 스타일은 테두리를 설정할 뿐만 아니라 날짜가 셀 가운데에 표시되도록 콘텐츠 정렬 속성을 설정하고 숫자를 기울임꼴로 만듭니다.
    기본 CalendarMonth 템플릿은 CalendarMonthStyle.xaml 파일에 포함됩니다. 이 템플릿은 Border로 시작하며, 그 다음에는 단추와 월 이름, 요일, 그리고 달력 틀 자체를 위한 3개의 수직 셀로 구성된 Grid가 나옵니다. Grid의 맨 위 셀은 단추와 월 이름을 위한 5개의 수평 셀이 있는 또 다른 Grid입니다.
    CalendarMonth 클래스는 템플릿이 텍스트 정보를 표시하는 데 사용할 수 있는 몇 가지 속성을 제공합니다(예: MonthName, AbbreviatedMonthName, 서식이 지정된 AbbreviatedYearMonth). CalendarMonth는 이러한 정보의 대부분을 DateTimeFormatInfo에서 가져옵니다. CalendarMonth는 또한 DayNames 및 AbbreviatedDayNames 속성으로 사용할 수 있는 두 개의 배열에 요일 이름을 저장합니다. DateTimeFormatInfo의 배열은 항상 Sunday로 시작하므로 이러한 속성은 DateTimeFormatInfo 클래스에 있는 같은 이름의 속성과는 조금 다릅니다.
    CalendarMonth에 정의되는 FirstDayInWeek 속성은 월의 첫 번째 날짜의 요일과 DateTimeFormatInfo의 FirstDayOfWeek 속성을 기반으로 합니다.
    CalendarMonthStyle.xaml의 템플릿에는 자체 Resources 섹션이 있습니다. 이 섹션은 템플릿 내에서 스타일을 설정할 때, 특히 태그에 명시적으로 나타나지 않는 요소 형식인 경우 유용합니다. 예를 들어 요일이 표시되는 방식을 살펴보십시오. AbbreviatedDayNames 속성은 ItemsPanel로 UniformGrid가 있는 StatusBar에 할당됩니다. 내부적으로 AbbreviatedDaysNames 배열의 문자열은 StatusBarItem 컨트롤의 Content 속성이 됩니다. StatusBarItem은 실제로 태그에 나타나지는 않지만 Style은 StatusBarItem 컨트롤을 가리켜 콘텐츠를 가운데로 맞추고 이름 사이에 약간의 공백을 추가할 수 있습니다.
    명명된 요소 액세스
    지금까지 XAML 파일이 TemplateBinding 확장 및 트리거를 통해 참조하는 속성을 클래스에서 정의하는 방법에 대한 예를 살펴보았습니다. 그러나 클래스에서 템플릿의 특정 요소를 액세스하는 것이 더 편리한 경우가 있습니다. 이 예에서 CalendarMonth 클래스는 특정 월에 대한 모든 CalendarDay 개체를 생성해야 하며 이러한 개체는 항상 일종의 패널이어야 합니다. 가장 편리한 방법은 코드에서 참조하는 이름을 이 패널에 할당하는 것입니다.
    CalendarMonth 템플릿의 두 번째 UniformGrid 이름은 PART_Panel로 지정됩니다. 이 이름은 몇몇 WPF 컨트롤에 대한 기본 템플릿에 정의된 이름과 비슷합니다. CalendarMonth 클래스 정의 앞에는 이 이름을 나타내는 특성과 코드에서 식별할 것으로 예상하는 요소의 형식이 나옵니다.
    [TemplatePart(Name = "PART_Panel", Type = typeof(Panel))]
    
    이 특성은 시각적 디자이너를 위한 것으로 필수 사항은 아닙니다. 그러나 Panel에서 파생된 것은 무엇이든 템플릿의 이 요소가 될 수 있음을 유의하십시오.
    CalendarMonth.cs에 있는 다음 문은 ApplyTemplate 호출에 대한 응답으로 이 패널을 가져와 필드로 저장합니다.
    pnl = Template.FindName("PART_Panel", this) as Panel;
    
    이는 일반적인 FindName 메서드가 아닙니다. 이 메서드는 ControlTemplate 및 ItemsPanelTemplate, DataTemplate, HierarchicalDataTemplate이 파생되는 FrameworkTemplate에 정의된 FindName 메서드이며 CalendarMonth 개체의 Template 속성을 통해 액세스됩니다.
    이 pnl 개체가 null이라면 어떻게 될까요? 이는 템플릿에 PART_Panel이라는 이름의 항목이 포함되어 있지 않거나 아니면 PART_Panel이라는 요소가 Panel에서 파생되지 않았음을 의미합니다. 클래스는 해당 템플릿에서 명명된 요소를 찾지 못한 경우 가능한 최선의 작업을 알아서 수행해야 합니다.
    템플릿의 도우미 요소에 이름과 형식을 연결할 때는 최대한 일반적인 방식을 사용하십시오. 이 예의 경우 MonthCalendar에 Children 속성이 있는 요소가 필요하므로 Panel을 사용하는 것이 좋습니다. UniformGrid가 문제 없이 작동한다는 점이 분명하더라도 MonthCalendar는 특정 패널 형식을 요구하지는 않습니다.

    명령 생성
    CalendarMonth의 템플릿에는 앞뒤로 이동하기 위한 단추가 포함되어야 합니다. 이러한 단추 누름에 반응하는 코드는 어떻게 작성해야 할까요? 가장 편리한 방법은 클래스에서 RoutedCommand 형식의 공용 정적 읽기 전용 필드 또는 가져오기 전용 속성을 정의하는 것입니다. 기존 WPF 컨트롤은 이러한 RoutedCommand 개체를 필드나 속성으로 정의하는 데 일관성이 없습니다. ScrollBar 클래스는 이를 읽기 전용 필드로 정의하고 이름을 LineDownCommand, LineLeftCommand 등으로 지정합니다. Slider 클래스는 이를 가져오기 전용 속성으로 정의하고 이름을 DecreaseLarge, DecreaseSmall, IncreaseLarge, IncreaseSmall 등으로 지정합니다. 필자는 ScrollBar 방식을 선택했으며 다음과 같이 RoutedCommand 개체를 필드로 정의했습니다.
    publicstaticreadonlyRoutedCommand NextMonthCommand =
        new RoutedCommand("NextMonth", 
        typeof(CalendarMonth));
    
    정적 개체임에 유의하십시오. 템플릿에서는 이를 컨트롤의 Command 속성에 할당된 정규화된 이름으로 참조합니다.
    <ToggleButton Command="
    CalendarMonth.NextMonthCommand" ...
    
    RoutedCommand 형식의 개체로 설정할 수 있는 Command 속성을 정의하는 컨트롤은 그리 많지 않아 실제로 ButtonBase, MenuItem 및 Hyperlink가 전부입니다.
    CalendarMonth 생성자는 개체를 자체 CommandBindings 컬렉션에 추가함으로써 이러한 RoutedCommand 개체를 실행 메서드와 연결합니다.
    CommandBindings.Add(new 
      CommandBinding(NextMonthCommand,
      NextMonthExecuted));
    
    또한 코드에서 컨트롤이 유효한지 여부를 나타내는 부울을 설정할 수 있도록 하는 can-execute 메서드를 지정할 수 있습니다. 유효하지 않은 경우 자동으로 비활성화됩니다.
    CalendarMonth.cs에 있는 NextMonthExecuted 메서드의 형태는 다음과 같습니다.
    void NextMonthExecuted(object sender, 
    ExecutedRoutedEventArgs args)
    {
        Date = Date.AddMonths(1);
    }
    
    RoutedCommand 개체를 사용할 때의 장점은 코드에서 Execute 메서드를 호출하여 매우 쉽게 개체를 트리거할 수 있다는 점입니다. 예를 들어 일부 키보드 입력을 통해 명령을 트리거하려는 경우가 있습니다. 많은 경우 InputGesture(KeyGesture 및 MouseGesture가 파생되는 추상 클래스) 형식의 개체를 RoutedCommand와 연결하면 명시적인 키보드 처리를 전혀 거치지 않고도 원하는 작업을 수행할 수 있습니다. CalendarMonth의 정적 생성자는 4개의 모든 RoutedCommand 필드에 다음과 같이 KeyGesture 개체를 추가합니다.
    NextMonthCommand.InputGestures.Add(
        new KeyGesture(Key.PageDown));
    
    이러한 동작을 RoutedCommand 개체에 대한 원래 필드 정의에 추가하는 것도 가능하지만 그러면 생성자에서 전체 InputGestureCollection을 제공해야 하므로 다소 거추장스럽게 됩니다.

    템플릿 대체
    기본 템플릿을 대체하여 전혀 다른 모양의 달력을 표시하는 응용 프로그램을 작성하면 지금까지의 모든 연습을 활용하는 실제 테스트를 수행할 수 있습니다. NewCalendar 프로그램은 날짜를 세로로 표시하는 CalendarMonth를 위한 새로운 템플릿을 정의합니다. 이 프로그램은 12개의 이러한 월별 달력을 만들어 나란히 표시합니다(그림 6 참조).
    그림 6 The NewCalendar 표시 화면 
    또한 CalendarDay에서 새 클래스를 파생하여 달력을 개선하는 것이 효과적이겠지만 언뜻 보기에는 이것이 불가능해 보입니다. CalendarMonth는 28, 29, 30 또는 31개의 CalendarDay 인스턴스 생성을 담당합니다. CalendarDay에서 파생하려면 이러한 논리를 변경하기 위해 CalendarMonth에서도 파생해야 하는 것 같습니다. 이를 피하기 위해 필자는 CalendarMonth에 Type 형식의 DayType이라는 새 속성을 정의했습니다. 기본적으로 이 속성은 typeof(CalendarDay)와 같지만 CalendarDay에서 파생되는 클래스의 형식으로도 설정할 수 있습니다.
    CalendarDayNotes는 CalendarDay에서 파생되며 템플릿에 TextBox 형식의 PART_TextBox라는 컨트롤이 포함된 것으로 간주합니다. TextBox 컨트롤에 입력한 내용은 CalendarDayNotes에 의해 작은 파일로 저장되며, 이러한 파일은 날짜에서 파생되는 이름으로 식별됩니다. 프로그램은 다음 번 실행될 때 이러한 파일을 모두 로드합니다. CalendarDayNotes의 기본 템플릿은 다소 평범하지만 ReminderCalendar 프로그램에는 그림 7과 같이 색 그라데이션을 적용하여 길쭉한 모양으로 날짜를 표시하는 조금 색다른 템플릿이 포함되어 있습니다.
    그림 7 ReminderCalendar 
    날짜를 좀더 독특하게 표시하기 위해 CalendarDay에서 파생해야 할 필요도 없습니다. 또한 CalendarControls 라이브러리에는 Viewport3D에서 파생되는 MoonDisk라는 클래스, 그리고 달 모양에 빛의 방향을 맞추는 DateTime 속성이 있습니다. MoonPhaseCalendar는 한 달을 표시하기 위해 최대 31개의 Viewport3D 개체를 생성해야 하므로 속도는 그리 빠르지 않지만 그림 8에서 볼 수 있듯이 독특한 달력을 생성합니다.
    그림 8 MoonPhaseCalendar 표시 화면 

    질문이나 의견이 있으면 다음 전자 메일 주소로 보내시기 바랍니다: mmnet30@microsoft.com.


    Charles Petzold는 MSDN Magazine에 기고하는 편집자이며, 최근 저서는 3D Programming for Windows(Microsoft Press, 2007)입니다.




    반응형

    댓글

Designed by Tistory.