While working on UrzaGatherer v3.0, I found myself in the need of a range selector control. Something like the slider control but with two thumbs.
Feel free to ping me on Twitter(@deltakosh) if you want to discuss about this article
because this control is not part of the default library, I decided to create one. Feel free to download the complete solution here.
For flexibility reason, I created a custom control named RangeSelector and based on this template:
<Style TargetType="local:RangeSelector" ><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="local:RangeSelector"><Grid Height="32"><Grid.Resources><Style TargetType="Thumb"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Thumb"><Ellipse Width="32" Height="32" Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Foreground}" StrokeThickness="4" RenderTransformOrigin="0.5 0.5"><Ellipse.RenderTransform><TranslateTransform X="-16"></TranslateTransform></Ellipse.RenderTransform></Ellipse></ControlTemplate></Setter.Value></Setter></Style></Grid.Resources><Rectangle Height="8" Fill="{TemplateBinding Background}" Margin="12,0"></Rectangle><Canvas x:Name="ContainerCanvas" Margin="16,0"><Rectangle x:Name="ActiveRectangle" Fill="{TemplateBinding Foreground}" Height="8" Canvas.Top="12"></Rectangle><Thumb x:Name="MinThumb" Background="{TemplateBinding Background}" /><Thumb x:Name="MaxThumb" Background="{TemplateBinding Background}"/></Canvas></Grid></ControlTemplate></Setter.Value></Setter></Style>
So we mainly have a canvas that hosts two thumbs and a rectangle.
The goal is to have this kind of rendering:
Image may be NSFW.
Clik here to view.
The C# control by itself is then based on a Control class with the following “plumbing” done to connect parts:
public sealed class RangeSelector : Control{Rectangle ActiveRectangle;Thumb MinThumb;Thumb MaxThumb;Canvas ContainerCanvas;public RangeSelector() { DefaultStyleKey = typeof(RangeSelector); }protected override void OnApplyTemplate() { ActiveRectangle = GetTemplateChild("ActiveRectangle") as Rectangle; MinThumb = GetTemplateChild("MinThumb") as Thumb; MaxThumb = GetTemplateChild("MaxThumb") as Thumb; ContainerCanvas = GetTemplateChild("ContainerCanvas") as Canvas; MinThumb.DragCompleted += Thumb_DragCompleted; MinThumb.DragDelta += MinThumb_DragDelta; MinThumb.DragStarted += MinThumb_DragStarted; MaxThumb.DragCompleted += Thumb_DragCompleted; MaxThumb.DragDelta += MaxThumb_DragDelta; MaxThumb.DragStarted += MaxThumb_DragStarted; ContainerCanvas.SizeChanged += ContainerCanvas_SizeChanged; base.OnApplyTemplate(); }private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e) { SyncThumbs(); }
Basically we need to connect to the drag events of our thumbs and provide a SyncThumbs() method to move the thumbs and the rectangle in sync with range values.
This range values are defined by regular dependencies properties:
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double), typeof(RangeSelector), new PropertyMetadata(0.0, null));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double), typeof(RangeSelector), new PropertyMetadata(1.0, null));
public static readonly DependencyProperty RangeMinProperty =
DependencyProperty.Register("RangeMin", typeof(double), typeof(RangeSelector), new PropertyMetadata(0.0, null));
public static readonly DependencyProperty RangeMaxProperty =
DependencyProperty.Register("RangeMax", typeof(double), typeof(RangeSelector), new PropertyMetadata(1.0, null));
The SyncThumbs() method is just a simple translation between Maximum<->Minimum and the canvas’ width:
public void SyncThumbs() {if (ContainerCanvas == null) {return; }var relativeLeft = ((RangeMin - Minimum) / (Maximum - Minimum)) * ContainerCanvas.ActualWidth;var relativeRight = ((RangeMax - Minimum) / (Maximum - Minimum)) * ContainerCanvas.ActualWidth;Canvas.SetLeft(MinThumb, relativeLeft);Canvas.SetLeft(ActiveRectangle, relativeLeft);Canvas.SetLeft(MaxThumb, relativeRight); ActiveRectangle.Width = Canvas.GetLeft(MaxThumb) - Canvas.GetLeft(MinThumb); }
The drag events are then responsible for doing the opposite transformation:
private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e) { RangeMin = DragThumb(MinThumb, 0, Canvas.GetLeft(MaxThumb), e); }private void MaxThumb_DragDelta(object sender, DragDeltaEventArgs e) { RangeMax = DragThumb(MaxThumb, Canvas.GetLeft(MinThumb), ContainerCanvas.ActualWidth, e); }private double DragThumb(Thumb thumb, double min, double max, DragDeltaEventArgs e) {var currentPos = Canvas.GetLeft(thumb);var nextPos = currentPos + e.HorizontalChange; nextPos = Math.Max(min, nextPos); nextPos = Math.Min(max, nextPos);Canvas.SetLeft(thumb, nextPos);return (Minimum + (nextPos / ContainerCanvas.ActualWidth) * (Maximum - Minimum)); ; }private void MinThumb_DragStarted(object sender, DragStartedEventArgs e) {Canvas.SetZIndex(MinThumb, 10);Canvas.SetZIndex(MaxThumb, 0); }private void MaxThumb_DragStarted(object sender, DragStartedEventArgs e) {Canvas.SetZIndex(MinThumb, 0);Canvas.SetZIndex(MaxThumb, 10); }
Please note that I also defined the ZIndex property to be sure to have the right thumb on top of the canvas
Using this control is then a piece of cake:
<StackPanel Orientation="Vertical" VerticalAlignment="Center"><TextBlock FontSize="20" Text="{Binding RangeMin, ElementName=RangeSelector}" HorizontalAlignment="Center" /><local:RangeSelector x:Name="RangeSelector" Background="Gray" Foreground="Red" BorderThickness="4"
Minimum="0" Maximum="100" RangeMin="20" RangeMax="80"/><TextBlock FontSize="20" Text="{Binding RangeMax, ElementName=RangeSelector}" HorizontalAlignment="Center" /></StackPanel>
Image may be NSFW.
Clik here to view.
I hope you will find this control useful!
Image may be NSFW.Clik here to view.