Click here to Skip to main content
Click here to Skip to main content

WPF Colour Slider

, 4 Jun 2009
Rate this:
Please Sign up or sign in to vote.
Creating an embedded, bindable WPF Colour Picker based on a Slider control using a LinearGradientBrush.

WpfColourSlider1.jpg

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:

A screenshot of the Colour Slider application with a black to green gradient background

<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)
{
    // prevent value change occurring when we set the colour
    if (Monitor.TryEnter(this.updateLock, 0) && !this.isValueUpdating)
    {
        try
        {
            this.isValueUpdating = true;
        
            // work out the track position based on the control's width
            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)
                {
                    //we cannot get a better match, break now
                    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

andywilsonuk
Software Developer (Senior) Play.com
United Kingdom United Kingdom
Hi, my name's Andy Wilson and I live in Cambridge, UK where I work as a Senior C# Software Developer.

Comments and Discussions

 
Questionvertical orientation PinmemberAdski_Proger9-Oct-11 7:10 
GeneralIt's handy, but some criticism PinmemberOleg V. Polikarpotchkin2-Jun-09 18:07 
GeneralRe: It's handy, but some criticism Pinmemberdevwilson2-Jun-09 23:50 
GeneralI like it PinmvpSacha Barber29-May-09 6:10 
GeneralGreat solution PinmemberMatt Sollars29-May-09 3:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140814.1 | Last Updated 4 Jun 2009
Article Copyright 2009 by andywilsonuk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid