
Introduction
This article focuses on creating an embedded (i.e., non-modal dialog), bindable WPF Colour Picker based on a Slider
control using a LinearGradientBrush
rather than static colour swatches.
Note: This article has been updated, scroll to the bottom to see the history.
Background
WPF contains a good array of built-in controls but is lacking embedded controls that provide the same functionality as the WinForms standard dialog boxes. A number of colour pickers have been written for WPF, most notably the WPFColorPicker, and although this control works great, I had two further requirements to fulfill in that I didn't want the control to use up too much visual real estate and I wanted to use WPF gradients rather than colour swatches.
Using the Code
From a usage standpoint, the control can be added to any Window
or UserControl
, and the SelectedColour
dependency property on the ColourSlider
provides the binding capability. To set the initial value, bind using the DataContext
, and set the value directly or in the code-behind. The code below creates a binding between the rectangle's fill colour and the currently selected colour on the slider, so that moving the slider updates the fill.
<Window x:Class="ColourSlider.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ColourSliderLibrary;assembly=ColourSliderLibrary"
Title="Colour Slider" Height="300" Width="300">
<Grid>
<local:ColourSlider Name="picker" Margin="16,22,16,0"
Height="30" VerticalAlignment="Top" SelectedColour="Yellow" />
<Rectangle Height="55" Margin="80,75,76,0" VerticalAlignment="Top">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding ElementName=picker, Path=SelectedColour}" />
</Rectangle.Fill>
</Rectangle>
</Grid>
</Window>
Microsoft has provided a great example of how to skin a slider in their Slider ControlTemplate Example, and the two major differences are the gradient background and the track thumb. Of course, as with any control, this is completely re-skinable within the application. The Track.Thumb
template with its top and bottom markers looks like this:
<Thumb.Template>
<ControlTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="*" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<Image Grid.Row="0" Width="10">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Geometry="M 30 50 L 50 0 10 0 Z">
<GeometryDrawing.Pen>
<Pen Brush="Crimson"
Thickness="25" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<Image Grid.Row="2" Width="10">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<GeometryDrawing Geometry="M 25 0 L 10 40 40 40 Z">
<GeometryDrawing.Pen>
<Pen Brush="Crimson"
Thickness="25" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</ControlTemplate>
</Thumb.Template>
The gradient background is set in the control's code-behind so that it can be re-skinned to suit the instance requirements.
public ColourSlider()
{
...
this.Background = new LinearGradientBrush(new GradientStopCollection() {
new GradientStop(Colors.Black, 0.0),
new GradientStop(Colors.Red, 0.1),
new GradientStop(Colors.Yellow, 0.25),
new GradientStop(Colors.Lime, 0.4),
new GradientStop(Colors.Aqua, 0.55),
new GradientStop(Colors.Blue, 0.7),
new GradientStop(Colors.Fuchsia, 0.9),
new GradientStop(Colors.White, 0.98),
new GradientStop(Colors.White, 1),
});
For example instead of the complete(ish) colour spectrum, the slider could be changed on the instance to a gradient of black to green, which, based on the original Window
code, looks like this:

<local:ColourSlider Name="picker" Margin="16,22,16,0"
Height="30" VerticalAlignment="Top"
SelectedColour="DarkGreen">
<local:ColourSlider.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF000000" Offset="0" />
<GradientStop Color="#FF00FF00" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</local:ColourSlider.Background>
</local:ColourSlider>
Into the Code
One of the challenges with using the slider control was that there are two ways to modify the selected colour, one through the new SelectedColour
dependency property, and one through the slider's built-in Value
property which is updated whenever the user drags the track marker, clicks before or after the marker, uses the keyboard to move the marker, or the value is set in the XAML or code-behind.
protected override void OnValueChanged(double oldValue, double newValue)
{
if (Monitor.TryEnter(this.updateLock, 0) && !this.isValueUpdating)
{
try
{
this.isValueUpdating = true;
int position = (int)(((newValue - base.Minimum) /
(base.Maximum - base.Minimum)) * this.VisualBounds.Width);
this.SelectedColour = GetColour(this.colourGradient, position);
}
finally
{
isValueUpdating = false;
Monitor.Exit(this.updateLock);
}
}
base.OnValueChanged(oldValue, newValue);
}
The important bit is scaling the value to the width of the control which can then be used to get the colour from the gradient and set the SelectedColour
property. The lock prevents the callback on the SelectedColour
dependency property from also setting the slider value, which causes a feedback loop until WPF halts it; this caused some weird behaviour when the track marker was being moved, whereby the marker would jump position because of a mismatch to a similar colour in a different part of the spectrum, or would get 'stuck' between two colours.
Getting the Colour
To get the colour from the gradient, I cached the rendered control as a bitmap within the OnRender
method; and then to get the colour, I create a CroppedBitmap
at the required position, copy out the three partial colours, and create a Color
object.
private void CacheBitmap()
{
Rect bounds = this.VisualBounds;
RenderTargetBitmap source = new RenderTargetBitmap((int)bounds.Width,
(int)bounds.Height, 96, 96, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using (DrawingContext dc = dv.RenderOpen())
{
VisualBrush vb = new VisualBrush(this);
dc.DrawRectangle(vb, null, new Rect(new Point(), bounds.Size));
}
source.Render(dv);
this.colourGradient = source;
}
private Color GetColour(BitmapSource bitmap, int position)
{
if (position >= bitmap.Width - 1)
{
position = (int)bitmap.Width - 2;
}
CroppedBitmap cb = new CroppedBitmap(bitmap, new Int32Rect(position,
(int)this.VisualBounds.Height / 2, 1, 1));
byte[] tricolour = new byte[4];
cb.CopyPixels(tricolour, 4, 0);
Color c = Color.FromRgb(tricolour[2], tricolour[1], tricolour[0]);
return c;
}
Setting the Colour
To move the track marker to the correct position when changing the SelectedColour
property required a callback method which iterates over each pixel within the cached bitmap strip, performing a similarity comparison to the target colour and setting the slider value to be the best match. To get the distance (i.e., the similarity) between two colours, I compared the Hue, Saturation, and Brightness, and combined the result into a single value.
private void SetColour(Color colour)
{
if (Monitor.TryEnter(this.updateLock, 0) "" !this.isValueUpdating)
{
try
{
Rect bounds = this.VisualBounds;
double currentDistance = int.MaxValue;
int currentPosition = -1;
for (int i = 0; i < bounds.Width; i++)
{
Color c = this.GetColour(this.colourGradient, i);
double distance = c.Distance(colour);
if (distance == 0.0)
{
currentPosition = i;
break;
}
if (distance < currentDistance)
{
currentDistance = distance;
currentPosition = i;
}
}
base.Value = (currentPosition / bounds.Width) *
(base.Maximum - base.Minimum);
}
finally
{
Monitor.Exit(updateLock);
}
}
}
public static double Distance(this Color source, Color target)
{
System.Drawing.Color c1 = source.ToDrawingColour();
System.Drawing.Color c2 = target.ToDrawingColour();
double hue = c1.GetHue() - c2.GetHue();
double saturation = c1.GetSaturation() - c2.GetSaturation();
double brightness = c1.GetBrightness() - c2.GetBrightness();
return (hue * hue) + (saturation * saturation) + (brightness * brightness);
}
Points of Interest
- The slider properties of
Minimum
, Maximum
, LargeChange
, and SmallChange
can be adjusted to allow greater or fewer possible colours - Comparing the Hue, Saturation, and Brightness between two colours produces much better matches than RGB
- WPF will not allow properties updating properties to recur indefinitely, but in my case, once was too many times
Conclusion
The creation of the WPF Colour Slider control was more challenging than I'd imagined; however, I'm really pleased with the way this has turned out. The flexibility of using gradients rather than manually created colour swatches allows for some great possibilities with different colour combinations, and the control's compact-size and embedded nature means it should fit neatly on an Option window or wherever.
I've wanted to create this control for months now, but only recently have all the pieces come together, specifically: converting a control's visual render to a bitmap, getting a colour from the bitmap, and comparing the similarity of colours (by far the most difficult of the three).
References
Here is a list of articles I found most useful when writing this article, many thanks to their creators:
History
- Version 1.0 - Initial release.
- Version 1.1 - Updated based on feedback from Oleg V. Polikarpotchkin
- Subtracting the minimum value when calculating the position on the bitmap
OnRender
calling SetColour
repeatedly when minimum value set issue
Hi, my name's Andy Wilson and I live in Cambridge, UK where I work as a Senior C# Software Developer.