XAML or HTML

032. 데이터템플릿 & 트리거(DataTemplate & Trigger) #3

XAML 뽀개기

이전 포스트에서는 데이타템플릿, 스타일을 컨버터, 셀렉터를 활용해 하나의 컨트롤에서 다양한 형태를 보여줄 있었습니다. 이번 포스트에서는 트리거를 이용하는 방법을 살펴봅니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Style & Template Trigger -->
<DataTemplate x:Key="DataTemplate_Message" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock x:Name="PART_Name" Text="{Binding Name}"/>
        <Border x:Name="PART_Background" Background="White" CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsMine}" Value="True">
            <Setter Property="Background" Value="#FFFFEB33" TargetName="PART_Background"/>
            <Setter Property="HorizontalAlignment" Value="Right" TargetName="PART_Time"/>
            <Setter Property="Visibility" TargetName="PART_Name">
                <Setter.Value>Collapsed</Setter.Value>
            </Setter>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>
cs

 

이번에는 데이타템플릿이 하나만 필요합니다. 다른 사람의 메시지를 기본 템플릿으로 정의하고 데이타트리거를 이용해 메시지일 때에 달라져야하는 속성들을 정의합니다. 이전 예제와 동일한 형태로 정의하므로 자세한 프로퍼티 사항들은 생략합니다. 필요한 요소들을 TargetName 지정해야하므로 x:Name 선언하는 것에 주의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Style x:Key="Style_Message" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Left" 
                                  Margin="5,0,20,5"/>
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding IsMine}" Value="True">
                        <Setter Property="HorizontalAlignment" Value="Right" TargetName="PART_ListBoxItem"/>
                        <Setter Property="Margin" Value="20,0,5,5" TargetName="PART_ListBoxItem"/>
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        <Setter.Value>
    </Setter>
</Style>
cs


스타일도 데이타템플릿과 동일한 방법으로 데이타프리거를 이용합니다. PART_ListBoxItem의 HorizontalAlignment 속성은 Left, Margin은 5,0,20,5로 선언합니다. 기본으로 선언한 스타일은 다른 사람의 메시지 스타일이 됩니다. 다음엔 데이터트리거를 이용해서 내 메시지일 떄에 달라져야하는 속성들의 값을 반대로 정의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Style & Template Trigger -->
<ListBox ItemTemplate="{StaticResource DataTemplate_Message}" 
         ItemContainerStyle="{StaticResource Style_Message}"
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemTemplate ItemContainerStyle 선언하는 것은 이전과 다를 것이 없습니다.

 

 

실행해서 태스트해봅니다

 

컨버터를 사용할지 셀렉터를 사용할지 트리거를 사용할지는 여러분이 판단하시길 바랍니다. 여러 조건과 상황에 따른 옳은 판단이 중요할 같습니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

031. 데이터템플릿 & 셀렉터(DataTemplate & Selector) #2

XAML 뽀개기

이전 포스트에서는 데이타템플릿(DataTemplate) 하는 기능을 알아보았고 컨버터(Converter) 이용해 내가 보낸 메시지와 다른 사람이 보낸 메시지를 다르게 보이게 하는 방법을 살펴봤습니다. 이번 포스트에서는 셀렉터(Selector) 어떤 기능을 하는지 어떻게 사용하는지 살펴봅니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Template Selector -->
<DataTemplate x:Key="DataTemplate_Message_Left" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
    <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
            CornerRadius="5" Margin="0,3">
        <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
    </Border>
    <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
</DataTemplate>
cs

 

기존의 데이타템플릿의 키값을 DataTemplate_Message_Left로 변경하고 전체 XAML 코드를 [복사 > 붙여넣기]합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<DataTemplate x:Key="DataTemplate_Message_Right" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
        <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"
                   HorizontalAlignment="Right"/>
    </StackPanel>
</DataTemplate>
cs

 

복사+붙여넣기한 데이타템플릿의 키값은 DataTemplate_Message_Right으로 변경합니다. 데이타템플릿이 2 준비되었습니다. 확인 시간을 오른쪽으로 정렬하기 위해 PART_Time의 HorizontalAlignment 속성을 Right 선언했습니다.

 

MessageStyleTemplateSelector.cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MessageTemplateSelector : DataTemplateSelector
{
    public DataTemplate LeftTemplate { get; set; }
    public DataTemplate RightTemplate { get; set; }
    public string PropertyToCheck { get; set; }
    public string PropertyValue { get; set; }
 
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var exchangeMessage = (ExchangeMessage)item;
        var type = exchangeMessage.GetType();
        var property = type.GetProperty(PropertyToCheck);
 
        if (property.GetValue(exchangeMessage, null).ToString() == PropertyValue)
        {
            return RightTemplate;
        }
        else
        {
            return LeftTemplate;
        }
    }
}
cs

 

DataTemplateSelector를 상속 받은 MessageTemplateSelector 추가합니다. SelectTemplate 메소드를 재정의합니다. Selector 작성할 메소드를 재정의하는 일은 필수입니다. 사용자가 지정한 프로퍼티(PropertyToCheck) 지정한 (PropertyValue) 일치하면 지정한 데이터템플릿을 리턴하도록 코드를 작성했습니다.

 

1
xmlns:slt="clr-namespace:Sample.Selector" 
cs

 

셀렉터를 XAML 코드에서 사용하려면 네임스페이스를 선언해야 합니다.

 

1
2
3
4
<slt:MessageTemplateSelector x:Key="MessageTemplateSelector" 
                             PropertyToCheck="IsMine" PropertyValue="True" 
                             LeftTemplate="{StaticResource DataTemplate_Message_Left}" 
                             RightTemplate="{StaticResource DataTemplate_Message_Right}"/>
cs

 

MessageTemplateSelector를 리소스에 선언합니다. 이전 포스트에서는 IsMine 프로퍼티에 True 값이 될때 컨버터가 동작하도록 했었습니다. 이번에도 마찬가지 방식으로 지정한 데이타템플릿이 리턴되도록 했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
<!-- Style & Template Selector -->
<ListBox ItemTemplateSelector="{StaticResource MessageTemplateSelector}" 
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemTemplateSelector 속성에 셀렉터를 선언합니다.

 


확인 시간이 오른쪽으로 정렬되었습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Style Selector -->
<Style x:Key="Style_Message_Left" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Left" 
                                  Margin="5,0,20,5"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<Style x:Key="Style_Message_Right" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Right" 
                                  Margin="20,0,5,5"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
cs

 

추가로 스타일셀렉터를 살펴봅니다. 아이템 전체를 오른쪽으로 정렬하려고 합니다. 데이타템플릿과 마찬가지로 Style_Message_Left, Style_Message_Right 2개의 스타일을 정의합니다. PART_ListBoxItem의 HorizontalAlignment, Margin 속성을 서로 반대로 정의합니다.

 

1
2
3
4
<slt:MessageStyleSelector x:Key="MessageStyleSelector" 
                          PropertyToCheck="IsMine" PropertyValue="True"
                          LeftStyle="{StaticResource Style_Message_Left}"
                          RightStyle="{StaticResource Style_Message_Right}"/>
cs

 

MessageTemplateSelector와 마찬가지로 MessageStyleSelector를 리소스에 선언합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Style & Template Selector -->
<ListBox ItemTemplateSelector="{StaticResource MessageTemplateSelector}" 
         ItemContainerStyleSelector="{StaticResource MessageStyleSelector}" 
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemContainerStyleSelector속성에 셀렉터를 선언합니다.


 

실행해서 태스트해봅니다

 

이처럼 셀렉터는 하나의 컨트롤에 여러 개의 템플릿이나 여러 개의 스타일을 함께 표현할 있게 해줍니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

030. 데이타템플릿 & 컨버터(DataTemplate & Converter) #1

XAML 뽀개기

이번 포스트 제목은 데이타템플릿(DataTemplate)이긴 하지만 예전 포스트들에서 다뤘던 다양한 기능들과 함께 살펴봅니다.

 

목록형 컨트롤들에서는 하나의 값을 보여주기도 하지만 다양한 형태의 값들을 함께 보여주기도 합니다. 데이타템플릿은 이처럼 다양한 데이터를 어떻게 구성해서 보이게 할지 정의하는 역할을 합니다.

 


이번 시리즈 포스트의 최종 결과물입니다. 평소에 흔하게 사용하는 채팅앱을 예로 들어 이해를 돕고자 합니다. 2개의 목록형 예제는 시각적인 결과는 동일하지만 전혀 다른 방법으로 만들었습니다. 이번 시리즈 포스트에서는 데이터템플릿뿐만 아니라 스타일(Style), 컨버터(Converter), 셀렉터(Selector), 트리거(Trigger) 다양한 기능들을 함께 다루게 됩니다.

 

1
2
3
4
5
6
7
public class ExchangeMessage
{
    public bool IsMine { get; set; }            // 메시지 구분자
    public string Name { get; set; }            // 발신자 이름
    public string Message { get; set; }         // 발신 내용
    public DateTime ConfirmedTime { get; set; } // 확인 시간
}
cs

 

먼저 리스트박스(ListBox) 보이게 샘플 데이터의 모델 클래스 ExchangeMessage입니다. Name 메시지를 발신자의 이름입니다. Message 발신자의 메시지 내용입니다. ConfirmedTime은 메시지를 확인한 시간입니다. 마지막으로 IsMine은 메시지가 것인지 다른 사람의 것인지 구분하기 위한 bool 타입의 구분자입니다. 다른 요소들이 있을 있지만 예제 코드를 최소화하기 위해 제외했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Grid.Resources>
    <Style TargetType="{x:Type ListBox}">
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
        <Setter Property="Background" Value="#FFB2C7D9"/>
    </Style>
</Grid.Resources>
 
<ListBox>
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

예제를 위한 기본 리스트박스 XAML 코드입니다. X라는 친구와 대화를 나누는 시나리오입니다. 디자인 스타일은 여러분이 생각하는 그 채팅앱에서 가져왔습니다. 그리고 가로 스크롤은 불필요하기 때문에 HorizontalScrollBarVisibility 속성을 Disabled을 선언하였고 바탕색(Background) 속성에 #FFB2C7D9 브러쉬를 선언했습니다.

 

 

리스트박스에 ItemsSource 속성만 정의한 디자인뷰에서 모든 값이 제대로 보이지 않습니다.


1
2
3
4
5
6
7
8
9
<ListBox DisplayMemberPath="Message">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

 

DisplayMemberPath 속성을 이용하면 위처럼 모델 데이터의 일부만 표현이 가능합니다. 하지만 표출하고자 하는 것은 위처럼 Message만이 아닙니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- DataTemplate-->
<DataTemplate x:Key="DataTemplate_Message_Basic">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine}"/>
        <Border Background="{Binding IsMine}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
</DataTemplate>
cs

 

드디어 데이타템플릿을 정의합니다. Blend for VS [추가 템플릿 편집 > 생성된 항목(ItemTemplate) 편집 > 현재 항목/복사본 편집] 기능을 이용하면 위지위스(WYSIWYG) 방식으로 작성할 수도 있습니다. 각각의 모델 데이터를 바인딩 문법을 이용해 정의합니다. 추가로 확인한 시간은 StringFormat 속성을 이용해 시간만 간략히 보이도록 합니다.

 

1
2
3
4
5
6
7
8
9
10
<!-- DataTemplate -->
<ListBox ItemTemplate="{StaticResource DataTemplate_Message_Basic}">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs


(x:Key) 이용해 정의한 DataTemplate을 리스트박스의 ItemTemplate 속성에 StaticResource 문법을 이용해 선언합니다.

 

 

DataTemplate을 선언한 리스트박스가 이전과 다르게 다양한 여러 데이터가 함께 보이게 됩니다. 여기서 끝이 아니겠죠?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityConverter : IValueConverter
{
    enum Parameters
    {
        NORMAL, INVERTED
    }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var boolValue = (bool)value;
        var strParam = parameter.ToString().ToUpper();
        var direction = (Parameters)Enum.Parse(typeof(Parameters), strParam);
 
        if (direction == Parameters.INVERTED)
        {
            return !boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
        else
        {
            return boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}
 
cs

 

BooleanToVisibilityConverter 컨버터를 추가합니다. 내가 보낸 메시지의 발신자 이름은 보이지않고고 X라는 친구의 이름은 보여야 합니다. 기능을 컨버터를 이용해 표현합니다. 파라미터(ConverterParameter) 이용해 정반대로 컨버팅하는 기능도 추가했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ValueConversion(typeof(bool), typeof(Brush))]
public class BooleanToBrushConverter : IValueConverter
{
    public Brush FalseBrush { get; set; }
    public Brush TrueBrush { get; set; }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? TrueBrush : FalseBrush;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}
cs

 

컨버터를 하나 추가합니다. 이번 컨버터는 내가 보낸 메시지와 친구가 보낸 메시지의 바탕색(Background) 다르게 하기 위한 것입니다. Brush 타입의 프로퍼티를 추가해서 고정된 브러쉬가 아닌 사용자가 컨버터를 리소스에 선언할 때마다 다른 브러쉬를 정의할 있도록 합니다.

 

1
2
3
4
<!-- Converter -->
<cvt:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<cvt:BooleanToBrushConverter x:Key="BooleanToBrushConverter" 
                             TrueBrush="#FFFFEB33" FalseBrush="White"/>
cs

 

준비된 컨버터를 XAML 코드에서 사용하기 위해서는 리소스로 선언해야 합니다. 컨버터에 추가한 Brush 타입의 2개의 프로퍼티에는 원하는 브러쉬를 선언합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- DataTemplate-->
<DataTemplate x:Key="DataTemplate_Message_Basic">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
        <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
</DataTemplate>
cs

 

리소스에 선언한 컨버터를 비인딩 문법에 Converter 속성을 이용해 데이타템플릿에 추가로 정의합니다.

 

 

2개의 컨버터를 이용해 발신자 이름의 표시 유무와 바탕색을 다르게 표현했습니다.

 

내가 보낸 메시지와 다른 사람이 보낸 메시지를 더욱 명확하게 구분해 보기 위해선 추가 작업이 필요합니다. 다른 사람이 보낸 메시지는 좌측으로 정렬하고 내가 보낸 메시지는 우측으로 정렬하면 나은 표현 방법이 같습니다.

 

추가할 방법들은 다음 포스트에서 살펴봅시다.

 

이번 포스트에서 추가로 사용된 Convertert StringFormat 예전 포스트를 참고하기 바랍니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

029. 툴팁 말풍선 슬라이더(ToolTip Balloon Slider) #3

XAML 뽀개기

이전 포스트들에서 여러가지 방법으로 (Thumb) 말풍선 툴팁(ToolTip) 따라다니는 슬라이더(Slider) 만들어 보았습니다. 그런데 뭔가 만족스럽지 않았습니다. 그래서  깔끔한 방법이 없을까 한참을 고민하고 태스트하고 찾아보던 끝에 하나의 포스트를 이어가기로 했습니다.

 

예제에 사용된 대부분의 코드는 동일하니 달라진 점만 일부 설명합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 툴팁 팝업 -->
<Popup x:Name="PART_ToolTipPopup"
       PlacementTarget="{Binding ElementName=Thumb}"
       Placement="Center" VerticalOffset="-30"
       AllowsTransparency="True">
    <StackPanel>
        <Border CornerRadius="5" Background="SkyBlue">
            <TextBlock Text="{Binding Value, ElementName=PART_Track, StringFormat=N0}"
                       Foreground="Black" HorizontalAlignment="Center" Margin="10,5" />
        </Border>
        <ed:RegularPolygon Fill="SkyBlue" Margin="0,-8,0,0" Panel.ZIndex="-1"                                               
                           PointCount="4" Width="10" Height="15" />
    </StackPanel>
</Popup>
cs

XAML 코드에서 사용된 Popup 그대로 사용됩니다. 추가로 이전 예제에서 불필요하게 사용되었던 VerticalOffset 프로퍼티는 -30으로 고정 시켰습니다. 팝업의 세로 위치는 변하지 않으므로 미리 고정되어도 같습니다.

 

1
2
3
4
5
6
7
8
9
<!-- New Behavior -->
<Slider Value="5" Minimum="0" Maximum="100" IsSnapToTickEnabled="True"
        TickFrequency="1" TickPlacement="Both"
        VerticalAlignment="Center" Margin="10,0"
        Style="{DynamicResource Style_Slider}">
    <i:Interaction.Behaviors>
        <bhv:ToolTipToPopupSliderBehavior />
    </i:Interaction.Behaviors>
</Slider>
cs

 

슬라이더의 XAML 코드입니다. 달라진 점은 IsSnapToTickEnabled 프로퍼티를 True 활성화하여 Tick 스냅이 걸리도록 점입니다. 썸이 이동할 1 단위로 스냅된다는 시나리오를 추가했습니다. TickFrequency, TickPlacement 등의 프로퍼티는 Tick 디자인 화면에 노출되도록 하는 프로퍼티들입니다.

 

아래부터는 새로운 방식으로 만든 비헤이비어(Behavior)입니다.

 

ToolTipToPopupSliderBehavior.cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void AssociatedObject_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    _popup.HorizontalOffset = 0;
    _popup.IsOpen = true;
 
    var xMoveOffset = AssociatedObject.ActualWidth / (AssociatedObject.Maximum - AssociatedObject.Minimum);
    if (e.NewValue > e.OldValue && e.NewValue != AssociatedObject.Maximum)
    {
        _popup.HorizontalOffset += xMoveOffset;
    }
    else if (e.NewValue < e.OldValue && e.OldValue != AssociatedObject.Maximum)
    {
        _popup.HorizontalOffset -= xMoveOffset;            
    }
}
cs

이번 새로운 비헤이비어에서는 마우스와 키보드 이벤트를 각각 따로 이용하지 않고 동시에 적용되는 ValueChanged 이벤트를 이용했습니다. 이전에는 각각에 해당하는 이벤트를 핸들링했기 때문에 조금 중복되는 느낌을 받았는데 공통으로 발생하는 ValueChanged 이벤트를 이용함으로서  깔끔해진 느낌이 나는 합니다.

 

그리고 이번에는 슬라이더의 MinimumMaximum 프로퍼티의 값과 실제 ActualWidth의 크기를 이용해 썸이 움직였을때 팝업이 움직여야하는 HorizontalOffset 값을 매번 계산하여 초기화하고 다시 넣어주도록 하였습니다슬라이더의 크기가 고정이 아니라 윈도우 리사이징  어떤 이유로 변경이 가능하다는 가정된 시나리오 아래에 그렇게 되도록 했습니다.

 

그리고 기존에 Key.Left와 Key.Right의 판단은 OldValue와 NewValue를 비교하는 것으로 대체했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#region Event handler < Mouse
 
private void _thumb_MouseLeave(object sender, MouseEventArgs e)
{
    _popup.IsOpen = false;
}
 
#endregion
 
#region Event handler < Keyboard
                
private void Slider_PreviewKeyUp(object sender, KeyEventArgs e)
{
    _popup.IsOpen = false;
}
 
#endregion
cs

 

팝업이 닫히는 동작의 경우는 이전 방법 그대로 각각의 이벤트 핸들러를 이용하도록 했습니다.



제가 사용했던 여러 방식들이 정답이 아니거나 옳지 못할 수도 있습니다. 하지만 나름대로는 몇일, 몇시간을 투자해서 얻은 결론이라 흥미로웠습니다. 나중에라도 나은 방법을 알게 된다면 기쁜 마음으로 다시 공유하게 같습니다.

 

마지막으로 아래 링크는 이번 예제에서는 직접적으로 사용되진 않았지만 예제 프로젝트 안에 일부 주석과 FollowingPopup.cs 파일 등에 남겨 두었습니다. 키보드도 함께 지원해야 한다는 저의 시나리오에 부합되지 않아 사용하지 않았습니다만 괜찮은 참고자료인 같아 남겨둡니다. 간단한 내용이지만 다른 시선으로 눈을 뜨게 해준 stackoverflow에서 찾은 질문과 답변입니다. 저에게는 QueryCursor라는 이벤트를 이렇게도 사용할 있구나 하는 새로운 발견이었습니다.

 

참고 : Make Tooltip of WPF slider stay on screen while dragging


028. 툴팁 말풍선 슬라이더(ToolTip Balloon Slider) #2

XAML 뽀개기

커스텀 비헤이비어를 작성하기 전에 SliderHorizontal 템플릿에 추가할 것이 있습니다. 지금까지는 ToolTip(툴팁) 어떻게든 이용해 보려고 했지만 시나리오에 부합되지 않는 면이 있었습니다. 그래서 툴팁을 대체할 Popup(팝업) 사용하려고 합니다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 툴팁 팝업 -->
<Popup x:Name="PART_ToolTipPopup" 
       AllowsTransparency="True"
       PlacementTarget="{Binding ElementName=Thumb}" 
       Placement="Center" >
    <StackPanel>
        <Border CornerRadius="5" Background="SkyBlue">
            <TextBlock Text="{Binding Value, ElementName=PART_Track, StringFormat=N0}" 
                       Foreground="Black" HorizontalAlignment="Center" Margin="10,5"/>
        </Border>
        <ed:RegularPolygon Fill="SkyBlue" Margin="0,-8,0,0" Panel.ZIndex="-1" 
                           PointCount="4" Width="10" Height="15"/>
    </StackPanel>
</Popup>
 
cs

 

이전 포스트에서 작성했던 툴팁은 잊어버리고 PART_Track 아래에 팝업을 추가합니다. 이전 포스트에서 다뤘던 툴팁 말풍선의 템플릿 그대로 가져왔습니다. 다른 점은 ContentPresenter 대신에 여러모로 다루기 쉬운 원래 TextBlock으로 대체했습니다. 값을 표출하기 위해 ElementName 문법을 이용해 PART_Track Value 속성과 바인딩했습니다. StringFormat 속성은 이전 포스트를 봤다면 설명이 이상 필요없을 거라 생각합니다.

 

ToolTipToPopupSliderBehavior.cs

 

1
2
3
4
5
6
public class ToolTipToPopupSliderBehavior : Behavior<Slider>
{
 
// 생략
 
}
cs

 

툴팁을 팝업으로 대체하는 의미로 ToolTipToPopupSliderBehavior 비헤이비어를 작성합니다. Behavior 상속 받았으며 Slider 지정할 요소로 정의했습니다.

 

1
2
3
4
5
6
7
#region Variable
 
private Thumb _thumb;
private Popup _popup;
private double _changedHorizontalOffset;
 
#endregion
cs

 

XAML 코드에 정의한 썸과 팝업을 찾아 담아놓을 변수를 정의합니다. 추가로 팝업이 가로 방향으로 움직일 크기에 필요한 변수를 함께 정의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#region VerticalOffset : 팝업 세로 위치
 
public int VerticalOffset
{
    get { return (int)GetValue(VerticalOffsetProperty); }
    set { SetValue(VerticalOffsetProperty, value); }
}
 
public static readonly DependencyProperty VerticalOffsetProperty =
    DependencyProperty.Register(
        "VerticalOffset"
        typeof(int), 
        typeof(ToolTipToPopupSliderBehavior), 
        new PropertyMetadata(0));
 
#endregion
cs

 

팝업의 세로 방향 이동은 움직이지 않고 고정됩니다. 팝업의 세로 위치를 정의할 프로퍼티를 추가합니다. 이번 포스트의 주안점은 아니지만 의존 속성으로 정의했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#region Proteced method 
 
protected override void OnAttached()
{
    base.OnAttached();
 
    AssociatedObject.Loaded += AssociatedObject_Loaded;
}
 
protected override void OnDetaching()
{
    AssociatedObject.Loaded -= AssociatedObject_Loaded;
    _thumb.DragDelta -= Thumb_DragDelta;
    _thumb.DragStarted -= Thumb_DragStarted;
    _thumb.DragCompleted -= Thumb_DragCompleted;
 
    AssociatedObject.PreviewKeyDown -= Slider_PreviewKeyDown;
    AssociatedObject.KeyUp -= Slider_KeyUp;
 
    _thumb = null;
    _popup = null;
 
    base.OnDetaching();
}
 
#endregion
cs

 

OnAttached와 OnDetaching 메소드를 재정의합니다Behavior를 작성할 때   메소드를 재정의하는 일은 필수라   있습니다AssociatedObject를 통해 XAML 코드에 적용한 UI 요소를 가져옵니다OnAttached 메소드에서는 가져온 요소에 Loaded 이벤트를 연결하고 OnDetaching 메소드에서는 Loaded 이벤트를 연결 해제합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#region Event handler 
 
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    _popup = GetVisualChild<Popup>(AssociatedObject);
    _thumb = GetVisualChild<Thumb>(AssociatedObject);
    if (_thumb == null || _popup == nullreturn;
            
    _thumb.DragStarted += Thumb_DragStarted;
    _thumb.DragDelta += Thumb_DragDelta;
    _thumb.DragCompleted += Thumb_DragCompleted;
 
    AssociatedObject.PreviewKeyDown += Slider_PreviewKeyDown;
    AssociatedObject.KeyUp += Slider_KeyUp;
}
 
// 생략
 
#endregion
cs

 

AssociatedObject_Loaded 이벤트 핸들러에서는 AssociatedObject 통해 받아온 슬라이더에서 팝업과 썸을 찾아 필요한 이벤트를 연결합니다. 썸을 드래그할 발생하는 이벤트 DragStarted, DragDelta, DragCompleted 3가지를 연결합니다. 키보드 이벤트를 지원하기 위해 PreviewKeyDown, KeyUp 이벤트 2가지도 연결합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#region Event handler < Mouse
 
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
    _popup.HorizontalOffset += +e.HorizontalChange;
    _popup.HorizontalOffset += -e.HorizontalChange;
    _changedHorizontalOffset = e.HorizontalChange;
}
 
private void Thumb_DragStarted(object sender, DragStartedEventArgs e)
{
    _popup.VerticalOffset = VerticalOffset + _thumb.ActualHeight;
    _popup.IsOpen = true;
}
 
private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
    _popup.IsOpen = false;
}
 
#endregion
cs

 

Thumb_DragStarted 이벤트 핸들러에서는 팝업을 열어주고 Thumb_DragCompleted 이벤트 핸들러에서는 팝업을 닫아줍니다. Thumb_DragDelta 이벤트 핸들러에서는 마우스가 움직인 크기만큼 팝업의 HorizontalOffset을 업데이트해줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#region Event handler < Keyboard
 
private async void Slider_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
    await Task.Delay(100);
 
    if (e.Key == System.Windows.Input.Key.Left ||
        e.Key == System.Windows.Input.Key.Right)
    {
        _popup.VerticalOffset = VerticalOffset + _thumb.ActualHeight;
        _popup.HorizontalOffset += +_changedHorizontalOffset;
        _popup.HorizontalOffset += -_changedHorizontalOffset;
        _popup.IsOpen = true;
    }
}
 
private async void Slider_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
    await Task.Delay(100);
 
    _popup.IsOpen = false;
}
 
#endregion
cs

 

키보드 관련 이벤트 핸들러도 비슷한 패턴으로 되어 있습니다. 약간의 차이점이라면 키보드를 누르고 있을 짧은 간격으로 반복해서 들어오는 이벤트를 지연시키기 위한 코드가 추가된 점입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#region UI Helper < Common 
 
public static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
    if (parent == nullreturn null;
 
    T child = default(T);
 
    int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < numVisuals; i++)
    {
        Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
        child = v as T;
        if (child == null)
        {
            child = GetVisualChild<T>(v);
        }
        if (child != null)                
        {
            break;
        }
    }
 
    return child;
}
        
#endregion
cs

 

GetVisualChild 메소드는 VisualTree를 뒤져서 원하는 요소를 찾기 윈한 UI Helper 클래스의 일부입니다. Name으로 찾을 수도 있습니다만 예제의 경우 썸과 팝업은 유일한 요소이기때문에 타입으로 찾는 메소드를 사용했습니다.

 

 

실행해서 태스트해봅니다. 사실 예제도 만족스럽지않습니다. 팝업의 움직임에서 약간의 떨림이 보입니다. 커스텀 비헤이비어 예제로서는 괜찮아 보이는데 결과물로서는 아쉬움이 남습니다. 다음 기회에 다른 방법을 시도해봐야 겠습니다.


027. 툴팁 말풍선 슬라이더(ToolTip Balloon Slider) #1

XAML 뽀개기

이전 포스트에서도 잠깐 설명했지만 Slider Thumb() 드래그하면 현재 Value() 툴팁 말풍선 안에서 표출되는 간단한 기능을 구현해보려 합니다. 여러가지 방식으로 접근해보면서 마주하게 되는 문제점들을 하나씩 짚어봅시다.

 

 

툴팁 말풍선이 Slider 썸을 따라다니도록 하고 싶습니다. 먼저 썸이 어디에 있는지 찾아 봅시다.

 


Blend for VS [템플릿 편집 > 복사본 편집기능을 이용해 Slider 기본 스타일 템플릿을 얻을 있습니다.

 

1
2
3
<!-- ToolTip of Thumb -->
<Slider Value="5" VerticalAlignment="Center" Margin="10,0" 
        Style="{DynamicResource Style_Slider}"/>
cs

 

자동으로 지정한 키네임이 스타일 속성에 선언됩니다.

 

 

[템플릿 편집 > 복사본 편집] 기능을 이용해 얻어낸 Slider 기본 스타일 템플릿의 양이 많아 혼란이 있습니다. 조금만 정리해서 보면 그렇게 복잡하지 않습니다.

 

 

비슷한 역할을 하는 것들 끼리 묶어서 정리한 주석을 약간 추가해주었습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Slider 기본 스타일 -->
<Style x:Key="Style_Slider" TargetType="{x:Type Slider}">
    <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource SliderThumb.Static.Foreground}"/>
    <Setter Property="Template" Value="{StaticResource SliderHorizontal}"/>
    <Style.Triggers>
        <Trigger Property="Orientation" Value="Vertical">
            <Setter Property="Template" Value="{StaticResource SliderVertical}"/>
        </Trigger>
    </Style.Triggers>
</Style>
cs

 

가장 먼저 Slider의 기본 스타일을 살펴보면 기본 템플릿이 SliderHorizontal이고 Orientation 속성이 Vertical (Trigger) SliderVertical 템플릿으로 변경된다는 것을 있습니다.

 

여러 템플릿들의 키네임을 보면 역활들이 어느정도 추측 가능합니다. 단지 슬라이더의 가로, 세로 방향에 따라 가로형, 세로형 템플릿들이 각각 준비되어 있는 뿐입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- Slider 가로형 템플릿 -->
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
    <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TickBar x:Name="TopTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/>
            <TickBar x:Name="BottomTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,2,0,0" Placement="Bottom" Grid.Row="2" Visibility="Collapsed"/>
            <Border x:Name="TrackBackground" BorderBrush="{StaticResource SliderThumb.Track.Border}" BorderThickness="1" Background="{StaticResource SliderThumb.Track.Background}" Height="4.0" Margin="5,0" Grid.Row="1" VerticalAlignment="center">
                <Canvas Margin="-6,-1">
                    <Rectangle x:Name="PART_SelectionRange" Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Height="4.0" Visibility="Hidden"/>
                </Canvas>
            </Border>
            <Track x:Name="PART_Track" Grid.Row="1">
                <Track.DecreaseRepeatButton>
                    <RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
                </Track.DecreaseRepeatButton>
                <Track.IncreaseRepeatButton>
                    <RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
                </Track.IncreaseRepeatButton>
                <Track.Thumb>
                    <Thumb x:Name="Thumb" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
                </Track.Thumb>
            </Track>
        </Grid>
    </Border>
    <ControlTemplate.Triggers>
 
    // 생략
 
    </ControlTemplate.Triggers>
</ControlTemplate>
 
cs

 

역으로 거슬러 올라가 기본 템플릿으로 정의되어 있는 SliderHorizontal 템플릿을 살펴보겠습니다. 여기서 썸을 찾을 있습니다. PART_Track이라는 이름의 요소 하위에 있는 것을 발견했습니다.

 

1
2
3
<Track.Thumb>
    <Thumb x:Name="Thumb" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
cs

 

부분이 썸의 역활을 하는 XAML 코드입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- ToolTip 스타일 & 템플릿 -->
<ControlTemplate x:Key="Template_ToolTip" TargetType="{x:Type ToolTip}">
    <StackPanel>
        <Border CornerRadius="5" Background="SkyBlue">
            <ContentPresenter HorizontalAlignment="Center" Margin="10,5"/>
        </Border>
        <ed:RegularPolygon Fill="SkyBlue" Margin="0,-8,0,0" Panel.ZIndex="-1" 
                           PointCount="4" Width="10" Height="15"/>
    </StackPanel>
</ControlTemplate>
<Style x:Key="Style_ToolTip" TargetType="{x:Type ToolTip}">
    <Setter Property="Template" Value="{StaticResource Template_ToolTip}"/>
</Style>
cs

 

리소스에 툴팁 말풍선의 스타일과 템플릿을 추가로 선언합니다. 이전 포스트에서 만들었던 말풍선 팝업 XAML 코드 첫번째 방법의 코드를 그대로 가지고 왔습니다. 약간 수정이 필요합니다. 기존에 TextBlock ContentPresenter 대체합니다. 예전 포스트들에서 ContentPresenter 암시적으로 바인딩되어 진다고 여러 설명했습니다. 여기서 Style_ToolTip과 Template_ToolTip으로 나누어 정의한 것에 의미는 현재 없습니다.

 

1
2
3
4
5
6
7
8
9
10
11
<Track.Thumb>
    <Thumb x:Name="Thumb" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11">
        <Thumb.ToolTip>
            <ToolTip x:Name="PART_ThumbToolTip" 
                     Template="{StaticResource Template_ToolTip}"
                     Content="{TemplateBinding Value}" 
                     Placement="Center" VerticalOffset="-30"
                     ContentStringFormat="N0"/>
        </Thumb.ToolTip>
    </Thumb>
</Track.Thumb>
cs

 

이렇게 만든 말풍선 툴팁 템플릿을 썸의 툴팁 템플릿으로 지정합니다. 그리고 툴팁의 Content 속성은 TemplateBinding 문법을 이용해 Value 바인딩합니다. 그런 Placement, VerticalOffset 속성을 이용해 원하는 위치에 말풍선 툴팁이 위치하도록 합니다.

 

 

불필요한 소숫점 자리의 값을 없애기 위해 ContentStringFormat 속성에 "N0" 선언합니다.

 

실행해서 태스트해보면 썸에 마우스를 올려놓았을 툴팁 말풍선이 노출됩니다. 하지만 드래그하는 순간 사라지고 맙니다. 이것은 제가 원하는 시나리오가 아닙니다. 드래그하는 도중에는 반드시 노출되었으면 합니다. 썸에 직접 툴팁을 선언하는 것은 옳은 방법이 아닌 같습니다.

 

1
2
3
<!-- Auto ToolTip : Only mouse-->
<Slider Value="5" VerticalAlignment="Center" Margin="10,0" Grid.Row="1" 
        AutoToolTipPlacement="TopLeft"/>
cs

 

두번째 Slider 새로히 만들어 다른 방식으로 접근해보겠습니다.

 

 

Slider AutoToolTipPlacement 속성을 정의하면 마우스 드래그를 하는 동안 툴팁이 자동으로 노출됩니다. 매우 간단하며 제가 원했던 시나리오이기도 합니다.

 

1
2
3
4
5
6
7
8
9
<!-- Auto ToolTip : Only mouse-->
<Slider Value="5" VerticalAlignment="Center" Margin="10,0" Grid.Row="1" 
        AutoToolTipPlacement="TopLeft">
    <Slider.Resources>
        <Style TargetType="{x:Type ToolTip}">
            <Setter Property="Template" Value="{StaticResource Template_ToolTip}"/>
        </Style>
    </Slider.Resources>
</Slider>
cs

 

툴팁 말풍선의 템플릿은 Slider 하위에 키네임을 생략한 리소스를 정의하는 방식으로 적용할 있습니다.

 

1
2
3
4
5
6
7
<!-- Auto ToolTip : Only mouse-->
<Slider Value="5" VerticalAlignment="Center" Margin="10,0" Grid.Row="1" 
        AutoToolTipPlacement="TopLeft">
    <Slider.Resources>
        <Style TargetType="{x:Type ToolTip}" BasedOn="{StaticResource Style_ToolTip}"/>            
    </Slider.Resources>
</Slider>
cs

 

아니면 스타일의 BasedOn 속성을 이용해 Style_ToolTip을 정의할 수도 있습니다. 결과적으로는 차이가 없지만 방법에는 차이가 있습니다. Style_ToolTip과 Template_ToolTip으로 나누어 정의한 것의 의도하는 바가 여기에 있습니다.

 

 

하지만 방법도 한계가 있습니다. 일반적으로 기본 슬라이더는 마우스를 사용하지 않고 키보드만으로도 사용할 있습니다. 슬라이더에 포커스가 있는 상태에서 키보드의 방향키를 누르는 방법으로도 값을 변경할 있습니다. 하지만 툴팁 말풍선이 노출되지 않는 문제를 발견할 있습니다.

 

다음 포스트에서는 마우스 키보드 모두 툴팁 말풍선이 노출되도록 하는 방법에 대해 알아보겠습니다.

026. 툴팁 말풍선(ToolTip Balloon)

XAML 뽀개기

이번 포스트는 잠시 쉬어가는 포스트입니다. 다음 포스트에 사용될 디자인 요소도 제작할 겸해서 말입니다. 구글에서 말풍선 이미지 검색해보면 매우 다양한 말풍선 디자인을 찾을 있습니다. 참고하세요. 저는 최대한 간소하게 표현해볼까 합니다.

 

 

다음 포스트에서 사용될 예제를 먼저 보도록 합시다. 기본 슬라이더의 Thumb() 드래그하면 현재 Value() 말풍선 안에서 표출되는 간단한 기능입니다. 실제 구현은 그리 간단하지 않습니다만 다음 포스트에서 만들어 보려고 합니다.

 

1
2
3
4
5
6
7
8
<!-- ToolTip balloon #1 -->
<StackPanel Margin="5">
    <Border CornerRadius="5" Background="SkyBlue">
        <TextBlock Text="Lorem Ipsum is simply dummy text." HorizontalAlignment="Center" Margin="10,5"/>
    </Border>
    <ed:RegularPolygon Fill="SkyBlue" Margin="0,-8,0,0" Panel.ZIndex="-1" 
                       PointCount="4" Width="10" Height="15"/>
</StackPanel>
cs

 

첫번째 방법입니다. 말풍선 몸체와 꼭지는 세로로 정렬되는 형태이므로 전체를 StackPanel 그룹화했습니다. 값이 들어갈 TextBlock Border 한번 감싸주었습니다. 이렇게 하면 TextBlock 문자열이 들어와도 자동으로 Border 밀어내 늘어나므로 나름 효율적입니다.

 

말풍선 꼭지는 Microsoft.Expression.Drawing.dll 어셈블리를 참조하면 사용할 있는 도형 RegularPolygon을 이용해 만들었습니다. 삼각형 꼭지를 사용할 수도 있지만 사각형(마름모) 꼭지를 만들어서 사용할 수도 있습니다. StackPanel로 감싸주었기 때문에 레이어 순서가 문제가 되지만 Panel.Zindex 속성에 "-"값을 주어 레이어 순서를 추가로 변경하면 문제가 되지 않습니다. 약간의 위치조정은 Margin 속성을 이용해 조정했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- ToolTip balloon #2 -->
<Grid Margin="5">
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
 
    <Path Fill="PaleVioletRed" Margin="0,-8,0,0" Grid.Row="1" 
          Width="10" Height="15" Data="M5,0 L10,7.5 L5,15 L0,7.5 z"/>
    <Rectangle RadiusX="5" RadiusY="5" Fill="PaleVioletRed"/>
    <TextBlock Text="Lorem Ipsum is simply dummy text." HorizontalAlignment="Center" Margin="10,5"/>
</Grid>
cs

 

두번째 방법입니다. Grid 이용해 말풍선 몸체와 꼭지를 그룹화했습니다. 말풍선 몸체와 꼭지는 Grid RowDefinition 속성을 이용해 2개로 나누어진 Row 각각 위치하도록 했습니다. 말풍선 몸체는 Rectangle 이용했습니다. 첫번째 방법과 조금 다른 점이라면 Border CornerRadius 속성이고 Rectangle RadiusX, RadiusY 속성을 이용해 코너의 라운드를 표현하는 점입니다. 그리고 Rectangle TextBlock 자식으로 가질 없습니다.

 

Grid StackPanel 달리 레이어 정렬에 강하게 영향을 주지 않으므로 말풍선 꼭지를 먼저 선언하고 몸체를 선언해 가려지지 않도록 했습니다. 꼭지는 Path 이용했습니다. Path Width, Height Data 속성을 이용해 Path Point들의 위치에 따라 그려지므로 다루기가 까다롭습니다. 깔끔한 픽셀 단위 작업이 쉽지 않습니다. 약간의 위치조정은 첫번째 방법과 동일하게 Margin 속성을 이용했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- ToolTip balloon #3 -->
<Grid Margin="5">
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
            
    <Border CornerRadius="5" Background="DarkSeaGreen">
        <TextBlock Text="Lorem Ipsum is simply dummy text." HorizontalAlignment="Center" Margin="10,5"/>
    </Border>
    <ed:RegularPolygon Fill="DarkSeaGreen" Margin="0,0,0,-4" Grid.Row="1"
                       PointCount="3" Width="10" Height="8" RenderTransformOrigin="0.5,0.5">
        <ed:RegularPolygon.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleY="-1" ScaleX="1"/>
            </TransformGroup>
        </ed:RegularPolygon.RenderTransform>
    </ed:RegularPolygon>
</Grid>
cs

 

세번째 방법입니다. 두번째 방법과 동일하게 Grid 이용해 말풍선 몸체와 꼭지를 그룹화했고 Row 위치하도록 했습니다. 값이 들어갈 TextBlock Border 한번 감싸주는 첫번째 방법을 이용했습니다.

 

말풍선 꼭지는 첫번째 방법과 동일하게 도형 RegularPolygon을 이용했습니다. 첫번째 방법과 다른 점은 이번엔 삼각형이라는 점입니다. 하지만 상하 방향을 뒤집어야 하기 때문에 ScaleTransform의 ScaleY 속성에 "-"값을 주어 반전 효과를 주었습니다. RotateTransform 이용해 회전 시켜도 동일한 효과를 얻을 있습니다. 약간의 위치조정은 첫번째, 두번째 방법과 동일하게 Margin 속성을 이용했습니다.

 

1
2
3
4
5
6
7
8
<!-- ToolTip balloon #4 -->
<DockPanel Margin="5">
    <Border CornerRadius="5" Background="Gainsboro" DockPanel.Dock="Top">
        <TextBlock Text="Lorem Ipsum is simply dummy text." HorizontalAlignment="Center" Margin="10,5"/>
    </Border>
    <ed:RegularPolygon Fill="Gainsboro" Margin="0,-8,0,0" Panel.ZIndex="-1" 
                       PointCount="4" Width="10" Height="15" DockPanel.Dock="Top"/>
</DockPanel>
cs

 

4번째 방법입니다. 이번에는 DockPanel 이용해 말풍선 몸체와 꼭지를 그룹화했습니다. DockPanel 사용했기 때문에 하위 자식들은 DockPanel.Dock 의존 속성을 정의해 위치를 각각 지정해 주어야 합니다. 외에는 StackPanel 이용했던 첫번째 방법과 다른 점은 없습니다. 동일한 표현이 가능은 하지만 DockPanel 사용했다는 점이 께름칙합니다.

 

 

위에서 알아본 4가지 방법 모두는 현재로서는 문제가 보이지 않습니다. 결과적으로는 동일한 결과를 보이고 있습니다. 인터페이스를 마주하는 사용자 입장에서는 더더욱 상관없습니다. 하지만 코드 간결성, 효율성 여러면에서 문제가 발생할 수도 있습니다. 그리고 만약에 요구사항이 변경되거나 기능이 추가되었을 수정의 용이성, 한계점 어떤 문제가 발생할지 예측하기 힘듭니다.

 

 

예를 들어 표출될 컨텐츠의 양이 늘어나 여러 줄로 표현이 되어야 한다면 그에 따라 말풍선의 크기도 늘어나야 것입니다. 예제에 사용된 4가지 방법 모두는 가능하도록 코드를 작성했습니다. 하지만 Blend for VS는 아시다시피 WYSIWYG(위지위그방식의 툴입니다. 심심치 않게 디자인 윈도우에서 마우스를 조작해 디자인을 수정할 수 있습니다.

 

4가지 방법에서 각각 그룹화에 쓰인 StackPanel, Grid, DockPanel 모두를 다시 한번 살펴보면 Width, Height 속성이 어느 하나도 쓰이지 않은 것을 있습니다. 이유는 하위 자식의 컨텐츠 양이 얼마나 될지 확신하지 못하기 때문에 의도적으로 제한하지 않은 것입니다. 다른 의미로는 컨텐츠 양에 따라 유연하게 늘어나도록 일부러 정의하지 않은 것입니다


물론 컨텐츠양이 변경될 때마다 크기를 변경하는 관련 코드를 추가할 수도 있겠습니다만 그리 좋지 않은 접근 방식이라 생각합니다. 이런 요구사항이 있을 때 위지위그 방식으로 작업을 하다보면 Width, Height, Margin 등의 속성이 원치않게 자동으로 생성되는 것을 쉽게 경험할 수 있습니다.

 


결론은 위지위그 방식의 툴을 사용할 주의해야 한다는 것입니다. C# 또는 XAML 코드를 다루는 우리는 Blend for VS Visual Studio 사용이 필수고 당연한 일이기도 합니다. 그리고 디자인 윈도우에서 마우스 조작을 이용해 디자인 관련 작업을 하는 것은 매우 쉽고 직관적입니다. 하지만 위지위그 방식의 툴들이 대부분 그러하듯 때론 사용자가 의도하지 않은 코드를 생성해내기도 합니다. 그렇기 때문에 위지위그 방식의 작업 중에도 XAML 코드 확인  수정해줘야 하는 일이 때에 따라 필요할 있습니다.


상황에 따라 어떤 코드를 선택할지는 여러분의 몫이지만 어떤 차이가 있는지 어떤 방법이 더 효과적인지를 판단하 점 중요할 것 있습니다.


025. 커스텀 트리거 & 액션(Custom Trigger & Action) #2

XAML 뽀개기

이전 포스트에서 기본 XAML 코드와 기본 CS 코드를 준비했습니다.



간단한 시계가 완성되었습니다. 이제부터 커스텀 트리거와 커스텀 액션을 정의해봅시다.


TimeChangedTrigger < TimeToAlarmBehavior.cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class TimeChangedTrigger : TriggerBase<TextBlock>
{
    public string SpecialTime
    {
        get { return (string)GetValue(SpecialTimeProperty); }
        set { SetValue(SpecialTimeProperty, value); }
    }
 
    public static readonly DependencyProperty SpecialTimeProperty =
        DependencyProperty.Register(
            "SpecialTime"
            typeof(string), 
            typeof(TimeChangedTrigger), 
            new PropertyMetadata(string.Empty));
 
    protected override void OnAttached()
    {
        base.OnAttached();
 
        AssociatedObject.Loaded += AssociatedObject_Loaded;
    }
 
    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= AssociatedObject_Loaded;
 
        base.OnDetaching();
    }
 
    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        var textBlock = AssociatedObject as TextBlock;
        if (textBlock == nullreturn;
 
        var dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
        if (dpd == nullreturn;
        dpd.AddValueChanged(textBlock, OnTextChanged);
    }
 
    private void OnTextChanged(object sender, EventArgs e)
    {
        var textBlock = sender as TextBlock;
        if (textBlock == nullreturn;
 
        if (textBlock.Text == SpecialTime)
        {
            InvokeActions("ALARM");
        }
        else
        {
            InvokeActions("");
        }
    }
}
 
cs


위 코드는 TimeToAlarmBehavior.cs 비헤이비어 전체 코드입니다. 부분으로 나누어 살펴보겠습니다.


1
2
3
4
5
6
public class TimeChangedTrigger : TriggerBase<TextBlock>
{
 
// 생략
 
}
cs


TimeChangedTrigger 커스텀 트리거는 TriggerBase를 상속 받아서 정의합니다. 그리고 TextBlock 요소에만 적용하도록 TextBlock 사용할 요소로 지정했습니다


1
2
3
4
5
6
7
8
9
10
11
12
public string SpecialTime
{
    get { return (string)GetValue(SpecialTimeProperty); }
    set { SetValue(SpecialTimeProperty, value); }
}
 
public static readonly DependencyProperty SpecialTimeProperty =
    DependencyProperty.Register(
        "SpecialTime"
        typeof(string), 
        typeof(TimeChangedTrigger), 
        new PropertyMetadata(string.Empty));
cs


지정 시간을 받기 위해 SpecialTime 의존 속성을 추가합니다. 의존 속성에 대해서는 이전 포스트 참고합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
protected override void OnAttached()
{
    base.OnAttached();
    
    AssociatedObject.Loaded += AssociatedObject_Loaded;
}
 
protected override void OnDetaching()
{
    AssociatedObject.Loaded -= AssociatedObject_Loaded;
 
    base.OnDetaching();
}
cs

 

OnAttached와 OnDetaching 메소드를 재정의합니다. Behavior에서 메소드를 재정의하는 일은 필수라 있습니다. AssociatedObject를 통해 XAML 코드의 UI 요소를 가져옵니다. OnAttached 메소드에서는 가져온 요소에 Loaded 이벤트를 연결하고 OnDetaching 메소드에서는 Loaded 이벤트를 연결 해제합니다.

 

1
2
3
4
5
6
7
8
9
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    var textBlock = AssociatedObject as TextBlock;
    if (textBlock == nullreturn;
 
    var dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
    if (dpd == nullreturn;
    dpd.AddValueChanged(textBlock, OnTextChanged);
}
cs

 

AssociatedObject_Loaded 이벤트 핸들러에서는 TextBlock OnTextChanged 이벤트를 추가합니다. 기본 TextBlock TextChanged 이벤트가 없습니다. 하지만 간단한 방법을 찾아 적용했습니다. 참고한 링크는 아래입니다.

 

참고 : stackoverflow > How to detect a change in the Text property of a TextBlock?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void OnTextChanged(object sender, EventArgs e)
{
    var textBlock = sender as TextBlock;
    if (textBlock == nullreturn;
 
    if (textBlock.Text == SpecialTime)
    {    
        InvokeActions("ALARM");            
    }
    else
    {
        InvokeActions("");
    }
}
cs

 

OnTextChanged 이벤트 핸들러에서는 InvokeActions을 호출할 조건을 정의해 주었습니다. 사용자가 지정한 시간과 동일한 시간이 되었을 커스텀 액션을 호출하도록 하는 간단한 조건문입니다. InvokeActions 메소드는 커스텀 액션을 호출하기 위해 반드시 필요한 주요 메소드입니다. 파라미터를 이용해 "ALARM"이라는 문자열을 전달하도록 했습니다.

 

ChangeForegroundAction < TimeToAlarmBehavior.cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ChangeForegroundAction : TriggerAction<TextBlock>
{
    public Brush Foreground
    {
        get { return (Brush)GetValue(ForegroundProperty); }
        set { SetValue(ForegroundProperty, value); }
    }
 
    public static readonly DependencyProperty ForegroundProperty =
        DependencyProperty.Register(
            "Foreground"
            typeof(Brush), 
            typeof(ChangeForegroundAction), 
            new PropertyMetadata(Brushes.Black));
 
    protected override void Invoke(object parameter)
    {
        var textBlock = AssociatedObject as TextBlock;
        if (textBlock == null || parameter == nullreturn;
 
        var param = parameter.ToString().ToUpper();
        if (param == "ALARM")
        {
            textBlock.Foreground = Foreground;
        }
        else 
        {
            textBlock.Foreground = Brushes.Black;
        }
    }
}
cs

 

ChangeForegroundAction 커스텀 액션은 TriggerAction을 상속 받습니다. 그리고 TextBlock 요소에 적용 가능하게 하기 위해 TextBlock 사용할 요소로 지정했습니다. 부분으로 나누어 살펴보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public Brush Foreground
{
    get { return (Brush)GetValue(ForegroundProperty); }
    set { SetValue(ForegroundProperty, value); }
}
 
public static readonly DependencyProperty ForegroundProperty =
    DependencyProperty.Register(
        "Foreground"
        typeof(Brush), 
        typeof(ChangeForegroundAction), 
        new PropertyMetadata(Brushes.Black));
cs

 

알람에 사용될 색상을 받기 위해 Foreground 의존 속성을 추가합니다. 의존 속성에 대해서는 이전 포스트 참고합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected override void Invoke(object parameter)
{
    var textBlock = AssociatedObject as TextBlock;
    if (textBlock == null || parameter == nullreturn;
 
    var param = parameter.ToString().ToUpper();
    if (param == "ALARM")
    {
        textBlock.Foreground = Foreground;
    }
    else 
    {
        textBlock.Foreground = Brushes.Black;
    }
}
cs

 

Invoke 메소드 재정의에서는 파라미터 값을 확인해 AssociatedObject를 통해 들어온 TextBlock Foreground 속성을 사용자가 지정한 색상으로 변경합니다.

 

 

적용하기 이전 TimeToAlarmBehavior 비헤이비어 코드가 막 준비된 빌드를 했다면 Blend for VS Assets(자산) 윈도우에서 보이게 됩니다. 이전 비헤이비어 포스트에서 설명했습니다만 드래그  드롭 동작을 이용해 디자인 뷰에서 패널에 적용하는 것이 훨씬 간편합니다. 네임스페이스 정의도 함께 이루어지므로 매우 편리합니다.


SDK에서 제공하는 ControlStoryboardAction 기본 비헤이비어도 함께 적용합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 초 -->
<TextBlock x:Name="txtSecond" Text="0" Grid.Column="2" RenderTransformOrigin="0.5,1">
    <TextBlock.RenderTransform>
        <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform/>
            <TranslateTransform/>
        </TransformGroup>
    </TextBlock.RenderTransform>
    <i:Interaction.Triggers>
        <bhv:TimeChangedTrigger SpecialTime="7">
            <ei:ControlStoryboardAction Storyboard="{StaticResource StoryboardSecond}"/>
            <bhv:ChangeForegroundAction Foreground="Blue"/>
        </bhv:TimeChangedTrigger>
    </i:Interaction.Triggers>
</TextBlock>
cs

 

TimeChangedTrigger 커스텀 트리거에는 SpecialTime 속성을 원하는 시간대로 지정합니다.

 

 

ControlStoryboardAction에서는 시간이 변경될 때 실행할 StoryboardSecond 스토리보드를 지정합니다.

 

 

추가한 ChangeForegroundAction 커스텀 액션에서는 SpecialTime과 일치했을 때 변경될 Foreground 색상을 지정합니다.

 

 

실행해서 올바르게 동작하는지 확인해 봅니다.

 

몇가지 기능에 대해 추가로 설명합니다. 액션은 하나의 트리거에 여러 개를 동시에 사용할 있습니다. 여기서는 ControlStoryboardAction과 ChangeForegroundAction 동시에 사용했습니다. 그리고 ChangeForegroundAction 기본으로 제공되는 ChangePropertyAction 이용해서도 동일한 효과를 있습니다.

 

참고 : MSDN > Creating custom triggers and actions

 

샘플 코드 : https://github.com/CharlesKwon/XamlSimplified

 

관련 목차

 

023. 사용자 추가 비헤이비어(Custom Behavior)

024. 사용자 추가 비헤이비어(Custom Trigger & Action) #1

025사용자 추가 비헤이비어(Custom Trigger & Action) #2 

024. 커스텀 트리거 & 액션(Custom Trigger & Action) #1

XAML 뽀개기

이전 포스트에서는 OnlyOneExpanderBehavior 비헤이비어 단독으로 사용되었지만 이전 포스트에서 살펴본 CallMethodAction 비헤이비어인 경우 Trigger(트리거) Action(액션)으로 구성되어 있던 것을 있었습니다. 이번 포스트에서는 Custom Trigger Custom Action 대해 알아 봅시다.

 

 

이번에 사용될 예제는 간단한 알람 시계입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<Grid>
    <Grid.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="FontSize" Value="100"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>
        </Style>
    </Grid.Resources>
 
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
 
    <!-- 시 -->
    <TextBlock x:Name="txtHour" Text="0"/>
 
    <!-- 분 -->
    <TextBlock x:Name="txtMinute" Text="0" Grid.Column="1"/>
 
    <!-- 초 -->
    <TextBlock x:Name="txtSecond" Text="0" Grid.Column="2"/>
 
    <!-- Separation points -->
    <TextBlock Text=":" HorizontalAlignment="Right" Margin="0,0,-10,0"/>
    <TextBlock Text=":" HorizontalAlignment="Right" Margin="0,0,-10,0" Grid.Column="1"/>
</Grid>
cs

 

기본 XAML 코드 : MainWindow.xaml

 

예제를 위한 XAML 코드입니다. , , 초를 TextBlock으로 나누어 표현합니다. CS 코드에서 접근하기 윈해 x:Name 각각 선언했습니다. 다른 특이점은 없습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
 
        var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
        timer.Tick += Timer_Tick;
        timer.Start();
    }
 
    private void Timer_Tick(object sender, EventArgs e)
    {
        if (DateTime.Now.Hour > 12)
        {
         txtHour.Text = (DateTime.Now.Hour - 12).ToString();
        }
        else
        {
            txtHour.Text = DateTime.Now.Hour.ToString();
        }
 
        txtMinute.Text = DateTime.Now.Minute.ToString();            
        txtSecond.Text = DateTime.Now.Second.ToString();
    }
}
cs

 

기본 CS 코드 : MainWindow.xaml.cs

 

예제를 위한 CS 코드입니다. XAML 코드에 선언된 각 TextBlock 현재 시간을 1 단위로 업데이트합니다. 12단위 시간을 표현하기 위한 코드를 제외하고 다른 특이점은 없습니다.

 

, , 모두 동일한 방법이 사용될 것이므로 이제부터는 단위만 분리해서 살펴보겠습니다. 예제 시나리오는 시간이 변경될 지정한 Storyboard 실행하고, 알람이 울릴 시간(트리거) 설정할 있으며 해당 시간이 되었을 색상을 변경(액션)하는 시나리오입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Storyboard x:Key="StoryboardSecond">
    <DoubleAnimationUsingKeyFrames 
        Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" 
        Storyboard.TargetName="txtSecond">
        <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1.1">
            <EasingDoubleKeyFrame.EasingFunction>
                <SineEase EasingMode="EaseIn"/>
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            <EasingDoubleKeyFrame KeyTime="0:0:1" Value="1">
            <EasingDoubleKeyFrame.EasingFunction>
                <SineEase EasingMode="EaseOut"/>
            </EasingDoubleKeyFrame.EasingFunction>
        </EasingDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>
cs

 

1
2
3
4
5
6
7
8
9
10
11
<!-- 초 -->
<TextBlock x:Name="txtSecond" Text="0" Grid.Column="2" RenderTransformOrigin="0.5,1">
    <TextBlock.RenderTransform>
        <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform/>
            <TranslateTransform/>
        </TransformGroup>
    </TextBlock.RenderTransform>
</TextBlock>
cs

 

StoryboardHour, StoryboardMinute, StoryboardSecond 스토리보드 추가

 

스토리보드는 리소스에 정의해야 합니다시간이 가고 있다는 느낌을 주기 위해 ScaleY축에 약간의 움직임을 반복하도록 했습니다 모두 각각 분리해서 작성합니다.


Blend for VS 이용하면 쉽게 작성할 있습니다. RenderTransform 속성을 이용한 스토리보드를 작성하기 위해서는 각 TextBlock에 RenderTransformOrigin과 RenderTransform 그룹이 미리 정의되어 있어야 하는 것 주의가 필요 합니다. Blend for VS 이용해 스토리보드를 작성한다면 자동으로 추가되므로 걱정할 필요가 없습니다

 

다음 포스트에서는 Custom Trigger Custom Action 추가해봅시다.

 

참고 : MSDN > Creating custom triggers and actions

 

샘플 코드 : https://github.com/CharlesKwon/XamlSimplified

 

관련 목차

 

023. 사용자 추가 비헤이비어(Custom Behavior)

024. 사용자 추가 비헤이비어(Custom Trigger & Action) #1

025사용자 추가 비헤이비어(Custom Trigger & Action) #2 


023. 커스텀 비헤이비어(Custom Behavior)

XAML 뽀개기

이전 포스트에서는 기본으로 제공되는 비헤이비어 사용법을 살펴보았습니다. 이번 포스트에서는 사용자가 직접 비헤이비어를 만들어보는 방법을 계속해서 살펴보겠습니다.

 

 

먼저 예제를 위한 상황 설정에 대한 설명입니다. 위 이미지처럼 Expander 컨트롤을 여러 동시에 사용하는 작은 인터페이스에서는 컨텐츠 노출을 효율적으로 하기 위해 한번에 하나씩만 열리도록 제약을 줄 때가 종종 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<StackPanel Margin="5">
    <TextBlock Text="Only one can expand at a time" FontWeight="Bold" Margin="0,0,0,5"/>
            
    <Expander Header="Header 01" IsExpanded="True">
        <TextBlock Text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." 
                   TextWrapping="Wrap"/>
    </Expander>
    <Expander Header="Header 02">
        <TextBlock Text="It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged." 
                   TextWrapping="Wrap"/>
    </Expander>
    <Expander Header="Header 03">
        <TextBlock Text="It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." 
                   TextWrapping="Wrap"/>
    </Expander>
</StackPanel>
cs

기본 XAML 코드 : MainWindow.xaml


XAML 코드는 최대한 간단히 StackPanel 이용해 여러 개의 Expander 컨트롤을 하나의 그룹으로 만들었습니다.

 

OnlyOneExpanderBehavior.cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class OnlyOneExpanderBehavior : Behavior<Panel>
{
    protected override void OnAttached()
    {
        base.OnAttached();
 
        AssociatedObject.Loaded += AssociatedObject_Loaded;
    }
 
    protected override void OnDetaching()
    {            
        AssociatedObject.Loaded -= AssociatedObject_Loaded;
        
        base.OnDetaching();
    }
        
    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        foreach (var child in AssociatedObject.Children)
        {
            if (child == nullreturn;
            var allExpander = child as Expander;
            if (allExpander != null)
            {
                allExpander.Expanded += Expander_Expanded;
            }
        }
    }
 
    private void Expander_Expanded(object sender, RoutedEventArgs e)
    {
        foreach (var child in AssociatedObject.Children)
        {
            if (child == nullreturn;
            var allExpander = child as Expander;
            if (allExpander != null)
            {
                allExpander.IsExpanded = false;
            }
        }
 
        var expander = sender as Expander;
        if (expander == nullreturn;
        expander.Expanded -= Expander_Expanded;
        expander.IsExpanded = true;
        expander.Expanded += Expander_Expanded;
    }
}
 
cs

 

코드는 상황을 해결하는데 사용하기 위해 추가한 OnlyOneExpanderBehavior 비헤이비어 전체 코드입니다. 부분으로 나누어 살펴보겠습니다.

 

1
2
3
4
5
6
public class OnlyOneExpanderBehavior : Behavior<Panel>
{
    
// 생략
 
}
cs

 

OnlyOneExpanderBehavior는 Behavior를 상속받습니다. StackPanel 뿐만 아니라 Grid, WrapPanel Panel 상속받아 자식을 가질 있는 모든 패널 요소에 적용 가능하게 하기 위해 Panel 사용할 요소로 지정했습니다. 예제에서는 StackPanel 사용할 요소로 제한해도 상관없습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
protected override void OnAttached()
{
    base.OnAttached();
 
    AssociatedObject.Loaded += AssociatedObject_Loaded;
}
 
protected override void OnDetaching()
{
    AssociatedObject.Loaded -= AssociatedObject_Loaded;
 
    base.OnDetaching();
}
cs

 

OnAttached와 OnDetaching 메소드를 재정의합니다. Behavior를 작성할 때  메소드를 재정의하는 일은 필수라 있습니다. AssociatedObject를 통해 XAML 코드에 적용한 UI 요소를 가져옵니다. OnAttached 메소드에서는 가져온 요소에 Loaded 이벤트를 연결하고 OnDetaching 메소드에서는 Loaded 이벤트를 연결 해제합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    foreach (var child in AssociatedObject.Children)
    {
        if (child == nullreturn;
        var allExpander = child as Expander;
        if (allExpander != null)
        {
            allExpander.Expanded += Expander_Expanded;
        }
    }
}
cs

 

AssociatedObject_Loaded 이벤트 핸들러에서는 패널의 하위 자식들 모든 Expander 컨트롤을 찾아서 Expanded 이벤트를 연결합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
    foreach (var child in AssociatedObject.Children)
    {
        if (child == nullreturn;
        var allExpander = child as Expander;
        if (allExpander != null)
        {
            allExpander.IsExpanded = false;
        }
    }
 
    var expander = sender as Expander;            
    if (expander == nullreturn;
    expander.Expanded -= Expander_Expanded;
    expander.IsExpanded = true;
    expander.Expanded += Expander_Expanded;
}
cs

 

Expander_Expanded 이벤트 핸들러에서는 모든 Expander 컨트롤을 IsExpanded 속성을 이용해 닫은 sender 통해 들어온 Expander 컨트롤만 다시 열어줍니다. 그냥 열어주게되면 이벤트 핸들러를 반복해서 타게 되므로 열어주기 전과 후에 이벤트 핸들러 해제 연결을 다시 한번 해줍니다.

 

1
2
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
xmlns:bhv="clr-namespace:Sample.Behavior"
cs

 

1
2
3
4
5
6
7
8
<StackPanel Margin="5">
    <i:Interaction.Behaviors>
        <bhv:OnlyOneExpanderBehavior/>
    </i:Interaction.Behaviors>
 
// 생략
 
</StackPanel>
cs

 

XAML 코드에 완성된 비헤이비어를 적용합니다. 추가한 비헤이비어를 사용하기 위해서는 두개의 네임스페이스가 우선 정의되어 있어야 합니다.

 

 

적용하기 이전 OnlyOneExpanderBehavior 비헤이비어 코드가 막 준비된 빌드를 했다면 Blend for VS Assets(자산) 윈도우에서 보이게 됩니다. 이전 비헤이비어 포스트에서 설명했습니다만 드래그  드롭 동작을 이용해 디자인 뷰에서 패널에 적용하는 것이 훨씬 간편합니다. 네임스페이스 정의도 함께 이루어지므로 매우 편리합니다.

 

 

실행해서 올바르게 동작하는지 확인해 봅니다.

 

샘플 코드 : https://github.com/CharlesKwon/XamlSimplified

 

관련 목차

 

023. 사용자 추가 비헤이비어(Custom Behavior)

024. 사용자 추가 비헤이비어(Custom Trigger & Action) #1

025. 사용자 추가 비헤이비어(Custom Trigger & Action) #2