Click here to Skip to main content
Click here to Skip to main content
Go to top

My Version of the Ubiquitous Color Picker

, 29 Nov 2009
Rate this:
Please Sign up or sign in to vote.
An article describing a color picker with independent control of seven variables and dynamically updating slider backgrounds.
Article_src

Introduction

I began this project with a desire to build a color picker which met three requirements:

  1. a large display area for showing the current color,
  2. ability to adjust each component of both the HSV and RGB color models independently, and
  3. make the relationships between the various color components clear and intuitive so that when you adjust one, you know ahead of time what the effect will be.

I specifically wanted to get away from the color "swatch" that is so common.

Background

I have always been frustrated by most color pickers. The display of the current color is always way too small to get a good idea of what the color will actually look like in my application. The color swatch that is so common is frustrating because it is difficult to find the right color or to make adjustments along one axis without drift along the other. Also, while many offer the ability to adjust the RGB components independently, and a few even allow adjusting HSV (Hue, Saturation, and Value) or HSL (Hue, Saturation, and Luminosity) values independently (Microsoft Word 2007, for example, though it has a very tiny display area for the current color), the relationship between the values still seems less intuitive or clear than it could be. The reason for this, I believe, is that it isn't made obvious how changing one component value will affect the resulting overall color, or how the different color models in use - usually HSV or HSL and RGB - relate to one another. One good way of dealing with this, I think, is to have sliders for each of the component values from both color models and to paint the background of each slider with a gradient brush that shows what the overall color would be if that slider were to be moved to any position along its range. This is what my version of the ubiquitous Color Picker does.

I first encountered the idea of using a linear gradient brush for the slider backgrounds in this article which describes a ColorSlider control that paints its background with a linear gradient brush, and has a SelectedColor property obtained by sampling the color of the slider's background at the point corresponding to its Value property. It's a fun idea, and I even played with it by setting up three sliders - the SelectedColor of one slider defining one end of the color gradient of the next. This is a fun idea, but it just wasn't practical for what I wanted to do.

I found the approach I would take reading the code for the color picker that comes with the Windows SDK, found here. It not only uses a slider for the Hue with a background created using a linear gradient brush, it does something so simple I couldn't believe I didn't think of it myself (sometimes the obvious is easy to miss...) The idea is this: Map the slider's value directly to the value of the color component it controls. That's it. The SDK sample uses a color swatch for adjusting the Saturation and Value components of the HSV color model, which is generated with a combination of linear gradient brushes, something I didn't want to duplicate, but which was instructive.

The Control

The control's interface is simple; it exposes a single dependency property named CurrentColor and a corresponding RoutedEvent: CorrentColorChanged. Through the magic of WPF automatic conversion, it is possible to two-way bind CurrentColor to a string, as well as a Color value, which is nice. The only limitation I have found with using it is as the content of a popup - because WPF grants a popup window a limited kind of focus, it is not possible to activate the text boxes for editing values directly, when the control is inside a popup. (If anyone has a workaround for this problem, I would be interested.)

There are seven sliders: three for the HSV (Hue, Saturation, Value) color model, and four for ARGB (Alpha, Red, Green, Blue). As discussed above, the backgrounds of the sliders are generated with Linear Gradient brushes. The really neat feature is that the backgrounds are kept in sync with the current color in such a manner that the background of each slider shows what the resulting color will be if that slider is moved. This turns out to be really interesting because it shows the relationship not only between values within the two color models, but how the models themselves relate to each other. Very instructive. Lastly, all the values, including the hexadecimal color value itself, can be set directly, with a text box.

EditableField.png

After implementing the slider backgrounds, I began to think of the control as a tool, and to that end, I added a few nice features:

  • Select a color from a display of the standard system colors (System.Windows.Media.Colors).
  • Set the control's background to equal the current color, or switch the background and current color, making it easy to see how two colors look together or how the text of one color looks in a specific background.
  • Support Ctrl-C and Ctrl-V to copy and paste the current color in hexadecimal notation.
  • Through a context menu, obtain a list of equidistant colors from the color range currently shown in each of the sliders.
ColorRange.png

The Code

The control is implemented as a custom control in WPF, meaning that it derives from System.Windows.Control and that it is a "lookless" control - 100% of its default visual appearance can be replaced without altering the code. It makes use of the theme system for applying the visual look. The default theme is implemented in the file generic.xaml, a file which is automatically generated when you create an item of type Custom Control in Visual Studio. (Note: For this project, I used Visual Studio 2010 Beta 2.)

In addition to the main control, there is a hierarchy of controls deriving from Windows.Controls.Slider which define the color sliders' behavior. The base class, which is abstract, is ColorSlider. ColorSlider derives directly from Slider. ColorSlider is responsible for ensuring that the Slider's background brush is a LinearGradientBrush and exposes helper methods for creating the brush and setting the gradient colors. These methods are virtual so that the Hue slider, which alone has more than two gradient stops in its background, can override the behavior to suit its needs. Below is the default implementation for creating the brush; note that it takes into account whether the slider is oriented horizontally or vertically and whether its IsDirectionReversed property is true.

/// <summary>
/// Creates the gradient brush used for the background.
/// Takes into account whether the slider is oriented
/// horizontally or vertically and whether or not its direction is reversed.
/// 
/// Note: All versions of ColorSlider, except Hue slider,
/// can use the same basic brush, only the colors differ. 
/// Hue slider overrides this method to create a brush with many more gradient stops.
/// </summary>
protected virtual LinearGradientBrush CreateGradientBrush()
{
    LinearGradientBrush brush = new LinearGradientBrush();
    brush.ColorInterpolationMode = ColorInterpolationMode.ScRgbLinearInterpolation; 
    if (this.Orientation == Orientation.Horizontal)
    {
        if (!this.IsDirectionReversed)
        {
            brush.StartPoint = new Point(0, 0.5);
            brush.EndPoint = new Point(1, 0.5);
        }
        else
        {
            brush.StartPoint = new Point(1, 0.5);
            brush.EndPoint = new Point(0, 0.5);
        }
    }
    else
    {
        if (!this.IsDirectionReversed)
        {
            // default direction for vertical slider
            // is to have Minimum (0) on bottom and Maximum (1) on top.
            // but the background brush is always orientated from top down.
            brush.StartPoint = new Point(0.5, 1);
            brush.EndPoint = new Point(0.5, 0);
        }
        else
        {
            brush.StartPoint = new Point(0.5, 0);
            brush.EndPoint = new Point(0.5, 1);
        }
    }
    // Not important what the colors are at this point.
    brush.GradientStops.Add(new GradientStop(Colors.Black, 0));
    brush.GradientStops.Add(new GradientStop(Colors.Black, 1));
    return brush;
}

ColorSlider's other responsibility is to create the lists of colors which are displayed when its context menu opens, from which the user can select a value indicating the number of discrete colors, evenly distributed from the slider's entire range, which he would like pasted into the clipboard. This requires the ability to determine the color at an arbitrary point along the primary axis of the linear gradient brush. With the understanding that the individual color component values are linearly distributed between any two adjacent gradient stops (which makes sense, given that Linear is in the brush's name):

private Color GetColorAtSliderPosition(double sliderPosition)
{
    LinearGradientBrush brush = this.LinearGradientBrush;

    // Normalize position to value between 0 and 1
    double normalized = (sliderPosition - Minimum) / (Maximum - Minimum);

    GradientStop gs0 = null;
    GradientStop gs1 = null; 

    // Find the two gradient stops which bound the normalized position
    for(int i = 1; i < brush.GradientStops.Count; i++)
    {
        if (brush.GradientStops[i].Offset >= normalized)
        {
            gs0 = brush.GradientStops[i - 1];
            gs1 = brush.GradientStops[i];
            break;
        }
    }

    // Now adjust the position so that it is relative
    // to the two gradient stops alone.
    float adjusted = (float)((normalized - gs0.Offset) / 
                             (gs1.Offset - gs0.Offset));

    // The individual color component values are linearly
    // distributed along the main axis, with the minimum and maximum
    // defined by the two bounding gradient stops, and the position
    // between them defined by the variable "adjusted".
    byte A = (byte)((gs1.Color.A - gs0.Color.A) * adjusted + gs0.Color.A);
    byte R = (byte)((gs1.Color.R - gs0.Color.R) * adjusted + gs0.Color.R);
    byte G = (byte)((gs1.Color.G - gs0.Color.G) * adjusted + gs0.Color.G);
    byte B = (byte)((gs1.Color.B - gs0.Color.B) * adjusted + gs0.Color.B);

    return Color.FromArgb(A, R, G, B);
}

Continuing with the color slider class hierarchy, deriving from ColorSlider are RgbSlider and HsvSlider. RgbSlider serves as the base class for the four RGB based sliders; it sets its Minimum and Maximum properties to 0 and 255, respectively. HsvSlider is the base class for the HSV based sliders, it's range is 0 - 1. (The odd ball is the Hue slider, which requires a range of 0 to 360.) Each of these classes defines an abstract method UpdateBackground. UpdateBackground is called on all seven sliders anytime CurrentColor changes. Both implementations of UpdateBackground take a single parameter holding the value of the color which serves as the basis for the background's color range. The difference between the two is the type of the color object. For the RGB version, this is the standard WPF Color type. The HSV version of UpdateBackground takes an instance of a class called HsvColor, which wraps the three HSV values (Hue, Saturation, and Value), and has methods for converting between the two color models. (The conversion methods are copied directly from the Color Picker sample in the Windows SDK.)

Updating the background of a color slider based on the currently selected color is straightforward. Since each slider controls one of the three values in (either) color model, all that is required is to hold constant the values that the slider does not control while varying the one value that it does control.

It was necessary to add one additional dependency property to the RgbSlider control, a string to hold the hexadecimal notation of the slider's value. The reason for this property is for binding to a text box, to allow the user to edit the hex value directly. While it is possible to format the display of a bound value as hex using the Binding object's StringFormat property, that is a one-way conversion only. The default method for converting strings to numbers does not allow Hexadecimal notation.

I decided to go ahead and define the slider classes which are specific to each of the individual color component values, even though there is really only ever a single instance of each. Deriving from RgbSlider are AlphaSlider, RedSlider, GreenSlider, and BlueSlider, and deriving from HsvSlider are HueSlider, SaturationSlider, and ValueSlider. Each of these is responsible for painting its own background, which is about it (other than HueSlider, which needs to set its own minimum and maximum and create its own linear gradient brush).

That's pretty much it for the class structure. Here's a quick overview of how it all works together:

The action starts in the overridden method ColorPicker.OnApplyTemplate(). First off, it gets handles to each of the seven color sliders along with the Selector which houses the list of standard colors. Note that all necessary checks are in place so that none of the objects is required. The Selector is the only one of the objects that are defined in XAML which requires a specific name in order for the code to find it. Since the color sliders are all strongly typed, they can easily be found in the visual tree. Once the sliders' handles have been assigned, their backgrounds are updated with the current color, their values are set, and finally, a separate handler is added to each slider's OnValueChanged event.

The control is now ready for input, which can come from one of three sources: the user drags a slider, the user enters a value in a textbox, or the control's CurrentColor property is updated by its host. As far as ColorPicker is concerned, it doesn't matter how a slider's value is changed - by sliding, or by entering a value in a text box which is bound to the slider's value - all that matters is the value changes. ColorPicker is informed of the event via the event handler which is assigned to the slider, and what it does depends on whether the slider that changed was of the HSV group or the RGB group - the new color is obtained from the slider value of this group. In both cases, CurrentColor is updated, the backgrounds (with their linear gradient brushes) are updated, and the values of the sliders in the Other group are updated.

If CurrentColor is updated from outside via the control's host, the process is the same, except that all slider values need to be updated and CurrentColor is given.

One of the problems that come up when CurrentColor is changed is the potential for an infinite recursion. When CurrentColor changes, the slider values are changed, then in the slider event handlers, CurrentColor is updated, which causes the slider values to be set again. It turns out the easiest and surest way to handle this is simply to remove the event handlers from the sliders before updating their values.

The XAML

Following the pattern set up by Visual Studio when creating a Custom Control project item, the entire visual aspect of the ColorPicker is defined inside a style - more particularly, the style's Template property:

<Style TargetType="{x:Type local:ColorPicker}"> 
     <Setter Property="Padding" Value="10" />           
     <Setter Property="Template">
         <Setter.Value>
             <ControlTemplate TargetType="{x:Type local:ColorPicker}">
            .
            .

Adding a ColorSlider is no different than adding any other slider. Note the context menu. The slider's context menu allows the user to copy to the clipboard a list of colors that are evenly distributed along the slider's background. Each menu item represents a different number of colors, and displays an array of Color objects. The style for that is interesting because it not only re-templates ContextMenu but also defines an ItemTemplate for displaying the Color array, including text that is formatted with the Binding object's StringFormat property. The item template contains a list box which itself is re-templated so that its contents are laid out horizontally.

<local:SaturationSlider x:Name="saturationSlider" 
                            Orientation="Vertical" 
                            IsDirectionReversed="True" 
                            IsMoveToPointEnabled="True">
    <Slider.ContextMenu>
        <ContextMenu Style="{StaticResource ColorRangeContextMenuStyle}" />
    </Slider.ContextMenu>
</local:SaturationSlider>

<Style x:Key="ColorRangeContextMenuStyle" TargetType="ContextMenu">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ContextMenu}">
                <Border BorderThickness="2" BorderBrush="Black"
                        Background="{Binding RelativeSource={RelativeSource 
                                    Mode=FindAncestor, 
                                    AncestorType=local:ColorPicker}, 
                                    Path=Background}">
                    <DockPanel LastChildFill="True">
                        <TextBlock Margin="5" 
                            DockPanel.Dock="Top" 
                            Text="Copy Color Range" />
                        <ItemsPresenter 
                            SnapsToDevicePixels=
                              "{TemplateBinding SnapsToDevicePixels}" />

                    </DockPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <ItemsControl ItemsSource="{Binding}">
                        <ItemsControl.Template>
                            <ControlTemplate>
                                <Border>
                                    <StackPanel 
                                      Orientation="Horizontal" 
                                      IsItemsHost="True" />
                                </Border>
                            </ControlTemplate>
                        </ItemsControl.Template>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Border Width="10" Height="10">
                                    <Border.Background>
                                        <SolidColorBrush Color="{Binding}" />
                                    </Border.Background>
                                </Border>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                    <TextBlock Margin="3,0,0,0" 
                      Text="{Binding Count, StringFormat={}({0})}" />
                </StackPanel>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

The Sliders' visual appearance is also defined in styles. All the RGB sliders share the same style, while the HSV sliders for saturation and value share a style. HueSlider is defined alone. The thumbs are interesting. You might have noticed in the pictures that arrow pointers slide all the way from min to max, with the arrow's body hanging out past the bounds of the control itself. This is possible simply by setting the Margin property to a negative value.

<Thumb Margin="0,-7,-3,-7" 
          Foreground="Black" 
          HorizontalAlignment="Right" 
          ToolTip="{TemplateBinding Value}">

Finally, using ColorPicker itself in XAML is as simple as this:

<Grid>
    <picker:ColorPicker CurrentColor="Blue"/>
</Grid>

That's it! I hope that you enjoyed this article and like the color picker. It was a fun project that taught me a lot about WPF and about how the different color variables from the two color models relate to one another.

Addendum

After writing this article, I discovered that the color picker in Paint.Net (a fine program) has sliders for all seven color values, and their backgrounds are painted with gradients that are updated as the current color changes, etc. Plus, it includes a color wheel. However, it is all so tiny that you can barely see any of its pieces!

History

  • 3rd November, 2009: Initial post
  • 14th November, 2009: Updated source code (bug fix)
  • 29th November, 2009: Updated demo

License

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

Share

About the Author

Donald Wingate

United States United States
No Biography provided

Comments and Discussions

 
GeneralThanks PinmemberDavid Veeneman26-Dec-09 3:05 
GeneralNice Pinmembermartyn_mcfly29-Nov-09 9:37 

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.140922.1 | Last Updated 29 Nov 2009
Article Copyright 2009 by Donald Wingate
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid