336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

삽질의 여정

윈도우 타이틀바에 마우스를 가져 갔을 경우 툴팁이 보여지도록 할 일이 생겼고, xaml로만 해결 하려 하다 보니 다음의 실패 케이스들이 발생되었습니다.

  • DXWindow 타이틀바 스타일 재정의

    DXWindow 타이틀바 스타일을 재정의하여 트리거에서 IsMouseOver 값이 true일 때 툴팁 콘트롤이 활성화 되도록 설정 할 생각이었습니다.

    하지만, DXWindow는 타이틀바 스타일 재정의하는 기능을 제공하지 않았습니다.

  • DXWindow 스타일에서 타이틀바 IsMouseOver 트리거 정의

    WPF 특성상 부모 Element에서 자식 Element를 트리거 할 수 없습니다. (방법이 있다면 알려주세요~)

  • DXWindow HeaderItemContainerStyle 스타일 재정의

    DXWindow는 타이틀바 커스터마이징을 위해 HeaderItemContainerStyle이 제공되며, 이를 통해 부모인 타이틀바(DXWindowHeader)의 IsMouseOver를 트리거 하려 하였습니다.

    <dx:DXDialogWindow.HeaderItemContainerStyle>
        <Style>
            <Style.Triggers>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=dx:DXWindowHeader}}" Value="True" />
                        <Condition Binding="{Binding ShowActivated, RelativeSource={RelativeSource AncestorType=dx:DXDialogWindow}}" Value="True" />
                    </MultiTrigger.Conditions>
                    <MultiTrigger.Setters>
                        <Setter TargetName="ToolTip" Property="IsOpen" Value="True" />
                    </MultiTrigger.Setters>
                </MultiTrigger>
            </Style.Triggers>
        </Style>
    </dx:DXDialogWindow.HeaderItemContainerStyle>
    <Grid>
        <dxe:FlyoutControl  IsOpen="False"  
                            PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}" 
                            x:Name="ToolTip"
                            AllowRecreateContent="False">
            <dxe:FlyoutControl.Style>
                <Style TargetType="dxe:FlyoutControl">
                    <Setter Property="Settings">
                        <Setter.Value>
                            <dxe:FlyoutSettings Placement="Right" ShowIndicator="True" />
                        </Setter.Value>
                    </Setter>
                    <Setter Property="ToolTipService.ShowDuration" Value="500" />
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                    <Setter Property="VerticalAlignment" Value="Top"/>
                    <Setter Property="AllowOutOfScreen" Value="True"/>
                    <Setter Property="StaysOpen" Value="False"/>
                    <Setter Property="AlwaysOnTop" Value="True" />
                </Style >
            </dxe:FlyoutControl.Style>
            <TextBlock Text="ESC키를 누르시면 창이 닫힙니다." />
        </dxe:FlyoutControl>
    </Grid>
    

    데이터 트리거는 잘 되었지만, Setter에서 문제가 발생 되었습니다.

    Setter에서는 TargetName 속성을 이용하여 Style 밖에 정의 되어 있는 툴팁 컨트롤에 접근 하려 했지만 Style은 대상 타입에 정의되어 있는 속성만 접근 할 수 있었습니다.

    Style 자체가 디자인 독립성을 가지고 있다는 점을 감안하면 지극히 당연한 일입니다.

   

Code behind에서 방법을 찾다.

타이틀바 Element에 직접 접근하여 마우스 이벤트를 정의 해야겠다는 판단에서 DXWindow의 타이틀바 구성을 확인하였습니다.

보시는 것처럼 DXWindow의 타이틀바는 DXWindowHeader 타입으로 "PART_Header"라는 이름을 가지고 있었습니다.

이 정보들을 활용하여 Code behind에서 DXWindow Loaded 이벤트를 구독하여 타이틀바인 DXWindowHeader 자식 Element를 찾아 mouseenter와 mouseleave 이벤트를 구독하였고, 각 이벤트에서는 툴팁의 활성화 여부를 처리하였습니다.

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    var window = sender as DXDialogWindow;
    var header = FindChild(window, "PART_Header");
    header.MouseEnter += HeaderOnMouseEnter;
    header.MouseLeave += HeaderOnMouseLeave;
}

private void HeaderOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
{
    if (this.ToolTip.IsOpen)
        this.ToolTip.IsOpen = false;
}

private void HeaderOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
{
    if(!this.ToolTip.IsOpen)
        this.ToolTip.IsOpen = true;
}

/// 
/// Finds a Child of a given item in the visual tree. 
/// 
/// A direct parent of the queried item.
/// The type of the queried item.
/// x:Name or Name of child. 
/// The first parent item that matches the submitted type parameter. 
/// If not matching item can be found, 
/// a null parent is being returned.
public static T FindChild(DependencyObject parent, string childName)
   where T : DependencyObject
{
    // Confirm parent and childName are valid. 
    if (parent == null) return null;

    T foundChild = null;

    int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < childrenCount; i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);
        // If the child is not of the request child type child
        T childType = child as T;
        if (childType == null)
        {
            // recursively drill down the tree
            foundChild = FindChild(child, childName);

            // If the child is found, break so we do not overwrite the found child. 
            if (foundChild != null) break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
            var frameworkElement = child as FrameworkElement;
            // If the child's name is set for search
            if (frameworkElement != null && frameworkElement.Name == childName)
            {
                // if the child's name is of the request name
                foundChild = (T)child;
                break;
            }
        }
        else
        {
            // child element found.
            foundChild = (T)child;
            break;
        }
    }

    return foundChild;
}

   

Attached Property 변경

다른 윈도우에서도 사용하기 위한 공용화 작업으로 DXWindowTooltipBehavior 클래스를 정의하고 앞서 소개 시켜 드린 Code behind 코드들을 Attached Property인 IsEnable과 Control 속성에서 동작하도록 변경하였습니다.

  • IsEnable 속성은 대상 윈도우의 Loaded 이벤트에서 Code behind에서 처리 한 것처럼 타이틀바 객체를 찾아 마우스 이벤트를 구독하였습니다.

    참고로, AttachedProperty의 대상 윈도우 Loaded 이벤트 처리 코드와 Code behind의 처리 코드가 약간 다릅니다.

    테스트를 해 보니 Attached Property에서 대상 윈도우의 Loaded 이벤트는 자식 Element가 만들어지기도 전에 호출되었습니다.

    그러다 보니 타이틀바 객체를 찾을 수 없었고, 이를 해결하기 위해 구글링을 해본 결과 Dispatcher.BeginInvoke 함수에 DispatcherPriority.Loaded 옵션을 주면 윈도우 자식 컨트롤이 모두 활성화 된 다음 호출 되는 점을 확인 할 수 있었습니다.

  • Control 속성은 툴팁 컨트롤을 지정하여 타이틀바의 MouseEnter, MouseLeave 이벤트 흐름에 따라 툴팁 활성화 여부를 처리하였습니다.

public static class DXWindowTooltipBehavior
{
    public static readonly DependencyProperty IsEnableProperty = DependencyProperty.RegisterAttached(
        "IsEnable", typeof(bool), typeof(DXWindowTooltipBehavior), new PropertyMetadata(false, OnToolTipEnableChanged));

    public static readonly DependencyProperty ControlProperty = DependencyProperty.RegisterAttached(
        "Control", typeof(FlyoutControl), typeof(DXWindowTooltipBehavior), new PropertyMetadata(default(FlyoutControl)));

    public static FlyoutControl GetControl(DependencyObject d)
    {
        return (FlyoutControl)d.GetValue(ControlProperty);
    }

    public static void SetControl(DependencyObject d, FlyoutControl control)
    {
        d.SetValue(ControlProperty, control);   
    }

    private static void OnToolTipEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var window = d as DXWindow;
        if (window == null)
        {
            throw new NotSupportedException("DXWindow 타입만 지원합니다.");
        }
        if ((bool) e.NewValue)
        {
            AttachHeaderEvent(window);
        }
        if ((bool) e.OldValue)
        {
            DetachHeaderEvent(window);
        }
    }

    

    private static void AttachHeaderEvent(DXWindow window)
    {
        window.Loaded += WindowOnLoaded;
        
    }
    

    private static void WindowOnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        var window = sender as DXWindow;
        window.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
        {
            var header = FindChild(window, "PART_Header");
            header.MouseEnter += HeaderOnMouseEnter;
            header.MouseLeave += HeaderOnMouseLeave;
        }));
    }

    private static void DetachHeaderEvent(DXWindow window)
    {
        window.Loaded -= WindowOnLoaded;
        var header = FindChild(window, "PART_Header");
        header.MouseEnter -= HeaderOnMouseEnter;
        header.MouseLeave -= HeaderOnMouseLeave;
    }

    private static void HeaderOnMouseLeave(object sender, MouseEventArgs e)
    {
        var header = sender as DXWindowHeader;
        var control = GetControl(header.GetRootParent());
        if (control != null && control.IsOpen)
        {
            control.IsOpen = false;
        }
    }

    private static void HeaderOnMouseEnter(object sender, MouseEventArgs e)
    {
        var header = sender as DXWindowHeader;
        var control = GetControl(header.GetRootParent());
        if (control != null && !control.IsOpen)
        {
            control.IsOpen = true;
        }
    }

    public static void SetIsEnable(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnableProperty, value);
    }

    public static bool GetIsEnable(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnableProperty);
    }

    /// 
    /// Finds a Child of a given item in the visual tree. 
    /// 
    /// A direct parent of the queried item.
    /// The type of the queried item.
    /// x:Name or Name of child. 
    /// The first parent item that matches the submitted type parameter. 
    /// If not matching item can be found, 
    /// a null parent is being returned.
    public static T FindChild(DependencyObject parent, string childName)
       where T : DependencyObject
    {
        // Confirm parent and childName are valid. 
        if (parent == null) return null;

        T foundChild = null;

        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            // If the child is not of the request child type child
            T childType = child as T;
            if (childType == null)
            {
                // recursively drill down the tree
                foundChild = FindChild(child, childName);

                // If the child is found, break so we do not overwrite the found child. 
                if (foundChild != null) break;
            }
            else if (!string.IsNullOrEmpty(childName))
            {
                var frameworkElement = child as FrameworkElement;
                // If the child's name is set for search
                if (frameworkElement != null && frameworkElement.Name == childName)
                {
                    // if the child's name is of the request name
                    foundChild = (T)child;
                    break;
                }
            }
            else
            {
                // child element found.
                foundChild = (T)child;
                break;
            }
        }

        return foundChild;
    }
}

DXWindow Style을 재정의하여 Attached Property를 사용하기 위한 속성들을 설정하였고, 툴팁 컨트롤은 활성화 시 부모 윈도우를 찾도록 하였습니다.

<Style TargetType="dx:DXDialogWindow" x:Key="TooltipWindow">
    <Setter Property="local:DXWindowTooltipBehavior.IsEnable" Value="True" />
    <Setter Property="local:DXWindowTooltipBehavior.Control">
        <Setter.Value>
            <dxe:FlyoutControl AllowRecreateContent="False" 
                               PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=dx:DXWindow}, Converter={StaticResource DummyConverter}}">
                <dxe:FlyoutControl.Style>
                    <Style TargetType="dxe:FlyoutControl">
                        <Setter Property="Settings">
                            <Setter.Value>
                                <dxe:FlyoutSettings Placement="Right" ShowIndicator="True" />
                            </Setter.Value>
                        </Setter>
                        <Setter Property="ToolTipService.ShowDuration" Value="500" />
                        <Setter Property="HorizontalAlignment" Value="Center"/>
                        <Setter Property="VerticalAlignment" Value="Top"/>
                        <Setter Property="AllowOutOfScreen" Value="True"/>
                        <Setter Property="StaysOpen" Value="False"/>
                        <Setter Property="AlwaysOnTop" Value="True" />
                        <Setter Property="IsOpen" Value="False" />
                    </Style >
                </dxe:FlyoutControl.Style>
                <TextBlock Text="ESC키를 누르시면 창이 닫힙니다." />
            </dxe:FlyoutControl>
        </Setter.Value>
    </Setter>
</Style>
 

실행 해 보면 동작하지 않습니다. 왜일까요? 원인은 Attached Property와 대상으로 지정된 DXWindow의 관계에 있습니다.

Attached Property는 DXWindow의 자식 Element가 아니기 때문에 Control로 지정한 툴팁 컨트롤 또한 DXWindow의 자식이 아닙니다.

툴팁 컨트롤을 활성화 시키기 위해서는 반드시 부모 Element가 설정 되어 있어야 됩니다.

이러한 이유로 DXWindow 자식 Element에 툴팁 컨트롤을 지정 할 수 있도록 DXWindow의 컨트롤 Template을 devexpress 소스에서 찾아 봤습니다.

<ControlTemplate x:Key="{dxt:DXWindowThemeKey ResourceKey=HeaderTemplate}" TargetType="{x:Type ContentControl}">
    <dxc:DXWindowHeader x:Name="PART_Header" CornerRadius="0" Background="{DynamicResource {dxt:DXWindowThemeKey ResourceKey=ActiveHeaderBackground}}" Focusable="False" >

        <Grid>
            <Thumb x:Name="PART_DragWidget"                        
                   Template="{DynamicResource {dxt:FloatingContainerThemeKey IsVisibleInBlend=True, ResourceKey=FloatingContainerDragWidgetTemplate}}"  />

            <DockPanel LastChildFill="True" Name="PART_HeaderDock" Margin="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderDockMargin}}">
                <Image x:Name="PART_Icon" 
                       Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=DXWindowIconStyle}}"
                       DockPanel.Dock="Left"
                       Source="{Binding Path=(dxc:FloatingContainer.FloatingContainer).Icon, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"  />

                <Grid x:Name="PART_Grid" DockPanel.Dock="Right" UseLayoutRounding="True" Focusable="False">
                    <ItemsControl Style="{DynamicResource {dxt:DXTabbedWindowThemeKey ResourceKey=ButtonContainerStyle}}" Visibility="Hidden">
                        <dxc:HeaderItemControl Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderCloseButtonStyle}}" DockPanel.Dock="Right"/>
                    </ItemsControl>
                    <ItemsControl x:Name="PART_HeaderButtons" Style="{DynamicResource {dxt:DXTabbedWindowThemeKey ResourceKey=ButtonContainerStyle}}" Focusable="False">
                        <dxc:HeaderItemControl x:Name="PART_CloseButton" 
                                           DockPanel.Dock="Right"
                                           Visibility="Collapsed"                                               
                                           Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderCloseButtonStyle}}"/>
                        <dxc:HeaderItemControl x:Name="PART_Maximize" 
                                           DockPanel.Dock="Right"                                                   
                                           Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderButtonStyle}}"                     
                                           Visibility="{Binding Path=(dxc:FloatingContainer.IsMaximized), Converter={dxc:BooleanToVisibilityConverter Invert=True}, RelativeSource={RelativeSource Self}}"/>
                        <dxc:HeaderItemControl x:Name="PART_Restore"                        
                                           DockPanel.Dock="Right"                                                      
                                           Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderButtonStyle}}"                           
                                           Visibility="{Binding Path=(dxc:FloatingContainer.IsMaximized), Converter={dxc:BooleanToVisibilityConverter Invert=False}, RelativeSource={RelativeSource Self}}"/>
                        <dxc:HeaderItemControl x:Name="PART_Minimize" 
                                           DockPanel.Dock="Right"                                                         
                                           Style="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerHeaderButtonStyle}}"/>
                        <dxcn:DXWindowHeaderItemsControl x:Name="PART_HeaderCustomItems" 
                                                         Focusable="False"    
                                                         Margin="{DynamicResource {dxt:FloatingContainerThemeKey ResourceKey=HeaderItemsControlMargin}}"
                                                         DockPanel.Dock="Right"                            
                                                         Style="{DynamicResource {dxt:DXTabbedWindowThemeKey ResourceKey=ControlContainerStyle}}"/>
                    </ItemsControl>
                </Grid>

                <ContentPresenter x:Name="PART_CaptionContentPresenter"                                   
                                  DockPanel.Dock="Left"
                                  Content="{x:Null}" 
                                  ContentTemplate="{DynamicResource {dxt:FloatingContainerThemeKey IsVisibleInBlend=True, ResourceKey=FloatingContainerCaptionTemplate}}" />
            </DockPanel>
        </Grid>
    </dxc:DXWindowHeader>

   

DXWindowHeader 자식으로 Grid가 있는 걸 확인하였고, Loaded 이벤트에서 타이틀바 DXWindowHeader 객체의 자식 Grid에 접근하여 툴팁 컨트롤을 자식 Element로 설정하였습니다.

private static void WindowOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    
    var window = sender as DXWindow;
    window.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
    {
        var header = FindChild(window, "PART_Header");
        AddTooltipControl(header);
        header.MouseEnter += HeaderOnMouseEnter;
        header.MouseLeave += HeaderOnMouseLeave;
    }));
}

private static void AddTooltipControl(DXWindowHeader header)
{
    var control = header.Child as Grid;
    var window = header.GetRootParent();
    var tooltip = GetControl(window);
    control.Children.Add(tooltip);
}

완성

타이틀바에 마우스를 가져다 놓으면 툴팁이 활성화 됩니다.


+ Recent posts