Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 879
Flack Avatar asked Mar 22 '11 18:03

Flack


1 Answers

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

And that's all there was to it. :)

One last thing: the above assumes that the stock style for a Slider won't change. I.e. the second slider's style was copied and hard-coded into the program, but that hard-coded style still depends on the layout of the stock style from which it was copied. If that stock style changes, then the layout of the first slider could change in a way that makes the second slider no longer line up or otherwise look correct.

If that is a concern, then you can approach the templating somewhat differently: instead of modifying the SliderHorizontal template, make a copy of it and the Style that references it, changing the names of both, and changing the copy of the Style so that it references the copied template instead of the original. Then you just modify the copy, and set the style of the first Slider to the unmodified Style, and the style of the second Slider to the modified one.

Beyond the techniques demonstrated here, others may want to do things slightly differently. For example, I discarded the repeat buttons altogether, which means you can only drag the thumb. Clicking in the track outside of the thumb doesn't affect it. Also, the thumbs still work as they do in the basic Slider control, with the middle of the thumb being the indicator for where the thumb value is. This means that when you drag one thumb to the other, they wind up with the second thumb on top of the first (i.e. the first thumb won't be draggable until you move the second thumb enough to see the first).

Changing these behaviors should not be too hard, but it does involve extra work. You can add a margin to the thumbs to keep them from overlaying each other (but then you'll want to also change the thumb shape when the ticks are shown, as well as also adjust the track margin, so that everything still lines up). Instead of removing the repeat buttons, you could leave them in but adjust their positions so that they operated the way you want with the two thumbs.

I leave those tasks as an exercise for the reader. :)

like image 176
Peter Duniho Avatar answered Oct 10 '22 18:10

Peter Duniho