目录
效果
进度条样式
- 设置Thumb为圆点,被拖拽时和鼠标悬浮时设置颜色效果
刻度
- 鼠标悬浮时展示固定刻度
- 鼠标移动时展示当前刻度
刻度支持展示在进度条上下左右 - 支持设置刻度的单位,例如%
了解Slider、Tracker、Thumb、TickBar
在 WPF 中,Slider
控件用于表示一个范围内的值,并允许用户通过拖动滑块来选择该值。Slider
由以下几个主要部分组成:
- Track:这是滑块的轨道,表示值的范围。
Track
包含两个部分:- DecreaseRepeatButton:表示轨道的左侧或下侧部分,用于减少值。
- IncreaseRepeatButton:表示轨道的右侧或上侧部分,用于增加值。
- Thumb:这是用户可以拖动的滑块,用于选择具体的值。
Thumb
通常是一个可视化的元素,用户可以通过鼠标或触摸来拖动它。 - TickBar:这是一个可选的部分,用于显示刻度标记。
TickBar
可以帮助用户更精确地选择值。刻度标记可以是离散的点,也可以是连续的线。
这些部分共同工作,使得 Slider
控件能够直观地表示和选择值。可以通过自定义这些部分的样式和模板来改变 Slider
的外观和行为。他们的关系如下图:
1. Slider 快速入门
Slider
概念
Slider 控件是一种双向交互式控件,允许用户在两个方向上拖动滑块以选择一个值。
它通常用于处理连续的数值数据,如音量、亮度或温度等。
常用属性
Value
:指定滑块当前的值。Minimum
:指定滑块的最小值。Maximum
:指定滑块的最大值。LargeChange
:指定每次鼠标滚动时Value属性的变化量。SmallChange
:指定每次按箭头键时Value属性的变化量。Orientation
:指定Slider控件的方向,可以是Horizontal或Vertical。IsSnapToTickEnabled
:指定是否使滑块 snap 到最近的刻度线。TickFrequency
:指定刻度线的间隔。Ticks
:指定刻度线的集合。
事件处理
Slider控件触发以下事件,可以在这些事件中执行特定的操作:
- ValueChanged:当滑块的值发生变化时触发。
- DragStarted:当开始拖动滑块时触发。
- DragCompleted:当完成拖动滑块时触发。
使用方法、示例、效果
<Slider
Minimum="0"
Maximum="100"
Value="50"
Orientation="Horizontal"
TickPlacement="BottomRight"
TickFrequency="10"
IsSnapToTickEnabled="True"
/>
1.1 Track
概念
Track是Slider控件的一部分,它作为滑块移动的路径容器。它的主要功能是限定Thumb(滑块)的移动范围,并可以提供视觉上的引导。
功能和应用
Track不仅作为Thumb的滑动路径,还可以通过样式和模板进行自定义设计。例如,可以通过修改**ControlTemplate
**来改变Track的外观,使其更具吸引力和实用性。
1.2 Thumb
概念
Thumb是Slider控件中的可移动部分,用户可以直接拖动它来改变Slider的值。Thumb的外观和行为也可以通过样式和模板进行自定义。
使用方法、示例、效果
通过设置Thumb的样式,可以改变其外观,例如大小、形状和颜色。以下是一个简单的Thumb样式示例:
<Style x:Key="SliderThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Height" Value="14"/>
<Setter Property="Width" Value="14"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Ellipse x:Name="Ellipse" StrokeThickness="1"
Fill="Red">
<Ellipse.Stroke>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0"/>
<GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Ellipse.Stroke>
</Ellipse>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在这个示例中,Thumb被定义为一个圆,具有线性渐变的描边效果。
1.3.TickBar
概念
TickBar是Slider控件的一部分,主要负责显示刻度线。当设置TickPlacement属性为非None值时,TickBar将会显示出来。
使用方法、示例、效果
TickBar的位置和频率可以通过TickPlacement和TickFrequency属性来控制。例如,以下示例展示了如何创建一个带有显示刻度线的TickBar的Slider:
<Slider Width="300" Height="50"
Minimum="0" Maximum="10"
TickPlacement="BottomRight"
TickFrequency="1"
IsSnapToTickEnabled="True"
AutoToolTipPlacement="BottomRight"
AutoToolTipPrecision="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
2. Slider自定义控件实现
1. 重写Thumb样式
这里分别给出鼠标悬浮样式、默认样式、拖拽样式
<!--#region ThumbEffect-->
<DropShadowEffect
x:Key="UnhoverThumbEffect"
BlurRadius="5"
Opacity="0.3"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<DropShadowEffect
x:Key="HoverThumbEffect"
BlurRadius="5"
Opacity="0.5"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<DropShadowEffect
x:Key="DragThumbEffect"
BlurRadius="8"
Opacity="1"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<Style
x:Key="DefaultSliderThumbStyle"
TargetType="Thumb">
<Setter Property="Width" Value="15" />
<Setter Property="Height" Value="15" />
<Setter Property="Background" Value="White" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Ellipse
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Panel.ZIndex="2"
Effect="{StaticResource UnhoverThumbEffect}"
Fill="{TemplateBinding Background}" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Effect" Value="{StaticResource HoverThumbEffect}" />
<Setter Property="Cursor" Value="Hand" />
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter Property="Effect" Value="{StaticResource DragThumbEffect}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--#endregion-->
2. 重写RepeatButton
这里主要设置了进度条圆角样式和已完成部分的颜色和未完成部分的颜色设定。
<Style
x:Key="DecreaseRepeatButtonStyle"
TargetType="RepeatButton">
<Setter Property="Height" Value="4" />
<Setter Property="Margin" Value="0" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="{StaticResource MainColor}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border
Background="{TemplateBinding Background}"
CornerRadius="2">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="IncreaseRepeatButtonStyle"
TargetType="RepeatButton">
<Setter Property="Height" Value="4" />
<Setter Property="Margin" Value="0" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="{StaticResource UnfinishedColor}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border
Background="{TemplateBinding Background}"
CornerRadius="2">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
3. 刻度渲染
从上文可以看出,Slider控件具有“AutoToolTip”功能,可以在拖动过程中显示当前刻度。然而,这个刻度不支持模板定制,也不支持自定义格式。以下提供了两种修改的方式。
方法1,反射+重写Thumb的拖拽效果
我在网上找到了这篇文章Modifying the auto tooltip of a Slider解决了这个问题,可以实现自定义显示格式。主要是使用了反射获取Slider 中的 _autoToolTip 字段,并重写两个虚拟方法中更新:OnThumbDragStarted 和 OnThumbDragDelta。
using System.Reflection;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
public class FormattedSlider : Slider
{
private ToolTip autoToolTip;
private string _autoToolTipFormat;
private int _decimalPlaces;
/// <summary>
/// Gets or sets the format string for the auto tooltip.
/// </summary>
public string AutoToolTipFormat
{
get { return _autoToolTipFormat; }
set { _autoToolTipFormat = value; }
}
/// <summary>
/// Gets or sets the number of decimal places to display.
/// </summary>
public int DecimalPlaces
{
get { return _decimalPlaces; }
set { _decimalPlaces = value; }
}
protected override void OnThumbDragStarted(DragStartedEventArgs e)
{
base.OnThumbDragStarted(e);
this.FormatAutoToolTipContent();
}
protected override void OnThumbDragDelta(DragDeltaEventArgs e)
{
base.OnThumbDragDelta(e);
this.FormatAutoToolTipContent();
}
private void FormatAutoToolTipContent()
{
if (!string.IsNullOrEmpty(this.AutoToolTipFormat))
{
string formatString = "{0:F" + this.DecimalPlaces + "}";
this.AutoToolTip.Content = string.Format(this.AutoToolTipFormat, string.Format(formatString, this.Value));
}
}
private ToolTip AutoToolTip
{
get
{
if (autoToolTip == null)
{
FieldInfo field = typeof(Slider).GetField("autoToolTip", BindingFlags.NonPublic | BindingFlags.Instance);
autoToolTip = field.GetValue(this) as ToolTip;
}
return autoToolTip;
}
}
}
实例
<local:FormattedSlider AutoToolTipFormat="{}{0}% used"
AutoToolTipPlacement="BottomRight"
TickPlacement="BottomRight"
SmallChange="0.1"
LargeChange="1"
Maximum="100"
TickFrequency="10"
DecimalPlaces="2" />
效果
方法2,重写TrackBar的OnRender方法
private const double Radius = 3;
private const double ShadowRadius = 4;
private static readonly Brush ShadowBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#adadad"));
protected override void OnRender(DrawingContext dc)
{
if (TickRenderMode == TickRenderMode.Base)
{
base.OnRender(dc);
}
else
{
double tickFrequency = this.TickFrequency;
if (tickFrequency <= 0)
return;
double minimum = this.Minimum;
double maximum = this.Maximum;
double range = maximum - minimum;
double tickCount = range / tickFrequency;
double width = this.ActualWidth - 2 * Radius;
double height = this.ActualHeight;
for (int i = 0; i <= tickCount; i++)
{
double tickValue = minimum + i * tickFrequency;
if (!Ticks.Contains(tickValue) && TickRenderMode == TickRenderMode.FixedTicksOnMouseOver)
continue;
double x = (tickValue - minimum) / range * width + Radius;
double y = height / 2;
if (TickRenderMode == TickRenderMode.FixedTicksOnMouseOver)
{
DrawFixedTicksOnMouseOver(dc, tickValue, x, y);
}
else if (TickRenderMode == TickRenderMode.AutoShowOnMouseMove &&
(NitaTickPlacement == NitaTickPlacement.Top || NitaTickPlacement == NitaTickPlacement.Bottom))
{
DrawAutoShowOnMouseMove(dc, tickValue, x, y);
}
}
}
}
private void DrawFixedTicksOnMouseOver(DrawingContext dc, double tickValue, double x, double y)
{
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
FormattedText formattedText = new FormattedText(
tickValue.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Arial"), 12, Brushes.Gray, pixelsPerDip);
double rectWidth = formattedText.Width + 10;
double rectHeight = formattedText.Height + 4;
double textY = NitaTickPlacement == NitaTickPlacement.Top ? y - rectHeight - 4 : y + 4;
dc.DrawText(formattedText, new Point(x - formattedText.Width / 2, textY + 2));
if (!CurrentTick.Equals(tickValue))
{
dc.DrawEllipse(ShadowBrush, null, new Point(x, y), ShadowRadius, ShadowRadius);
dc.DrawEllipse(Fill, null, new Point(x, y), Radius, Radius);
}
}
private void DrawAutoShowOnMouseMove(DrawingContext dc, double tickValue, double x, double y)
{
bool isShowCurrentValue = CurrentTick.Equals(tickValue);
tickValue = Math.Round(tickValue, 2);
if (isShowCurrentValue)
{
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
FormattedText formattedText = new FormattedText(
tickValue.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Arial"), 12, Brushes.Gray, pixelsPerDip);
double rectWidth = formattedText.Width + 10;
double rectHeight = formattedText.Height + 4;
double textY = NitaTickPlacement == NitaTickPlacement.Top ? y - rectHeight - 10 : y + 10;
Rect rect = new Rect(new Point(x - rectWidth / 2, textY), new Size(rectWidth, rectHeight));
dc.DrawRoundedRectangle(ShadowBrush, null, rect, 5, 5);
dc.DrawRoundedRectangle(Fill, null, new Rect(rect.X + 1, rect.Y + 1, rect.Width - 2, rect.Height - 2), 5, 5);
dc.DrawText(formattedText, new Point(x - formattedText.Width / 2, textY + 2));
}
}
至于其他实现单位显示之类,比较简单,这里就不赘述了
3. 主要部分代码
Slider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls.Primitives;
using System.Windows;
namespace WPFSlider
{
public class Slider : System.Windows.Controls.Slider
{
private TickBar _tickBar;
private Thumb _thumb;
#region Constructors
static Slider()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Slider),
new FrameworkPropertyMetadata(typeof(Slider)));
}
public Slider()
{
this.MouseEnter += Slider_MouseEnter;
this.MouseLeave += Slider_MouseLeave;
this.Loaded += Slider_Loaded;
this.ValueChanged += Slider_ValueChanged;
}
#endregion
#region Properties
#region IsShowTick
public static readonly DependencyProperty IsShowTickProperty =
DependencyProperty.Register("IsShowTick",
typeof(bool), typeof(Slider), new PropertyMetadata(false));
public bool IsShowTick
{
get { return (bool)GetValue(IsShowTickProperty); }
set { SetValue(IsShowTickProperty, value); }
}
#endregion
#region TickRenderMode
public TickRenderMode TickRenderMode
{
get { return (TickRenderMode)GetValue(TickRenderModeProperty); }
set { SetValue(TickRenderModeProperty, value); }
}
public static readonly DependencyProperty TickRenderModeProperty =
DependencyProperty.Register("TickRenderMode",
typeof(TickRenderMode),
typeof(Slider),
new UIPropertyMetadata(TickRenderMode.FixedTicksOnMouseOver));
public bool IsNeedShowOnMouseOver
{
get
{
var TickRenderModeStr = TickRenderMode.ToString();
return TickRenderModeStr.Contains("OnMouseOver");
}
}
public bool IsNeedShowOnMouseMove
{
get
{
var TickRenderModeStr = TickRenderMode.ToString();
return TickRenderModeStr.Contains("OnMouseMove");
}
}
#endregion
#region IsDragging
public static readonly DependencyProperty IsDraggingProperty =
DependencyProperty.Register("IsDragging",
typeof(bool),
typeof(Slider),
new UIPropertyMetadata(false, OnIsDraggingChanged));
private static void OnIsDraggingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var slider = d as Slider;
if (slider == null)
return;
if (slider.IsDragging && (slider.IsNeedShowOnMouseMove || slider.IsNeedShowOnMouseOver))
{
slider.IsShowTick = true;
}
else
{
slider.IsShowTick = false;
}
}
public bool IsDragging
{
get { return (bool)GetValue(IsDraggingProperty); }
set { SetValue(IsDraggingProperty, value); }
}
#endregion
#region NitaTickPlacement
public NitaTickPlacement NitaTickPlacement
{
get { return (NitaTickPlacement)GetValue(NitaTickPlacementProperty); }
set { SetValue(NitaTickPlacementProperty, value); }
}
public static readonly DependencyProperty NitaTickPlacementProperty =
DependencyProperty.Register("NitaTickPlacement",
typeof(NitaTickPlacement),
typeof(Slider),
new UIPropertyMetadata(NitaTickPlacement.Bottom));
public bool IsShowRightTick
{
get
{
return NitaTickPlacement == NitaTickPlacement.Right;
}
}
public bool IsShowLeftTick
{
get
{
return NitaTickPlacement == NitaTickPlacement.Left;
}
}
#endregion
#region ValueUnit
public string ValueUnit
{
get { return (string)GetValue(ValueUnitProperty); }
set { SetValue(ValueUnitProperty, value); }
}
public static readonly DependencyProperty ValueUnitProperty =
DependencyProperty.Register("ValueUnit",
typeof(string),
typeof(Slider),
new UIPropertyMetadata(null));
#endregion
#region DisplayValue
public string DisplayValue
{
get { return (string)GetValue(DisplayValueProperty); }
set { SetValue(DisplayValueProperty, value); }
}
public static readonly DependencyProperty DisplayValueProperty =
DependencyProperty.Register("DisplayValue",
typeof(string),
typeof(Slider),
new UIPropertyMetadata(null));
#endregion
#endregion
#region Event
private void Slider_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
if (IsNeedShowOnMouseOver)
{
IsShowTick = true;
}
}
private void Slider_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
if (IsNeedShowOnMouseOver)
{
IsShowTick = false;
}
}
private void Slider_Loaded(object sender, RoutedEventArgs e)
{
this.DisplayValue = Value + ValueUnit;
}
private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
this.DisplayValue = Value + ValueUnit;
}
#region DragCompleted
public static readonly RoutedEvent DragCompletedEvent = EventManager.RegisterRoutedEvent(
nameof(DragCompleted),
RoutingStrategy.Bubble,
typeof(DragCompletedEventHandler),
typeof(Slider)
);
/// <summary>
/// 暂停播放事件
/// </summary>
public event DragCompletedEventHandler DragCompleted
{
add => AddHandler(DragCompletedEvent, value);
remove => RemoveHandler(DragCompletedEvent, value);
}
#endregion
#endregion
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_tickBar = GetTemplateChild("TopTick") as TickBar;
_thumb = GetTemplateChild("PART_Thumb") as System.Windows.Controls.Primitives.Thumb;
if (_thumb != null)
{
_thumb.PreviewMouseLeftButtonDown += Thumb_MouseLeftButtonDown;
_thumb.PreviewMouseLeftButtonUp += Thumb_MouseLeftButtonUp;
_thumb.DragStarted += Thumb_DragStarted;
_thumb.DragCompleted += Thumb_DragCompleted;
}
}
private void Thumb_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
IsDragging = true;
}
private void Thumb_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
IsDragging = false;
}
private void Thumb_DragStarted(object sender, DragStartedEventArgs e)
{
IsDragging = true;
}
private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
{
if (IsDragging)
{
IsDragging = false;
}
}
}
public enum NitaTickPlacement
{
Left,
Right,
Top,
Bottom
}
}
Slider.XAML
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="clr-namespace:WPFSlider.Converters"
xmlns:local="clr-namespace:WPFSlider"
xmlns:n="http://Nita.ToolKit.BaseUI.io/v1.0">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/WPFSlider;component/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<converter:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<!--#region ThumbEffect-->
<DropShadowEffect
x:Key="UnhoverThumbEffect"
BlurRadius="5"
Opacity="0.3"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<DropShadowEffect
x:Key="HoverThumbEffect"
BlurRadius="5"
Opacity="0.5"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<DropShadowEffect
x:Key="DragThumbEffect"
BlurRadius="8"
Opacity="1"
ShadowDepth="0"
Color="{StaticResource MainMediaColor}" />
<Style
x:Key="DefaultSliderThumbStyle"
TargetType="Thumb">
<Setter Property="Width" Value="15" />
<Setter Property="Height" Value="15" />
<Setter Property="Background" Value="White" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Ellipse
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Panel.ZIndex="2"
Effect="{StaticResource UnhoverThumbEffect}"
Fill="{TemplateBinding Background}" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Effect" Value="{StaticResource HoverThumbEffect}" />
<Setter Property="Cursor" Value="Hand" />
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter Property="Effect" Value="{StaticResource DragThumbEffect}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--#endregion-->
<Style
x:Key="DecreaseRepeatButtonStyle"
TargetType="RepeatButton">
<Setter Property="Height" Value="4" />
<Setter Property="Margin" Value="0" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="{StaticResource MainColor}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border
Background="{TemplateBinding Background}"
CornerRadius="2">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="IncreaseRepeatButtonStyle"
TargetType="RepeatButton">
<Setter Property="Height" Value="4" />
<Setter Property="Margin" Value="0" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="{StaticResource UnfinishedColor}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border
Background="{TemplateBinding Background}"
CornerRadius="2">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type local:Slider}">
<Setter Property="Width" Value="Auto" />
<Setter Property="MinHeight" Value="15" />
<Setter Property="Margin" Value="20" />
<Setter Property="Maximum" Value="100" />
<Setter Property="IsSnapToTickEnabled" Value="True" />
<Setter Property="IsMoveToPointEnabled" Value="True" />
<Setter Property="LargeChange" Value="10" />
<Setter Property="SmallChange" Value="1" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Slider}">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{Binding DisplayValue,
RelativeSource={RelativeSource AncestorType={x:Type local:Slider}}}"
Visibility="{Binding IsShowLeftTick,
RelativeSource={RelativeSource AncestorType={x:Type local:Slider}},
Converter={StaticResource BoolToVisibilityConverter}}" />
<Grid
Grid.Column="1"
Margin="0"
HorizontalAlignment="Stretch">
<Track
Name="PART_Track"
Width="{TemplateBinding Width}"
Margin="0">
<Track.DecreaseRepeatButton>
<RepeatButton
Command="{x:Static Slider.DecreaseLarge}"
Style="{StaticResource DecreaseRepeatButtonStyle}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb
x:Name="PART_Thumb"
Style="{StaticResource DefaultSliderThumbStyle}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton
Height="4"
Background="Gray"
Command="{x:Static Slider.IncreaseLarge}"
Style="{StaticResource IncreaseRepeatButtonStyle}" />
</Track.IncreaseRepeatButton>
</Track>
<local:TickBar
x:Name="TopTick"
Width="{TemplateBinding Width}"
Height="4"
CurrentTick="{TemplateBinding Value}"
Fill="White"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Maximum="{TemplateBinding Maximum}"
Minimum="{TemplateBinding Minimum}"
NitaTickPlacement="{TemplateBinding NitaTickPlacement}"
Opacity="1"
TickFrequency="{TemplateBinding TickFrequency}"
TickRenderMode="{TemplateBinding TickRenderMode}"
Ticks="{TemplateBinding Ticks}"
Visibility="{Binding IsShowTick,
Converter={StaticResource BoolToVisibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type local:Slider}}}" />
</Grid>
<TextBlock
Grid.Column="2"
Text="{Binding DisplayValue,
RelativeSource={RelativeSource AncestorType={x:Type local:Slider}}}"
Visibility="{Binding IsShowRightTick,
RelativeSource={RelativeSource AncestorType={x:Type local:Slider}},
Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
TickBar.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows;
namespace WPFSlider
{
public class TickBar : System.Windows.Controls.Primitives.TickBar
{
#region CurrentTick
public static readonly DependencyProperty CurrentTickProperty =
DependencyProperty.Register("CurrentTick",
typeof(double),
typeof(TickBar),
new UIPropertyMetadata(0.0, OnCurrentTickChanged));
public double CurrentTick
{
get { return (double)GetValue(CurrentTickProperty); }
set { SetValue(CurrentTickProperty, value); }
}
private static void OnCurrentTickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//触发渲染
if (d is TickBar tickBar)
{
tickBar.InvalidateVisual();
}
}
#endregion
#region TickRenderMode
public static readonly DependencyProperty TickRenderModeProperty =
DependencyProperty.Register("TickRenderMode",
typeof(TickRenderMode),
typeof(TickBar),
new UIPropertyMetadata(TickRenderMode.FixedTicksOnMouseOver));
public TickRenderMode TickRenderMode
{
get { return (TickRenderMode)GetValue(TickRenderModeProperty); }
set { SetValue(TickRenderModeProperty, value); }
}
#endregion
#region PlacementMode
public PlacementMode PlacementMode
{
get { return (PlacementMode)GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
public static readonly DependencyProperty PlacementModeProperty =
DependencyProperty.Register("PlacementMode", typeof(PlacementMode),
typeof(TickBar),
new PropertyMetadata(PlacementMode.Right));
#endregion
#region NitaTickPlacement
public NitaTickPlacement NitaTickPlacement
{
get { return (NitaTickPlacement)GetValue(NitaTickPlacementProperty); }
set { SetValue(NitaTickPlacementProperty, value); }
}
public static readonly DependencyProperty NitaTickPlacementProperty =
DependencyProperty.Register("NitaTickPlacement",
typeof(NitaTickPlacement),
typeof(TickBar),
new UIPropertyMetadata());
#endregion
private const double Radius = 3;
private const double ShadowRadius = 4;
private static readonly Brush ShadowBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#adadad"));
protected override void OnRender(DrawingContext dc)
{
if (TickRenderMode == TickRenderMode.Base)
{
base.OnRender(dc);
}
else
{
double tickFrequency = this.TickFrequency;
if (tickFrequency <= 0)
return;
double minimum = this.Minimum;
double maximum = this.Maximum;
double range = maximum - minimum;
double tickCount = range / tickFrequency;
double width = this.ActualWidth - 2 * Radius;
double height = this.ActualHeight;
for (int i = 0; i <= tickCount; i++)
{
double tickValue = minimum + i * tickFrequency;
if (!Ticks.Contains(tickValue) && TickRenderMode == TickRenderMode.FixedTicksOnMouseOver)
continue;
double x = (tickValue - minimum) / range * width + Radius;
double y = height / 2;
if (TickRenderMode == TickRenderMode.FixedTicksOnMouseOver)
{
DrawFixedTicksOnMouseOver(dc, tickValue, x, y);
}
else if (TickRenderMode == TickRenderMode.AutoShowOnMouseMove &&
(NitaTickPlacement == NitaTickPlacement.Top || NitaTickPlacement == NitaTickPlacement.Bottom))
{
DrawAutoShowOnMouseMove(dc, tickValue, x, y);
}
}
}
}
private void DrawFixedTicksOnMouseOver(DrawingContext dc, double tickValue, double x, double y)
{
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
FormattedText formattedText = new FormattedText(
tickValue.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Arial"), 12, Brushes.Gray, pixelsPerDip);
double rectWidth = formattedText.Width + 10;
double rectHeight = formattedText.Height + 4;
double textY = NitaTickPlacement == NitaTickPlacement.Top ? y - rectHeight - 4 : y + 4;
dc.DrawText(formattedText, new Point(x - formattedText.Width / 2, textY + 2));
if (!CurrentTick.Equals(tickValue))
{
dc.DrawEllipse(ShadowBrush, null, new Point(x, y), ShadowRadius, ShadowRadius);
dc.DrawEllipse(Fill, null, new Point(x, y), Radius, Radius);
}
}
private void DrawAutoShowOnMouseMove(DrawingContext dc, double tickValue, double x, double y)
{
bool isShowCurrentValue = CurrentTick.Equals(tickValue);
tickValue = Math.Round(tickValue, 2);
if (isShowCurrentValue)
{
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
FormattedText formattedText = new FormattedText(
tickValue.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Arial"), 12, Brushes.Gray, pixelsPerDip);
double rectWidth = formattedText.Width + 10;
double rectHeight = formattedText.Height + 4;
double textY = NitaTickPlacement == NitaTickPlacement.Top ? y - rectHeight - 10 : y + 10;
Rect rect = new Rect(new Point(x - rectWidth / 2, textY), new Size(rectWidth, rectHeight));
dc.DrawRoundedRectangle(ShadowBrush, null, rect, 5, 5);
dc.DrawRoundedRectangle(Fill, null, new Rect(rect.X + 1, rect.Y + 1, rect.Width - 2, rect.Height - 2), 5, 5);
dc.DrawText(formattedText, new Point(x - formattedText.Width / 2, textY + 2));
}
}
}
//刻度渲染类型
public enum TickRenderMode
{
Base,
/// <summary>
/// 鼠标移动时自动展示当前刻度
/// </summary>
AutoShowOnMouseMove,
/// <summary>
/// 根据设置的刻度显示固定刻度
/// </summary>
FixedTicksOnMouseOver,
}
}
参考内容
- Slider 类 (System.Windows.Controls) | Microsoft Learn
- Track 类 (System.Windows.Controls.Primitives) | Microsoft Learn
- Thumb 类 (System.Windows.Controls.Primitives) | Microsoft Learn
- TickBar 类 (System.Windows.Controls.Primitives) | Microsoft Learn
- Modifying the auto tooltip of a Slider | Josh Smith on WPF (wordpress.com)
项目代码
https://github.com/Nita121388/NitasDemo/tree/main/05WPFSlider/WPFSlider
编程小白,代码多有疏漏,请多指教!
👶💻,👀🐞❌💯,🙏😁😜!