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:
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:
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:
- Switch to the "Design" mode in the WPF designer for your
UserControl
- 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 RepeatButton
s 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>