【问题标题】:WPF slider with two thumbs带有两个拇指的 WPF 滑块
【发布时间】:2011-03-22 18:21:42
【问题描述】:

我正在尝试为我的应用创建一个带有两个拇指的滑块,用作范围滑块,但我遇到了问题。我的基本要求是获得一个带有刻度线和两个拇指的滑块,它们都设置为 IsSnapToTickEnabled="true"。

我在搜索帮助时发现了一些范围滑块示例(例如 this one),但我无法对其进行修改以添加刻度线并强制拇指与刻度线对齐。不过,为链接中的范围滑块获取刻度线和捕捉工作将是理想的。

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

有没有人有一个带有两个拇指、刻度线和对齐刻度的滑块示例?我发现的所有范围滑块示例都使用两个相互重叠的滑块,并且它们都不允许刻度线或对齐刻度线。

谢谢。

【问题讨论】:

    标签: wpf templates wpf-controls slider


    【解决方案1】:

    我意识到这个问题已经超过三年了。但是,我一直在使用带有多个拇指的滑块的示例作为练习来了解有关 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);
    }
    

    就我而言,我只需要来自底层滑块的MinimumMaximumIsSnapToTickEnabledTickFrequencyTickPlacementTicks,以及两个新属性来映射到各个滑块值、LowerValueHigherValue。首先,我必须声明 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 的相应LowerValueUpperValue 属性。

    现在,这是最棘手的部分。如果你只是停在这里,你会得到如下所示的东西: 第二个Slider 对象完全位于第一个对象之上,导致其轨道覆盖了第一个Slider 拇指。这也不仅仅是一个视觉问题。第二个Slider 对象位于顶部,接收所有鼠标点击,从而完全防止第一个Slider 被调整。

    为了解决这个问题,我编辑了第二个滑块的样式,以移除那些妨碍您的视觉元素。我将它们留给第一个滑块,为控件提供实际的轨道视觉效果。不幸的是,我想不出一种方法来声明性地覆盖我需要更改的部分。但是使用 Visual Studio,您可以创建现有样式的完整副本,然后可以根据需要对其进行编辑:

    1. 为您的UserControl 切换到 WPF 设计器中的“设计”模式
    2. 右键单击滑块并从弹出菜单中选择“编辑模板/编辑副本...”

    就这么简单。 :) 这将为您的UserControl XAML 中的Slider 声明添加一个Style 属性,引用您刚刚创建的新样式。

    Slider 控件实际上有两个主要的控件模板,一个用于水平方向,一个用于垂直方向。我将在此处描述对水平模板的更改;我认为如何对垂直模板进行类似的更改是显而易见的。

    我使用 Visual Studio 的“Go To Definition”功能快速找到我需要的模板部分:在您的UserControlSlider 中找到Style 属性,单击样式名称并按 F12。这将带您进入主要的Style 对象,您将在其中找到水平模板的Setter(垂直模板由Trigger 中的Setter 控制,基于Orientation 值)。点击水平模板的名称(我这样做的时候是“SliderHorizo​​ntal”,但我猜它可能会改变,当然对于其他类型的控件会有所不同)。

    到达ControlTemplate 后,从不应使用的元素中删除所有视觉属性。这意味着删除一些元素,并从无法完全删除的元素中删除BackgroundBorderBrushBorderThicknessFill 等。就我而言,我完全删除了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 控件中一样工作,拇指的中间是拇指值所在的指示器。这意味着当您将一个拇指拖到另一个拇指时,它们会以第二个拇指在第一个拇指上结束(即,在您将第二个拇指移动到足以看到第一个拇指之前,第一个拇指将无法拖动)。

    改变这些行为应该不会太难,但确实需要额外的工作。您可以为拇指添加边距以防止它们相互重叠(但是您还需要在显示刻度时更改拇指形状,以及调整轨道边距,以便所有内容仍然对齐) .您可以将重复按钮留在原处,但可以调整它们的位置,以便用两个拇指按您想要的方式操作,而不是移除重复按钮。

    我将这些任务留给读者作为练习。 :)

    【讨论】:

    • 您好,我如何找到SliderThumbHorizontalDefault 模板?
    • @CKocer:如果您按照上面的说明进行操作,即使用“编辑模板/编辑副本...”命令,Visual Studio 会将该模板以及所需的所有其他内容添加到资源中您正在编辑的UserControl。如果在 XAML 中找到对它的引用,则可以使用 Visual Studio 的“转到定义”命令(即 F12)快速导航到它。
    • 这个应该是答案。标记的答案没有显示任何代码,对社区没有帮助。问题也是如此。
    • 刚刚尝试过,效果很好!但是,我不明白为什么我们需要参考 ElementName=root。事实上,如果我删除它,它就不起作用。只是好奇。 :)
    • @chick3n0x07CC:如果没有这个,绑定会引用当前数据上下文的任何内容,它通常要么是空的,要么是包含程序特定属性的视图模型对象,这些属性可能会或可能不会被命名为这些绑定引用。在这种情况下,我们确实希望这些元素专门引用父 UserControl 属性,该属性将该接口呈现给客户端代码,而不是依赖客户端代码来提供具有正确值的任意数据上下文对象。
    猜你喜欢
    • 1970-01-01
    • 2011-02-10
    • 2021-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多