Help Center

PsColorPicker

A modal colour picker popup for FreeBASIC / Win32, owner-drawn throughout, built on AfxNova and PsBufferPaint.

Repository: github.com/PaulSquires/PsColorPicker

PsColorPicker_DoModal puts a chromeless popup on screen, blocks until the user answers, and returns TRUE with the chosen colour written back — or FALSE if they abandoned it, in which case your variables are left exactly as they were. There is no caption bar, no Cancel button, and nothing to place in a parent window: it is a call you make, not a control you lay out.

The popup holds a tab strip (Web, System, Custom), a body that is either a scrolling list of named colours or a tint/shade matrix, an Initial/Current preview split across one box, a column of R/G/B/Alpha entry fields, and an OK button. OK is the only thing that commits; Escape and a click anywhere outside both cancel.

It is a complete interaction on its own — you do not build a dialog around it, and it adds nothing to your message loop.

What it looks like

PsColorPicker

The popup dropped under the button that opened it: the Web tab scrolled to the currently selected colour, the split Initial/Current preview with its labels above and below, the field column, and the OK button. Alpha is hidden here — it is shown by default and this host turned it off.

Requirements

FileWhy
PsColorPicker.biThe public surface.
PsColorPicker.incThe implementation.
PsBufferPaint.bi / PsBufferPaint.incThe flicker-free drawing surface everything is painted through.
PsVScrollBar.bi / PsVScrollBar.incThe list scrollbar. You never reference it, but the files must be present.

PsColorPicker_SelfTest.inc is not optional if you take PsColorPicker.inc unaltered — the implementation includes it at the end. Either copy it along with the rest, or delete that #include once line.

Include order

PsColorPicker.bi includes PsBufferPaint.bi and PsVScrollBar.bi for you, and PsColorPicker.inc includes PsVScrollBar.inc for you. So the only thing you must get right is that PsBufferPaint.inc precedes PsColorPicker.inc:

#include once "windows.bi"
#include once "AfxNova\CWindow.inc"
#include once "AfxNova\AfxStr.inc"
#include once "AfxNova\AfxGdiplus.inc"

using AfxNova

#include once "PsBufferPaint.inc"
#include once "PsColorPicker.inc"

GDI+ must be running

All geometry is drawn through GDI+. Initialise it before the first repaint and shut it down after every window is gone; without the bracket the popup draws nothing at all.

dim as ULONG_PTR gdipToken = AfxGdipInit()
' ... run your application ...
AfxGdipShutdown( gdipToken )

Never name an identifier ok

GDI+ defines Ok = 0 as a Status enum value in namespace AfxNova, and your host almost certainly says using AfxNova. Any variable, parameter or function of yours called ok becomes a duplicate definition the moment you adopt this control. Use bOK.

There is no pump obligation

There is no PsColorPicker_FilterMessage, and you should not go looking for one. If you are coming from PsComboBox, PsDatePicker, PsTextBox or PsMenuBar, each of which requires a call in your message loop, this control is the opposite case: PsColorPicker_DoModal runs its own GetMessage loop, and that loop owns IsDialogMessage, the Escape key and the outside-click test. While the popup is up, your loop is not running at all.

You do not need to SetFocus anything beforehand either — the popup focuses itself.

The one thing worth doing on your side is restoring focus after DoModal returns. The popup took the focus and has since been destroyed, so your window is left with no focused child and the next Tab does nothing:

if PsColorPicker_DoModal( hwnd, gMyColor, gMyAlpha ) then
    ' ... use the new colour ...
end if
SetFocus( hButtonThatOpenedIt )

Quick start

The whole integration, for a host that wants the default popup dropped under the control that opened it:

private sub OnPickColour( byval hwnd as HWND, byval hButton as HWND )

    ' Screen coordinates. The popup hangs off this rect, flipping above it if it will not fit
    ' below.
    dim as RECT rcAnchor
    GetWindowRect( hButton, @rcAnchor )

    ' gMyColor and gMyAlpha are read on the way in as the starting value, and written on the way
    ' out ONLY if the user pressed OK -- so a cancel needs no handling.
    if PsColorPicker_DoModalForRect( hwnd, rcAnchor, gMyColor, gMyAlpha ) then
        InvalidateRect( hwnd, NULL, TRUE )
    end if

    SetFocus( hButton )
end sub

PsColorPicker_DoModal( hwnd, gMyColor, gMyAlpha ) is the same thing centred on hwnd instead of anchored to a control.

If you need to configure the popup first — a font, a paint callback, a starting tab, the alpha row turned off — build it the long way instead. These calls are exactly what DoModal does:

dim as HWND hPick = PsColorPicker_Create( hwnd, 0 )
PsColorPicker_SetFont( hPick, ghFontGui )
PsColorPicker_ShowAlpha( hPick, false )
PsColorPicker_SetColor( hPick, gMyColor, 255, false )
PsColorPicker_PositionForRect( hPick, rcAnchor )

if PsColorPicker_RunModal( hPick ) then
    gMyColor = PsColorPicker_GetColor( hPick )
    gMyAlpha = PsColorPicker_GetAlpha( hPick )
end if
DestroyWindow( hPick )

Concepts

The value is a COLORREF and a separate alpha

GetColor returns a COLORREF; GetAlpha returns a ubyte alongside it. Alpha never travels inside the colour, and committing the alpha field cannot disturb the RGB. That is why the modal entry points take two byref out-parameters rather than returning a single value — and it is also what lets a cancel leave both untouched, which a sentinel return could not express.

Initial and Current

SetColor establishes both the live value and the "Initial" baseline, unless you pass bKeepInitial = true. The preview box is split across its middle: the top half is Initial, the bottom half Current, with the two labels drawn above and below the box. Clicking the Initial half reverts — that pair is the popup's only reset, so it behaves as the thing it looks like. Clicking the Current half does nothing.

Placement

Both placement routines size the window to GetIdealSize first, then position it, then clamp it onto the work area of the monitor it lands on.

PositionForRect drops the popup under the anchor and flips it above when it will not fit below. That is not cosmetic: a popup merely clamped upward ends up covering its own anchor, and then the opening click's release lands inside it and dismisses it immediately.

Geometry is derived, never set

Every rect is computed from the layout inputs; there are no rect setters. The layout is lazy — mutators mark it stale and the next paint or geometry query does one measuring pass, so a burst of setters costs one re-layout rather than one each.

The bottom block is three columns:

[ "Initial" label      ]
[ preview, top half    ]   [ Red:   ][field]
[ preview, bottom half ]   [ Green: ][field]
[ "Current" label      ]   [ Blue:  ][field]          [   OK   ]
                           [ Alpha: ][field]

The block is as tall as its tallest column, and the preview column is taller than the fields. One consequence is worth knowing: ShowAlpha adds or removes a whole field row but does not change the popup's size, because the row comes and goes inside space the preview had already claimed. You can flip it without re-placing an anchored popup.

The OK button is bottom-aligned on the last visible field row, so hiding alpha moves it up.

The matrix is a law, not a table

The Custom tab's grid is generated: lightness runs down the rows as 1 - (row + 0.5) / rows, hue runs across the chromatic columns, and the last column is a greyscale ramp. There is no table of colours to look up. Changing SetMatrixSize therefore gives you a finer or coarser sweep of the same colours, not a cropped or stretched version of a fixed picture.

PsColorPicker_MatrixColorAt is public and pure, so you can generate the same colours yourself without a window.

Tabs

Three, always present. Web and System are scrolling lists of named rows — a swatch, then the name — sharing one renderer and one hit test; Custom is the matrix. Each tab is as wide as its own caption plus padding, laid left to right from the strip's left edge, so the strip's right-hand remainder is empty.

The body is a fixed height across all three tabs. A body that resized as the user browsed would grow an already-placed popup off the bottom of the monitor.

Selection follows the value

There is one selection index, shared by whichever tab is showing, and it is re-derived from the colour whenever the colour or the tab changes. A colour that is not present on the current tab selects nothing rather than snapping to a nearest match. When a list holds the same colour twice, the first entry wins.

Focus

The entry fields are drawn and key-handled by the control itself — there is no inner child holding a caret — so GetFocus() = hCtrl is true when the popup has focus. There is no _HasFocus helper and none is needed.

Behaviour and limits

  • OK is the only commit. Escape, and a button-down anywhere outside the popup, both cancel. There is no Cancel button.
  • The parent is not disabled while the popup is up. It cannot be — a click on a disabled window generates nothing, and that click is how the popup is cancelled. "Modal" here means the call blocks, not that the rest of your application is inert.
  • Accepting commits a half-typed field; cancelling discards it. A user who types 128 and presses OK gets 128. One who types 128 and presses Escape does not.
  • The entry fields are deliberately minimal. Digits, Backspace, Delete, arrows, Home/End and Tab, and nothing else. No selection, no clipboard, no undo. If you need those, put a PsTextBox on your own dialog rather than expecting them here.
  • There is no hex field. PsColorPicker_FormatHex / ParseHex are public and pure if you want #RRGGBB for storage or display.
  • There is no palette of your own named colours, and no eyedropper, no multi-select, no tooltips, and no way to type a colour by name — although Web and System are named lists you pick from.
  • Values clamp, they do not wrap. A channel field takes at most three digits and is clamped to 0..255 on commit; the field is then re-seeded so the user sees what their 999 became. An empty field is discarded and the value left alone.
  • A double-click on a body cell accepts — the mouse's equivalent of Enter. It adopts the cell under the cursor and then commits, so double-clicking a cell that is not the selected one answers with the one you hit. A double-click anywhere else — a tab, a field, the preview, the OK button — behaves as an ordinary click there and cannot close the popup. The cost is on the matrix: a rapid second click commits rather than starting a second drag.
  • Overflow is honest, not squeezed. If you set a matrix bigger than the window, cells fall outside the body and are clipped rather than being scaled down.

API reference

Running it

FunctionBehaviour
PsColorPicker_DoModal( hParent, byref clr, byref nAlpha ) as booleanCreates, centres on hParent, blocks, destroys. TRUE on OK, with both out-parameters written; FALSE on cancel, with neither touched. hParent may be NULL, in which case it centres on the monitor holding the cursor.
PsColorPicker_DoModalForRect( hParent, byref rcAnchor, byref clr, byref nAlpha ) as booleanThe same, dropped under rcAnchor (screen coordinates) and flipped above it when it will not fit below.
PsColorPicker_Create( hParent, id = 0 ) as HWNDCreates the popup without showing it, so you can reach the setters first. hParent becomes the owner, not the parent. You own the window and must DestroyWindow it.
PsColorPicker_RunModal( hCtrl ) as booleanShows the popup and runs the nested loop. TRUE on OK. Does not destroy the window.
PsColorPicker_PositionForRect( hCtrl, byref rcAnchor )Sizes to ideal, places under the anchor with the flip-above rule, clamps to the work area. Does not show.
PsColorPicker_PositionCentered( hCtrl, hParent )Sizes to ideal and centres on hParent, or on the monitor holding the cursor when hParent is NULL. Does not show.

Value

FunctionBehaviour
PsColorPicker_SetColor( hCtrl, clr, nAlpha = 255, bKeepInitial = false )Sets the live value. Silent. Also re-establishes the Initial baseline unless bKeepInitial is true.
PsColorPicker_GetColor( hCtrl ) as COLORREFThe live colour.
PsColorPicker_GetAlpha( hCtrl ) as ubyteThe live alpha.
PsColorPicker_SetInitial( hCtrl, clr, nAlpha = 255 )Sets the baseline shown as "Initial" without touching the live value. Silent.
PsColorPicker_GetInitial( hCtrl ) as COLORREFThe baseline colour.
PsColorPicker_RevertToInitial( hCtrl )Sets the live value back to the baseline. An action, not a setter, so it fires the change callback with a single END phase.

Tabs and fields

FunctionBehaviour
PsColorPicker_SetTab( hCtrl, nTab )Selects a tab. Silent. Refuses anything outside CLR_TAB_WEB..CLR_TAB_CUSTOM, leaving the current tab in place. Resets the scroll position and re-derives the selection.
PsColorPicker_GetTab( hCtrl ) as longThe current tab.
PsColorPicker_ShowAlpha( hCtrl, bShow )Shows or hides the Alpha field row. Shown by default. Hiding it while the caret is in it commits that field first. Changes the field count and the OK button's position, but not the popup's size.
PsColorPicker_IsAlphaShown( hCtrl ) as booleanWhether the Alpha row is placed.
PsColorPicker_TabName( idx ) as DWSTRINGThe caption on tab idx; "" out of range. The tab strip measures this to size each tab.

Appearance

FunctionBehaviour
PsColorPicker_SetColors( hCtrl, pColors )Copies your PSCOLORPICKER_COLORS in. The control keeps a copy and never reads a host global.
PsColorPicker_GetColors( hCtrl, pColors )Copies the current colours out.
PsColorPicker_SetFont( hCtrl, hFont )Sets the text font. You keep ownership of the HFONT. Re-measures, because the tab strip is as wide as its captions.
PsColorPicker_GetFont( hCtrl ) as HFONTThe font, or 0 if none was set (the stock GUI font is used).
PsColorPicker_SetOKText( hCtrl, Text )The OK button's caption.
PsColorPicker_SetPreviewText( hCtrl, TextInitial, TextCurrent )The two preview labels.
PsColorPicker_SetFieldText( hCtrl, nField, Text )The label beside one field, in CLR_FIELD_* order. Defaults carry their trailing colon ("Red:"); nothing is appended, so omit it if you do not want one. Re-measures.
PsColorPicker_Refresh( hCtrl )Marks the layout stale and repaints.

State

FunctionBehaviour
PsColorPicker_SetEnabled( hCtrl, bEnabled )Goes through EnableWindow, so the system enforces it rather than it being a cosmetic flag.
PsColorPicker_GetEnabled( hCtrl ) as booleanWhether the popup is enabled.
PsColorPicker_GetFocused( hCtrl ) as booleanWhether the popup has the keyboard focus.

Geometry and layout

FunctionBehaviour
PsColorPicker_GetIdealSize( hCtrl, byref cx, byref cy )The size the popup wants. Valid before the window has ever been sized, which is what the placement routines rely on. Varies with the matrix and cell size and with the font; does not vary with ShowAlpha.
PsColorPicker_GetMatrixSize( hCtrl, byref nCols, byref nRows )The Custom grid's dimensions.
PsColorPicker_SetMatrixSize( hCtrl, nCols, nRows )Sets them. Silent, and re-measures — this changes the ideal size.
PsColorPicker_GetCellSize( hCtrl, byref cx, byref cy )One matrix cell, in pixels.
PsColorPicker_SetCellSize( hCtrl, cx, cy )Sets it. Silent, and re-measures. Raw pixels — you scale for DPI.
PsColorPicker_GetPartRect( hCtrl, nPart, byref rc ) as booleanA CLR_PART_* rect. FALSE means the part is legitimately empty on this tab (CLR_PART_MATRIX off Custom, CLR_PART_ALPHA with alpha hidden) or there is no geometry yet.
PsColorPicker_GetFieldRect( hCtrl, nField, byref rc ) as booleanOne field's box. FALSE when the field is not placed.
PsColorPicker_GetTabRect( hCtrl, idx, byref rc ) as booleanOne tab cell.

Hit tests

All take client coordinates.

FunctionBehaviour
PsColorPicker_HitTestTab( hCtrl, x, y ) as longTab index, or -1.
PsColorPicker_HitTestField( hCtrl, x, y ) as longCLR_FIELD_*, or -1. Only visible fields are reachable.
PsColorPicker_HitTestBody( hCtrl, x, y ) as longThe body cell index, or -1. On Custom that is row * cols + col; on a list tab it is the entry index, and a partial row drawn past the bottom of the viewport is deliberately not hit-testable.

Colour tables

The two fixed lists, exposed so you can offer the same colours elsewhere. An out-of-range index gives 0 or "" rather than failing.

FunctionBehaviour
PsColorPicker_WebColorCount() as longHow many named web colours there are.
PsColorPicker_WebColorAt( idx ) as COLORREFOne of them.
PsColorPicker_WebColorName( idx ) as DWSTRINGIts name.
PsColorPicker_SystemColorCount() as longHow many system colours there are.
PsColorPicker_SystemColorAt( idx ) as COLORREFOne of them, read live through GetSysColor.
PsColorPicker_SystemColorName( idx ) as DWSTRINGIts name.

Pure helpers

No window needed; safe to call before anything is created.

FunctionBehaviour
PsColorPicker_HSLtoRGB( h, s, l ) as COLORREFHSL to RGB. Every axis clamps out-of-range input rather than wrapping, so a hue of 1.4 does not silently become 0.4.
PsColorPicker_MatrixColorAt( nCol, nRow, nCols, nRows ) as COLORREFThe colour the matrix law puts at that cell. Indices clamp.
PsColorPicker_MatrixIndexOf( clr, nCols, nRows ) as longThe cell index holding clr, or -1.
PsColorPicker_CellFromPoint( rc, cellW, cellH, nCols, nRows, x0, y0, byref c, byref r ) as booleanPoint to grid cell. FALSE when the point is outside the grid.
PsColorPicker_FormatHex( clr ) as DWSTRING#RRGGBB, upper case.
PsColorPicker_ParseHex( wszText, byref clr ) as booleanReads #RRGGBB with or without the #, either case. FALSE — leaving clr alone — on empty, short, long or non-hex input.
PsColorPicker_ContrastColor( clr ) as COLORREFBlack or white, whichever is legible on clr.
PsColorPicker_FieldAccepts( nField, ch, nPos ) as booleanWhether a field would take that character. Digits only; nPos is accepted for symmetry and never affects the answer. An out-of-range field accepts nothing.
PsColorPicker_FieldMaxLen( nField ) as longHow long a field may get — three digits.
PsColorPicker_PtIn( rc, x, y ) as booleanHalf-open containment: left/top inclusive, right/bottom exclusive, matching the grid arithmetic.

Callback registration and painting

FunctionBehaviour
PsColorPicker_SetColorChangedCallback( hCtrl, usersub )See Callbacks below.
PsColorPicker_SetTabChangedCallback( hCtrl, usersub )See Callbacks below.
PsColorPicker_SetMessageCallback( hCtrl, userfunc )See Callbacks below.
PsColorPicker_SetPaintCallback( hCtrl, usersub )Replaces the built-in painter wholesale.
PsColorPicker_RenderInfo( p )Runs the built-in painter into the buffer your paint callback was handed. Pass it the same pointer. This is what lets you decorate rather than replace.
PsColorPicker_CountRenderedTones( hCtrl, nPart ) as longRenders the popup offscreen through the real paint path and counts the distinct colours in one part rect, capped at 64. Public so you can assert your own paint callback has not flooded the control.
PsColorPicker_HashRenderedPart( hCtrl, nPart ) as ulongThe same render, hashed, so you can assert a state change actually reached the pixels.

Colors

Fill a PSCOLORPICKER_COLORS and hand it to PsColorPicker_SetColors. These paint the popup's own chrome — the colours it is editing are content and are never themed.

FieldPaintsWhen
BackColorThe popup's background.Always.
ForeColorField labels and the two preview labels.Always.
BorderColorThe frame around the preview box, and each list swatch.Always.
TabBackColorA tab cell's background.An unselected, unhovered tab.
TabBackColorSelA tab cell's background.The selected tab, and also the hovered one.
TabForeColorA tab caption.An unselected tab, and every tab while the popup is disabled.
TabForeColorSelA tab caption.The selected tab.
FieldBackColorAn entry field's box.Always.
FieldForeColorThe text and caret in an entry field.Always.
FieldBorderColorAn entry field's frame.A field without the caret.
FieldBorderColorSelAn entry field's frame.The field holding the caret.
HotRingColorThe ring around a swatch.The swatch under the cursor.
SelRingColorThe ring around a swatch, and the rule under the selected tab.The selected swatch.
FocusRingColorA ring around the whole content area.The popup has focus and no field holds the caret.
OKBackColorThe OK button's fill.Idle.
OKBackColorHotThe OK button's fill.Cursor over it.
OKBackColorPressedThe OK button's fill.Held down with the cursor still on it.
OKForeColorThe OK button's caption.Always.
OKBorderColorThe OK button's frame.Always.

The OK button's fill resolves pressed > hot > idle. There is no disabled pair, because OK is never disabled — every reachable state of the popup has a committable colour in it.

A swatch ring is kept distinct from the swatch fill so a selection stays visible on a swatch of any colour, including one identical to the panel behind it.

Callbacks

Value changed

type CLR_ColorChangedCallbackSub as sub( byval hCtrl as HWND, _
                                         byval clr as COLORREF, _
                                         byval nAlpha as ubyte, _
                                         byval nPhase as long )

Fires for user action only. Every programmatic setter is silent, which is what lets you call PsColorPicker_SetColor from inside this callback without re-entering.

nPhase is the distinction that makes live preview possible. Dragging across the matrix emits one CLR_PHASE_BEGIN, many CLR_PHASE_MOVEs and one CLR_PHASE_END; a click on a swatch, a commit in a field, or a click on the Initial preview each emit a single CLR_PHASE_END. Coalesce the moves, commit on the end.

If a stolen capture abandons a drag, no END is sent. Abandoned is not the same as finished, and inventing an END would tell you the user settled on a colour they were merely passing over. Treat a missing END the way you treat a cancel.

You do not need this callback at all if you only want the final answer — DoModal reports that itself. Note that it is reachable only through Create + RunModal; DoModal has nowhere in its signature to take one.

Tab changed

type CLR_TabChangedCallbackSub as sub( byval hCtrl as HWND, byval nTab as long )

Fires when the user switches tabs. PsColorPicker_SetTab is silent.

Message observer

type CLR_MessageCallbackFunc as function( byval m as PSCOLORPICKER_MESSAGEINFO ptr ) as boolean

Return TRUE to suppress the control's own handling of that message, FALSE to let it proceed.

The result is ignored for three messages, and the reasons are worth knowing:

  • WM_LBUTTONUP — the control holds mouse capture across a matrix drag and across the OK button's press, and this message is what releases it. A callback that suppressed it would strand the capture and route every later click into the popup.
  • WM_SETFOCUS and WM_KILLFOCUS — focus is a fact the system reports, not an action to veto. WM_KILLFOCUS is also what commits a half-typed field, so suppressing it would silently discard the user's typing.
PSCOLORPICKER_MESSAGEINFO fieldMeaning
hColorPickerThe popup.
uMsgThe message.
wParamIts wParam.
lParamIts lParam.

Painting

type CLR_PaintCallbackSub as sub( byval p as any ptr )

Cast p to a PSCOLORPICKER_PAINTINFO ptr. The control has already filled the client with BackColor before calling you.

Do not use PaintBorderRect to draw an outline. It fills unconditionally before it strokes, so used as a frame it erases everything beneath it and the whole popup renders as one flat rectangle. PaintRoundOutline strokes without filling. More generally: draw your additions, not a background — a callback that fills a rectangle covering the control erases everything already drawn. PsColorPicker_CountRenderedTones exists so you can assert you have not done this.

To decorate rather than replace, call PsColorPicker_RenderInfo( p ) first and then add your own drawing on top:

private sub MyPickerPaint( byval p as any ptr )
    dim as PSCOLORPICKER_PAINTINFO ptr pi = cast( PSCOLORPICKER_PAINTINFO ptr, p )
    if pi->b = 0 then exit sub

    PsColorPicker_RenderInfo( p )

    dim as RECT rc = pi->rcBody
    InflateRect( @rc, 2, 2 )
    pi->b->SetPenColor( BGR(86,156,214) )
    pi->b->PaintRoundOutline( @rc, 6, 1 )
end sub

Every rect below is precomputed. Never re-derive one from another.

PSCOLORPICKER_PAINTINFO fieldMeaning
hColorPickerThe popup, so you can query it.
bThe buffer for this repaint. Not a copy — draw into it.
rcClientThe whole client area.
rcContentrcClient deflated by the padding.
rcTabsThe whole tab strip. The tabs themselves do not fill it.
rcBodyThe tab's content area. Fixed height across all tabs; never empty.
rcMatrixThe tint/shade grid, or empty when not on Custom.
rcSwatchesThe list area, scrollbar excluded, or empty when on Custom.
rcOKThe OK button. Never empty.
rcPreviewThe whole split preview box.
rcInitialIts top half, the baseline.
rcCurrentIts bottom half, the live value.
rcInitialLabelThe "Initial" label, above the box.
rcCurrentLabelThe "Current" label, below the box.
rcFieldsThe entry block, previews excluded.
rcAlphaThe alpha field's box, or empty when alpha is hidden.
clrCurrentThe live colour.
nAlphaCurrentThe live alpha.
clrInitialThe baseline colour.
nAlphaInitialThe baseline alpha.
nTabCLR_TAB_*.
isEnabledWhether the popup is enabled.
isFocusedWhether it has focus.
isDraggingA live matrix drag is in progress.
isOKHotThe cursor is over OK.
isOKPressedOK is held down and the cursor has not slid off.
nFocusFieldCLR_FIELD_*, or -1 when no field holds the caret.

rcMatrix and rcSwatches are mutually exclusive — whichever the current tab does not use is empty — so you can branch on IsRectEmpty instead of re-implementing the tab-to-body mapping.

Keyboard

KeyEffect
EscapeCancels. DoModal returns FALSE and your variables are untouched. Works from inside a field, and deliberately does not commit that field on the way out.
EnterWith the caret in a field: commits it, clamps it, re-seeds it, and stays there. Otherwise: accepts, exactly as OK does.
Tab / Shift+TabWalks the visible fields, wrapping at both ends. Hidden fields are skipped. From the body, Tab enters the first field.
Left / RightIn a field: moves the caret. Otherwise: moves the body selection by one cell and adopts that colour.
Up / DownOutside a field: moves the body selection by one row — one list row, or one whole row of matrix cells.
Home / EndMoves the caret within a field.
Backspace / DeleteEdits the field.
DigitsTyped into the focused field. The first character after entering a field replaces the seeded value; any editing key switches it to amending.
Mouse wheelScrolls the list on the Web and System tabs.
Double-clickOn a body cell: adopts it and accepts, exactly as Enter does. Anywhere else: an ordinary click.

Constants

Tabs

ConstantValue
CLR_TAB_WEB0
CLR_TAB_SYSTEM1
CLR_TAB_CUSTOM2
CLR_TAB_COUNT3

Change phases

ConstantMeaning
CLR_PHASE_BEGINA drag started.
CLR_PHASE_MOVEOne step of a drag.
CLR_PHASE_ENDA settled value.

Parts

CLR_PART_TABS, CLR_PART_MATRIX, CLR_PART_SWATCHES, CLR_PART_INITIAL, CLR_PART_CURRENT, CLR_PART_FIELDS, CLR_PART_ALPHA, CLR_PART_OK.

Fields

CLR_FIELD_R (0), CLR_FIELD_G, CLR_FIELD_B, CLR_FIELD_A, CLR_FIELD_COUNT (4). The enum order is the visual order, top to bottom, and the Tab order. CLR_FIELD_A keeps its id whether or not the row is shown, so a stored "which field had focus" never changes meaning.

Defaults, in unscaled pixels

DPI scaling is applied once at creation. Every setter afterwards takes raw pixels and you scale.

ConstantDefaultMeaning
PSCOLORPICKER_DEFAULT_PAD8Padding around the content.
PSCOLORPICKER_DEFAULT_GAP6Gap between blocks and between field rows.
PSCOLORPICKER_DEFAULT_TABHEIGHT24Tab strip height.
PSCOLORPICKER_DEFAULT_TABPAD14Padding either side of a tab's caption.
PSCOLORPICKER_DEFAULT_MATRIXCOLS16Matrix columns.
PSCOLORPICKER_DEFAULT_MATRIXROWS14Matrix rows.
PSCOLORPICKER_DEFAULT_CELLW18Matrix cell width.
PSCOLORPICKER_DEFAULT_CELLH22Matrix cell height.
PSCOLORPICKER_DEFAULT_SWATCHW34Swatch width in a list row.
PSCOLORPICKER_DEFAULT_LISTROWS14Visible list rows.
PSCOLORPICKER_DEFAULT_SCROLLW14Scrollbar strip width.
PSCOLORPICKER_DEFAULT_PREVIEWW92Preview box width.
PSCOLORPICKER_DEFAULT_PREVIEWH46Height of one half of the preview box.
PSCOLORPICKER_DEFAULT_FIELDW58Entry field width.
PSCOLORPICKER_DEFAULT_FIELDH24Entry field height.
PSCOLORPICKER_DEFAULT_OKW92OK button width.
PSCOLORPICKER_DEFAULT_OKH30OK button height.
PSCOLORPICKER_DEFAULT_LABELW52Field label gutter.
PSCOLORPICKER_DEFAULT_ROWH22List row height, and each preview label row.
PSCOLORPICKER_DEFAULT_CURVATURE4Corner rounding, as an ellipse diameter.
PSCOLORPICKER_DEFAULT_BORDERTHICK1Border pen width. Not scaled here — the painter scales the pen.
PSCOLORPICKER_DEFAULT_FOCUSTHICK1Focus ring pen width. Not scaled here, for the same reason.
PSCOLORPICKER_RINGINSET1How far a hot/selected ring is inset into its swatch.

The popup embeds a PsVScrollBar for the Web and System lists, and paints everything through PsBufferPaint. You never call into either directly, but their files must be present — see Requirements.

Licence

Mozilla Public License 2.0.

MPL-2.0 is file-level copyleft, chosen deliberately for a drop-in control:

  • You may use this in closed-source software, commercial or otherwise. §3.2 permits static linking with no additional conditions.
  • If you modify these files, publish those files' changes. The obligation is per-file — your own sources are unaffected however tightly they are combined with these.
  • The Exhibit B "Incompatible With Secondary Licenses" notice is not applied, which keeps this GPL-compatible.