![]() |
Desktop Development »
Miscellaneous »
Colour Selection Controls
Intermediate
License: The Code Project Open License (CPOL)
XColorHexagonCtrl - a non-MFC color picker control that displays a color hexagonBy Hans DietrichXColorHexagonCtrl displays a color hexagon that allows user selection, and provides APIs for color based on RGB and HSL color models. |
C++ (VC6, VC8.0), Windows, Win32, Visual Studio (VS2005), Dev
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
These user interface behaviors are implemented in XColorHexagonCtrl:
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.
|
|||||
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.
|
|||||
|
The selector changes appearance according to whether the control has focus:
|
|||||
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.
|
|||||
The selector may be clicked and dragged to new position.
This will result in multiple
WM_XCOLORPICKER_SELCHANGE messages sent to parent window.
|
|||||
|
Tooltips in four formats may be optionally displayed:
|
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 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.
CXDC,
CXRect, and CXToolTipCtrl)
I wrote for my
XColorSpectrumCtrl article,
I was able to quickly convert XColorHexagonCtrl from MFC.
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 structsThe 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.
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_CELLBut 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:
COLOR_CELL struct.
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.
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:
CFormView or
CPropertyPage.
To integrate CXColorHexagonCtrl into your app, you first need to
add following files to your project:
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
Note that this step is not required, if you have some other way to specify where XColorHexagonCtrl should be displayed.
#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);
// 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.
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.
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.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 4 Apr 2008 Editor: |
Copyright 2008 by Hans Dietrich Everything else Copyright © CodeProject, 1999-2009 Web16 | Advertise on the Code Project |