XAML or HTML

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으로 찾을 수도 있습니다만 예제의 경우 썸과 팝업은 유일한 요소이기때문에 타입으로 찾는 메소드를 사용했습니다.

 

 

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