我意识到这个问题已经超过三年了。但是,我一直在使用带有多个拇指的滑块的示例作为练习来了解有关 WPF 的更多信息,并且在我试图弄清楚如何做到这一点时遇到了这个问题。不幸的是,链接的示例似乎不再存在(这是一个很好的示例,说明为什么 StackOverflow 问题和答案不应该使用对问题或答案至关重要的任何细节的链接)。
我查看了有关该主题的大量示例和文章,虽然我没有找到专门启用刻度的文章,但那里有足够的信息让我弄清楚。我发现 one article 特别好,因为它相当清晰和中肯,同时揭示了一些非常有用的技术,它们是完成这项任务的关键。
我的最终结果如下所示:
因此,为了其他可能想要做同样事情或只是想更好地理解一般技术的其他人的利益,这里是您如何制作一个支持基本的各种刻度功能的双拇指滑块控件滑块…
起点是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);
}
就我而言,我只需要来自底层滑块的Minimum、Maximum、IsSnapToTickEnabled、TickFrequency、TickPlacement 和Ticks,以及两个新属性来映射到各个滑块值、LowerValue 和 HigherValue。首先,我必须声明 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); }
}
现在,这些需要连接到构成UserControl 的底层Slider 控件。所以我添加了两个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 属性。
现在,这是最棘手的部分。如果你只是停在这里,你会得到如下所示的东西:
第二个Slider 对象完全位于第一个对象之上,导致其轨道覆盖了第一个Slider 拇指。这也不仅仅是一个视觉问题。第二个Slider 对象位于顶部,接收所有鼠标点击,从而完全防止第一个Slider 被调整。
为了解决这个问题,我编辑了第二个滑块的样式,以移除那些妨碍您的视觉元素。我将它们留给第一个滑块,为控件提供实际的轨道视觉效果。不幸的是,我想不出一种方法来声明性地覆盖我需要更改的部分。但是使用 Visual Studio,您可以创建现有样式的完整副本,然后可以根据需要对其进行编辑:
- 为您的
UserControl 切换到 WPF 设计器中的“设计”模式
- 右键单击滑块并从弹出菜单中选择“编辑模板/编辑副本...”
就这么简单。 :) 这将为您的UserControl XAML 中的Slider 声明添加一个Style 属性,引用您刚刚创建的新样式。
Slider 控件实际上有两个主要的控件模板,一个用于水平方向,一个用于垂直方向。我将在此处描述对水平模板的更改;我认为如何对垂直模板进行类似的更改是显而易见的。
我使用 Visual Studio 的“Go To Definition”功能快速找到我需要的模板部分:在您的UserControl 的Slider 中找到Style 属性,单击样式名称并按 F12。这将带您进入主要的Style 对象,您将在其中找到水平模板的Setter(垂直模板由Trigger 中的Setter 控制,基于Orientation 值)。点击水平模板的名称(我这样做的时候是“SliderHorizontal”,但我猜它可能会改变,当然对于其他类型的控件会有所不同)。
到达ControlTemplate 后,从不应使用的元素中删除所有视觉属性。这意味着删除一些元素,并从无法完全删除的元素中删除Background、BorderBrush、BorderThickness、Fill 等。就我而言,我完全删除了RepeatButtons,并修改了我需要的其他元素,以使它们不会出现或占用任何空间(因此它们不会收到鼠标点击)。我结束了这个:
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TickBar x:Name="TopTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/>
<TickBar x:Name="BottomTick" Fill="{TemplateBinding Foreground}" 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="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
</Track>
</Grid>
</Border>
<!-- I left the ControlTemplate.Triggers element just as it was, no changes -->
</ControlTemplate>
仅此而已。 :)
最后一件事:以上假设Slider 的股票样式不会改变。 IE。第二个滑块的样式被复制并硬编码到程序中,但该硬编码样式仍然取决于复制它的原始样式的布局。如果该股票样式发生变化,则第一个滑块的布局可能会发生变化,导致第二个滑块不再对齐或看起来不正确。
如果这是一个问题,那么您可以以不同的方式处理模板:不要修改 SliderHorizontal 模板,而是复制它和引用它的 Style,更改两者的名称,然后更改Style 的副本,以便它引用复制的模板而不是原始模板。然后修改副本,将第一个Slider的样式设置为未修改的Style,将第二个Slider的样式设置为修改后的样式。
除了这里展示的技术之外,其他人可能想要做的事情略有不同。例如,我完全放弃了重复按钮,这意味着您只能拖动拇指。在拇指外的轨道中单击不会对其产生影响。此外,拇指仍然像在基本的Slider 控件中一样工作,拇指的中间是拇指值所在的指示器。这意味着当您将一个拇指拖到另一个拇指时,它们会以第二个拇指在第一个拇指上结束(即,在您将第二个拇指移动到足以看到第一个拇指之前,第一个拇指将无法拖动)。
改变这些行为应该不会太难,但确实需要额外的工作。您可以为拇指添加边距以防止它们相互重叠(但是您还需要在显示刻度时更改拇指形状,以及调整轨道边距,以便所有内容仍然对齐) .您可以将重复按钮留在原处,但可以调整它们的位置,以便用两个拇指按您想要的方式操作,而不是移除重复按钮。
我将这些任务留给读者作为练习。 :)