Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

XColorHexagonCtrl - a non-MFC color picker control that displays a color hexagon

0.00/5 (No votes)
4 Apr 2008 1  
XColorHexagonCtrl displays a color hexagon that allows user selection, and provides APIs for color based on RGB and HSL color models.

Introduction

XColorHexagonCtrl mimics behavior of hexagon control on standard page of Microsoft Office® color picker:

screenshot

This is companion article to my XColorSpectrumCtrl article, but you need not read that article to make use of this control.

XColorHexagonCtrl Features and Behaviors

Visually the XColorHexagonCtrl control has two separate areas of user interaction, a large hexagon comprised of many smaller hexagon cells, each displaying a different color; and underneath that, grayscale hexagon cells that include a large white and a large black cell. In actual use, both areas function the same, with only one noticeable difference, concerning the behavior of the two cells filled with the color white (#FFFFFF). When the small white cell is clicked, the large white cell is also selected; but when the large white cell is clicked, the small white cell is not also selected. No other cells behave this way, and no other colors are duplicated. In every other respect, the cells in the large hexagon and the cells in the grayscale area behave exactly the same.

screenshot

These user interface behaviors are implemented in XColorHexagonCtrl:

screenshot Clicking on a cell selects that cell, and selection indicator, or selector, is displayed. The WM_XCOLORPICKER_SELCHANGE message is sent to parent window, with RGB (COLORREF) color as WPARAM and control id as LPARAM.
screenshot Double-clicking on a cell selects cell, and selector is displayed. The WM_XCOLORPICKER_SELENDOK message is sent to parent window, with RGB (COLORREF) color as WPARAM and control id as LPARAM.
screenshot The selector changes appearance according to whether the control has focus:

With Focus
screenshot
   Without Focus
screenshot

screenshot The arrow, Home and End keys may be used when control has focus. Left and Right arrows do what you would expect, and wrap to previous/next line. Down arrow always goes down and to the right (since cells are staggered). When going down is no longer possible, it simply goes to right. Up does similar thing, going up and to the left. Home goes to first cell, End goes to large black cell. All nav keys cause WM_XCOLORPICKER_SELCHANGE message to be sent to parent window.
screenshot The selector may be clicked and dragged to new position. This will result in multiple WM_XCOLORPICKER_SELCHANGE messages sent to parent window.
screenshot Tooltips in four formats may be optionally displayed:

RGB
screenshot
HTML
screenshot
VB
screenshot
HSL
screenshot

The programmatic interface to XColorHexagonCtrl attributes is very simple: just eight functions to get/set RGB and HSL values, background color, and tooltip format:

Function Description
COLORREF GetBackground() Retrieves current background color
void GetHSL(BYTE* h, BYTE* s, BYTE* l) Retrieves HSL values for current color
COLORREF GetRGB() Retrieves RGB value for current color
void GetTooltipFormat() Retrieves tooltip format
CXColorHexagonCtrl& SetBackground(COLORREF cr) Sets background color
CXColorHexagonCtrl& SetHSL(BYTE h, BYTE s, BYTE l) Sets color from HSL values
CXColorHexagonCtrl& SetRGB(COLORREF cr) Sets color from RGB value
CXColorHexagonCtrl& SetTooltipFormat(TOOLTIP_FORMAT eFormat) Sets tooltip format

XColorHexagonCtrl Color Models

XColorHexagonCtrl allows you to work with either RGB or HSL color model, depending on requirements of your application. For more details about these color models, please see my XColorSpectrumCtrl article.

Implementation Notes

Taking advantage of the helper classes (CXDC, CXRect, and CXToolTipCtrl) I wrote for my XColorSpectrumCtrl article, I was able to quickly convert XColorHexagonCtrl from MFC.

Internal Design

Just like in XColorSpectrumCtrl, I use old Win32 trick of caching DC. The first time the hexagon needs to be drawn, I create a persistent DC and bitmap. For subsequent drawing, I BitBlt the saved DC to target DC, and then do any additional drawing on that. This draws the hexagon instantly with no discernible flicker.

One big difference between this control and XColorSpectrumCtrl is that with the color hexagon, you are dealing with discrete colors, rather than spectrum. Moreover, these colors must match those used in MS Office® color picker. In all, there are 143 unique colors displayed in the hexagon (including grayscale colors). While some of them are standard named colors, most of them are not. Handling this many non-standard colors took some effort - color-picking the colors and setting up an array for the hexagon display - but it provided me with an understanding of why the MS Office® color picker behaves the way it does. In fact, the XColorHexagonCtrl implementation was really driven by the structure of the color data.

Here is the struct used to define each cell:

    struct COLOR_CELL
    {
        COLOR_CELL(int x, int y, COLORREF crFill)
        {
            index = 0;
            startx = x;
            starty = y;
            cr = crFill;
        }

        int index;      // index inro m_paColorCells, 
                        // used for left/right arrows
        int startx;     // starting left coord
        int starty;     // starting top coord
        COLORREF cr;    // RGB color value
    };

    enum { NUMBER_COLOR_CELLS = 144 };  // includes both whites

    COLOR_CELL * m_paColorCells[NUMBER_COLOR_CELLS];
                        // array of pointers to 
                        // COLOR_CELL structs
The first time hexagon is painted, the array m_paColorCells is filled from pre-defined list of colors. This array is in sequential order, starting with the top-left cell, and ending with the large black cell. This sequence is what allows the Left arrow and Right arrow keys to work correctly.

Navigation Details

But what if the user clicks on a cell? How does navigation work from a random cell in hexagon? This is where index member of the COLOR_CELL comes in. You can find which cell the cursor is over with Win32 GetPixel() function, using saved DC (we don't want to use window DC because there might be a selector displayed, and GetPixel() could pick up selector colors). Once we have color, it is simple to search through array to find matching cell (remember that colors are unique, except for white). This gives us pointer to COLOR_CELL

But wait! What if user clicks on large white cell? Won't the search find small cell first? Well, yes, it would, but simple test determines whether cursor is in color hexagon or grayscale area.

Once we have pointer to COLOR_CELL struct, the index member can be used to access next or previous cells. However, accessing cells above or below current cell takes a bit more work. Once again we use the saved DC. We know that cells are staggered, so imagine that cells are squares instead of hexagons; then going up means taking top left corner of current cell, and going down means taking bottom right corner:

screenshot

When we have X and Y coordinates that we know will be in desired cell (if there is one), we can get color of that pixel, and from there we can once again search array for color match, to retrieve pointer to COLOR_CELL struct.

Drawing Details

There is one oddity about the hexagon cells in MS Office® color picker that is not immediately apparent: they are not symmetrical, left to right:

screenshot

As you see, the right side of individual cells is short by one column of pixels. This is true of every cell in hexagon and in grayscale area. Possibly this is to give the whole color hexagon a more symmetrical appearance.

This discovery led me to completely change my thinking about how to draw color hexagon. Up to this point, I had been thinking of some nice algorithms to compute the vertices, side lengths, etc. Now I could see that the algorithms had taken a turn for the baroque, which made them much less appealing.

I now faced challenge: how to reproduce the visual appearance of the MS Office® color hexagon, given its non-symmetrical structure? I began thinking of individual hexagon cells - they were all identical in size, shape, and even the missing column of pixels was identical. For some reason I started thinking about a factory stamping out these cells like cookies, and that is when I realized I could draw cells with same technique I used in drawing indicators for XColorSpectrumCtrl. Using this approach, cell is defined by internal bit array; each pixel is represented by one BYTE, whose value determines whether pixel should be drawn (if non-zero).

Here is bit array for small cell:

    // For each byte in this array:
    //    0 = skip
    //    1 = set pixel to fill color
    static BYTE pixels[SMALL_CELL_HEIGHT][SMALL_CELL_WIDTH] = 
    {
        0,0,0,0,0,0,1,1,1,0,0,0,0,0,    // 1
        0,0,0,0,1,1,1,1,1,1,1,0,0,0,    // 2
        0,0,1,1,1,1,1,1,1,1,1,1,1,0,    // 3
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 4
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 5
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 6
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 7
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 8
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 9
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 10
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 11
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,    // 12
        0,0,1,1,1,1,1,1,1,1,1,1,1,0,    // 13
        0,0,0,0,1,1,1,1,1,1,1,0,0,0,    // 14
        0,0,0,0,0,0,1,1,1,0,0,0,0,0,    // 15
    };
I also used this technique to draw two large cells, as well as selector. Again borrowing idea from XColorSpectrumCtrl, for selector I assigned different values to pixel positions, depending on what color they should be painted:
    // For each byte in this array:
    // 0 = skip
    // 1 = COLOR_WINDOWTEXT (COLOR_BTNSHADOW if bHasFocus == FALSE)
    // 2 = COLOR_WINDOW
    // 3 = COLOR_BTNSHADOW
    static BYTE pixels[SMALL_SELECTOR_HEIGHT][SMALL_SELECTOR_WIDTH] = 
    {
        0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,  // 1
        0,0,0,0,0,1,1,2,2,2,2,2,1,1,0,0,0,0,0,  // 2
        0,0,0,1,1,2,2,2,2,2,2,2,2,2,1,1,0,0,0,  // 3
        0,1,1,2,2,2,2,2,3,3,3,2,2,2,2,2,1,1,0,  // 4
        1,2,2,2,2,2,3,3,0,0,0,3,3,2,2,2,2,2,1,  // 5
        1,2,2,2,3,3,0,0,0,0,0,0,0,3,3,2,2,2,1,  // 6
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 7
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 8
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 9
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 10
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 11
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 12
        1,2,2,3,0,0,0,0,0,0,0,0,0,0,0,3,2,2,1,  // 13
        1,2,2,2,3,3,0,0,0,0,0,0,0,3,3,2,2,2,1,  // 14
        1,2,2,2,2,2,3,3,0,0,0,3,3,2,2,2,2,2,1,  // 15
        0,1,1,2,2,2,2,2,3,3,3,2,2,2,2,2,1,1,0,  // 16
        0,0,0,1,1,2,2,2,2,2,2,2,2,2,1,1,0,0,0,  // 17
        0,0,0,0,0,1,1,2,2,2,2,2,1,1,0,0,0,0,0,  // 18
        0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0   // 19
    };
There are three colors used in the selector, with one color (the outer border) dependent on whether the control has focus.

A Design Hole Emerges

After I had finished with drawing code and navigation code, I thought XColorHexagonCtrl was nearly finished. Then I realized there was a big hole in the design, caused by one of the assumptions I made early on. I had assumed that if a color was found in the m_paColorCells array, that it meant that the color was that of a valid cell, and therefore the cell was valid part of the color hexagon. It turns out this might not be true.

It is possible - though perhaps unlikely - that user has set system colors to match one of the colors in the hexagon. This is especially true of the grayscale colors. If this happened, and the dialog background matched a hexagon color, then clicking anywhere in hexagon or anywhere in its entire client area would be interpreted as clicking on a valid cell. Except it might not be valid!

How to prevent this from happening? I ran down list of things I could do: grab all the system colors, check for match; do some fancy geometry calculations, check for out-of-bounds; paint non-hexagon bits a "special" color that I could check for. It was this last idea that led me to simple yet effective solution: I would create a validating mirror image of the persistent color hexagon DC. This validating DC would be painted with only two colors: black and white. First I filled the validating DC completely with black. Then, when I was setting up the persistent color DC, I painted a white pixel in validating DC wherever I painted a color pixel in persistent color DC. The final validating DC looked like this:

screenshot

Given an X,Y pixel position, it was now easy to validate it: a color value of 0 in the validating DC meant that it was not valid. Hence any clicks in the black area could be ignored.

Demo App

Here is what demo app looks like:

screenshot

How to use

The following steps assume you want to add XColorHexagonCtrl to a dialog. Steps would be similar for CFormView or CPropertyPage.

Step 1 - Add Files

To integrate CXColorHexagonCtrl into your app, you first need to add following files to your project:

  • CXCD.h
  • CXRect.h
  • CXToolTipCtrl.h
  • XColorHexagonCtrlConstants.h
  • XColorHexagonCtrl.cpp
  • XColorHexagonCtrl.h
  • rgbhsl.cpp
  • rgbhsl.h

The .cpp files should be set to Not using precompiled header in Visual Studio. Otherwise, you will get error

    fatal error C1010: unexpected end of file while looking for precompiled header directive

Step 2 - Add Placeholder Rect to Dialog Resource

Next add a STATIC or other control to dialog resource, where you want the XColorHexagonCtrl to be displayed. The dialog for demo app looks like this:

screenshot

Note that this step is not required, if you have some other way to specify where XColorHexagonCtrl should be displayed.

Step 3 - Create the Control

You need to do two things here: first, add #include statement to dialog class header file:
#include "XColorHexagonCtrl.h"
and insert variable that looks like:
    CXColorHexagonCtrl m_ColorHexagon;
Second, add code to OnInitDialog() function:
    CRect rect;
    GetDlgItem(IDC_FRAME)->GetWindowRect(&rect);
    ScreenToClient(&rect);
    GetDlgItem(IDC_FRAME)->ShowWindow(SW_HIDE);         // hide placeholder
    VERIFY(m_ColorHexagon.Create(AfxGetInstanceHandle(), 
        WS_CHILD | WS_VISIBLE | WS_TABSTOP,             // styles 
        rect,                                           // control rect
        m_hWnd,                                         // parent window
        9001,                                           // control id
        RGB(0,255,0)),                                  // initial color
        CXColorHexagonCtrl::XCOLOR_TOOLTIP_HTML));      // tooltip format

    // call SetWindowPos to insert control in proper place in tab order
    ::SetWindowPos(m_ColorHexagon.m_hWnd, ::GetDlgItem(m_hWnd, IDC_FRAME), 
        0,0,0,0, SWP_NOMOVE|SWP_NOSIZE);

Step 4 - Add Message Handlers

According to your app's requirements, you may need to add handlers for one or both of the XColorHexagonCtrl registered messages. Here are handlers used in demo app:
// handler for WM_XCOLORPICKER_SELCHANGE
LRESULT CXColorHexagonCtrlTestDlg::OnSelChange(WPARAM wParam, LPARAM lParam)
{
    KillTimer(1);        // stop animation
    CString s = _T("");
    GetDlgItem(IDC_COLOR_NAME)->SetWindowText(s);
    GetDlgItem(IDC_COLOR_RGB)->SetWindowText(s);
    s.Format(_T("WM_XCOLORPICKER_SELCHANGE       RGB(%d,%d,%d)"), 
        GetRValue(wParam), GetGValue(wParam), GetBValue(wParam));
    if (lParam == 9001)
        GetDlgItem(IDC_SELECTION)->SetWindowText(s);
    return 0;
}

// handler for WM_XCOLORPICKER_SELENDOK
LRESULT CXColorHexagonCtrlTestDlg::OnSelendOk(WPARAM wParam, LPARAM lParam)
{
    KillTimer(1);        // stop animation
    CString s = _T("");
    GetDlgItem(IDC_COLOR_NAME)->SetWindowText(s);
    GetDlgItem(IDC_COLOR_RGB)->SetWindowText(s);
    s.Format(_T("WM_XCOLORPICKER_SELENDOK       RGB(%d,%d,%d)"), 
        GetRValue(wParam), GetGValue(wParam), GetBValue(wParam));
    if (lParam == 9001)
        GetDlgItem(IDC_SELECTION)->SetWindowText(s);
    return 0;
}

Both messages send RGB color as wParam, and control id as lParam.

If you use XColorHexagonCtrl and XColorSpectrumCtrl together in the same project, you will notice that the extern variable names of the registered messages are different for the two controls. However, the actual message strings that are registered are the same, and so you can use the same handlers to handle both controls. You can tell which is which by using the control id returned in lParam.

Revision History

Version 1.1 - 2008 April 4

  • Bug fixes

Version 1.0 - 2008 March 15

  • Initial public release

Usage

This software is released into the public domain. You are free to use it in any way you like, except that you may not sell this source code. If you modify it or extend it, please to consider posting new code here for everyone to share. This software is provided "as is" with no expressed or implied warranty. I accept no liability for any damage or loss of business that this software may cause.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here