Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
493 views
in Technique[技术] by (71.8m points)

templates - WPF slider with two thumbs

I'm trying to create a slider with two thumbs for my app, to use as a range slider, but am running into issues. The basic requirement for me is to get a single slider with tick marks and two thumbs, which are both set with IsSnapToTickEnabled="true".

I found some samples of range sliders when searching for help (for example this one) but I was not able to modify it to add tick marks and force the thumbs to snap to ticks. Getting tick marks and snapping working for the range slider in the link would be ideal though.

I tried modifying a slider's template and adding another thumb to it but then I do not know how to get the value of the selected thumb.

Does anyone have a sample of a slider with two thumbs, tick marks, and snap to tick enabled? All of the range slider samples I have found use two sliders on top of each other and none of them allow for tick marks or snapping to tick marks.

Thanks.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I realize that this question is over three years old. However, I've been using the example of a slider with more than one thumb as an exercise to learn more about WPF, and came across this question when I was trying to figure out how to do this. Unfortunately, the linked example appears to no longer exist (a good example of why StackOverflow questions and answers should not use links for any detail that is critical for the question or answer).

I've looked at a large number of samples and articles on the topic, and while I did not find one that specifically enabled ticks, there was enough information there for me to figure it out. I found one article particularly good, in that it was reasonably clear and to the point, and at the same time revealed a couple of really useful techniques that are key in accomplishing this task.

My end result looks like this:

correct DoubleThumbSlider

So for the benefit of others who may want to either do the same thing, or who would simply like to understand the general techniques better, here's how you make a two-thumb slider control that supports the various tick features of the basic slider…


The starting point is the UserControl class itself. In Visual Studio, add a new UserControl class to the project. Now, add all the properties that you want to support. Unfortunately, I have not found a mechanism that would allow simply delegating the properties to the appropriate slider instance(s) in the UserControl, so this means writing new properties for each one.

Working from the prerequisites (i.e. the members required by the other members to be declared), one of the features I wanted was to limit the travel of each slider so it could not be dragged past the other. I decided to implement this using CoerceValueCallback for the properties, so I needed the callback methods:

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);
}

In my case, I only needed Minimum, Maximum, IsSnapToTickEnabled, TickFrequency, TickPlacement, and Ticks from the underlying sliders, and two new properties to map to the individual slider values, LowerValue and HigherValue. First, I had to declare the DependencyProperty objects:

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));

That done, I could now write the properties themselves:

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); }
}

Now, these need to be hooked up to the underlying Slider controls that will make up the UserControl. So I added the two Slider controls, with bindings for the properties to the appropriate properties in my 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>

Note that here, I've given my UserControl the name "root", and referenced that in the Binding declarations. Most of the properties go directly to the identical property in the UserControl, but of course the individual Value properties for each Slider control is mapped to the appropriate LowerValue and UpperValue property of the UserControl.

Now, here's the trickiest part. If you just stop here, you'll get something that looks like this: incorrect DoubleThumbSlider The second Slider object is entirely on top of the first, causing its track to cover up the first Slider thumb. It's not just a visual problem either; the second Slider object, being on top, receives all of the mouse clicks, preventing the first Slider from being adjusted at all.

To fix this, I edited the style for the second slider to remove those visual elements that get in the way. I left them for the first slider, to provide the actual track visuals for the control. Unfortunately, I could not figure out a way to declaratively override just the parts I needed to change. But using Visual Studio, you can create a whole copy of the existing style, which can then be edited as needed:

  1. Switch to the "Design" mode in the WPF designer for your UserControl
  2. Right-click on the slider and chose "Edit Template/Edit a Copy..." from the pop-up menu

It's that easy. :) This will add a Style attribute to the Slider declaration in your UserControl XAML, referencing the new style you just created.

The Slider control actually has two main control templates, one for the horizontal orientation and one for the vertical. I'll describe the changes to the horizontal template here; I assume it will be obvious how to make similar changes to the vertical template.

I use Visual Studio's "Go To Definition" feature to quickly get to the part of the template I need: find the Style attribute in the Slider of your UserControl, click on the name of the style and press F12. That will take you to the main Style object, where you'll find a Setter for the horizontal template (the vertical template is controlled by a Setter in a Trigger based on the Orientation value). Click on the name of the horizontal template (it was "SliderHorizontal" when I did this, but I guess it could change, and of course would be different for other types of controls).

Once you get to the ControlTemplate, remove all of the visual attributes from the elements that should not be used. This means removing some elements, and removing Background, BorderBrush, BorderThickness, Fill, etc. from the elements you can't remove entirely. In my case, I removed the RepeatButtons entirely, and modified the other elements I needed to so that they didn't show up or take up any space (so they wouldn't receive mouse clicks). I wound up with this:

<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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...