Introduction
This colour picker allows you to easily choose a colour either using RGB or HSV, it includes an alpha slider (that can be disabled or hidden, if needed), and it displays the colour value in hexadecimal. The code is fully written using Windows API, without MFC.
Using the code
This control is really straightforward to use: just put the CPicker.dll file in your executable folder, link your project with CPicker.lib and don't forget to include CPicker.h to your CPP file(s)!
CPicker.h defines the SColour struct
that handles RGBA/HSV colours and the CColourPicker
class that is used to manage the dialog.
I'll examine the SColour struct
first:
struct SColour
{
unsigned short r, g, b;
unsigned short h, s, v;
unsigned short a;
void UpdateRGB ();
void UpdateHSV ();
};
Well, that's easy! The struct
just stores red, green, blue, hue, saturation, value and alpha in some unsigned short variables and it also has two methods for updating the colour components. So, for example, if you have RGB, you can set r
, g
, and b
and then call UpdateHSV
and hue, saturation and value will be automatically updated!
This is the definition of the CColourPicker
class:
class CColourPicker
{
public:
CColourPicker(HWND hParentWindow);
CColourPicker(HWND hParentWindow, unsigned short r,
unsigned short g, unsigned short b,
unsigned short a, bool IsRGB);
void CreatecolourPicker(short AlphaUsage);
void SetRGB(unsigned short r, unsigned short g, unsigned short b);
void SetHSV(unsigned short h, unsigned short s, unsigned short v);
void SetAlpha(unsigned short a);
SColour GetCurrentColour();
SColour GetOldColour();
private:
SColour CurrCol, OldCol;
short UseAlpha;
HWND hParent;
};
We have two constructors: both need a handle to the parent window of the dialog and the second also initializes r
, g
, b
and a
. You can also use the second constructor to initialize h
, s
and v
, by passing false
as IsRGB
. Note that calling the constructor just initializes the colours, it does not create the colour picker.
To create the dialog you should call CreateColourPicker
: this function expects one parameter, to know if it should display the full dialog with the alpha slider or not. There are three possible values for this parameter, defined in CPicker.h:
CP_NO_ALPHA |
0 |
The alpha slider and value are not displayed. Alpha is set to 100%, colors are opaque. |
CP_USE_ALPHA |
1 |
The alpha slider and value are displayed and can be modified, colours can be transparent. |
CP_DISABLE_ALPHA |
2 |
The alpha slider and value are displayed but disabled and set to 100%, colours are opaque. |
Other useful functions are SetRGB
, SetHSV
, and SetAlpha
, that are used to set the components of the current color. SetRGB
calls the UpdateHSV
method of CurrCol
, and SetHSV
calls UpdateRGB
so, when you update RGB, HSV will automatically be updated and vice versa.
Finally GetCurrentColour
and GetOldColour
are used to retrieve the currently selected colour and the previously selected one.
The sample application (CPickerTest) shows you how to use the control. It's really simple so I won't comment it here, just remember to link with the CPicker.lib file and put the DLL in your exe folder (or in a system folder, but I don't recommend it!).
Points of Interest
There are some interesting points in the program, which I would like to remark here.
First of all, the HSV/RGB conversion routines : While RGB to HSV is quite easy to understand, HSV to RGB can be quite tricky. The problem is that, due to the fact that RGB is a "cubic" colour space while HSV is a "cylindrical" one, there is no straightforward conversion matrix between these systems (like there is, for example, from RGB to NTSC YIQ), so you'll find a lot of if statements and some calculation that, at first, can be quite obscure. I suggest to open up the colour picker and start moving the sliders and look at how the number vary: this can help a lot to understand the correspondences between the two systems.
I will give you a brief explication of how this works. Take a look at these images:
Fig. 1 - The colour wheel
Fig. 2 - The HSV colour space
Figure 1 represents a slice of the "cylindrical" HSV colour space which is schematically represented by figure 2. So, essentially at points 1, 3 and 5 you have the primary colours red, green and blue, that correspond to hue values of 0�, 120� and 240� and at the points 2, 4 and 6 you have the secondary colours yellow, cyan and magenta that correspond to hue values of 60�, 180� and 300�. So, red prevails between 6 and 2, green between 2 and 4 and blue between 4 and 6. Keep it in mind, we'll use it later.
In the following image you see the RGB space:
Fig. 3 - The RGB colour space
RGB colour space can be represented by a cube with the primary colours at three non-consecutive vertices (green is on the unseen vertex in the image above) and the secondary colours on other three vertices. The remaining two vertices are white and black (that in the HSV space were the center of the bases of the cylinder). We need to map a cube to a cylinder... it's not so intuitive!
RGB to HSV
So, let's start from the easy thing... You have an RGB colour and you want to convert it into HSV.
First of all, we find the maximum (max) and the minimum (min) between the RGB components, and calculate the difference (delta) between them. The value represents how dark is the colour and ranges from 0% (black) to 100% (pure colour); this means that to calculate the value we just need to take max and divide it by 255.
The saturation represents the purity of the colour and represents the percentage of gray in the colour. To calculate it, we take delta and divide it by max. In fact, to obtain gray you need to have red, green and blue at the same time. So a fully saturated (100%) colour will have one of the component set at 0 (so, there's no gray). In that case delta will be equal to max and delta/max will be 100%! On the opposite, a fully unsaturated colour will be some shade of gray, with the same value for red, green and blue: in that case delta will be 0 and delta/max will be 0%. All the intermediate situations will give a saturation value between 0% and 100%.
To calculate the hue you need to remember what I told you before about the prevalence of red, green and blue in the colour wheel: red prevails between 240� (or -60�, if you prefer) and 60�, green between 60� and 180� and blue between 180� and 240�. So, each colour has a 120� angle in which it prevails. To get the hue we take max, and see to which RGB component it corresponds. Let's suppose it corresponds to red: this means the hue will be between -60� and 60� (note: I'll use also negative hue numbers here, because it's easier to explain the problem in this way: if you obtain a negative hue, simply add 360� to obtain the correct hue degrees). If we subtract the two other components in anticlockwise sense, so green-blue, we obtain a positive number if the hue is greater than 0� (we are going toward green), a negative number in the opposite case (heading toward blue). A hue of 60� would mean that there is no blue (apart from what comes from the saturation), while a hue of -60� would mean no green. If we calculate (green-blue)/delta, we get a value between 0 and 1 that, multiplied by 60 will give us a value between -60 and 60. Note that dividing by delta eliminates the saturation component, like you can see by this picture:
Fig. 4 - A practical example of calculating hue.
In the above example the upper colour is RGB 209,146,65. The difference g-b is positive, so we know that the hue will be between 0� and 60�. Dividing by delta we find the percentage of the intermediate component (in this case green) relative to the maximum component (red), excluding the minimum component (blue). Essentially we calculate the hue of the fully saturated colour: RGB(delta, g-b, 0). So, if green equals red (some shade of yellow), delta and g-b will be the same, so (g-b)/delta will give us 1. If green equals blue, (g-b)/delta will be 0 and we have some shade of red. If we multiply this by 60 we get the correct hue. The reasoning is the same in the case that green or blue is the maximum, we just need to offset the results by 120� or 240�.
HSV to RGB
The code for this function can appear more difficult to understand than the previous one, but it's only in appearance! In this explanation I'll consider value and saturation between 0 and 1 and hue between 0� and 360�. First of all, we see if we have an unsaturated colour: in this case we just take the value, multiply it by 255 and assign the result to all the three RGB components. In the most common case in which the colour is saturated, we divide the hue by 60 and get a result from 0 to 5, representing the six parts of the colour wheel (again, red prevails in 5 and 0, green in 1 and 2, blue in 3 and 4).
So we just check in which slice we are, and then calculate the RGB components in this way:
- The main component is value * 255 (eg. green, if hue/60 = 2).
- The less present component (eg. red if hue/60 = 2) is calculated with this formula:
base = (255.0f * (1.0 - sat) * val);
This base value will be used also for the other component, so we'll store it in a variable. It can be thought of as the gray quantity in the colour (that is equal to the minimum of the RGB components), as 1-sat is 1 if the colour is unsaturated and 0 if it is saturated. We multiply by val
to take in account the lightness of the colour and then multiply by 255 to have a value in the correct range.
- The intermediate component (eg. blue if hue/60=2) is calculated with this formula:
(255.0f * val - base) * ((h%60)/60.0f) + base;
if we are in a slice that starts with a primary colour (0, 2 and 4) or
(255.0f * val - base) * (1.0f - ((h%60)/ 60.0f)) + base;
if we are in a slice that starts with a secondary colour (1, 3 and 5).
In this case we multiply val
by 255, obtaining the RGB components of a gray having our colour's lightness value. We then subtract the previously obtained base value from this, eliminating the less present component (remember what you saw in Fig. 4, this is a similar reasoning).
Now we calculate (h%60), that returns a number between 0 and 59, no matter what slice we are in, representing the degrees from the start of the slice; dividing this result by 60 gives us a value in the range [0, 1). For slices 1, 3 and 5 we just take 1 minus this value so that 0 always represent the shades of a primary colour and 1 those of a secondary colour. So, if we are at the beginning of the slice, multiplying these values we obtain 0; summing the basal gray quantity we stored in base we obtain the correct component value. If we're in some other place of the slice the product will give us a value between 0 and 255 and summing this to base (for the same reason) will give us the correct value. And that's all for the conversions!
Another interesting part is the drawing of the coloured sliders. This is done when processing WM_PAINT
messages and it just involves getting the HDC of the control that has to be drawn, selecting a bitmap into it and using SetPixel
to draw the colours one pixel at a time into the bitmap. After the drawing has been done BitBlt
is used to show up the bitmap.
Depending on what it is selected with the radio buttons, the sliders represent different things: the small vertical slider represent the selected feature, the big square one represent the other two features. For example, if G is selected the big square shows R vs. B and the small shows the various values of R. The third slider, if present, shows the alpha: I had quite a hard time figuring how to draw this! I also wrote the DrawCheckedRect
function that draws a rectangle with b/w checkers "covered" with the colour you choose (obviously, you see the checks only if the color is somewhat transparent!). The sample application uses this function to draw the colour the user selected.
History
- 02-04-2006
Updated the code with an optimized version. Thanks to Fausto Cardone for sending me the optimized routines for drawing the colour picker.
- 4-1-2005
I added a better and [much] longer explanation of the RGB/HSV conversion algorithms: I hope it's clear, if not tell me!
- 3-1-2005
Well, this is the first release, so no history to write...
If you find any bug, or have any suggestion, feel free to contact me!