Click here to Skip to main content
13,146,742 members (79,742 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

3.6K views
4 bookmarked
Posted 17 Sep 2016

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 one way to implement a color picker and most closely resemble's Adobe's.

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 isn't the best and there are many things that can be improved.

My goal was to condense excess code, create more uniformity, and make mathematical logic easy to follow in such a way additional color spaces can be added and removed with ease. Johnson creates a UserControl for almost all components that make up the ColorPicker; upon taking a closer look, I noticed much repetition that simply isn't necessary and found it a pain to support additional models like XYZ and LCH (the copying and pasting was unreal). Now, to add support for additional color models, you require two things:

  1. The color space model and a model for each of it's components contained therein, and
  2. A primitive type that represents the color space and handles converting to/from other color spaces.

This leaves us with a few advantages:

  1. The view is completely separate from the logic.
  2. The view can be changed easily and is uniform for all models.
  3. The view can be customized depending on the type of color space (e.g., CMYK should not be selectable for the reason it's color space is literally the inverse of RGB and would be unnecessary; it also has a horizontal orientation versus the default vertical).
  4. DataTemplates and DataTemplateSelectors can most certainly be used.

And also some disadvantages:

  1. The view for all models are uniform to an extent.
  2. It is somewhat complicated to understand the logic at first.
  3. Changing component value representation is no longer supported though it is possible to implement this functionality in the model.
  4. You can't display a view for a single color space (without the entire color picker); this can still be accomplished using a ContentControl and a DataTemplateSelector, but minor refactoring will be involved.

Download

You may find the latest version and a demo on CodePlex.

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 the latter in addition to HSL, XYZ, and LCH (with LUV under wraps).

One of the most mysterious to me was the CIE series, which consists of XYZ (with many variations), LAB, LCH, and so forth. They are considered non-traditional in that they are rarely used in the real world and are often ignored. Having never seen a true representation of XYZ, it was a must and a satisfying learning experience at that.

[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 color was to define a number of primitive types that handle math and conversion, and a model for each primitive that describes the primitive more in-depth. All primitives are value types and all models are intended to communicate the primitive with the view.

Let's take a look at an example model:

public class RgbModel : ColorSpaceModel
{
    public override Color GetColor()
    {
        return new Rgba(this.Components[typeof(RComponent)].CurrentValue.ToByte(), this.Components[typeof(GComponent)].CurrentValue.ToByte(), this.Components[typeof(BComponent)].CurrentValue.ToByte()).ToColor();
    }

    public RgbModel() : base()
    {
        this.Components.Add(new RComponent());
        this.Components.Add(new GComponent());
        this.Components.Add(new BComponent());
    }

    public sealed class RComponent : NormalComponentModel
    {
        public override string ComponentLabel
        {
            get
            {
                return "R";
            }
        }

        public override string UnitLabel
        {
            get
            {
                return "";
            }
        }

        public override int MaxValue
        {
            get
            {
                return 255;
            }
        }

        public override Color ColorAtPoint(Point SelectionPoint, int ComponentValue)
        {
            var blue = SelectionPoint.X.Round().ToByte();
            var green = (255.0 - SelectionPoint.Y).Round().ToByte();
            var red = ComponentValue.ToByte();
            return Color.FromRgb(red, green, blue);
        }

        public override int GetValue(Color Color)
        {
            return Color.R;
        }

        public override Point PointFromColor(Color Color)
        {
            return new Point(Color.B, 255 - Color.G);
        }

        public override void UpdateSlider(WriteableBitmap Bitmap, Color Color, Func<Color, double, Rgba> Action, bool Reverse = false)
        {
            base.UpdateSlider(Bitmap, Color, new Func<Color, double, Rgba>((c, CurrentRow) =>
            {
                return new Rgba((255 - CurrentRow).ToByte(), Color.G, Color.B);
            }), true);
        }

        public override void UpdatePlane(WriteableBitmap Bitmap, int ComponentValue, Func<RowColumn, int, Rgba> Action = null, RowColumn? Unit = null)
        {
            base.UpdatePlane(Bitmap, ComponentValue, new Func<RowColumn, int, Rgba>((RowColumn, Value) =>
            {
                return new Rgba(ComponentValue.ToByte(), RowColumn.Row.ToByte(), RowColumn.Column.ToByte());
            }), new RowColumn(255, 255));
        }
    }

    public sealed class GComponent : NormalComponentModel
    {
        public override string ComponentLabel
        {
            get
            {
                return "G";
            }
        }

        public override string UnitLabel
        {
            get
            {
                return "";
            }
        }

        public override int MaxValue
        {
            get
            {
                return 255;
            }
        }

        public override Color ColorAtPoint(Point SelectionPoint, int ComponentValue)
        {
            var blue = (byte)Math.Round(SelectionPoint.X);
            var green = (byte)ComponentValue;
            var red = (byte)Math.Round(255 - SelectionPoint.Y);
            return Color.FromRgb(red, green, blue);
        }

        public override int GetValue(Color Color)
        {
            return Color.G;
        }

        public override Point PointFromColor(Color Color)
        {
            return new Point(Color.B, 255 - Color.R);
        }

        public override void UpdateSlider(WriteableBitmap Bitmap, Color Color, Func<Color, double, Rgba> Action, bool Reverse = false)
        {
            base.UpdateSlider(Bitmap, Color, new Func<Color, double, Rgba>((c, CurrentRow) =>
            {
                return new Rgba(Color.R, (255.ToDouble() - CurrentRow).ToByte(), Color.B);
            }), true);
        }

        public override void UpdatePlane(WriteableBitmap Bitmap, int ComponentValue, Func<RowColumn, int, Rgba> Action = null, RowColumn? Unit = null)
        {
            base.UpdatePlane(Bitmap, ComponentValue, new Func<RowColumn, int, Rgba>((RowColumn, Value) =>
            {
                return new Rgba(RowColumn.Row.ToByte(), ComponentValue.ToByte(), RowColumn.Column.ToByte());
            }), new RowColumn(255, 255));
        }
    }

    public sealed class BComponent : NormalComponentModel
    {
        public override string ComponentLabel
        {
            get
            {
                return "B";
            }
        }

        public override string UnitLabel
        {
            get
            {
                return "";
            }
        }

        public override int MaxValue
        {
            get
            {
                return 255;
            }
        }

        public override Color ColorAtPoint(Point SelectionPoint, int ComponentValue)
        {
            var blue = (byte)ComponentValue;
            var green = (byte)Math.Round(255 - SelectionPoint.Y);
            var red = (byte)Math.Round(SelectionPoint.X);
            return Color.FromRgb(red, green, blue);
        }

        public override int GetValue(Color Color)
        {
            return Color.B;
        }

        public override Point PointFromColor(Color Color)
        {
            return new Point(Color.R, 255 - Color.G);
        }

        public override void UpdateSlider(WriteableBitmap Bitmap, Color Color, Func<Color, double, Rgba> Action, bool Reverse = false)
        {
            base.UpdateSlider(Bitmap, Color, new Func<Color, double, Rgba>((c, CurrentRow) =>
            {
                return new Rgba(Color.R, Color.G, (255.ToDouble() - CurrentRow).ToByte());
            }), true);
        }

        public override void UpdatePlane(WriteableBitmap Bitmap, int ComponentValue, Func<RowColumn, int, Rgba> Action = null, RowColumn? Unit = null)
        {
            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));
        }
    }
}

Every color space model follows this pattern and each new one you create must follow it as well. Does this code confuse you? If it doesn't, I'd be surprised. It is not clear just by observing the model what this model is or isn't supposed to do, nor is it clear what type of view this model was designed for. To better understand this model, let's skip to the primitive type associated with this model and then come back.

[Serializable]
/// <summary>
/// Structure to define RGBA.
/// </summary>
public struct Rgba
{
    #region Properties

    public struct MaxValue
    {
        public static byte R = 255;

        public static byte G = 255;

        public static byte B = 255;

        public static byte A = 255;
    }

    public struct MinValue
    {
        public static byte R = 0;

        public static byte G = 0;

        public static byte B = 0;

        public static byte A = 0;
    }

    byte r;
    /// <summary>
    /// Gets or sets the red component (0 to 255).
    /// </summary>
    public byte R
    {
        get
        {
            return r;
        }
        set
        {
            r = value.Coerce(Rgba.MaxValue.R, Rgba.MinValue.R);
        }
    }

    byte g;
    /// <summary>
    /// Gets or sets the green component (0 to 255).
    /// </summary>
    public byte G
    {
        get
        {
            return g;
        }
        set
        {
            g = value.Coerce(Rgba.MaxValue.G, Rgba.MinValue.G);
        }
    }

    byte b;
    /// <summary>
    /// Gets or sets the blue component (0 to 255).
    /// </summary>
    public byte B
    {
        get
        {
            return b;
        }
        set
        {
            b = value.Coerce(Rgba.MaxValue.B, Rgba.MinValue.B);
        }
    }

    byte a;
    /// <summary>
    /// Gets or sets the alpha component (0 to 255).
    /// </summary>
    public byte A
    {
        get
        {
            return a;
        }
        set
        {
            a = value.Coerce(Rgba.MaxValue.A, Rgba.MinValue.A);
        }
    }

    #endregion

    #region Rgba

    public static bool operator ==(Rgba a, Rgba b)
    {
        return a.R == b.R && a.G == b.G && a.B == b.B && a.A == b.A;
    }

    public static bool operator !=(Rgba a, Rgba b)
    {
        return a.R != b.R || a.G != b.G || a.B != b.B || a.A != b.A;
    }

    /// <summary>
    /// Creates an instance of a Rgba structure.
    /// </summary>
    public Rgba(int R, int G, int B, int A = 255)
    {
        this.r = R.Coerce(Rgba.MaxValue.R, Rgba.MinValue.R).ToByte();
        this.g = G.Coerce(Rgba.MaxValue.G, Rgba.MinValue.G).ToByte();
        this.b = B.Coerce(Rgba.MaxValue.B, Rgba.MinValue.B).ToByte();
        this.a = A.Coerce(Rgba.MaxValue.A, Rgba.MinValue.A).ToByte();
    }

    /// <summary>
    /// Creates an instance of a Rgba structure.
    /// </summary>
    public Rgba(byte R, byte G, byte B, byte A = 255)
    {
        this.r = R.Coerce(Rgba.MaxValue.R, Rgba.MinValue.R);
        this.g = G.Coerce(Rgba.MaxValue.G, Rgba.MinValue.G);
        this.b = B.Coerce(Rgba.MaxValue.B, Rgba.MinValue.B);
        this.a = A.Coerce(Rgba.MaxValue.A, Rgba.MinValue.A);
    }

    #endregion

    #region Methods

    public override bool Equals(Object Object)
    {
        if (Object == null || GetType() != Object.GetType()) return false;

        return (this == (Rgba)Object);
    }

    public override int GetHashCode()
    {
        return R.GetHashCode() ^ G.GetHashCode() ^ B.GetHashCode();
    }

    public override string ToString()
    {
        return string.Format("R => {0}, G => {1}, B => {2}", this.R.ToString(), this.G.ToString(), this.B.ToString());
    }

    public double Distance(Color First, Color Second)
    {
        return Math.Sqrt(Math.Pow(First.R - Second.R, 2) + Math.Pow(First.G - Second.G, 2) + Math.Pow(First.B - Second.B, 2));
    }

    public Color ToColor()
    {
        return Color.FromArgb(this.A, this.R, this.G, this.B);
    }

    #endregion
}

All primitive types MUST accomplish the following:

  1. Each color component must have a clear min and max value. 
  2. All color component values must be coerced to range from the moment the primitive is created to the moment it is destroyed.
  3. All conversion algorithms must recognize the appropriate component value representation and be able to convert to and from this representation. This is where things can get confusing quickly. We all know RGB values are most commonly represented as an integer between 0 and 255. The byte type stores RGB values perfectly; however, RGB values can also be represented as a decimal between 0 and 1 (by dividing the RGB value by 255) and as a decimal between 0 and 2.55 (by dividing the RGB value by 100) with the latter used the LEAST often. A primitive recognizes only one of these representations for simplicity and uniformity. The other representations are only needed when displaying these values in the view and the primitive type should disregard them. NOTE, the 0 to 1 representation is preferred, but only used for Hsb, Hsl, and Cmyk structures currently; Rgba uses 0 to 255.

Other goals were:

  1. To show primitive value as string (helpful for quick debugging).
  2. To convert primitive to a hashcode.
  3. To support basic type comparisons.

Another note about the 0 to 1 representation:

In order to correctly put a color space value in this representation, you must divide the current component value by it's maximum possible value. For instance, if we have an HSB value of (245, 60, 10), you'd calculate new HSB value as follows:

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.

  • You do not have to be the best at math to understand the algorithms, though some math knowledge does come in handy.

  • Many extensions are used to aid with value coercion and type casting, two drastically important concepts that can produce unexpected results if used incorrectly; e.g., failing to a cast to the appropriate type during conversion between two models might go unnoticed, look innocent, and leave you scratching your head for hours. Remember this before questioning why they're there! Furthermore, allowing an Hsb structure to accept any value below 0 or above 1 would produce incorrect conversion values and you're stuck making sure every instance of Hsb does not go unchecked.

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.170915.1 | Last Updated 17 Sep 2016
Article Copyright 2016 by James J M
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid