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

Color Picker with Color Wheel and Eye Dropper

, 9 Dec 2007
Rate this:
Please Sign up or sign in to vote.
Color picker with color wheel and eye dropper
Screenshot - mainimage.png

Introduction

There are already several articles here on CodeProject describing different types of color pickers, and here is yet another example.

I needed a color picker for a project I was working on, and I wanted something different from the standard ColorDialog. After looking at a couple of different color pickers, I decided to go ahead and implement my own version which would meet these few requirements:

  • Color table with some predefined colors
  • Color picker (eye dropper) which lets you pick a color from anywhere on the screen
  • Color wheel for selecting any arbitrary color
  • Opacity slider to adjust the opacity of the color
  • Color control should appear as a combobox with the color picker in the dropdown container

Color Wheel – HSL Color

The color in the color wheel is represented by its hue and saturation value, where hue is the angle (0-360) with red being 0°, green 120° and blue 240°, and where the saturation is the distance from the center, where 0 (center) is white and 1 (on the circumference) is the fully saturated color. The Color class already implements a GetHue, Saturation and Lightness (Brightness), but unfortunately does not implement the Set method, so I needed a conversion from a RGB color to an HSL color.

After a bit of online searching, I came across this article in Wikipedia which explains the HSL color space and most conveniently also includes the equations for converting between RGB - HSL and vice versa.

I created the HSLColor class for wrapping the RGB-HSL, and with the help of Wikipedia, it was just a simple matter of typing in the formula.

The ColorWheel class is the actual color wheel control itself. The drawing of the wheel is done using PathGradientBrush. This brush lets you specify a series of points with corresponding colors and a center point with the corresponding color. The points and colors are calculated in RecalcWheelPoints() which is called when the control is resized. Since the color in the color wheel is represented by the angle (Hue), it is a matter of looping from 0 to 360 in ‘degrees’ steps. I found that I cannot tell the difference between 1° and 5° steps, so I used 5° steps.

void RecalcWheelPoints()
     …
double fullcircle = 360;
double step = 5;
while (angle < fullcircle)
{
    double angleR = angle * (Math.PI/180);
    double x = center.X + Math.Cos(angleR) * radius;
    double y = center.Y - Math.Sin(angleR) * radius;
   
    m_path.Add(new PointF((float)x,(float)y));
    m_colors.Add(new HSLColor(angle, 1, 0.5).Color);

    angle += step; 
 }

In this loop, the {x,y} coordinates are calculated, and the color is calculated by creating an HSL color and getting the Color property out of it.

Get and Set the Color Selector

Calculating the position for the color selector is done in ColorSelectorRectangle. The angle at which to place the selector is given by the Hue and the distance from the center is given by the Saturation. A saturation of 0 means the selected color is at the center of the circle, and a saturation of 1 means the selector's distance from the center is equal to the radius of the circle.

Calculating the selected color from the mouse position when the selector is moved around the wheel is done in SetColor(PointF mousepoint). Here the Hue and Saturation is calculated based on the mouse position and the SelectedHSLColor is then updated causing part of the wheel to be redrawn.

void SetColor(PointF mousepoint)
{
    PointF center = Util.Center(ColorWheelRectangle);
    double radius = Radius(ColorWheelRectangle);
    double dx = Math.Abs(mousepoint.X - center.X);
    double dy = Math.Abs(mousepoint.Y - center.Y);
    double angle = Math.Atan(dy / dx) / Math.PI * 180;
    double dist = Math.Pow((Math.Pow(dx, 2) + (Math.Pow(dy, 2))), 0.5);
    double saturation = dist/radius;
    if (dist < 6)
        saturation = 0; // snap to center


    if (mousepoint.X < center.X)
        angle = 180 - angle;

    if (mousepoint.Y > center.Y)
        angle = 360 - angle;
 
    SelectedHSLColor = new HSLColor(angle, saturation, SelectedHSLColor.Lightness);
}

Lightness / Opacity Slider

Screenshot - lightnessbar.png

For the lightness slider, I first had to write a label class which lets you specify the angle at which the text is drawn. This is done with the LabelRotate class. In this class, if the angle of the text is 0, the regular formatting is used for positioning the text, but if any angle is used then an extra alignment property is used called RotatePointAlignment. Once angled, the GDI DrawString is drawn at point (0,0), the ‘rotating point’ is aligned according to the RotatePointAlignment setting and rotating is done by setting TranslateTransform and RotateTransform.

LabelRotate.OnPaint 
          e.Graphics.TranslateTransform(center.X, center.Y);
          e.Graphics.RotateTransform(TextAngle);
          e.Graphics.DrawString(Text, Font, b, new PointF(0,0), format);
          e.Graphics.ResetTransform();

The next step in creating the slider was to create a color bar which would show a gradient color going from black to the selected color and into white. The GradientBrush allows you to fill a rectangle with a starting and an ending color and the brush then fills the gradient color between. For this bar I needed three colors, and the only way (I could think of) to do this is to use two brushes and fill half the area using a color1-color2 brush and the other half with a color2-color3 brush which is done in Draw3ColorBar utility method.

Util.Draw3ColorBar(Graphics dc, RectangleF r, Orientation orientation, 
    Color c1, Color c2, Color c3)

The base class slider control is ColorSlider, this class lets you specify if you are using two or three colors, the orientation of the slider and the orientation of the value, if the value goes from 0-1 or 0-1. The lightness slider is a derived class HSLColorSlider that includes the selected HSLColor.

To get and set the value of the slider control use the Percent property, which (just for confusion) takes a value between 0-1, not 0-100 as you might expect (there is something to fix later).

Eye Dropper

Screenshot - eyedropper.png

The eye dropper control EyedropColorPicker lets you pick a color off the screen. When you click in the eye dropper area, the cursor changes to a cross and the image in the eye dropper control is 15x15 pixel rectangle magnified 4 times.

To get the color off the screen is very simple. A bitmap with the size of the source rectangle (15x15) is created and then the screen is copied into this bitmap, this is done in GetSnapshot.

void GetSnapshot()
{
…
    using (Graphics dc = Graphics.FromImage(m_snapshot))
    {
        dc.CopyFromScreen(p, new Point(0,0), m_snapshot.Size);
        Refresh();
        PointF center = Util.Center(new RectangleF
            (0, 0, m_snapshot.Size.Width, m_snapshot.Size.Height));
        Color c = m_snapshot.GetPixel((int)Math.Round(center.X), 
            (int)Math.Round(center.Y));
        …
    }
}

The location of the ‘pixel indicator’ shown at the center of the image while capturing had to be fine tuned by one pixel to show at the right location, so if the control has any other size, the pixel indicator might be off.

I did run into one issue while writing this. By default the GDI DrawImage tries to smooth out the color transition between pixels. I found the answer in this article where it is mentioned that the property to set to disable the smoothing is the InterpolationMode on the graphics.

e.Graphics.InterpolationMode = 
    System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 

Color Table

Screenshot - colortable.png

The color table class ColorTable lets you set the number of columns and the colors you want to show.

I chose to have 16 columns and in the first row, show 10 gray scale colors ranging from black to white and 6 base colors and then 48 colors (3 rows) spanning from hue 0 to 360 at saturation 1 and another 3 rows with saturation 0.5.

The last color in the table is the custom color which is added and changed whenever the color is changed in either the color wheel or through the eye dropper.

Putting It All Together

The actual color picker control, the class that ties these controls together is the ColorPickerCtrl class. This class gets notified when the value / color changes in any of the controls and it then updates the other controls. It exposes only one property, namely:

public Color SelectedColor 

ComboBox with DropdownContainer

I wanted the color picker control as the ‘dropdown’ part of the combo box instead of a modal dialog. I found a strange behavior when trying to use a derived ComboBox class, it would always show an empty one line dropdown before showing the custom control, so I ended up creating a class derived from Control instead.

The base class for this control is DropdownContainerControl<T> and the dropdown container class DropdownContainer<T> is the container for the custom control shown when dropped down.

For drawing the control I use ComboBoxRenderer.DrawDropDownButton in the base class while the derived class is responsible for drawing the item by overwriting DrawItem.

protected override void OnPaint(PaintEventArgs e)
{
    …
    ComboBoxRenderer.DrawTextBox(e.Graphics, r, 
        System.Windows.Forms.VisualStyles.ComboBoxState.Normal);
    …
    r = ButtonRectangle;
    ComboBoxRenderer.DrawDropDownButton
        (e.Graphics, r, System.Windows.Forms.VisualStyles.ComboBoxState.Normal);
    …
    DrawItem(e.Graphics, ItemRectangle);
    if (Focused)
        ControlPaint.DrawFocusRectangle(e.Graphics, ItemRectangle);
    …
}

On a normal combobox, the dropdown control is closed without saving the selection whenever the user clicks anywhere outside the control. To achieve this, I override OnDeactive and call Cancel which will hide the dropdown without saving.

To capture the keyboard Esc and Enter was a little more complicated. For this, I needed to install a hook (done in Hook). I found this article which explains how to set up hook in C#, however I couldn't get it to work right as it would either throw exceptions or not install the hook (returning 0). The answer was found in this article, where it is explained that the hook will only be installed if the ‘host process’ is disabled, and once disabled, everything worked as expected.

Enhancements

There are a couple of enhancements I can think of which I might add later on.

  • Drawing performance. In most cases I call Refresh instead of Invalidate to get the immediate response to a mouse move. Ideally you would call Invalidate on the dirty area only, but I found Invalidate to give a sluggish performance since the update is delayed slightly. To improve the performance, it would be better to do an immediate redraw but of the dirty area only.
  • Add slider and edit control for RGB and HSL values.
  • Eyedropper – set sample size. Currently only the center pixel is used to select the color. It might be helpful to take the average of for instance a 2x2, 3x3 or 4x4 rectangle.

License

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

Share

About the Author

jkristia
Software Developer (Senior)
United States United States
No Biography provided

Comments and Discussions

 
QuestionGreat Work! Small problem with colorpicker. PinmemberKaerugreen15-Jul-14 8:18 
Questionfantastic Pinmemberadudley25614-Oct-13 11:26 
GeneralMy vote of 5 PinmemberEtherealMonkey25-Jul-13 19:57 
GeneralMy vote of 5 PinmemberTG_Cid28-Dec-12 12:56 
Questionvery helpful Pinmemberarvindps18-Sep-12 19:59 
AnswerTroubles Pinmemberkreont7-Oct-09 3:25 
GeneralRe: Troubles - DropdownContainerControl-Bug PinmemberMr.PoorEnglish29-Mar-10 9:31 
GeneralNice job, but a question about the RGB &lt;-&gt; HSV conversions PinmemberDelphy26-Jun-09 3:18 
GeneralAn excellent work :) PinmemberMohammad Dayyan8-Apr-09 6:10 
GeneralRe: An excellent work :) PinmemberSikk-SikTh13-May-09 8:00 
QuestionCan't load embedded resource? PinmemberQwertie19-Dec-08 13:31 
AnswerRe: Can't load embedded resource? PinmemberQwertie24-Dec-08 5:56 
GeneralDemo exe crash PinmemberKobKob9-Sep-08 13:48 
GeneralRe: Demo exe crash Pinmembereinismotic23-May-10 5:54 
GeneralExcellent! PinmemberChandiramani29-Aug-08 0:38 
GeneralCannot run from exe file when build control on release mode Pinmemberlalunegrosse26-Aug-08 16:50 
GeneralRe: Cannot run from exe file when build control on release mode Pinmemberjkristia26-Aug-08 17:28 
GeneralRe: Cannot run from exe file when build control on release mode PinmemberMichael.Fischer24-Mar-09 11:01 
GeneralRe: Cannot run from exe file when build control on release mode PinmemberNinja-the-Nerd24-Jul-10 11:38 
Generalstrange bug for design time and release configuration PinmemberJulian Benkov8-Jun-08 9:51 
GeneralDualscreen Support PinmemberSHartmann1-Apr-08 19:54 
GeneralRe: Dualscreen Support Pinmemberjkristia2-Apr-08 3:28 
GeneralRe: Dualscreen Support Pinmemberhdsrob10-Dec-09 7:57 
QuestionMy program goes to a crash at design time PinmemberJack Sparrowhawk7-Jan-08 0:27 
GeneralRe: My program goes to a crash at design time Pinmemberjkristia20-Feb-08 4:02 
Sorry, I didn't get any mail notification regarding this post and I didn't see it until now.
 
I just tried to drop the ColorPickerCtrl onto a form in the designer, and it seems to work fine for me in VS2005 and Vista. What if you compile the code (CommonTools dll) will it work then ?

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
Web04 | 2.8.140827.1 | Last Updated 9 Dec 2007
Article Copyright 2007 by jkristia
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid