Repository navigation
PsColumnHeader
Home › All Categories › PsControls › PsColumnHeader
An owner-drawn column header band for tiko Win32 applications: a horizontal strip of column cells with draggable dividers between them, the last (or a nominated) column absorbing whatever width the others leave over.
It is the header of a listview report view, separated out and made generic. The control owns the column model — captions, widths, minimum widths, the fill designation — and the geometry derived from it; you own every pixel, drawn through a per-column paint callback. It knows nothing about lists: it is equally at home over a grid, a table you draw yourself, or anything else that needs resizable column geometry.
What it gives you interactively: live divider drag with an ESC cancel that restores the pre-drag width, a resize cursor over the gutters, divider double-click to auto-fit (you supply the measurement), a click-on-the-caption notification for host-driven sorting, and per-column tooltips.
The control never sorts, never takes focus, and never creates a child window. PsListTree embeds one above its rows, but the control works perfectly well on its own.
The module is vendor/PsControls/PsColumnHeader, comes with the toolchain, and brings in
PsBufferPaint (the flicker-free drawing surface it paints through) and PsTipHost (the
tooltip backend switch, which in turn brings in PsTooltip) itself. Import PsBufferPaint
yourself as well for the paint callback:
#import "sys/win"
#import "sys/win/gui"
#import "sys/win/gdiplus"
#import "vendor/PsControls/PsBufferPaint"
#import "vendor/PsControls/PsColumnHeader"
using gui
using gdiplus
using PsBufferPaint
using PsColumnHeaderThe control is a CWindow (from sys/win/gui) and PsBufferPaint draws through
sys/win/gdiplus. Build with tiko build in a folder whose tiko.toml defines UNICODE:
[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.
PsColumnHeader has no demo of its own; the PsListTree demo in the toolchain's
examples/PsControls/PsListTree/ exercises it.
If you are using the header inside a PsListTree, PsListTree imports this module itself.
Import PsColumnHeader as well only to name its own things — the HDR_* callback types,
PSCOLUMNHEADER_PAINTINFO, or a PsColumnHeader_* call on the handle from
PsListTree_GetHeader.
GDI+ must be running before the first repaint and must outlive the last one. All geometry is rendered through GDI+, so bracket your message loop:
dim as ULONG_PTR gdipToken = gdiplus.Init()
' ... create windows, run the message loop ...
gdiplus.Uninit( gdipToken )gdiplus.Uninit must come after every window is destroyed, because each repaint builds and
tears down a PsPaintBuffer.
There is no message-pump obligation. No filter to call, and nothing to add to your loop.
The control is not a tabstop and never takes focus — deliberately, since a header must not
steal the caret from whatever the host is editing — so it imposes no IsDialogMessage
requirement of its own either.
' Create it and place it. The control is created zero-sized and hidden.
dim as HWND hHdr = PsColumnHeader_Create( hWndParent, IDC_MYFORM_HEADER )
SetWindowPos( hHdr, 0, x, y, cx, headerHeight, SWP_NOZORDER or SWP_SHOWWINDOW )
' You MUST supply a painter -- without one only the flat background is drawn.
PsColumnHeader_SetPaintCallback( hHdr, @MyHeader_PaintColumn )
' Appearance. The font is borrowed -- you keep ownership and destroy it yourself.
PsColumnHeader_SetBackColor( hHdr, theme.BackColorScrollBar )
PsColumnHeader_SetFont( hHdr, ghFont(GUIFONTBOLD_10) )
' Columns. Widths are PIXELS. The last one is the fill column by default.
PsColumnHeader_AddColumn( hHdr, "Name", 160 )
PsColumnHeader_AddColumn( hHdr, "Size", 70 )
PsColumnHeader_AddColumn( hHdr, "Type" )
' Interaction hooks, all optional.
PsColumnHeader_SetWidthChangedCallback( hHdr, @MyHeader_WidthChanged )
PsColumnHeader_SetClickCallback( hHdr, @MyHeader_Click )
PsColumnHeader_SetAutoSizeCallback( hHdr, @MyHeader_AutoSize )And the painter:
sub MyHeader_PaintColumn( byval p as PSCOLUMNHEADER_PAINTINFO ptr )
' State priority for the cell: pressed > hot > normal
dim as COLORREF backclr, foreclr
if p->isPressed then
backclr = theme.BackColorSelect : foreclr = theme.ForeColorSelect
elseif p->isHot then
backclr = theme.BackColorHot : foreclr = theme.ForeColorHot
else
backclr = theme.BackColor : foreclr = theme.ForeColor
end if
p->b->SetColors( foreclr, backclr )
p->b->PaintRect( @p->rc )
' Caption, inset by the control's padding -- the space the layout accounts for.
dim as RECT rcText = p->rc
dim as integer pad = PsColumnHeader_GetPadding( p->hHeader )
rcText.left += pad : rcText.right -= pad
p->b->SetFont( ghFont(GUIFONTBOLD_10) )
p->b->PaintText( p->wszCaption, @rcText, DT_LEFT )
' Divider tick at the right edge, accented while its gutter is hovered or dragged.
' The fill column has no grabbable divider, so it gets none.
if p->isFill = false then
dim as RECT rcDiv = p->rc
rcDiv.left = rcDiv.right - 1
dim as COLORREF divclr = theme.ForeColorScrollBar
if p->isResizeHot orelse p->isResizing then
divclr = theme.FocusAccent
rcDiv.left -= 1
end if
p->b->SetBackColor( divclr )
p->b->PaintRect( @rcDiv )
end if
end subAuto-fit needs a measurement only you can make, because you own the cell data:
function MyHeader_AutoSize( byval hHeader as HWND, byval idx as integer ) as integer
' Measure your widest cell in that column, add the padding on both sides.
' Return <= 0 for "no change".
return bestW + PsColumnHeader_GetPadding( hHeader ) * 2
end functionInside a PsListTree, do not create the header — the list already owns one. Reach it with
PsListTree_GetHeader, define columns with the PsListTree_*Column* wrappers, and register
callbacks with PsListTree_SetHeader* / PsListTree_SetColumn*. See PsListTree's page.
PsColumnHeader_Create returns an ordinary window handle, and every PsColumnHeader_* function
takes it. It is not an opaque type, so you can treat the control as the window it is —
SetWindowPos to place and size it, ShowWindow to show it, GetDlgItem to find it by the
CtrlID you passed at creation.
There are no child windows. Every message is handled by the control's own procedure.
PsColumnHeader_Create gives the control the styles WS_CHILD, WS_CLIPSIBLINGS and
WS_CLIPCHILDREN, and adds CS_DBLCLKS to its window class so divider double-clicks arrive.
WS_VISIBLE is deliberately absent, so a newly created control shows nothing until you size it
and show it.
The control has no intrinsic height. You decide how tall the band is when you place it, and every column spans that full height.
Column widths, minimum widths and the caption padding are all stored in pixels and used as given. Drag deltas and auto-fit answers are pixels, and round-tripping those through unscaled units would accumulate rounding drift, so the control does not.
The only DPI scaling that happens is on the control's own three defaults, and it happens where they are used: the caption padding is scaled once at create, and the divider gutter and the default minimum width are scaled each time they are consulted. If you set a padding of your own afterwards, scale it yourself.
You set widths; the control computes rects. There is no way to write a rect.
for each fixed column: w = max( nWidth, effectiveMinWidth )
fill column: w = max( clientWidth - sum(fixed widths), its effectiveMinWidth )
x starts at clientLeft - xOffset, and each column is
rc = ( x, clientTop, x + w, clientBottom )
laid left to right, contiguous, spanning the full client height.
effectiveMinWidth is the column's own nMinWidth, or — when that is 0 — the DPI-scaled
control default of 16 pixels. A column can never be laid out or dragged below it.
Layout is lazy. Every mutator marks it stale and asks for a repaint; the next paint, or any query that needs a rect, recomputes it. Setting six widths in a row costs one layout pass, not six. There is no begin-update / end-update pair to remember.
While the control has no client area yet — created zero-sized, before your first
SetWindowPos — layout does nothing and stays stale, so the first real resize recomputes it.
The fill column absorbs whatever width the fixed columns leave over. It shrinks as well as grows with the window, but never below its own minimum. That is what stops a dead strip appearing to the right of the last column when the window widens.
PsColumnHeader_SetFillColumn takes three kinds of value:
| Value | Meaning |
|---|---|
CCOLHDR_FILL_LAST |
Whichever column is currently last (the default) |
CCOLHDR_FILL_NONE |
No fill column; columns end where their widths end |
| a column index | That specific column, tracked across inserts and deletes |
PsColumnHeader_GetFillColumn returns the resolved index, or -1 for none.
A stored index looks after itself: inserting at or before it shifts it up, deleting before it
shifts it down, and deleting the fill column itself falls back to CCOLHDR_FILL_LAST.
PsColumnHeader_Clear resets the designation to CCOLHDR_FILL_LAST as well.
The fill column's own right-hand divider is not grabbable. Its width is derived, so there
is nothing meaningful to drag it to. Your painter is told which column that is
(isFill) so it can omit the divider tick.
When the columns are collectively wider than the client, the layout does not squeeze them: the later rects simply extend past the right edge. Clipping is the paint pass's job, and since the cursor cannot reach past the edge, hit-testing stays correct for free.
There is no horizontal scrolling. PsColumnHeader_SetXOffset shifts the entire column run left
by a pixel amount and every rect follows, which is the hook a horizontal scrollbar would drive;
nothing in the control moves it for you, and it is 0 unless you set it.
PsColumnHeader_GetPadding is a number your painter is expected to inset the caption by. The
control does not measure text and does not use padding in the layout — widths are stored, not
derived from captions. Read it in your paint callback (and in your auto-fit measurement) so
captions and any cells you draw underneath line up.
A boundary is grabbable within ± 3 pixels (DPI-scaled) of a column's right edge. Inside a
gutter, the gesture is a resize, not a column press — which is why
PSCOLUMNHEADER_MESSAGEINFO.idx is -1 there even though the cursor is over a column.
When two boundaries nearly coincide, because a column sits at its minimum width, the gutters are tested right to left, so the later divider wins. That matches listview feel.
PsColumnHeader_SetColumnWidth stores a width, re-lays out and repaints, but fires no
WidthChanged callback. That notification is reserved for user interaction — a divider drag or
an applied auto-fit. It means you can call the setter from inside your own resize handler
without recursing.
The control takes mouse capture on a left press, because both of its gestures consume the guaranteed down → up pairing:
-
Divider drag. The width is applied live: every mouse move that actually changes it stores
the clamped value, re-lays out, repaints, and fires
WidthChangedwithbLive = true. Releasing fires one final notification withbLive = false. Pressing ESC mid-drag, or losing the capture to anything else, restores the pre-drag width and fires that final notification carrying the restored value — so a host that consumed the live updates converges back on its own. -
Column body click. The click fires only on a matched gesture: pressed on a column's body
and released still on it. Press, slide off, release does nothing. Capture is what makes that
reliable, by routing an outside release back here as
idx = -1.
ESC is polled, not read from the keyboard queue, so a motionless cancel works: the same 100 ms timer that acts as the hover safety net doubles as the ESC poll while a drag is live.
Hover tracking is suspended for the whole drag. With capture held the cursor legitimately roams outside the client, and clearing the hot column there would strip the resize visuals.
CS_DBLCLKS is on. A double-click delivers down, up, dblclk, up — the dblclk substituting
for the second down — and the control runs the same press bookkeeping for both, or the trailing
up would release a capture never taken.
- On a divider, the dblclk asks your auto-size callback for a best-fit width and applies it, replacing any press or drag semantics for that press. The trailing up finds no press state and does nothing.
- On a column body, it falls through to an ordinary press. So a rapid double-click on a caption fires your click callback twice — one per matched press/release pair. If your click handler toggles a sort direction, that is exactly the behaviour you want; if it is destructive, guard it.
ClickCallback is the hook and nothing more. Reorder your own data, then repaint a sort glyph
from your paint callback and call PsColumnHeader_Refresh. The control has no notion of a sort
column.
One tool covers the whole band — the columns are painted rects, not windows — and its text is
resolved on demand for whichever column is currently hot. With no tooltip callback, that
column's own caption is used; with one, whatever it returns, and "" suppresses the tip. The
tip is popped whenever the hot column changes, so the next hover re-queries.
The control frees itself and its tooltip when its window is destroyed, and releases any capture
it still holds. Fonts you pass in stay yours: the control stores the HFONT and never destroys
it.
Firm properties of the control, not settings:
-
Nothing is drawn without a paint callback except the flat
BackColorfill of the whole band. - The control has no intrinsic height, and every column spans the full client height. There are no sub-rows and no column groups.
- Widths are stored, never measured. The control does not look at your captions, your font or your data, which is why auto-fit has to ask you.
- Padding does not affect layout. It is a number for your painter to honour; a caption wider than its cell is your problem to ellipsize.
-
A column can never go below its effective minimum — its own
nMinWidth, or the scaled 16-pixel default when that is 0 — by drag, by layout, or bySetColumnWidth. -
GetColumnWidthon the fill column reports its laid-out width, not the stored one. Setting a width on the fill column stores it but has no visual effect until that column stops being the fill. -
SetColumnMinWidthdoes not immediately raise a smaller stored width. The clamp is applied when the column is laid out and when its width is read, soGetColumnWidthreflects it at once even though the stored value is unchanged. - The fill column's divider is not grabbable, and its gutter is not hit-tested at all.
-
idxis -1 while the cursor is in a divider gutter, even though a column is under it. - A body double-click fires the click callback twice.
-
The message callback's result is ignored for
WM_LBUTTONUP. That message releases the mouse capture, and a callback able to swallow it could strand the capture and route every later click here. The result is also ignored forWM_MOUSEMOVEwhile a divider drag is live — the gesture is not suppressible in flight. -
Suppressing
WM_LBUTTONDOWNsuppresses the press semantics, not the capture. Capture is taken before your callback runs, precisely so its release stays guaranteed. - The right mouse button is reported, never acted on. No capture is taken for it and there are no press semantics, so a right-up can arrive without a matching down. A context menu is your business.
-
No horizontal scrolling.
SetXOffsetexists and works, but nothing drives it for you. - No sorting, no column reordering by drag, no built-in sort glyph, no hidden columns. Draw and drive all of that yourself.
- Inserting or deleting a column cancels any live press or drag without firing callbacks, and clears the hover state — a gesture whose target just shifted is ambiguous, so it is abandoned rather than guessed at.
-
Inside a PsListTree, the
WidthChangedslot is taken. UsePsListTree_SetColumnResizeCallbackinstead; see Related controls.
Every function takes the handle from PsColumnHeader_Create as its first argument, written
h in the tables below.
| Function | Description |
|---|---|
PsColumnHeader_Create( hWndParent, CtrlID ) as HWND |
Creates the control as a child of hWndParent and returns its window handle. CtrlID becomes the window's GWLP_ID, so GetDlgItem finds it. There are no child controls. Created zero-sized and hidden — place it with SetWindowPos. |
AddColumn returns the new column's index; InsertColumn the index actually used. Both return
-1 on failure.
| Function | Description |
|---|---|
PsColumnHeader_AddColumn( h, Text, nWidth = 100, nMinWidth = 0, itemData = 0 ) as integer |
Appends a column. Widths are pixels. A nWidth of 0 or less leaves the column at its 100-pixel default; a nMinWidth of 0 or less leaves it at 0, meaning the scaled CCOLHDR_DEFAULT_MINWIDTH floor applies. |
PsColumnHeader_InsertColumn( h, idx, Text, nWidth = 100, nMinWidth = 0, itemData = 0 ) as integer |
Inserts at idx, shifting later columns up. idx is clamped to [0, count] and the index used is returned. A stored fill index shifts up with it; any live press or drag is cancelled and the hover state cleared. |
PsColumnHeader_DeleteColumn( h, idx ) as boolean |
Deletes the column. FALSE for an invalid index. A stored fill index shifts down, or falls back to CCOLHDR_FILL_LAST if it was the deleted column. Cancels any live gesture. |
PsColumnHeader_Clear( h ) |
Removes every column and resets the fill designation to CCOLHDR_FILL_LAST. |
PsColumnHeader_GetCount( h ) as integer |
How many columns are defined. |
PsColumnHeader_Refresh( h ) |
Marks the layout stale and repaints with a background erase. Rarely needed — every mutator does it for you. Useful after changing host state your painter reads, such as a sort column. |
| Function | Description |
|---|---|
PsColumnHeader_GetColumnText( h, idx ) as ustring |
The column's caption. "" for an invalid index. |
PsColumnHeader_SetColumnText( h, idx, Text ) as boolean |
Sets it and repaints that cell only — captions do not drive layout, since widths are stored. FALSE for an invalid index. |
PsColumnHeader_GetColumnItemData( h, idx ) as integer |
The column's user value. 0 for an invalid index. |
PsColumnHeader_SetColumnItemData( h, idx, itemData ) as boolean |
Sets it. FALSE for an invalid index. Does not repaint — the control never draws it. |
| Function | Description |
|---|---|
PsColumnHeader_GetColumnWidth( h, idx ) as integer |
The column's width in pixels, already raised to its effective minimum. For the fill column this is the laid-out width, forcing a pending layout first. 0 for an invalid index. |
PsColumnHeader_SetColumnWidth( h, idx, nWidth ) as boolean |
Stores the width, clamped up to the column's effective minimum, then re-lays out and repaints. Silent — no WidthChanged callback. FALSE for an invalid index. Called on a column whose divider is being dragged at that moment, the new value becomes the drag's base: a later ESC restores to it, and the drag resumes from it re-anchored to the current cursor position. |
PsColumnHeader_GetColumnMinWidth( h, idx ) as integer |
The column's own stored minimum. 0 means "use the control default". |
PsColumnHeader_SetColumnMinWidth( h, idx, nMinWidth ) as boolean |
Sets it; negatives become 0. Re-lays out and repaints. Does not rewrite a smaller stored width — the clamp is applied at layout and read time. FALSE for an invalid index. |
PsColumnHeader_GetFillColumn( h ) as integer |
The resolved index of the fill column, or -1 for none. |
PsColumnHeader_SetFillColumn( h, idx ) as boolean |
Takes a column index, CCOLHDR_FILL_LAST or CCOLHDR_FILL_NONE. FALSE for an index that is neither valid nor one of those two constants. |
PsColumnHeader_GetColumnRect( h, idx, byref rc ) as boolean |
The column's rect in client coordinates. Forces any pending layout, so the result is always current — an embedding host reads its row-cell x-coordinates from here. Returns FALSE and empties rc for an invalid index. |
PsColumnHeader_HitTestDivider( h, x, y ) as integer |
Which column's right-edge divider gutter contains this client-coordinate point? -1 if none. Gutters are tested right to left, and the fill column's own divider is never reported. |
PsColumnHeader_GetPadding( h ) as integer |
The caption inset, in pixels. Read it from your paint callback. |
PsColumnHeader_SetPadding( h, nPadding ) |
Sets it, in raw pixels — scale it yourself. Negatives become 0. Repaints, but never re-lays out: padding is paint-side only. |
PsColumnHeader_SetXOffset( h, xOffset ) |
Shifts the whole column run left by this many pixels; every rect follows. Negatives become 0, and setting the value it already has does nothing. Defaults to 0 and nothing moves it for you. |
| Function | Description |
|---|---|
PsColumnHeader_GetBackColor( h ) as COLORREF |
The band's flat background colour. |
PsColumnHeader_SetBackColor( h, clr ) as COLORREF |
Sets it, repaints, and returns the previous value. |
PsColumnHeader_GetFont( h ) as HFONT |
The font stored for the band. |
PsColumnHeader_SetFont( h, hFont ) as boolean |
Sets it and repaints. Borrowed, never owned — keep it alive and destroy it yourself. It does not drive layout, and your painter is free to select a different font per column. |
The control ships on the system tooltip (comctl32) and stays there unless you ask
otherwise. PsColumnHeader_SetTooltipMode( h, PSTIP_MODE_PS ) moves that one instance onto
PsTooltip instead: owner-drawn and themeable, it word-wraps to a maximum width without a
hand-sent TTM_SETMAXTIPWIDTH, and it does not subclass the control it serves.
The default is deliberate rather than timid — PsTooltip's colour defaults are dark and the
system tip is light, so a control that switched on its own would put a dark tip on a light form.
Switching backend does not change what a tip says. Text resolution is the same either way:
your HDR_TooltipCallbackFunc if one is set, otherwise the column's own caption, with ""
suppressing the tip. Only the drawing and the driving differ.
| Function | Description |
|---|---|
PsColumnHeader_SetTooltipMode( h, nMode ) as boolean |
PSTIP_MODE_SYSTEM (default) or PSTIP_MODE_PS. Destroys the outgoing tip, builds the incoming one and re-applies the stored delays. A no-op when the mode is already the one asked for. Returns TRUE if the requested mode is live on return. |
PsColumnHeader_GetTooltipMode( h ) as long |
The current mode. PSTIP_MODE_SYSTEM for an invalid handle. |
PsColumnHeader_GetTooltipHandle( h ) as HWND |
The comctl32 tooltip window, for direct TTM_* messages — and 0 while the instance is on PsTooltip, since a TTM_* sent to a PsTooltip window is silently ignored. |
PsColumnHeader_GetPsTooltipHandle( h ) as HWND |
The reverse: the PsTooltip window, and 0 on the system backend. This is the door to PsTooltip_SetColors / SetFonts / SetStyle / SetMaxWidth / SetTitle / SetGlyph, which are deliberately not mirrored on this control. |
PsColumnHeader_SetHoverTime( h, milliseconds ) |
The initial dwell — how long the cursor must rest on a column before a tip appears. Default 250. Double duty: the same value is TrackMouseEvent's dwHoverTime, so this also decides when the control considers a column hot. |
PsColumnHeader_SetAutoPopTime( h, milliseconds ) |
How long a shown tip stays up before it hides itself. |
PsColumnHeader_SetReshowTime( h, milliseconds ) |
The delay before a tip reappears when the cursor moves to another column while a tip is already up. |
All three delays are honoured by both backends, and are stored rather than pushed straight at the live tooltip, so a delay set before a mode switch is still in force after it. A delay you never set keeps the backend's own value, derived from the system double-click time. A delay set to 0 is a real request for "no delay" and survives a switch as such.
To theme every tip in the process at once — the intended way to use PSTIP_MODE_PS — call
PsTooltip_SetDefaultColors / SetDefaultFonts / SetDefaultStyle / SetDefaultMaxWidth /
SetDefaultDelays before any control is created, and then just opt each control in. The
fonts are borrowed: you keep ownership and must outlive every tip that uses them.
PSTIP_MODE_* belong to the PsTipHost module and the PsTooltip_* calls to PsTooltip; this
module imports both itself, but to name them your file imports them too
(#import "vendor/PsControls/PsTipHost", #import "vendor/PsControls/PsTooltip", plus using
each). See the
Tooltips section of PsListTree's page for a worked example.
A header embedded in a PsListTree has its own independent tooltip and its own mode — setting
the list's does not set the header's.
| Function | Description |
|---|---|
PsColumnHeader_SetPaintCallback( h, usersub ) |
Installs the per-column painter and repaints. Required — without it only the background is drawn. |
PsColumnHeader_SetMessageCallback( h, userfunc ) |
Installs an observer for the mouse messages. |
PsColumnHeader_SetTooltipCallback( h, userfunc ) |
Installs the on-demand tooltip text supplier. Unset, the column's caption is used. |
PsColumnHeader_SetClickCallback( h, usersub ) |
Installs the completed-click-on-a-column-body handler — the sorting hook. |
PsColumnHeader_SetWidthChangedCallback( h, usersub ) |
Installs the user-resize handler. Not for a header embedded in a PsListTree — that slot belongs to the list; use PsListTree_SetColumnResizeCallback. |
PsColumnHeader_SetAutoSizeCallback( h, userfunc ) |
Installs the divider-double-click best-fit measurer. |
Passing 0 to any callback setter clears it.
There is no colour struct. The control paints one thing — the band's flat background — and every column cell is yours, drawn from whatever palette your host already has.
| Setting | Paints |
|---|---|
PsColumnHeader_SetBackColor |
The whole client band, filled before any column callback runs. It therefore also covers any strip no column owns, which is what the far right of the band looks like when the fill designation is CCOLHDR_FILL_NONE. It is the only thing painted when no paint callback is set |
The control does not decide; it hands you the state flags and you resolve them. It does guarantee what can be true at once, which is what makes a precedence order well defined:
isPressed > isHot > plain
with isResizeHot / isResizing layered on the divider itself, not on the cell body.
-
At most one column is
isHot, and at most one isisPressed. Both clear deterministically. -
isPressedimplies a live left press on that column's body; it is never set for a divider drag. -
isResizeHotmeans the cursor is in that column's right-hand gutter — mutually exclusive withisHoton the same column, because a gutter hit takes the body hit away. -
isResizingmeans that column's divider is being dragged right now. While it is set, hover tracking is frozen, soisHotkeeps whatever value it had when the drag started. -
isFillis a property of the column, not a mood. It is the flag to test when deciding whether to draw a divider tick.
| Part | Drawn by |
|---|---|
| Band background | The control, in BackColor, every repaint |
| Column cells, captions, dividers, sort glyphs | Your paint callback, into the same buffer |
| Resize cursor over a gutter | The control (IDC_SIZEWE) |
Only the columns intersecting the update rect are handed to your callback, so a hover change repaints two cells rather than the whole band.
All six typedefs carry the HDR_ prefix.
type HDR_PaintCallbackSub as sub( byval p as PSCOLUMNHEADER_PAINTINFO ptr )Draws one column's header cell. Called for each column touched by the repaint, so keep it
cheap. Paint through p->b — the control's buffer for this repaint — using p->rc as the cell
rect, and never touch a screen DC. The control has already filled the band with BackColor.
Inset your caption by PsColumnHeader_GetPadding( p->hHeader ).
PSCOLUMNHEADER_PAINTINFO:
| Field | Meaning |
|---|---|
hHeader |
The control, so the callback can query it |
itemID |
The column index |
b |
The control's PsPaintBuffer for this repaint (borrowed, not owned) |
rc |
This column's rect, in client coordinates |
isHot |
The mouse is over this column's body |
isPressed |
A live left press on this column's body |
isResizeHot |
The cursor is in this column's right-hand divider gutter |
isResizing |
This column's divider is being dragged right now |
isFill |
This is the effective fill column — draw no divider tick |
wszCaption |
The column's text |
A paint callback that fills a rectangle covering the whole band will erase everything under it. Draw your cell, not a background — the control has already painted one.
type HDR_MessageCallbackFunc as function( byval m as PSCOLUMNHEADER_MESSAGEINFO ptr ) as booleanObserves mouse messages. Return TRUE to suppress the control's own handling of that message, FALSE to let it proceed.
PSCOLUMNHEADER_MESSAGEINFO:
| Field | Meaning |
|---|---|
hHeader |
The control |
uMsg |
The message |
wParam / lParam
|
Its parameters, unmodified |
idx |
The column index under the mouse — -1 if none, and -1 on a divider gutter |
Which messages arrive, and what TRUE does:
| Message | idx |
Effect of returning TRUE |
|---|---|---|
WM_MOUSEMOVE |
the hot column | Suppresses the control's handling. Ignored while a divider drag is live |
WM_MOUSEHOVER |
the hot column | Suppresses the control's handling |
WM_MOUSELEAVE |
always -1 | Suppresses the control's handling. The hot state has already been cleared by the time you are called |
WM_LBUTTONDOWN |
the pressed column | Suppresses the press semantics — no drag armed, no press state — but never the capture, which is taken first |
WM_LBUTTONDBLCLK |
the column under the cursor | As WM_LBUTTONDOWN, and it also suppresses the auto-fit |
WM_LBUTTONUP |
the released-over column | Ignored. See below |
WM_RBUTTONDOWN |
the column under the cursor | Suppresses the control's handling. No capture is taken and there are no press semantics either way |
Your return value is ignored for WM_LBUTTONUP. The control holds mouse capture across a
press, and the up-message is what releases it. A callback that suppressed it would strand the
capture and route every later click to this control.
It is ignored for WM_MOUSEMOVE during a drag for the same family of reason: the gesture
is already under way and cannot be halted mid-flight.
type HDR_TooltipCallbackFunc as function( byval hHeader as HWND, byval idx as integer ) as ustringSupplies the tooltip text for a column, on demand — called only when a tip is about to show.
Return "" for no tooltip. With no callback installed, the column's own caption is used.
type HDR_ClickCallbackSub as sub( byval hHeader as HWND, byval idx as integer )A completed click on a column's body — pressed and released on the same column, and not on
a divider. This is the sorting hook: the control never sorts, so reorder your own data, repaint
a sort glyph from your paint callback, and call PsColumnHeader_Refresh.
Press, slide off, release fires nothing. A rapid double-click on a body fires it twice.
type HDR_WidthChangedCallbackSub as sub( byval hHeader as HWND, byval idx as integer, byval nWidth as integer, byval bLive as boolean )A column's width changed through user interaction. nWidth is the new width in pixels,
already clamped to the column's minimum.
| When | bLive |
|---|---|
| Each mouse move during a live divider drag that actually changes the width | true |
| The drag commits on release | false |
| The drag is cancelled by ESC or by losing the capture — carrying the restored width | false |
| An auto-fit is applied | false |
So a drag always ends with exactly one bLive = false notification, whether it committed or
cancelled, and that final value is the truth. A host that consumed the live updates converges
back on a cancel for free.
PsColumnHeader_SetColumnWidth does not fire this — programmatic setters are silent, which
is what makes it safe to call from inside this handler.
Embedded in a PsListTree, subscribe with PsListTree_SetColumnResizeCallback instead.
type HDR_AutoSizeCallbackFunc as function( byval hHeader as HWND, byval idx as integer ) as integerA divider was double-clicked. Return the best-fit width for that column, in pixels; return 0 or less for "no change". The control cannot do this itself — you own the cell data and the fonts, so only you can measure. Add the padding on both sides if you want the caption to breathe.
The returned width is clamped up to the column's effective minimum before being applied, and
applying it fires WidthChanged with bLive = false.
With no auto-size callback installed, a divider double-click does nothing.
Defined in the PsColumnHeader module:
| Constant | Value | Meaning |
|---|---|---|
CCOLHDR_DEFAULT_PADDING |
8 | Caption inset in pixels. DPI-scaled once at create; SetPadding takes raw pixels thereafter |
CCOLHDR_DIVIDER_GUTTER |
3 | Half-width of the divider hit zone: a boundary is grabbable within ± this many pixels of a column's right edge. DPI-scaled at use |
CCOLHDR_DEFAULT_MINWIDTH |
16 | The floor applied to any column whose own nMinWidth is 0. DPI-scaled at use |
CCOLHDR_FILL_LAST |
-2 | Fill designation: whichever column is currently last (the default) |
CCOLHDR_FILL_NONE |
-1 | Fill designation: no column absorbs leftover width |
IDT_CCOLHDR_HOTTRACK |
&hCB30 |
Timer id for the hover safety-net poll, which doubles as the ESC poll during a drag. Timer ids are per-window, so every instance shares it |
CCOLHDR_HOTTRACK_MS |
100 | How often that poll runs |
The tooltip backend values passed to PsColumnHeader_SetTooltipMode come from the PsTipHost
module (import it, and using PsTipHost, to name them):
| Constant | Value | Meaning |
|---|---|---|
PSTIP_MODE_SYSTEM |
0 | The comctl32 tooltip. The default |
PSTIP_MODE_PS |
1 |
PsTooltip — owner-drawn, themeable, word-wrapping |
Defaults a new column starts with:
| Setting | Default |
|---|---|
| Width | 100 pixels |
| Minimum width | 0, meaning the scaled CCOLHDR_DEFAULT_MINWIDTH floor |
itemData |
0 |
And the control itself:
| Setting | Default |
|---|---|
| Fill column | CCOLHDR_FILL_LAST |
| Hover time | 250 ms |
| X offset | 0 |
PsColumnHeader creates nothing and embeds nothing. It is, however, embedded by
PsListTree, which places one above its rows and uses it as the single store for
its column model — every PsListTree_*Column* function delegates here.
If your header is a PsListTree's, three rules apply:
| Do | Instead of |
|---|---|
Define columns with PsListTree_AddColumn and friends |
PsColumnHeader_AddColumn on the child |
Subscribe to resizes with PsListTree_SetColumnResizeCallback
|
PsColumnHeader_SetWidthChangedCallback — that slot is the list's, and it needs it to repaint rows on every live drag |
Size the band with PsListTree_SetHeaderHeight and show it with PsListTree_ShowHeader
|
SetWindowPos / ShowWindow on the child, which the list re-lays out anyway |
Everything else — padding, back colour, font, the paint, click, auto-size and tooltip
callbacks, GetColumnRect, HitTestDivider — is safe to call directly on the handle from
PsListTree_GetHeader, and several of those have PsListTree_* pass-through wrappers for
convenience.
tiko-lang · Home · Feature Index · Keyword Index · License
FreeBASIC manual pages reproduced under the GNU FDL 1.3.
- 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)
- 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