WPF学习:Slider — 冒泡显示值
阅读原文时间:2023年09月05日阅读:1

想做一个下图所示的Slider,以冒泡的方式显示其Value值,该怎么做呢?

功能要求,当鼠标放在滑块上的时候,冒“泡”显示值;当滑块移动的时候,“泡”跟随移动。

看似简单的功能,但要完美实现,确实要花费不少心思的。

方法一:使用Canvas面板。

1. 改造Slider

Slider的样式改造,比较简单,重写其模板即可。

        <Slider.Template>  
            <ControlTemplate TargetType="Slider">  
                <Border Name="border" SnapsToDevicePixels="True"  
                        Background="{TemplateBinding Panel.Background}"  
                        BorderBrush="{TemplateBinding Border.BorderBrush}"  
                        BorderThickness="{TemplateBinding Border.BorderThickness}" >  
                    <Grid>  
                        <Border Background="Gray" Height="4" CornerRadius="2"/>  
                        <Track Name="PART\_Track" Grid.Row="1">  
                            <Track.DecreaseRepeatButton>  
                                <RepeatButton>  
                                    <RepeatButton.Template>  
                                        <ControlTemplate TargetType="RepeatButton">  
                                            <Border Height="4" CornerRadius="2" Background="#FF006CBE"/>  
                                        </ControlTemplate>  
                                    </RepeatButton.Template>  
                                </RepeatButton>  
                            </Track.DecreaseRepeatButton>

                            <Track.Thumb>  
                                <Thumb Name="thumb" VerticalAlignment="Center" Focusable="False">  
                                    <Thumb.Template>  
                                        <ControlTemplate TargetType="Thumb">  
                                            <Grid>  
                                                <Ellipse Name="thumbEllipse" Width="10" Height="10" Fill="White" StrokeThickness="1" Stroke="#FF006CBE" />  
                                            </Grid>  
                                        </ControlTemplate>  
                                    </Thumb.Template>  
                                </Thumb>  
                            </Track.Thumb>  
                        </Track>  
                    </Grid>  
                </Border>  
            </ControlTemplate>  
        </Slider.Template>  
    </Slider>

2. 添加“泡”

很明显,这个“泡”的形状、文字都是一个元素,因此需要使用面板来将两个元素组合起来。这里最好使用Canvas,因为它的子元素是绝对定位的,可以通过Canvas.Left、Canvas.Top等属性安排子元素的位置,且Canvas也不会影响父元素的布局。因此将Slider的模板继续改造一下,如下所示:

    <Slider Name="slider" Width="255">  
        <Slider.Template>  
            <ControlTemplate TargetType="Slider">  
                <Border Name="border" SnapsToDevicePixels="True"  
                        Background="{TemplateBinding Panel.Background}"  
                        BorderBrush="{TemplateBinding Border.BorderBrush}"  
                        BorderThickness="{TemplateBinding Border.BorderThickness}" >  
                    <Grid>  
                        <Border Background="Gray" Height="4" CornerRadius="2"/>  
                        <Track Name="PART\_Track" Grid.Row="1">  
                            <Track.DecreaseRepeatButton>  
                                <RepeatButton>  
                                    <RepeatButton.Template>  
                                        <ControlTemplate TargetType="RepeatButton">  
                                            <Border Height="4" CornerRadius="2" Background="#FF006CBE"/>  
                                        </ControlTemplate>  
                                    </RepeatButton.Template>  
                                </RepeatButton>  
                            </Track.DecreaseRepeatButton>  

                            <Track.Thumb>  
                                <Thumb Name="thumb" VerticalAlignment="Center" Focusable="False">  
                                    <Thumb.Template>  
                                        <ControlTemplate TargetType="Thumb">  
                                            <Grid>  
                                                <Ellipse Name="thumbEllipse" Width="10" Height="10" Fill="White" StrokeThickness="1" Stroke="#FF006CBE" />  
                                                <Canvas Name="canvas" Opacity="0">  
                                                    <Grid Canvas.Bottom="10" Canvas.Left="-8">  
                                                        <Path Width="25" Stretch="Uniform" Fill="White" Stroke="#FF006CBE" StrokeThickness="1" Data="M172,633.14a339.4,339.4,0,0,1-169.9-294C2.13,151.76,154.2-.29,341.57-.29S681,151.76,681,339.11a339.38,339.38,0,0,1-169.9,294h0a183.88,183.88,0,0,0-84.27,106.38h0l-85.26,283.61L256.3,739.52A183.88,183.88,0,0,0,172,633.14Z"/>  
                                                        <TextBlock HorizontalAlignment="Center" Margin="0,5,0,0" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}, Path=Value, Converter={StaticResource double2Int}}"/>  
                                                    </Grid>  
                                                </Canvas>  
                                            </Grid>

                                            <ControlTemplate.Triggers>  
                                                <Trigger Property="IsMouseOver" Value="True">  
                                                    <Setter TargetName="canvas" Property="Opacity" Value="1"/>  
                                                </Trigger>  
                                                <Trigger Property="IsDragging" Value="True">  
                                                    <Setter TargetName="canvas" Property="Opacity" Value="1"/>  
                                                </Trigger>  
                                            </ControlTemplate.Triggers>  
                                        </ControlTemplate>  
                                    </Thumb.Template>  
                                </Thumb>  
                            </Track.Thumb>  
                        </Track>  
                    </Grid>  
                </Border>  
            </ControlTemplate>  
        </Slider.Template>  
    </Slider>

红色部分为添加的代码。有两点需要注意:

1. TextBlock.Text属性的绑定,使用了RelativeSource模式。

2. 记得要添加触发器(Trigger),使的Canvas能在指定的状态下显示出来。

以这种方式改的Slider控件,有一个问题:“泡”不能超出窗体显示。也就是说当滑块处于屏幕的边缘时,这个“泡”是显示不出来的。

方法二:使用Window

WPF框架中有Popup控件和Tooltip控件。通过微软的源代码可知,他们的实现都是使用Window控件。即在一定的条件下打开一个Windows,并显示内容,在另一个条件下,则关闭这个Window。

根据这个思路,就可以自定义一个控件,并实现更多的功能

1. 自定义控件

控件类:FellowPopup,其他的几个类为辅助类。

/// <summary>  
/// 坐标系转换类  
/// </summary>  
public static class CoordinateHelper  
{  
    /// <summary>  
    /// 将用户空间坐标系的点,转换为屏幕坐标系的点。  
    /// 已考虑系统Dpi问题  
    /// </summary>  
    /// <param name="relativeTo">可视化对象。它的坐标系原点在左上角</param>  
    /// <param name="point">在可视化对象坐标系中的点</param>  
    /// <returns></returns>  
    public static Point ClientToScreen(Visual relativeTo, Point point)  
    {  
        if (relativeTo == null) return point;  
        Point targetToScreen = relativeTo.PointToScreen(point);            // 将point的点转换为屏幕坐标系的点  
        var source = PresentationSource.FromVisual(relativeTo);  
        double dpiX = 96.0, dpiY = 96.0;  
        if (source?.CompositionTarget != null)  
        {  
            dpiX = 96.0 \* source.CompositionTarget.TransformToDevice.M11;  
            dpiY = 96.0 \* source.CompositionTarget.TransformToDevice.M22;  
        }  
        return new Point(targetToScreen.X \* 96.0 / dpiX, targetToScreen.Y \* 96 / dpiY);  
    }  
}

public static class DependencyPropertyHelper  
{  
    /// <summary>  
    /// 为依赖属性注册值发生改变时的回调方法  
    /// </summary>  
    /// <param name="Register">含有依赖属性的对象</param>  
    /// <param name="dependencyProperty">依赖属性</param>  
    /// <param name="action">要注册的值发生改变时的回调方法</param>  
    /// <param name="runOnceImmediately">是否立即执行一次</param>  
    public static void RegisterDependencyPropertyChanged(this DependencyObject Register, DependencyProperty dependencyProperty, EventHandler action, bool runOnceImmediately = false)  
    {  
        DependencyPropertyDescriptor obj = DependencyPropertyDescriptor.FromProperty(dependencyProperty, Register.GetType());  
        if(obj != null) obj.AddValueChanged(Register, action);  
        if(runOnceImmediately) action?.Invoke(Register,new EventArgs());  
    }  
}

/// <summary>  
/// 放置模式  
/// </summary>  
public enum PlacementMode  
{  
    /// <summary>  
    /// 顶部靠左  
    /// </summary>  
    TopLeft,  

    /// <summary>  
    /// 顶部居中  
    /// </summary>  
    TopMiddle,

    /// <summary>  
    /// 顶部靠右  
    /// </summary>  
    TopRight,  

    /// <summary>  
    /// 左侧居中  
    /// </summary>  
    LeftMiddle,

    /// <summary>  
    /// 右侧居中  
    /// </summary>  
    RightMiddle,

    /// <summary>  
    /// 底部靠左  
    /// </summary>  
    BottomLeft,

    /// <summary>  
    /// 底部居中  
    /// </summary>  
    BottomMiddle,

    /// <summary>  
    /// 底部靠右  
    /// </summary>  
    BottomRight  
}

/// <summary>  
/// 可跟随控件移动的弹出提示框。  
/// </summary>  
public class FellowPopup : ContentControl  
{  
    private Window \_popupWnd;

    /// <summary>  
    /// 水平位置偏移量  
    /// </summary>  
    public double HorizontalOffset  
    {  
        get => (double)GetValue(HorizontalOffsetProperty);  
        set => SetValue(HorizontalOffsetProperty, value);  
    }

    internal static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(  
        "HorizontalOffset", typeof(double), typeof(FellowPopup), new PropertyMetadata(0.0d, OnValueChanged));

    /// <summary>  
    /// 垂直偏移量  
    /// </summary>  
    public double VerticalOffset  
    {  
        get => (double)GetValue(VerticalOffsetProperty);  
        set => SetValue(VerticalOffsetProperty, value);  
    }  

    internal static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register(  
        "VerticalOffset", typeof(double), typeof(FellowPopup), new PropertyMetadata(0.0d, OnValueChanged));

    /// <summary>  
    /// 放置位置  
    /// </summary>  
    public PlacementMode Placement  
    {  
        get => (PlacementMode)GetValue(PlacementProperty);  
        set => SetValue(PlacementProperty, value);  
    }

    internal static readonly DependencyProperty PlacementProperty = DependencyProperty.Register(  
        "Placement", typeof(PlacementMode), typeof(FellowPopup), new PropertyMetadata(PlacementMode.BottomRight,OnValueChanged));

    /// <summary>  
    /// 放置的目标元素  
    /// </summary>  
    public FrameworkElement PlacementTarget  
    {  
        get => (FrameworkElement)GetValue(PlacementTargetProperty);  
        set => SetValue(PlacementTargetProperty, value);  
    }

    internal static readonly DependencyProperty PlacementTargetProperty = DependencyProperty.Register(  
        "PlacementTarget", typeof(FrameworkElement), typeof(FellowPopup), new PropertyMetadata(null,OnValueChanged));

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        FellowPopup popup = (FellowPopup)d;  
        popup.UpdateLocation();  
    }

    /// <summary>  
    /// 是否打开。当其为true时,则显示提示框,false则不显示提示框  
    /// </summary>  
    public bool IsOpen  
    {  
        get => (bool)GetValue(IsOpenProperty);  
        set => SetValue(IsOpenProperty, value);  
    }

    internal static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(  
        "IsOpen", typeof(bool), typeof(FellowPopup), new PropertyMetadata(false,OnIsOpenChanged));

    private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        FellowPopup fp = d as FellowPopup;  
        if((bool)e.NewValue ) fp.Open();  
        else  fp.Close();  
    }  
    #region 触发位置更新

    /// <summary>  
    /// 触发位置更新的控件  
    /// </summary>  
    public FrameworkElement TriggerElement  
    {  
        get => (FrameworkElement)GetValue(TriggerElementProperty);  
        set => SetValue(TriggerElementProperty, value);  
    }

    internal static readonly DependencyProperty TriggerElementProperty = DependencyProperty.Register(  
        "TriggerElement", typeof(UIElement), typeof(FellowPopup), new PropertyMetadata(null, OnTriggerChanged));

    /// <summary>  
    /// 触发位置更新的控件的属性  
    /// 此属性应该是TriggerElement的属性。  
    /// 当此属性的值发生变化时,会触发FellowPopup的UpdateLocation方法。  
    /// </summary>  
    public DependencyProperty TriggerProperty  
    {  
        get => (DependencyProperty)GetValue(TriggerPropertyProperty);  
        set => SetValue(TriggerPropertyProperty, value);  
    }  

    internal static readonly DependencyProperty TriggerPropertyProperty = DependencyProperty.Register(  
        "TriggerProperty", typeof(DependencyProperty), typeof(FellowPopup), new PropertyMetadata(null, OnTriggerChanged));

    private static void OnTriggerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        FellowPopup fp = (FellowPopup)d;  
        if (fp.TriggerElement == null || fp.TriggerProperty == null) return;

        // 如果定义属性的类,与TriggerElement类相同,或者TriggerElement类是它的子类  
        Type propertyType = fp.TriggerProperty.OwnerType;  
        Type elementType = fp.TriggerElement.GetType();

        if ((elementType != propertyType) && (!elementType.IsSubclassOf(propertyType))) throw new ArgumentException("TriggerElement不包含TriggerProperty属性");  
        DependencyPropertyHelper.RegisterDependencyPropertyChanged(fp.TriggerElement, fp.TriggerProperty, delegate (object sender, EventArgs args)  
        {  
            fp.UpdateLocation();  
        }, false);  
    }  
    #endregion

    static FellowPopup()  
    {  
        DefaultStyleKeyProperty.OverrideMetadata(typeof(FellowPopup), new FrameworkPropertyMetadata(typeof(FellowPopup)));  
    }

    /// <summary>  
    /// 打开窗体  
    /// </summary>  
    private void Open()  
    {  
        \_popupWnd = new Window()  
        {  
            Topmost = true,  
            ShowActivated = false,  
            AllowsTransparency = true,  
            WindowStyle = WindowStyle.None,  
            BorderThickness = new Thickness(0),  
            SizeToContent = SizeToContent.WidthAndHeight,  
            WindowStartupLocation = WindowStartupLocation.Manual,

            Background = this.Background,  
            Content = this.Content,  
            ContentStringFormat = this.ContentStringFormat,  
            ContentTemplate = this.ContentTemplate,  
            ContentTemplateSelector = this.ContentTemplateSelector,  
        };  
        UpdateLocation();  
        \_popupWnd.Show();  
    }

    // 关闭\_popup窗体,并回收  
    private void Close()  
    {  
        if (\_popupWnd != null)  
        {  
            \_popupWnd.Close();  
            GC.SuppressFinalize(\_popupWnd);  
        }  
    }

    /// <summary>  
    /// 更新弹出窗体的位置。  
    /// </summary>  
    private void UpdateLocation()  
    {  
        if (\_popupWnd == null) return;  
        if(PlacementTarget == null)  
        {  
            \_popupWnd.Left = 0;  
            \_popupWnd.Top = 0;  
            return;  
        }

        double contentWidth = \_popupWnd.Content == null ? 0 : ((FrameworkElement)\_popupWnd.Content).ActualWidth;  
        double contentHeght = \_popupWnd.Content == null ? 0 : ((FrameworkElement)\_popupWnd.Content).ActualHeight;

        Point targetToScreen;  
        switch (Placement)  
        {  
            case PlacementMode.TopLeft:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget,new Point(0, 0));  
                \_popupWnd.Left = targetToScreen.X - contentWidth - HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y - contentHeght - VerticalOffset;  
                break;

            case PlacementMode.TopMiddle:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(PlacementTarget.ActualWidth/2, 0));  
                \_popupWnd.Left = targetToScreen.X - contentWidth / 2 + HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y - contentHeght - VerticalOffset;  
                break;

            case PlacementMode.TopRight:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(PlacementTarget.ActualWidth, 0));  
                \_popupWnd.Left = targetToScreen.X + HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y - contentHeght - VerticalOffset;  
                break;

            case PlacementMode.LeftMiddle:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(0, PlacementTarget.ActualHeight / 2));  
                \_popupWnd.Left = targetToScreen.X - contentWidth - HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y - contentHeght / 2;  
                break;

            case PlacementMode.RightMiddle:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(PlacementTarget.ActualWidth, PlacementTarget.ActualHeight / 2));  
                \_popupWnd.Left = targetToScreen.X + HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y - contentHeght / 2;  
                break;

            case PlacementMode.BottomLeft:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(0, PlacementTarget.ActualHeight));  
                \_popupWnd.Left = targetToScreen.X - contentWidth - HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y + VerticalOffset;  
                break;

            case PlacementMode.BottomMiddle:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(PlacementTarget.ActualWidth / 2, PlacementTarget.ActualHeight));  
                \_popupWnd.Left = targetToScreen.X - contentWidth / 2 - HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y + VerticalOffset;  
                break;

            case PlacementMode.BottomRight:  
                targetToScreen = CoordinateHelper.ClientToScreen(PlacementTarget, new Point(PlacementTarget.ActualWidth, PlacementTarget.ActualHeight));  
                \_popupWnd.Left = targetToScreen.X + HorizontalOffset;  
                \_popupWnd.Top = targetToScreen.Y + VerticalOffset;  
                break;  
        }  
    }  
}

(1)在FellowPopup类中,定义了一个Window类型的变量_popup,用于展示“泡”的内容。PlacementTarget、Placement、HorizontalOffset、VerticalOffset四个属性,用于设置_popup的Left和Top属性,以确定_popup在屏幕中的位置。

  这里需要注意,Visual.PointToScreen()方法,参数point是在Visual坐标空间的点,其返回值为将这个点,转换为屏幕坐标系的点。但转换后的点,未考虑到系统DPI及屏幕缩放的情况。因此,需要将这个点再运算,使之成为当前屏幕DPI及缩放模式下的点坐标。

(2)TriggerElement、TriggerProperty属性,用于指示,当哪个元素的哪个属性值发生变化时,更新位置。

  要想让一个窗体跟着控件移动,就必须告诉窗体,控件当前的位置在哪儿,然后去计算窗体应该在哪儿。而在WPF中,几乎没有控件会实时报告自己的位置。因此,必须找到元素的一个属性,当元素的“位置”发生改变的时候,这个属性值会发生变化。

  除了找到这样的一个属性外,还需要注册一个属性值发生改变时的回调方法。在本例中,滑块在滑动的过程中,本身没有属性(公开的)值发生改变,但是Slider的Value属性会发生改变。因此可以选择它,来触发FellowPopup的位置改变。

  由此可见,PlacementTarge系列与TriggerElement可以是不同的。一个用于“布置”窗体,一个用于“触发”窗体。

2. 使用自定义控件

定义好了控件后,就可以在Xaml中使用它了。继续改造Slider的模板:

    <!-- 别忘了转换器 -->  
    <local:Double2IntConverter  x:Key="double2Int" /> 

    <Slider Name="slider" Width="255">  
        <Slider.Template>  
            <ControlTemplate TargetType="Slider">  
                <Border Name="border" SnapsToDevicePixels="True"  
                        Background="{TemplateBinding Panel.Background}"  
                        BorderBrush="{TemplateBinding Border.BorderBrush}"  
                        BorderThickness="{TemplateBinding Border.BorderThickness}" >  
                    <Grid>  
                        <Border Background="Gray" Height="4" CornerRadius="2"/>  
                        <Track Name="PART\_Track" Grid.Row="1">  
                            <Track.DecreaseRepeatButton>  
                                <RepeatButton>  
                                    <RepeatButton.Template>  
                                        <ControlTemplate TargetType="RepeatButton">  
                                            <Border Height="4" CornerRadius="2" Background="#FF006CBE"/>  
                                        </ControlTemplate>  
                                    </RepeatButton.Template>  
                                </RepeatButton>  
                            </Track.DecreaseRepeatButton>  
                            <Track.Thumb>  
                                <Thumb Name="thumb" VerticalAlignment="Center" Focusable="False">  
                                    <Thumb.Template>  
                                        <ControlTemplate TargetType="Thumb">  
                                            <Grid>  
                                                <Ellipse Name="thumbEllipse" Width="10" Height="10" Fill="White" StrokeThickness="1" Stroke="#FF006CBE" />  
                                                <local:FellowPopup x:Name="popup" Background="Transparent"  
                                                                   Placement="TopMiddle" PlacementTarget="{Binding ElementName=thumbEllipse}"  
                                                                   TriggerElement="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}}"  
                                                                   TriggerProperty="Slider.Value">  
                                                    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">  
                                                        <Path Width="25" Stretch="Uniform" Fill="White" Stroke="#FF006CBE" StrokeThickness="1" Data="M172,633.14a339.4,339.4,0,0,1-169.9-294C2.13,151.76,154.2-.29,341.57-.29S681,151.76,681,339.11a339.38,339.38,0,0,1-169.9,294h0a183.88,183.88,0,0,0-84.27,106.38h0l-85.26,283.61L256.3,739.52A183.88,183.88,0,0,0,172,633.14Z"/>  
                                                        <TextBlock HorizontalAlignment="Center" Margin="0,5,0,0"  
                                                                   Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}, Path=Value, Converter={StaticResource double2Int}}"/>  
                                                    </Grid>  
                                                </local:FellowPopup>  
                                            </Grid>  
                                            <ControlTemplate.Triggers>  
                                                <Trigger Property="IsMouseOver" Value="True">  
                                                    <Setter TargetName="popup" Property="IsOpen" Value="True"/>  
                                                </Trigger>  
                                                <Trigger Property="IsDragging" Value="True">  
                                                    <Setter TargetName="popup" Property="IsOpen" Value="True"/>  
                                                </Trigger>  
                                            </ControlTemplate.Triggers>  
                                        </ControlTemplate>  
                                    </Thumb.Template>  
                                </Thumb>  
                            </Track.Thumb>  
                        </Track>  
                    </Grid>  
                </Border>

            </ControlTemplate>  
        </Slider.Template>  
    </Slider>

注意Placement、TriggerElement、TriggerProperty、Text等属性的绑定方式。

至此,改造搞定。以第二种方式改造的Slider,“泡”可以显示在主窗体之上。没有任何东西会挡住他,以为它是TopMost的。另外也可以再添加自己的逻辑去设置“泡”的位置。

以上为自己学习总结,如有错误,请指正。

转摘请注明出处。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章