WPF带有两个滑块的滑动条

12

我正试图为我的应用程序创建一个具有两个拇指的滑块,以用作范围滑块,但遇到了问题。对我来说,最基本的要求是获得一个带有刻度线和两个拇指的单个滑块,这两个拇指都设置了IsSnapToTickEnabled="true"。

在寻求帮助时,我找到了一些范围滑块的示例(例如这个),但我无法修改它以添加刻度线并强制拇指捕捉到刻度线。虽然将链接中的范围滑块的刻度和捕捉效果实现是理想的。

我尝试修改滑块的模板并添加另一个拇指,但我不知道如何获取所选拇指的值。

是否有人有一个具有两个拇指、刻度线和刻度捕捉功能的滑块示例?我找到的所有范围滑块示例都使用了两个相互重叠的滑块,没有一个允许使用刻度线或刻度捕捉。

谢谢。

1个回答

32

我意识到这个问题已经超过三年了。然而,我一直在使用有多个滑块的例子作为学习WPF更多知识的练习,并且在尝试解决这个问题时遇到了这个问题。不幸的是,链接的例子似乎已经不存在了(这是为什么StackOverflow的问题和答案不应该使用任何关键细节的链接的一个很好的例子)。

我查看了大量关于这个主题的示例和文章,虽然没有找到一个明确启用刻度的示例,但那里有足够的信息让我理解并解决了这个问题。我特别喜欢一篇文章,因为它相当清晰简洁,并同时揭示了一些非常有用的技巧,在完成这个任务时至关重要。

我的最终结果如下:

correct DoubleThumbSlider

为了让其他可能想要做同样事情的人,或者只是想更好地理解一般技术的人受益,这里是如何制作一个支持基本滑块的各种刻度特性的双拇指滑块控件...
起点是`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 第二个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,请删除那些不需要使用的元素的所有视觉属性。这意味着要删除一些元素,并从无法完全删除的元素中删除BackgroundBorderBrushBorderThicknessFill等。在我的情况下,我完全删除了RepeatButton,并修改了其他我需要的元素,使它们不显示或占用任何空间(这样它们就不会接收到鼠标点击)。最终得到的结果如下:
<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>

那就是全部了。:)

最后一件事:上述假设滑块的样式不会改变。也就是说,第二个滑块的样式是复制并硬编码到程序中的,但是这个硬编码的样式仍然依赖于复制自哪个原始样式的布局。如果那个原始样式发生了改变,那么第一个滑块的布局可能会发生变化,导致第二个滑块无法对齐或者看起来不正确。

如果这是一个问题,你可以稍微改变模板的方法:不要修改SliderHorizontal模板,而是复制它以及引用它的Style,并更改两者的名称,还要更改复制的Style,使其引用复制的模板而不是原始模板。然后只需修改复制品,并将第一个Slider的样式设置为未修改的Style,将第二个Slider的样式设置为修改后的Style

在这里演示的技术之外,其他人可能希望以稍微不同的方式进行操作。例如,我完全舍弃了重复按钮,这意味着你只能拖动滑块。在滑块以外的轨道上点击不会对它产生影响。此外,滑块仍然像基本的Slider控件一样工作,滑块的中间指示了滑块值的位置。这意味着当你将一个滑块拖动到另一个滑块上时,它们会互相覆盖(即第一个滑块在移动第二个滑块足够看到第一个滑块之前是不可拖动的)。
更改这些行为应该不会太难,但需要额外的工作。你可以给滑块添加边距,以防止它们重叠在一起(但是当显示刻度时,你还需要改变滑块的形状,并且还要调整轨道的边距,以确保一切仍然对齐)。你也可以保留重复按钮,但调整它们的位置,使其按照你想要的方式工作。
我将这些任务留给读者自己来完成。 :)

你好,我该如何找到“SliderThumbHorizontalDefault”模板? - cKNet
@CKocer:如果您按照上面的步骤操作,即使用“编辑模板/编辑副本…”命令,则 Visual Studio 将将该模板以及其他所需内容添加到您正在编辑的UserControl资源中。如果您在 XAML 中找到对它的引用,请使用 Visual Studio 的“转到定义”命令(即 F12)快速导航到它。 - Peter Duniho
1
这个应该是答案。标记的答案没有显示任何代码,对社区也没有帮助。问题也是如此。 - user5000935
刚试了一下,效果很好!但是我不明白为什么需要引用 ElementName=root。实际上,如果我把它移除掉,就不能正常工作。只是好奇。 :) - chick3n0x07CC
2
@chick3n0x07CC:如果没有这个,绑定将引用当前数据上下文,通常是空或包含特定于程序的属性的视图模型对象,这些属性可能与那些绑定引用的属性名称相同或不同。在这种情况下,我们确实希望这些元素特别引用父UserControl属性,该属性向客户端代码呈现该接口,而不是依赖于客户端代码提供具有正确值的任意数据上下文对象。 - Peter Duniho

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接