Click here to Skip to main content
13,554,127 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

5.1K views
5 bookmarked
Posted 17 Sep 2016
Licenced CPOL

WPF Color Picker

, 17 Sep 2016
Rate this:
Please Sign up or sign in to vote.
An advanced color picker for WPF.

Introduction

This article explores a couple different ways to implement a color picker.

Table Of Contents

Background

The project started out as a modification of the one designed by Ken Johnson and so I would like to give him a shoutout for laying out the groundwork. The problem with his project, unfortunately, is reusability is poor and there are many things that can be improved.

The goal was to reduce excess code, establish uniformity, and define underlying logic in such a way additional color spaces can be added and removed with ease. Johnson defines a UserControl for several components: Upon taking a closer look, you will notice repetition that simply isn't necessary and makes it difficult to support additional models like XYZ and LCH without tedious copying and pasting.

Now, to add support for additional color models, you require two essential components:

  1. A view model that encapsulates a given color space and defines the logic of its associated components therein.
  2. A structure that represents a color in a given color space and is capable of converting to and from other color spaces.

This leaves us with a few advantages:

  1. The view is further separated from the logic.
  2. The view may be arranged more easily and introduces a uniformity absent from other implementations.
  3. The orientation of the input for a given color space may be changed. For instance, in Photoshop, the input for CMYK is arranged horizontally whereas others are arranged vertically.
  4. You may also specify whether or not a color space should be represented visually. CMY is the inverse of RGB; therefore, there is no need to represent it visually.
  5. You may add and remove support for a given color space at run time.

And also some disadvantages:

  1. The logic is somewhat complicated to learn, initially
  2. It is not possible to represent the value of a component in more than one way (e.g., Rgb values are represented in a range from 0 to 255, but cannot be represented in other ranges, such as from 0 to 1).

Download

You may find the latest version and demo on GitHub.

Preview

What's A Color Space?

A color space, also known as a color model (or color system), is an abstract mathematical model which simply describes the range of colors as tuples of numbers, typically as 3 or 4 values or color components[1]. The color spaces you're probably most familiar with are RGB, HSB, and LAB (courtesy of Adobe).

The color spaces implemented in this solution are:

  1. CMYK*
  2. HSB
  3. HSL
  4. LAB
  5. LCH
  6. LUV
  7. RGB
  8. XYZ
  9. YUV (or YXY)

* Logic representation only

Perhaps the most mysterious is the CIE series, which consists of XYZ, LAB, LCH, and so forth. They are non-traditional in that they are rarely used in the real world and are easily misunderstood. I've never seen a practical representation of XYZ so including it in my solution was a must.

[1] http://www.arcsoft.com/topics/photostudio-darkroom/what-is-color-space.html

How Do We Model Color?

Ken seemed to have the right idea at first glance, but as the number of color spaces in this project increased, so did my frustation and patience. The best way for me to model a color was to define a structure that encapsulated the math and logic and a corresponding view model to communicate this logic to the view.

Structure

Note, some conversion logic is omitted for brevity...

/// <summary>
/// Structure to define a color in <see cref="ColorSpace.Rgb"/>.
/// </summary>
[Serializable]
public struct Rgb : IColor, IEquatable<Rgb>
{
    #region Properties

    /// <summary>
    /// Specifies a <see cref="Rgb"/> component.
    /// </summary>
    public enum Component
    {
        /// <summary>
        /// Specifies the <see cref="Rgb.R"/> component.
        /// </summary>
        R,
        /// <summary>
        /// Specifies the <see cref="Rgb.G"/> component.
        /// </summary>
        G,
        /// <summary>
        /// Specifies the <see cref="Rgb.B"/> component.
        /// </summary>
        B
    }

    public const byte Maximum = byte.MaxValue;

    public const byte Minimum = byte.MinValue;

    public Color Color
    {
        get => Color.FromArgb(255, r, g, b);
    }

    readonly byte r;
    /// <summary>
    /// Gets the <see cref="Component.R"/> component (0 to 255).
    /// </summary>
    public byte R
    {
        get => r;
    }

    readonly byte g;
    /// <summary>
    /// Gets the <see cref="Component.G"/> component (0 to 255).
    /// </summary>
    public byte G
    {
        get => g;
    }

    readonly byte b;
    /// <summary>
    /// Gets the <see cref="Component.B"/> component (0 to 255).
    /// </summary>
    public byte B
    {
        get => b;
    }

    #endregion

    #region Rgb

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Color source) : this(source.R, source.G, source.B) {}

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="r"></param>
    /// <param name="g"></param>
    /// <param name="b"></param>
    public Rgb(int r, int g, int b) : this(r.Coerce(Maximum).ToByte(), g.Coerce(Maximum).ToByte(), b.Coerce(Maximum).ToByte()) {}

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="_r"></param>
    /// <param name="_g"></param>
    /// <param name="_b"></param>
    public Rgb(byte _r, byte _g, byte _b)
    {
        r = _r;
        g = _g;
        b = _b;
    }

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Cmyk source)
    {
        //Conversion logic...
    }

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Hsb source)
    {
        //Conversion logic...
    }

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Hsl source)
    {
        //Conversion logic...
    }

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Lab source) : this(new Xyz(source)) {}

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Lch source) : this(new Lab(source)) {}

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Luv source) : this(new Xyz(source)) {}

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Xyz source)
    {
        //Conversion logic...
    }

    /// <summary>
    /// Initializes an instance of the <see cref="Rgb"/> structure.
    /// </summary>
    /// <param name="source"></param>
    public Rgb(Yuv source) : this(new Xyz(source)) {}

    public static bool operator ==(Rgb left, Rgb right)
    {
        if (ReferenceEquals(left, null))
        {
            if (ReferenceEquals(right, null))
                return true;

            return false;
        }
        return left.Equals(right);
    }

    public static bool operator !=(Rgb left, Rgb right) => !(left == right);

    #endregion

    #region Methods

    public bool Equals(Rgb o)
    {
        if (ReferenceEquals(o, null))
            return false;

        if (ReferenceEquals(this, o))
            return true;

        if (GetType() != o.GetType())
            return false;

        return (R == o.R) && (G == o.G) && (B == o.B);
    }

    public override bool Equals(object o) => Equals((Rgb)o);

    public override int GetHashCode() => new { R, G, B }.GetHashCode();

    public override string ToString() => "R => {0}, G => {1}, B => {2}".F(r, g, b);

    public static double Linear(byte value) => Linear(value.ToInt32());

    public static double Linear(int value) => value.ToDouble() / Maximum.ToDouble();

    #endregion
}

Each structure MUST accomplish the following:

  1. Define a maximum and minimum value for each component.
  2. Coerce all incoming values to the appropriate range (e.g., Rgb.R should be coerced to a range of [0, 255]).
  3. Convert to a color of any given representation and then back.
  4. Provides ability to check for equality.
  5. Implement the interface, IColor.
/// <summary>
/// Specifies a color.
/// </summary>
public interface IColor
{
    /// <summary>
    /// The color.
    /// </summary>
    Color Color
    {
        get;
    }
}

It's all about representation

This is where things can get confusing quickly. RGB values are most commonly represented as a whole number with range [0, 255]. The byte type stores RGB values perfectly; however, RGB values can also be represented as an irrational number with range [0, 1] (by dividing the value by 255) or with range [0, 2.55] (by dividing the value by 100); the latter is also perhaps the rarest and least practical.

Despite these many representations, the structure should recognize only one. Alternative representations are sometimes necessary when the view model must communicate with the structure, but should be absent entirely within the structure itself to avoid confusion.

There are two major ways to represent the value of a given component:
  1. Logically 
    • The value is represented as a whole number with a range comprised of all logical values defined by the color space.
    •  
  2. Mathematically 
    • The value is represented as a percentage based on its logical counterpart.
How do we represent the values of a component?

We represent the values logically. Some values are stored using byte, int, or double depending on whether or not accuracy is a concern or even relevant. Accuracy only becomes a concern when dealing with XYZ or XYZ-derived color spaces, such as LAB, LCH, and so forth.

Should each structure utilize a logical representation?

Ideally, yes. This avoids confusion surrounding the representation of a value as its passed from one conversion method to the next.

For example, in HSB, the Hue component has the range [0, 359], which corresponds to a given degree or radian. It is more intuitive to represent the degree directly [0, 359] versus a ratio [0, 1].

Are mathematical representations used at all?

Previous implementations did utilize this representation, but have since been replaced.

 

H = 245 = 245 / 359
S = 60 = 60 / 100
B = 10 = 10 / 100

It is VERY easy to accidentally divide by the wrong value so pay special attention to that in every place this matters (note, this should only be addressed in the model, not the primitive).

The last thing to note about primitives is they handle ALL conversion to and from each color space as required. So what are the additional models for then? Well, binding to the view, for one, and handling mathematical logic not related to conversion, but instead to the view and how you interact with it. Interaction with the view will change mathematically for each color space. That is because for each color component of each color space, we have to draw it's color representation onto an image mathematically, which is based on it's value, it's value's range, and, in some cases, the x/y coordinate of the mouse relative to a given UI element.

The model specifies some very important things:

  • What unit it should display for the color component.
  • The min and max values for each component in integer form (not primitive form!). For instance, the min and max value for the h component in HSB is 0.0 and 1.0, respectively; to find the max value in integer form, you'd have to know the highest possible value for the h component as an integer and multiply that by the h component value. The max value for hue is 359 so the max value for the h component in integer form would be h component value * 359, which makes sense because the highest value in decimal form is 1.0, which, when multiplied by 359, equals the highest value in integer form. 
  • How to draw the slider, which changes value of selected component only
  • How to draw the color plane, which is comprised of two opposing components going in opposite directions
  • How to get a color from a point (e.g., when you click anywhere on the color plane, we get that point and, based on the selected component, turn that into a color to show as the selected color.
  • How to get a point from a color

The model base class is where things might get a little confusing, but what ultimately helped in the copy/paste dilemma I stated in the beginning. Before, each UserControl that represented a color space contained all this in a litter of methods that were literally the same for each control (what a nightmare, I know). What I found out though, is that despite each color space's many differences, all can be drawn in a super uniform fashion: Hence, why you see calls to base methods in each component's model.

base.UpdatePlane(Bitmap, ComponentValue, new Func<RowColumn, int, Rgba>((RowColumn, Value) =>
{
    return new Rgba(RowColumn.Column.ToByte(), RowColumn.Row.ToByte(), ComponentValue.ToByte());
}), new RowColumn(255, 255));

The base method takes care of the actual drawing part, which just enumerates the rows/columns of the image we want to draw on and inserts the appropriate color based on the conversion specified in the model. Bitmap parameter gets the bitmap we want to draw on: There are two main bitmaps we use to draw on, a) the slider bitmap and, b) the color plane bitmap. The ComponentValue parameter remains static while two other components increase in opposing directions. This opposition is implemented by specifying a max value for both the row and the column (dependant on their corresponding components) and the color is created by inserting these values into a primitive type, which handles the actual color conversion. We return the obtained color in a Func, which exposes the current row and column. Once this Func is returned, the base method handles assigning this color to it's corresponding pixel location. Good enough for me!

Now let's take a look at the base method:

unsafe
{
    Bitmap.Lock();

    int CurrentPixel = -1;
    byte* Start = (byte*)(void*)Bitmap.BackBuffer;

    var u = Unit.Value;
    u.Row = u.Row / 256.0;
    u.Column = u.Column / 256.0;

    double CurrentRow = u.Row * 256.0;

    for (int Row = 0; Row < Bitmap.PixelHeight; Row++)
    {
        double CurrentCol = 0;
        for (int Col = 0; Col < Bitmap.PixelWidth; Col++)
        {
            var c = Action.Invoke(new RowColumn(CurrentRow, CurrentCol), ComponentValue);

            CurrentPixel++;
            *(Start + CurrentPixel * 3 + 0) = c.B;
            *(Start + CurrentPixel * 3 + 1) = c.G;
            *(Start + CurrentPixel * 3 + 2) = c.R;
            CurrentCol += u.Column;
        }
        CurrentRow -= u.Row;
    }
    Bitmap.AddDirtyRect(new Int32Rect(0, 0, Bitmap.PixelWidth, Bitmap.PixelHeight));
    Bitmap.Unlock();
}

We are forced to use unsafe code as we're working with pointers now and this is especially imporant. Above you will find one of the fastest ways I've found to draw images, even with questionable conversion algorithms. As you can see, there are some values that remain static and for good reason.

The last parameter for this method is Unit, of type RowColumn, which specifies the unit to increment/decrement from rows/columns and is unique to the color space's opposing component values. The starting row is always the last and the row unit is decremented from it. That is because the image is 256 pixels by 256 pixels and the max values for opposing components are dissimilar. Consider this: If the color space was RGB, this extra math would not even be needed as the range for any RGB value contains a total of 256 values! This is perhaps one of the most confusing aspects about this algorithm, but hopefully it's organized in a way that makes sense for everybody.

Drawing the slider is very much the same, except a color found for one row is repeated for all columns, but different for all rows.

XYZ & It's Many Forms

If one thing's for sure, the year 1931 was a confusing one for mathematicians. That is because instead of just developing one color space, they decided it was best to develop many, and many variations of said many. One such is CIE XYZ, sometimes referred to as CIE 1931 or CIE 1964: It is important to recognize the differences between the two (I'm still having issues understanding myself, to be honest) and what exactly makes them so different.

From what I understand this is how XYZ works:

  • XYZ acts as a "gateway" to other CIE models like LAB, LCH, LUV, and many, many more; i.e., in order to calculate RGB from LAB, you'd first convert LAB to XYZ, then XYZ to RGB.
  • XYZ itself has many variations, which are determined via "illuminants" and "observers".
  • There are seemingly lots of illuminants to choose from, but the most widely used is D65, which my solution uses by default; for convenience, I defined about 6 other ones, the others I could not find values for. In fact, though F2 and F7 are the most popular, there is actually a whole series starting with F1 and ending at F11 with the possibility it goes even further.
  • There are two main observers: 1931 or 2°, and 1964 or 10°; 1931 is the observer I chose.
  • An illuminant essentially determines the min and max values for X, Y, & Z, with Y consistently and almost always having a max value of 1.0.
  • All illuminants have a min value of 0.
  • There is one "theoretical" illuminant that I know of "E," and it is not clear if this should be represented.
  • It is not certain if all illuminants can be accurately represented.
  • My XYZ conversion seems right, but seems off, and this is mostly apparent with LCH implementation. 
  • CIE LAB and Hunter LAB are completely different models! CIE LAB is used in my solution and requires converting to and from CIE XYZ; to my knowledge, Adobe has and continues to use Hunter LAB.
  • It is not clear what the minimum and maximum values for LUV are, though, I suspect it is 0 to 1 for L and -1 to 1 for U and V. For this reason, LUV is excluded by default, but can be included at any time for those who like to experiment, such as myself. 
  • Most XYZ variants look more or less the same, but it would be nice to have a way to quickly and easily change the illuminant and observer if so desired.
  • There is little mathematical research to prove some of my observations so a lot has been inferred and assumed as a result.

Refactoring The View

One thing that made me unhappy with Ken's solution is having a separate UserControl for each color space. I mean, why? They all share the same attributes and though technically some are different from others in one way or another, I saw no good reason to do it that way. MVVM and binding do exist for a reason, after all. 

Having said, let's take a look at the new view:

<ItemsControl ItemsSource="{Binding Models, ElementName=PART_ColorPicker}" VerticalAlignment="Center">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:ColorSpaceModel}">
            <DataTemplate.Resources>
                <Common.Mvvm:BindingProxy x:Key="ComponentProxy" Data="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}"/>
            </DataTemplate.Resources>
            <local:ColorSpaceView x:Name="PART_Components" ItemsSource="{Binding Components}" Margin="0,0,0,15">
                <local:ColorSpaceView.ItemTemplate>
                    <DataTemplate DataType="{x:Type local:ComponentModel}">
                        <DataTemplate.Resources>
                            <Common.Data.Converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
                        </DataTemplate.Resources>
                        <local:ComponentView 

                            ColorSpaceModel="{Binding DataContext, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type local:ColorSpaceView}}}"

                            Color="{Binding SelectedColor, RelativeSource={RelativeSource AncestorType={x:Type local:ColorPicker}}}"

                            ComponentModel="{Binding Mode=OneWay}"

                            CurrentValue="{Binding CurrentValue}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="50" />
                                    <ColumnDefinition Width="15" />
                                </Grid.ColumnDefinitions>
                                <RadioButton 

                                    Checked="OnComponentChecked"

                                    Content="{Binding ComponentLabel}"

                                    GroupName="ColorSpace"

                                    IsChecked="{Binding IsEnabled}"

                                    HorizontalAlignment="Center"

                                    Margin="5,0"

                                    VerticalAlignment="Center" 

                                    Tag="{Binding Mode=OneWay}"

                                    Visibility="{Binding CanSelect, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                                <TextBlock 

                                    HorizontalAlignment="Center"

                                    VerticalAlignment="Center" 

                                    Margin="5,0"

                                    Text="{Binding ComponentLabel}" 

                                    Visibility="{Binding CanSelect, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverted}"/>
                                <Controls.Common:AdvancedTextBox

                                    Grid.Column="1" 

                                    HorizontalAlignment="Center"  

                                    HorizontalContentAlignment="Center" 

                                    VerticalAlignment="Center" 

                                    Text="{Binding CurrentValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

                                    Width="40"/>
                                <TextBlock 

                                    Grid.Column="2"

                                    Text="{Binding UnitLabel}"

                                    VerticalAlignment="Center"/>
                            </Grid>
                        </local:ComponentView>
                    </DataTemplate>
                </local:ColorSpaceView.ItemTemplate>
            </local:ColorSpaceView>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Orientation}" Value="Horizontal">
                    <Setter TargetName="PART_Components" Property="ItemsPanel">
                        <Setter.Value>
                            <ItemsPanelTemplate>
                                <Controls.Common:Spacer Spacing="0,0,5,0" Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
                <DataTrigger Binding="{Binding Orientation}" Value="Vertical">
                    <Setter TargetName="PART_Components" Property="ItemsPanel">
                        <Setter.Value>
                            <ItemsPanelTemplate>
                                <Controls.Common:Spacer Spacing="0,0,0,10"/>
                            </ItemsPanelTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

The view basically consists of this:

  • The ItemsControl that houses all available color models. To achieve the same look as before, all color spaces go in a WrapPanel, perhaps giving the illusion they are still in a Grid.
  • The color space is then represented by a control called ColorSpaceView, which does NOTHING except facilitate communication between the ColorPicker control and the color space.
  • Each color space model has an array of components, which are added when the model is initialized. These are bound to ColorSpaceView.
  • Each color component is represented by the control ComponentView, which both facilitates communication between the color space and each of it's individual components AND handles the majority of view-related concerns. One such concern is being able to change the color component value based on text typed and the text based on selected color without causing either to fall into an infinite loop; e.g., if one property changes, we must change the other, but if the other changes, it will cause the other to change again. That concern is addressed by paying attention to those changes.
  • If a color space has horizontal orientation (so far, CmykModel is the only one), it is displayed horizontally versus the default vertical orientation.

Final Remarks

Aside from the primitives, models, and refactored view, everything else is about the same as what you will find in Ken's project. The difference is, in every place Ken chose not to use binding, I chose to use binding. This required the use of a few converters (such as one that converts hex to a Color, a Color to a SolidColorBrush and so forth) and takes away much of the excess code I complained about before.

One other major change was I chose to go for one color picker design with the option to hide and show elements as desired, whereas Ken's had ability to display different types of pickers (e.g., a color picker with alpha slider and one without). In my opinion, I could not see any reason for wanting so many kinds of pickers and even then, it seemed more logical to just enable toggling visibility of the elements rather than define whole new controls. 

Points Of Interest

  • Algorithms are based on those published on EasyRGB.

  •  

Question For Readers

  • Do you think it is unnecessary to have so many color space models in one color picker or the more the merrier? For me, it's always merrier!

History

  • 17th of September, 2016
    • Initial post

Future

The code in this article is now part of the open source project, Imagin.NET

License

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

Share

About the Author

James J M
Software Developer Imagin
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.180515.1 | Last Updated 17 Sep 2016
Article Copyright 2016 by James J M
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid