我意识到这个问题已经超过三年了。然而,我一直在使用有多个滑块的例子作为学习WPF更多知识的练习,并且在尝试解决这个问题时遇到了这个问题。不幸的是,链接的例子似乎已经不存在了(这是为什么StackOverflow的问题和答案不应该使用任何关键细节的链接的一个很好的例子)。
我查看了大量关于这个主题的示例和文章,虽然没有找到一个明确启用刻度的示例,但那里有足够的信息让我理解并解决了这个问题。我特别喜欢一篇文章,因为它相当清晰简洁,并同时揭示了一些非常有用的技巧,在完成这个任务时至关重要。
我的最终结果如下:
![correct DoubleThumbSlider](https://istack.dev59.com/S5cLQ.webp)
为了让其他可能想要做同样事情的人,或者只是想更好地理解一般技术的人受益,这里是如何制作一个支持基本滑块的各种刻度特性的双拇指滑块控件...
起点是`UserControl`类本身。在Visual Studio中,将一个新的`UserControl`类添加到项目中。现在,添加所有你想要支持的属性。不幸的是,我没有找到一种机制可以简单地将属性委托给`UserControl`中适当的滑块实例,所以这意味着需要为每个属性编写新的代码。
从先决条件(即其他成员声明所需的成员)开始工作,我希望其中一个功能是限制每个滑块的移动范围,使其不能拖动到其他滑块之后。我决定使用`CoerceValueCallback`来实现这一点,因此我需要回调方法:
private static object LowerValueCoerceValueCallback(DependencyObject target, object valueObject)
{
DoubleThumbSlider targetSlider = (DoubleThumbSlider)target;
double value = (double)valueObject;
return Math.Min(value, targetSlider.UpperValue);
}
private static object UpperValueCoerceValueCallback(DependencyObject target, object valueObject)
{
DoubleThumbSlider targetSlider = (DoubleThumbSlider)target;
double value = (double)valueObject;
return Math.Max(value, targetSlider.LowerValue);
}
在我的情况下,我只需要底层滑块的
最小值
、
最大值
、
是否启用对齐到刻度
、
刻度频率
、
刻度放置
和
刻度
,以及两个新属性来映射到各个滑块的值,
较低值
和
较高值
。首先,我必须声明
DependencyProperty
对象:
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double), typeof(DoubleThumbSlider), new UIPropertyMetadata(0d));
public static readonly DependencyProperty LowerValueProperty =
DependencyProperty.Register("LowerValue", typeof(double), typeof(DoubleThumbSlider), new UIPropertyMetadata(0d, null, LowerValueCoerceValueCallback));
public static readonly DependencyProperty UpperValueProperty =
DependencyProperty.Register("UpperValue", typeof(double), typeof(DoubleThumbSlider), new UIPropertyMetadata(1d, null, UpperValueCoerceValueCallback));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double), typeof(DoubleThumbSlider), new UIPropertyMetadata(1d));
public static readonly DependencyProperty IsSnapToTickEnabledProperty =
DependencyProperty.Register("IsSnapToTickEnabled", typeof(bool), typeof(DoubleThumbSlider), new UIPropertyMetadata(false));
public static readonly DependencyProperty TickFrequencyProperty =
DependencyProperty.Register("TickFrequency", typeof(double), typeof(DoubleThumbSlider), new UIPropertyMetadata(0.1d));
public static readonly DependencyProperty TickPlacementProperty =
DependencyProperty.Register("TickPlacement", typeof(TickPlacement), typeof(DoubleThumbSlider), new UIPropertyMetadata(TickPlacement.None));
public static readonly DependencyProperty TicksProperty =
DependencyProperty.Register("Ticks", typeof(DoubleCollection), typeof(DoubleThumbSlider), new UIPropertyMetadata(null));
那样做之后,我现在可以写出这些属性本身了:
public double Minimum
{
get { return (double)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
public double LowerValue
{
get { return (double)GetValue(LowerValueProperty); }
set { SetValue(LowerValueProperty, value); }
}
public double UpperValue
{
get { return (double)GetValue(UpperValueProperty); }
set { SetValue(UpperValueProperty, value); }
}
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public bool IsSnapToTickEnabled
{
get { return (bool)GetValue(IsSnapToTickEnabledProperty); }
set { SetValue(IsSnapToTickEnabledProperty, value); }
}
public double TickFrequency
{
get { return (double)GetValue(TickFrequencyProperty); }
set { SetValue(TickFrequencyProperty, value); }
}
public TickPlacement TickPlacement
{
get { return (TickPlacement)GetValue(TickPlacementProperty); }
set { SetValue(TickPlacementProperty, value); }
}
public DoubleCollection Ticks
{
get { return (DoubleCollection)GetValue(TicksProperty); }
set { SetValue(TicksProperty, value); }
}
现在,这些需要连接到底层的
Slider
控件,这将构成
UserControl
。因此,我添加了两个
Slider
控件,并将属性绑定到
UserControl
中相应的属性上。
<Grid>
<Slider x:Name="lowerSlider"
VerticalAlignment="Center"
Minimum="{Binding ElementName=root, Path=Minimum}"
Maximum="{Binding ElementName=root, Path=Maximum}"
Value="{Binding ElementName=root, Path=LowerValue, Mode=TwoWay}"
IsSnapToTickEnabled="{Binding ElementName=root, Path=IsSnapToTickEnabled}"
TickFrequency="{Binding ElementName=root, Path=TickFrequency}"
TickPlacement="{Binding ElementName=root, Path=TickPlacement}"
Ticks="{Binding ElementName=root, Path=Ticks}"
/>
<Slider x:Name="upperSlider"
VerticalAlignment="Center"
Minimum="{Binding ElementName=root, Path=Minimum}"
Maximum="{Binding ElementName=root, Path=Maximum}"
Value="{Binding ElementName=root, Path=UpperValue, Mode=TwoWay}"
IsSnapToTickEnabled="{Binding ElementName=root, Path=IsSnapToTickEnabled}"
TickFrequency="{Binding ElementName=root, Path=TickFrequency}"
TickPlacement="{Binding ElementName=root, Path=TickPlacement}"
Ticks="{Binding ElementName=root, Path=Ticks}"
/>
</Grid>
请注意,在这里,我给我的UserControl命名为"root",并在Binding声明中引用了它。大多数属性直接传递给UserControl中的相同属性,但是每个Slider控件的Value属性当然映射到UserControl的相应的LowerValue和UpperValue属性。
现在,这是最棘手的部分。如果你只停留在这里,你会得到一个看起来像这样的东西:
![incorrect DoubleThumbSlider](https://istack.dev59.com/hf5oL.webp)
第二个Slider对象完全覆盖在第一个上面,导致其轨道遮挡住第一个Slider的滑块。这不仅仅是一个视觉问题;第二个Slider对象位于顶部,接收所有鼠标点击事件,完全阻止了第一个Slider的调整。
为了解决这个问题,我编辑了第二个滑块的样式,去除了那些妨碍视觉效果的元素。我保留了第一个滑块的元素,以提供控制的实际轨道视觉效果。不幸的是,我无法找到一种方法来声明性地覆盖我需要更改的部分。但是使用Visual Studio,您可以创建现有样式的完整副本,然后根据需要进行编辑:
1. 切换到WPF设计器中的“设计”模式,针对您的UserControl。
2. 在滑块上右键单击,并选择弹出菜单中的“编辑模板/编辑副本...”。
就是这么简单。:) 这将在您的UserControl XAML中的Slider声明中添加一个Style属性,引用您刚刚创建的新样式。
Slider控件实际上有两个主要的控件模板,一个用于水平方向,一个用于垂直方向。我将在这里描述水平模板的更改;我假设如何对垂直模板进行类似的更改是显而易见的。
我使用Visual Studio的“转到定义”功能快速定位到模板中我需要的部分:在您的UserControl的Slider中找到Style属性,点击样式名称并按下F12键。这将带您到主要的Style对象,您会在那里找到一个用于水平模板的Setter(垂直模板由基于Orientation值的Trigger中的Setter控制)。点击水平模板的名称(当我执行此操作时,它是“SliderHorizontal”,但我猜它可能会改变,并且对于其他类型的控件肯定会有所不同)。
一旦你进入
ControlTemplate
,请删除那些不需要使用的元素的所有视觉属性。这意味着要删除一些元素,并从无法完全删除的元素中删除
Background
、
BorderBrush
、
BorderThickness
、
Fill
等。在我的情况下,我完全删除了
RepeatButton
,并修改了其他我需要的元素,使它们不显示或占用任何空间(这样它们就不会接收到鼠标点击)。最终得到的结果如下:
<ControlTemplate x:Key="SliderHorizontal" TargetType="">
<Border x:Name="border" BorderBrush="" BorderThickness="" SnapsToDevicePixels="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight=""/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TickBar x:Name="TopTick" Fill="" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/>
<TickBar x:Name="BottomTick" Fill="" Height="4" Margin="0,2,0,0" Placement="Bottom" Grid.Row="2" Visibility="Collapsed"/>
<Border x:Name="TrackBackground" Grid.Row="1" VerticalAlignment="center">
<Canvas>
<Rectangle x:Name="PART_SelectionRange" />
</Canvas>
</Border>
<Track x:Name="PART_Track" Grid.Row="1">
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
</Track>
</Grid>
</Border>
<!-- I left the ControlTemplate.Triggers element just as it was, no changes -->
</ControlTemplate>
那就是全部了。:)
最后一件事:上述假设滑块的样式不会改变。也就是说,第二个滑块的样式是复制并硬编码到程序中的,但是这个硬编码的样式仍然依赖于复制自哪个原始样式的布局。如果那个原始样式发生了改变,那么第一个滑块的布局可能会发生变化,导致第二个滑块无法对齐或者看起来不正确。
如果这是一个问题,你可以稍微改变模板的方法:不要修改SliderHorizontal
模板,而是复制它以及引用它的Style
,并更改两者的名称,还要更改复制的Style
,使其引用复制的模板而不是原始模板。然后只需修改复制品,并将第一个Slider
的样式设置为未修改的Style
,将第二个Slider
的样式设置为修改后的Style
。
在这里演示的技术之外,其他人可能希望以稍微不同的方式进行操作。例如,我完全舍弃了重复按钮,这意味着你只能拖动滑块。在滑块以外的轨道上点击不会对它产生影响。此外,滑块仍然像基本的
Slider
控件一样工作,滑块的中间指示了滑块值的位置。这意味着当你将一个滑块拖动到另一个滑块上时,它们会互相覆盖(即第一个滑块在移动第二个滑块足够看到第一个滑块之前是不可拖动的)。
更改这些行为应该不会太难,但需要额外的工作。你可以给滑块添加边距,以防止它们重叠在一起(但是当显示刻度时,你还需要改变滑块的形状,并且还要调整轨道的边距,以确保一切仍然对齐)。你也可以保留重复按钮,但调整它们的位置,使其按照你想要的方式工作。
我将这些任务留给读者自己来完成。 :)
UserControl
资源中。如果您在 XAML 中找到对它的引用,请使用 Visual Studio 的“转到定义”命令(即 F12)快速导航到它。 - Peter DunihoElementName=root
。实际上,如果我把它移除掉,就不能正常工作。只是好奇。 :) - chick3n0x07CCUserControl
属性,该属性向客户端代码呈现该接口,而不是依赖于客户端代码提供具有正确值的任意数据上下文对象。 - Peter Duniho