Repository navigation
PsCheckBox
Home › All Categories › PsControls › PsCheckBox
An owner-drawn checkbox for tiko + Win32, built on sys/win and PsBufferPaint. It draws a
box glyph from a font — Segoe Fluent Icons by default — and an optional caption, with a focus ring
around the pair. The box can sit on either side of the caption. You supply the colours and the
fonts; the control owns the geometry and the state.
It is a real HWND, so you place and size it with SetWindowPos like any other control. It takes
focus, tracks hover and press, greys itself when disabled through EnableWindow, and tells you
when the user changed its state. It has two states — checked and unchecked. There is no
indeterminate state.
The box is one composite glyph per state: U+E739, an empty outlined square, for unchecked,
and U+E73A, the same square with a tick in it, for checked. Both are settable. That single
decision shapes the rest of the control: the outline and the tick are one piece of ink in one
colour, so an accent-filled box with a contrasting white tick is not reachable without a paint
callback — but everything about the box's appearance is swappable by changing a font or a
codepoint.

The module is vendor/PsControls/PsCheckBox. It comes with the toolchain and brings in
PsBufferPaint, PsTipHost and PsTooltip itself.
#import "sys/win"
#import "sys/win/gui"
#import "sys/win/gdiplus"
#import "vendor/PsControls/PsTipHost"
#import "vendor/PsControls/PsTooltip"
#import "vendor/PsControls/PsCheckBox"
using gui
using gdiplus
using PsTipHost
using PsTooltip
using PsCheckBoxThe PsTipHost and PsTooltip lines are needed only to name PSTIP_MODE_* and to call the
PsTooltip_* procedures — see Tooltips: two backends.
You also need a font supplying the box glyph — Segoe Fluent Icons; see Running the demo.
Your program must define UNICODE, as the vendor package does for its modules. Set it in
tiko.toml, as the demo does:
[target.main]
main = "main.tiko"
output = "main.exe"
resources = ["main.rc"]
[target.main.defines]
UNICODE = "1"UNICODE has to come from the manifest (or -d UNICODE on a tiko command line): a source
file cannot #define it ahead of #import "sys/win", and without it every bare Win32 name —
DrawText, SetWindowText, CreateFontIndirect — binds its ANSI form.
The demo is in the toolchain's examples/PsControls/PsCheckBox/: tiko build there (debug
profile: -g -exx) or tiko build --profile release writes main.exe there.
PsBufferPaint renders all geometry through GDI+. Initialize it before the first repaint and shut
it down after every window is destroyed:
dim as ULONG_PTR gdipToken = gdiplus.Init()
function = frmMain_Show( 0 )
gdiplus.Uninit( gdipToken )Without the bracket the control draws nothing.
There is no PsCheckBox_FilterMessage. This control owns no popup and no second top-level
window, so it adds no pump obligation — unlike PsComboBox, PsNumericUpDown and PsTextBox, which
all do. If you arrived from one of those, there is nothing to look for here.
Two things your pump does need, because the control is focusable. First, IsDialogMessage:
do while GetMessage(@uMsg, null, 0, 0)
if uMsg.message = WM_QUIT then exit do
' IsDialogMessage is what makes Tab move between controls.
if IsDialogMessage(pWindow->hWindow, @uMsg) = 0 then
TranslateMessage @uMsg
DispatchMessage @uMsg
end if
loopSecond, once at startup, focus on one of your controls:
SetFocus( hCheck )IsDialogMessage only acts when the focused window is already a descendant of the window you pass
it. Without that SetFocus, Tab does nothing until the user clicks something — a symptom that
reads as broken tabstops. A dialog does this for you in WM_INITDIALOG; an ordinary CWindow
host must do it itself.
Without IsDialogMessage the control still works fully with the mouse, and with Space and Enter
once it has focus. Only Tab navigation is lost.
dim as HWND hCheck = PsCheckBox_Create( hWndParent, IDC_MYCHECK )
PsCheckBox_SetFont( hCheck, ghFont(GUIFONT_10) ) ' caption font, and the MEASURING font
PsCheckBox_SetGlyphFont( hCheck, ghFont(SYMBOLFONT_12) ) ' Segoe Fluent Icons
PsCheckBox_SetText( hCheck, "Enable dark mode" )
PsCheckBox_SetChecked( hCheck, true ) ' silent: fires no callback
PsCheckBox_SetCheckChangedCallback( hCheck, @OnDarkModeChanged )
' Size it to what it actually needs, then place it.
dim as long iw, ih
PsCheckBox_GetIdealSize( hCheck, iw, ih )
SetWindowPos( hCheck, 0, 20, 20, iw, ih, SWP_NOZORDER )
ShowWindow( hCheck, SW_SHOW )sub OnDarkModeChanged( byval hCheckBox as HWND, byval isChecked as boolean )
dim as string s = "dark mode = " & str(isChecked)
print s
end subThat callback fires for a completed click, for Space or Enter, and for PsCheckBox_Toggle. It does
not fire for PsCheckBox_SetChecked.
PsCheckBox_Create returns an HWND, not an opaque type, because you legitimately need to treat
the control as a window — SetWindowPos, ShowWindow, EnableWindow, GetDlgItem. The CtrlID
you pass becomes the window's GWLP_ID and the initial SetID value.
The control frees itself when its window is destroyed, and destroys its tooltip with it. The two fonts you hand it stay yours to delete.
You never assign a rectangle. You set inputs — padding, box size, gap, focus ring — and the control derives four rectangles from them, lazily: a setter marks the layout stale, and the next paint or rect query does a single measuring pass. A burst of setters costs one measurement, not one each.
ringPad = nFocusGap + nFocusThick reserved ALWAYS, focused or not
textW = measured caption width 0 when there is no caption
textH = GetTextMetrics.tmHeight one height for the control
textBlock = hasText ? nBoxGap + textW : 0
contentH = max( hasText ? textH : 0 , nBoxHeight )
nIdealW = 2*ringPad + nPadLeft + nBoxWidth + textBlock + nPadRight
nIdealH = 2*ringPad + nPadTop + contentH + nPadBottom
rcContent = rcClient deflated by ringPad on all four sides
rcVisual = rcContent inflated by ringPad ( = rcClient when sized ideally )
BOXALIGN_LEFT rcBox at rcContent.left + nPadLeft, nBoxWidth x nBoxHeight, v-centred
rcText = ( rcBox.right + nBoxGap , rcContent.right - nPadRight )
BOXALIGN_RIGHT rcBox at rcContent.right - nPadRight - nBoxWidth, same, v-centred
rcText = ( rcContent.left + nPadLeft , rcBox.left - nBoxGap )
PsCheckBox_GetIdealSize is valid before the control has ever been sized — the measuring pass
runs ahead of the check for a client area, precisely so you can call it to decide how big to make
the control in the first place. It is also unaffected by starving the control's client area, so a
host that always sizes with it can never see an ellipsis.
The box cell is pinned to the padding on whichever side you put it, and the caption takes the whole span left over. One model, two looks, decided entirely by the width you give the control:
sized to GetIdealSize [x] Enable dark mode packed
sized to a row width Enable dark mode [x] settings row
So a column of CHK_BOXALIGN_RIGHT checkboxes all given the same width will line their boxes up
for free. Flipping the side does not change the ideal size — the box cell, the gap and both
paddings are all still charged, they have only swapped ends.
PsCheckBox_SetTextAlign places the caption inside that span, not inside the control. With the
box on the left and CHK_TEXTALIGN_RIGHT, the caption goes to the right-hand end of the span, not
to the right-hand edge of the window. Sized to its ideal width the span is the caption, so all
three alignments render identically — alignment only becomes visible once the control has slack.
PsCheckBox_SetBoxSize states how much room the glyph gets. The control never measures the glyph
font, so a glyph too large for its cell clips rather than resizing the control. This is why
PsCheckBox_SetGlyphFont repaints but never re-lays-out, and why swapping either glyph string can
never change the ideal width.
PsCheckBox_SetChecked changes the state and fires nothing — Win32's BM_SETCHECK / BN_CLICKED
split. That is what makes it safe to call from inside your own change handler without recursing.
The one deliberate exception is PsCheckBox_Toggle, which flips the state and fires the
callback. It is an action, not a setter — the equivalent of BM_CLICK, and the door your
accelerator uses, since this control parses no mnemonics.
The control is a WS_TABSTOP with real focus tracking and a painted focus ring. Space and
Enter both toggle it while it is focused.
Enter is a departure from a system checkbox, which ignores it and lets it reach the dialog's
default button. If that matters to your dialog, intercept WM_KEYDOWN through the message
callback and return TRUE for VK_RETURN.
The focus ring's band is reserved whether or not the control has focus, so nothing moves when focus arrives — which also means the ring settings contribute to the ideal size.
The control takes mouse capture on a press, because press → slide off → release must not toggle. Only a matched press and release with the cursor still inside counts as a click. While the press is live the hover state keeps tracking, so sliding off visibly un-flashes the control before the release decides that nothing happened.
-
Two states only.
isCheckedis abooleaneverywhere, including in the change callback. There is no indeterminate / tri-state. - One colour per box. The composite glyph is a single piece of ink, so the box outline and the tick inside it always share a colour. A two-colour box needs a paint callback.
-
No mnemonics.
"&Enable"draws a literal ampersand.PsPaintBuffer.PaintTextforcesDT_NOPREFIX, so this is enforced by the renderer rather than merely intended. Wire an accelerator toPsCheckBox_Toggleinstead. -
No multiline. One line, ellipsized into the caption span. Wrapping would make the ideal
height depend on the assigned width, which would cost
GetIdealSizeits "valid before sizing" guarantee. -
No
HICONpath. The box comes from a font, or from your paint callback. -
No
WM_COMMAND/BN_CLICKEDto the parent. State changes reach you through the callback only. -
No
CS_DBLCLKS. A rapid second click is a legitimate second toggle; enabling double-clicks would turn it into aWM_LBUTTONDBLCLKthe control would drop. -
No hit test. The whole client is the hit area — clicking the caption toggles, exactly as a
system checkbox does — so a hit test could only ever be
PtInRect(client). - Overflow is honest, not squeezed. Too narrow, and the box keeps its declared size and its padding while the caption span collapses to zero width and the caption ellipsizes. Too short, and the content clips rather than deforming.
- Removing the caption drops the content height to the box alone. A caption-less checkbox is therefore never taller than a captioned one, and is shorter whenever the box is smaller than a line of text — which it is at the defaults. If you are laying out a column of mixed rows, take the max of the ideal heights yourself.
-
SetGlyphFontis worth setting. Unset, the glyph falls back to the caption font, which will draw the Fluent codepoint as a missing-glyph box.
| Function | Behaviour |
|---|---|
PsCheckBox_Create( hWndParent, CtrlID ) as HWND |
Creates the control, zero-sized and hidden. CtrlID becomes GWLP_ID and the initial SetID value. Size it with GetIdealSize and place it with SetWindowPos, or turn on SetAutoSize. |
All silent — they change what is drawn, never what was clicked.
| Function | Behaviour |
|---|---|
PsCheckBox_GetText( hCheckBox ) as ustring |
The caption. |
PsCheckBox_SetText( hCheckBox, Text ) |
Sets the caption and re-measures. "" removes it, which also gives back the box gap and drops the content height to the box alone. Also reachable as the window text — see below. |
PsCheckBox_GetGlyphUnchecked( hCheckBox ) as ustring |
The codepoint(s) drawn when unchecked. |
PsCheckBox_SetGlyphUnchecked( hCheckBox, Glyph ) |
Replaces it. Repaints only — the cell is declared, so this can never change the ideal size. "" draws nothing while still charging the cell and the gap. |
PsCheckBox_GetGlyphChecked( hCheckBox ) as ustring |
The codepoint(s) drawn when checked. |
PsCheckBox_SetGlyphChecked( hCheckBox, Glyph ) |
As above, for the checked state. |
PsCheckBox_GetID( hCheckBox ) as long |
The stored host id. |
PsCheckBox_SetID( hCheckBox, id ) |
A host payload only. The control never uses it as a command id, so it cannot collide with anything. |
PsCheckBox_GetItemData( hCheckBox ) as integer |
Free-form host payload. |
PsCheckBox_SetItemData( hCheckBox, itemData ) |
Stores it. The control never reads it. |
The caption has one store reached through two doors: the control handles WM_SETTEXT,
WM_GETTEXT and WM_GETTEXTLENGTH against the same field, so SetWindowText and GetWindowText
work and generic code that walks children and reads their text sees a real caption.
SetWindowText re-measures exactly as SetText does.
| Function | Behaviour |
|---|---|
PsCheckBox_GetChecked( hCheckBox ) as boolean |
The current state. |
PsCheckBox_SetChecked( hCheckBox, isChecked ) |
Silent — fires no callback. Repaints only; never re-measures, since both glyphs share one declared cell. Does nothing if the state already matches. |
PsCheckBox_GetEnabled( hCheckBox ) as boolean |
|
PsCheckBox_SetEnabled( hCheckBox, isEnabled ) |
Goes through EnableWindow, so the disable is enforced by the system: the control gets no mouse input and Tab skips it. Clears hover and any live press at once. Does not change the checked state. |
PsCheckBox_GetFocused( hCheckBox ) as boolean |
Answers for this window directly — there is no inner child holding focus. |
PsCheckBox_Refresh( hCheckBox ) |
Marks the layout stale and invalidates with a background erase. |
Reaching for EnableWindow yourself instead of SetEnabled also works: the control handles
WM_ENABLE and syncs its own flag, so it still renders greyed rather than looking live while
eating input.
| Function | Behaviour |
|---|---|
PsCheckBox_Toggle( hCheckBox ) |
Flips the state and fires CheckChangedCallback — an action, not a setter. Refused outright on a disabled control. Does not paint a press flash. |
Every setter takes raw pixels; you do the DPI scaling. Only the Create-time defaults are scaled
for you, and nFocusThick is never scaled at all — a hairline should stay a hairline.
| Function | Behaviour |
|---|---|
PsCheckBox_GetBoxAlign( hCheckBox ) as long |
CHK_BOXALIGN_LEFT or CHK_BOXALIGN_RIGHT. |
PsCheckBox_SetBoxAlign( hCheckBox, nBoxAlign ) |
Puts the box on that side, pinned to that side's padding. Values outside the enum are ignored. Does not change the ideal size, so it never triggers auto-size. |
PsCheckBox_GetTextAlign( hCheckBox ) as long |
CHK_TEXTALIGN_*. |
PsCheckBox_SetTextAlign( hCheckBox, nTextAlign ) |
Places the caption within its span, not within the control. Values outside the enum are ignored. Repaints only — the span does not move, just the ink inside it. |
PsCheckBox_GetPadding( hCheckBox, nLeft, nTop, nRight, nBottom ) |
All four, by reference. |
PsCheckBox_SetPadding( hCheckBox, nLeft, nTop, nRight, nBottom ) |
Negative values are clamped to 0. Changes the ideal size. |
PsCheckBox_GetBoxGap( hCheckBox ) as long |
|
PsCheckBox_SetBoxGap( hCheckBox, nBoxGap ) |
Space between the box cell and the caption. Charged only when there is a caption. Negative clamps to 0. |
PsCheckBox_GetBoxSize( hCheckBox, nBoxWidth, nBoxHeight ) |
|
PsCheckBox_SetBoxSize( hCheckBox, nBoxWidth, nBoxHeight ) |
The declared cell for the glyph. Nothing is measured, so this is the only thing deciding how much room the glyph gets. Negative clamps to 0. |
PsCheckBox_GetFocusRing( hCheckBox, nGap, nThickness ) |
|
PsCheckBox_SetFocusRing( hCheckBox, nGap, nThickness ) |
Gap from the content to the ring, and the ring's own thickness. Both are reserved unconditionally, so changing either changes the ideal size. Negative clamps to 0; thickness 0 disables the ring. |
PsCheckBox_GetFocusCurvature( hCheckBox ) as long |
|
PsCheckBox_SetFocusCurvature( hCheckBox, nCurvature ) |
An ellipse diameter, not a radius: 8 draws a 4px radius. 0 gives square corners. Repaints only — the band's width is the gap plus the thickness, not the curvature. Negative clamps to 0. |
PsCheckBox_GetAutoSize( hCheckBox ) as boolean |
|
PsCheckBox_SetAutoSize( hCheckBox, bAutoSize ) |
When true the control SetWindowPoses itself to its ideal size, keeping its top-left, and applies it immediately. Default false. |
PsCheckBox_GetIdealSize( hCheckBox, nWidth, nHeight ) |
Forces any pending layout and returns the measured size. Valid before the control has ever been sized, and unaffected by starving the control's client area. |
PsCheckBox_GetContentRect( hCheckBox, rc ) as boolean |
The client deflated by the focus-ring band. Returns FALSE if the control has no geometry yet. |
PsCheckBox_GetBoxRect( hCheckBox, rc ) as boolean |
The box cell. Never empty. |
PsCheckBox_GetTextRect( hCheckBox, rc ) as boolean |
The caption span. Empty when there is no caption; degenerate (left = right) when the client is too narrow. Returns TRUE either way — empty is the honest answer. |
PsCheckBox_GetVisualRect( hCheckBox, rc ) as boolean |
The content inflated by the ring band; equals the client when the control is sized ideally. |
All four rect queries force a pending layout first, so results are always current.
With auto-size on, the setters that re-apply it are exactly the ones that can move the ideal size:
SetText (and WM_SETTEXT), SetFont, SetPadding, SetBoxGap, SetBoxSize, SetFocusRing,
and SetAutoSize itself.
| Function | Behaviour |
|---|---|
PsCheckBox_GetColors( hCheckBox, pColors ) |
Fills your struct. The read-modify-write idiom is Get, assign one field, Set. Does nothing if pColors is null. |
PsCheckBox_SetColors( hCheckBox, pColors ) |
Copies the whole struct and repaints. Does nothing if pColors is null. |
PsCheckBox_GetFont( hCheckBox ) as HFONT |
|
PsCheckBox_SetFont( hCheckBox, hTextFont ) |
The caption font, and the measuring font. Changing it re-measures and can change the ideal size. Borrowed, never owned. Unset, the control measures and paints with the stock GUI font. |
PsCheckBox_GetGlyphFont( hCheckBox ) as HFONT |
|
PsCheckBox_SetGlyphFont( hCheckBox, hGlyphFont ) |
The box font. A paint-time input only: repaints, never re-lays-out, never resizes. Unset, it falls back to the caption font. Borrowed, never owned. |
Both are pure functions — no control handle, no globals — so you can call them from your own paint callback to get exactly the colours the built-in painter would have used.
| Function | Behaviour |
|---|---|
PsCheckBox_ResolveMood( isEnabled, isPressed, isHot ) as long |
Returns a CHK_MOOD_* value. Precedence is disabled > pressed > hot > idle. |
PsCheckBox_ResolveColors( pColors, nMood, isChecked, clrBack, clrFore, clrBox ) |
Fills the three output colours by reference. isChecked selects between the two box colour sets within the mood and affects nothing else. Does nothing if pColors is null. |
| Function | Behaviour |
|---|---|
PsCheckBox_GetTooltipText( hCheckBox ) as ustring |
|
PsCheckBox_SetTooltipText( hCheckBox, Text ) |
The control's own tip text. When non-empty it wins over the callback. Stores only — the tip is pulled on demand, so there is nothing to repaint. |
PsCheckBox_GetTooltipHandle( hCheckBox ) as HWND |
The comctl32 tooltip window, for any TTM_* message you want to send it yourself. The control owns it and destroys it. Returns 0 while this control is on the PsTooltip backend — the honest answer, since a TTM_* sent to a PsTooltip window is silently ignored. |
PsCheckBox_GetPsTooltipHandle( hCheckBox ) as HWND |
The PsTooltip window, or 0 while on the system backend. The door to PsTooltip_SetColors / SetFonts / SetStyle / SetMaxWidth / SetTitle / SetGlyph — none of which is mirrored here. |
PsCheckBox_SetTooltipMode( hCheckBox, nMode ) as boolean |
PSTIP_MODE_SYSTEM (default) or PSTIP_MODE_PS. See below. |
PsCheckBox_GetTooltipMode( hCheckBox ) as long |
Which backend is live. |
PsCheckBox_SetHoverTime( hCheckBox, milliseconds ) |
Initial delay (TTDT_INITIAL) — how long the cursor must rest. Honoured by both backends. |
PsCheckBox_SetAutoPopTime( hCheckBox, milliseconds ) |
How long the tip stays up (TTDT_AUTOPOP). |
PsCheckBox_SetReshowTime( hCheckBox, milliseconds ) |
The shorter delay after a tip was recently dismissed (TTDT_RESHOW). |
Resolution order: the control's own text, then TooltipCallback, then nothing. There is no
caption fallback — a checkbox whose caption is already on screen would otherwise pop a tip that
just repeats it.
The control ships on the system (comctl32) tooltip it has always used. PSTIP_MODE_PS
switches this instance to PsTooltip: owner-drawn, themeable, word-wrapping without a
hand-sent TTM_SETMAXTIPWIDTH, and — the one that matters structurally — not a subclass of
the control it serves, so it still works when the window under the cursor is a descendant
rather than the control itself. Either way the control adds no pump obligation: PsTooltip
has no FilterMessage.
The default is deliberate, not caution. PsTooltip's colour defaults are dark, so a control
that switched itself would put a dark tip on a light form. Theme every tip in the process with
PsTooltip_SetDefaultColors and friends, then opt in per instance.
The mode changes how a tip is drawn, never what it says. Both backends resolve text through
the same rule — this control's own text first, then TooltipCallback, then nothing.
A delay you set is stored as well as pushed, so it survives a switch in either direction. A delay you never set keeps the backend's own derivation from the system double-click time, which is what makes a tip appear on the same beat as every other tip on the machine.
PsTooltip_SetDefaultColors( @myTipColors ) ' once, at startup, for every tip in the process
PsTooltip_SetDefaultFonts( ghFontUI )
PsCheckBox_SetTooltipMode( hCheck, PSTIP_MODE_PS )
PsCheckBox_SetHoverTime( hCheck, 400 )
dim as HWND hTip = PsCheckBox_GetPsTooltipHandle( hCheck )
if hTip then PsTooltip_SetTitle( hTip, "Dark mode" ) ' not reachable on the system backend| Function | Behaviour |
|---|---|
PsCheckBox_SetPaintCallback( hCheckBox, usersub ) |
Replaces the built-in painter wholesale, and repaints. |
PsCheckBox_SetMessageCallback( hCheckBox, userfunc ) |
Observe mouse, focus and key messages. |
PsCheckBox_SetTooltipCallback( hCheckBox, userfunc ) |
Supply tip text on demand. |
PsCheckBox_SetCheckChangedCallback( hCheckBox, usersub ) |
User-driven state changes. |
Pass 0 to any of them to remove the callback.
Neither is called by the control. They exist so a host writing its own paint callback can assert two specific defects rather than eyeball them. Both render the control offscreen with its current painter and with focus forced on, so the focus-ring step — the one the flood defect lives on — is actually exercised.
| Function | Behaviour |
|---|---|
PsCheckBox_CountRenderedTones( hCheckBox, nPart ) as long |
How many distinct colours land inside one CHK_PART_* rect, capped at 64. Returns 0 if the control has no geometry or the part rect is empty. A part wiped by an unintended fill is literally one tone, so the useful assertion is > 1. |
PsCheckBox_HashRenderedPart( hCheckBox, nPart ) as ulong |
An FNV-1a hash of the pixels in one CHK_PART_* rect. Compare two states to prove a change actually reached the surface. Returns 0 on failure. |
HashRenderedPart can only ever prove difference, never correctness — and only for what you
isolate. Comparing checked against unchecked proves "the state reached the pixels", but not that
the glyph did, because the box colour changes too. To isolate one variable, flatten the other
first: set all eight box colours to the same value and the only thing left that can move a pixel
is the glyph.
PSCHECKBOX_COLORS is a flat struct of COLORREF fields, all with defaults. Copy semantics: Set
takes a copy, Get fills one out.
| Field | Paints | When |
|---|---|---|
BackColor |
The whole client | Idle |
BackColorHot |
The whole client | Cursor over the control |
BackColorPressed |
The whole client | Live left press, cursor still inside |
BackColorDisabled |
The whole client | Disabled |
ForeColor |
The caption | Idle |
ForeColorHot |
The caption | Hot |
ForeColorPressed |
The caption | Pressed |
ForeColorDisabled |
The caption | Disabled |
BoxColor |
The box glyph | Unchecked, idle |
BoxColorHot |
The box glyph | Unchecked, hot |
BoxColorPressed |
The box glyph | Unchecked, pressed |
BoxColorDisabled |
The box glyph | Unchecked, disabled |
BoxColorChecked |
The box glyph | Checked, idle |
BoxColorCheckedHot |
The box glyph | Checked, hot |
BoxColorCheckedPressed |
The box glyph | Checked, pressed |
BoxColorCheckedDisabled |
The box glyph | Checked, disabled |
FocusRingColor |
The focus ring | Whenever the control has focus |
Precedence: disabled > pressed > hot > idle. The checked flag is not a mood — it selects
between the two box colour sets inside whichever mood resolved, and touches nothing else. That
is also why a disabled ticked box still reads as ticked: it has its own colour.
The control reads completely flat out of the box. BackColorHot, BackColorPressed and
BackColorDisabled all default equal to BackColor, so hovering changes only the box and the
caption. That is usually what you want in a settings pane. To get a menu-style highlight across the
whole row, set one field:
dim as PSCHECKBOX_COLORS c
PsCheckBox_GetColors( hCheck, @c )
c.BackColorHot = BGR( 44, 49, 58)
PsCheckBox_SetColors( hCheck, @c )type CHK_CheckChangedCallbackSub as sub( byval hCheckBox as HWND, byval isChecked as boolean )The state changed through user interaction — a completed click, Space, or Enter — or through
PsCheckBox_Toggle. Fired after the state is updated, so PsCheckBox_GetChecked() already agrees
with the isChecked you are handed.
PsCheckBox_SetChecked does not fire it, which is what makes it safe to call SetChecked from
inside this handler.
Nothing fires for a cancelled gesture: press the control, slide off it, release, and the state does not change.
type CHK_PaintCallbackSub as sub( byval p as PSCHECKBOX_PAINTINFO ptr )Draws the whole control instead of the built-in painter. Paint through p->b — the control's
double buffer for this repaint. Do not touch the screen DC.
The control has already filled the client with the resolved mood's BackColor before calling you,
so a callback that only adds a foreground does not have to repaint the background. The focus ring
is the built-in painter's job too, so a callback that wants one must draw it.
Two rules worth honouring:
- Draw the caption with the same font you handed to
PsCheckBox_SetFont. The ideal width was measured with it; a wider font here makes that measurement a lie and the caption clips. -
Do not use
PaintBorderRectto draw a frame or a ring. It fills before it strokes, so used as an outline it erases everything beneath it and the control renders as one solid block. UsePaintRoundOutline, which strokes without filling, orPaintRectFactorywhen you do want the fill.PsCheckBox_CountRenderedTonesexists so you can assert you have not made this mistake.
More generally: a callback that fills a rectangle covering the whole control erases everything already drawn. Draw your additions, not a background.
type CHK_MessageCallbackFunc as function( byval m as PSCHECKBOX_MESSAGEINFO ptr ) as booleanObserve messages. Return TRUE to suppress the control's default handling, FALSE to let it proceed.
You see the mouse messages (WM_MOUSEMOVE, WM_MOUSELEAVE, WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_RBUTTONDOWN, WM_RBUTTONUP), both focus messages, and WM_KEYDOWN. The right-button messages
are reported and never acted on — a context menu is your business — and because no capture is taken
for the right button, an up can arrive without a down.
The result is ignored for three messages:
| Message | Why |
|---|---|
WM_LBUTTONUP |
The control holds mouse capture across a press and this message releases it. A callback that suppressed it would strand the capture and route every later click to this control. Suppressing WM_LBUTTONDOWN suppresses the press itself, which is the supported way to veto a click. |
WM_SETFOCUS |
Focus is a fact the system reports, not an action to veto. The state is already updated by the time you are called. A host that does not want the control focusable must not make it a tabstop. |
WM_KILLFOCUS |
As above. |
type CHK_TooltipCallbackFunc as function( byval hCheckBox as HWND ) as ustringCalled only when a tip is about to show, and only when the control has no tooltip text of its own.
Return "" for no tooltip.
| Field | Meaning |
|---|---|
hCheckBox |
The control, so the callback can query it |
b |
The control's PsPaintBuffer for this repaint — paint through this |
rcClient |
The whole client area |
rcContent |
rcClient deflated by the focus-ring band |
rcBox |
The box cell. Never empty |
rcText |
The caption span, not the ink. Empty when there is no caption; degenerate when the client is too narrow |
rcVisual |
rcContent inflated by the ring band — where the focus ring goes |
isChecked |
|
isHot |
The mouse is over the control |
isPressed |
A live left press and the cursor is still inside. Slide off and this goes false without ending the gesture |
isEnabled |
|
isFocused |
Draw a focus ring when true |
wszText |
The caption |
wszGlyph |
The glyph already resolved for isChecked, so you do not repeat the choice and cannot disagree with the control about which glyph belongs to which state |
nBoxAlign |
CHK_BOXALIGN_* |
nTextAlign |
CHK_TEXTALIGN_* — map to DT_LEFT / DT_CENTER / DT_RIGHT
|
rcText is the span, not a tight box around the glyphs. That is what DT_END_ELLIPSIS needs;
handed a tight box the ellipsis would have nowhere to happen. It also means filling rcText fills
the whole span, not just behind the words.
Test presence on the rects, not on the strings: an absent caption leaves rcText empty, so
IsRectEmpty is the single source of truth — and a degenerate span on a too-narrow control drops
out of the same test.
| Field | Meaning |
|---|---|
hCheckBox |
The control |
uMsg |
|
wParam |
|
lParam |
| Value | Meaning |
|---|---|
CHK_BOXALIGN_LEFT |
Box pinned to the left padding, caption takes the span to its right (default) |
CHK_BOXALIGN_RIGHT |
Box pinned to the right padding, caption takes the span to its left |
| Value | Meaning |
|---|---|
CHK_TEXTALIGN_LEFT |
Default |
CHK_TEXTALIGN_CENTER |
|
CHK_TEXTALIGN_RIGHT |
| Value | Meaning |
|---|---|
CHK_MOOD_IDLE |
Nothing is happening |
CHK_MOOD_HOT |
The cursor is over the control |
CHK_MOOD_PRESSED |
A live left press, cursor still inside |
CHK_MOOD_DISABLED |
Beats all three |
| Value | Rect measured |
|---|---|
CHK_PART_CONTROL |
rcContent |
CHK_PART_BOX |
rcBox |
CHK_PART_TEXT |
rcText |
| Constant | Codepoint | Segoe Fluent Icons name |
|---|---|---|
PSCHECKBOX_GLYPH_UNCHECKED |
U+E739 |
Checkbox — an empty outlined square |
PSCHECKBOX_GLYPH_CHECKED |
U+E73A |
CheckboxComposite — the square with a tick |
Both are written in the declarations at the top of PsCheckBox.tiko as \u escapes rather than
as literal characters, because a BOM-less source file is read as bytes and a pasted U+E739
would arrive as its three UTF-8 bytes. Do the same if you replace them from source.
All are DPI-scaled once at Create except PSCHECKBOX_DEFAULT_FOCUSTHICK, which is never scaled.
| Constant | Value | |
|---|---|---|
PSCHECKBOX_DEFAULT_PADLEFT |
4 | |
PSCHECKBOX_DEFAULT_PADTOP |
2 | |
PSCHECKBOX_DEFAULT_PADRIGHT |
4 | |
PSCHECKBOX_DEFAULT_PADBOTTOM |
2 | |
PSCHECKBOX_DEFAULT_BOXGAP |
8 | Charged only when there is a caption |
PSCHECKBOX_DEFAULT_BOXWIDTH |
16 | Declared, never measured |
PSCHECKBOX_DEFAULT_BOXHEIGHT |
16 | |
PSCHECKBOX_DEFAULT_FOCUSGAP |
2 | |
PSCHECKBOX_DEFAULT_FOCUSCURV |
4 | Ellipse diameter |
PSCHECKBOX_DEFAULT_FOCUSTHICK |
1 | Not DPI-scaled |
WM_MOUSELEAVE is not reliably delivered on fast exits, so the control also polls the cursor while
it is hot. The poll is suppressed while a press is live, because the press legitimately lets the
cursor wander off and come back.
| Constant | Value | |
|---|---|---|
IDT_CCHECKBOX_HOTTRACK |
&hCBA0 |
Timer id, per-window so every instance can share it |
PSCHECKBOX_HOTTRACK_MS |
100 | Poll interval |
The demo draws its glyphs with Segoe Fluent Icons, installed on Windows 11, and falls back to Segoe MDL2 Assets where it is absent (Windows 10) — the same codepoints, drawn in Windows 10's style. There is no font file to copy.
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.
tiko-lang · Home · Feature Index · Keyword Index · License
FreeBASIC manual pages reproduced under the GNU FDL 1.3.
- Home
- What Is tiko-lang
- Quick Start
- Installing on macOS
- Debugging
- Feature Index
- Compatibility
- FAQ
- AI Cheatsheet
- Generics
- Interfaces
- For Each
- Iterators
- Tuples
- Array Results
- Defer
- Lambdas
- Block Strings
- Partial Types
- String Slices
- Embed
- Typed Variadics
- C Header Import
- Foreign Library Linking
- UString
- Fixed-Width Types
- Nullptr
- String Library
- Format
- Array
- Map
- Set
- LinkedList
- OrderedMap
- OrderedSet
- Sorting
- Optional
- Result
- Smart Pointers
- Date and Time (Chrono)
- Time Zones
- Filesystem
- File Streams
- Config (INI and TOML)
- Codec
- Hash
- UUID
- OS
- Process
- Net
- HTTP and TLS
- Math Library
- Finance
- Logging
- Terminal TUI Library
- PsControls
- tkControls
- sys/win
- Goroutines and Channels
- LLVM Backend
- tiko build
- TIKO_COMPILER
- Variable Declarations
- Standard Data Types
- User Defined Types
- Procedures
- Operators
- Control Flow Statements
- Converting Data Types
- Array Functions
- String Functions
- Mathematical Functions
- Bit Manipulation
- Date and Time Functions
- File I/O Functions
- Console Functions
- User Input (Runtime Library)
- User Input Functions (Graphics Library)
- Screen Functions
- 2D Drawing Functions
- Graphics Library Reference
- Graphics Keyword List
- Memory Functions
- Threading Support Functions
- Error Handling Functions
- Operating System Functions
- Preprocessor
- Intrinsic Defines
- Miscellaneous Keywords
- Modularizing