Repository navigation
PsListTree
Home › All Categories › PsControls › PsListTree
An owner-drawn list and tree control for tiko Win32 applications: a scrolling list of fixed-height rows that you paint yourself, with unlimited-depth tree nodes, control-drawn expand/collapse twisties, in-place label editing, collapsible group headers, single / multiple / extended selection, per-row tooltips, and an optional multi-column report mode driven by a resizable header band.
The name says it: PsListTree is a list box and a tree view in one control. As a tree it does
what the traditional Win32 tree view (SysTreeView32) does — parent/child hierarchy of any
depth, +/− twisties, per-level indentation, Left/Right collapse and expand, and F2
rename-in-place with begin/end callbacks that mirror TVN_BEGINLABELEDIT / TVN_ENDLABELEDIT.
The differences are deliberate: it is fully owner-drawn (no visual-style dependency and every
row pixel is yours), nodes are addressed by plain model index rather than opaque HTREEITEM
handles, and a flat list, a grouped list and a deep tree are all the same control — a classic
header/item group is simply the depth-1 case. See Tree view and in-place editing below.
It is the control you reach for when a system LISTBOX or SysTreeView32 almost fits but its
appearance does not — a file explorer pane, a results list, a symbol browser, an autocomplete
popup, a settings tree. There is no system control underneath: the whole surface is drawn in one
pass through a double buffer, so nothing about the look depends on the visual style the user
happens to be running.
The control owns three child windows for you — the row surface, an owner-drawn vertical scrollbar that appears only while the rows overflow, and a column header band that starts hidden. You never create or position any of them. What you supply is a row paint callback: the control decides which rows are drawn, where they sit, and what state each is in, and you decide what a row looks like.
Rows are addressed by model index — the order you added them. Collapsing a group hides rows but never renumbers anything.
As a tree — tree_demo.exe:

A multi-level tree built with PsListTree_AddNode: per-depth indentation, control-drawn chevron twisties (˅ on expanded parents, › on the collapsed math node), leaf nodes with no twisty, the selected root with its focus bar, and F2 / click-to-rename editing. This is exactly the behaviour of a traditional tree view, drawn entirely by the control.
As a list and a report — main.exe:

The same control in its list modes. On the left a grouped single-column list — a collapsible group header, one selected row, and rows that keep their selection across a collapse. On the right, report mode: a PsColumnHeader band (Name / Size / Type), a group row, and an embedded PsVScrollBar down the right edge. Public indices in both are model indices; the visible index space never leaks out.
The control is the module vendor/PsControls/PsListTree, which comes with the toolchain and
imports what it is built from itself: sys/win/richedit, PsBufferPaint, PsTipHost (the
tooltip backend switch, which brings in PsTooltip), PsColumnHeader (the header band — see
PsColumnHeader), PsTextBox (the rename editor, which brings in
PsPopupMenu for its context menu) and PsVScrollBar (the embedded scrollbar). They are part of
the build whether or not you ever define a column, leave the system tooltip or enable editing.
Import one of them too only to name something of its own — below, PsBufferPaint for the row
painter, PsColumnHeader for PsColumnHeader_GetPadding, PsTipHost for PSTIP_MODE_PS,
PsTooltip for the process-wide tip defaults and PsTextBox for PsTextBox_GetBorderColor:
#import "sys/win"
#import "sys/win/gui"
#import "sys/win/gdiplus"
#import "vendor/PsControls/PsBufferPaint"
#import "vendor/PsControls/PsColumnHeader"
#import "vendor/PsControls/PsTipHost"
#import "vendor/PsControls/PsTooltip"
#import "vendor/PsControls/PsTextBox"
#import "vendor/PsControls/PsListTree"
using gui
using gdiplus
using PsBufferPaint
using PsColumnHeader
using PsTipHost
using PsTooltip
using PsTextBox
using PsListTreeThe pump call under In-place label editing is needed only if you enable editing
(PsListTree_EnableLabelEdit).
UNICODE must be defined for your program, as it is for the module. Set it in tiko.toml,
as the demo's 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 demos are in the toolchain's examples/PsControls/PsListTree/: tiko build (debug profile,
-g -exx) or tiko build --profile release there writes both main.exe and tree_demo.exe —
two targets of one tiko.toml; tiko build tree_demo builds just the one.
The control's own signatures name types from two of its imports: VScrollPaintCallbackSub from
PsVScrollBar, and HDR_WidthChangedCallbackSub, HDR_ClickCallbackSub,
HDR_AutoSizeCallbackFunc, HDR_PaintCallbackSub and HDR_TooltipCallbackFunc from
PsColumnHeader. To write one of those callbacks, import that module too.
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.
The message-pump filter is needed only for label editing. If — and only if — you call
PsListTree_EnableLabelEdit, you must call PsListTree_FilterMessage( @msg ) in your loop,
ahead of IsDialogMessage (see Tree view and in-place editing). Without editing there is
nothing to call, and the only pump requirement is the ordinary dialog-manager one below.
For Tab navigation, call IsDialogMessage in your message pump. The row surface is a
tabstop, and the container declares itself a control parent, so the dialog manager will step
into the control — but only if you give it the chance:
do while GetMessage( @uMsg, null, 0, 0 )
if uMsg.message = WM_QUIT then exit do
if IsDialogMessage( hWndForm, @uMsg ) = 0 then
TranslateMessage @uMsg
DispatchMessage @uMsg
end if
loopWithout it, mouse operation and the keyboard are unaffected once the list has focus — only Tab
navigation is lost. The arrow keys are safe either way: the surface answers WM_GETDLGCODE
with DLGC_WANTARROWS or DLGC_WANTCHARS, so IsDialogMessage does not steal them for
tab-navigation.
Give something the focus at startup. IsDialogMessage acts only when the focused window is
already a descendant of the window you pass it. A real dialog does this in WM_INITDIALOG; an
ordinary window must call SetFocus on its first control itself. Skip it and the first Tab
does nothing, which is indistinguishable from the tabstops being broken. Clicking the list also
gives it focus, so this only affects a keyboard-first start.
' Create it. The control is created zero-sized and hidden; place it yourself.
dim as HWND hList = PsListTree_Create( hWndParent, IDC_MYFORM_LIST )
SetWindowPos( hList, 0, x, y, cx, cy, SWP_NOZORDER or SWP_SHOWWINDOW )
' You MUST supply a row painter -- without one, rows are not drawn at all.
PsListTree_SetPaintCallback( hList, @MyList_PaintRow )
' Optional: observe mouse messages, supply tooltips, hear about selection moves.
PsListTree_SetMessageCallback( hList, @MyList_Message )
PsListTree_SetTooltipCallback( hList, @MyList_Tooltip )
PsListTree_SetSelChangeCallback( hList, @MyList_SelChange )
' Appearance. The font is borrowed -- you keep ownership and destroy it yourself.
PsListTree_SetBackColor( hList, theme.BackColor )
PsListTree_SetFont( hList, ghFont(GUIFONT_10) )
PsListTree_SetScrollBarColors( hList, theme.BackColorScrollBar, _
theme.ForeColorScrollBar, theme.ForeColorScrollBarHot )
' Selection mode. Both off = single-select.
PsListTree_SetExtendedSelect( hList, true ) ' Shift ranges / Ctrl toggles
' Bulk load, batched: one map rebuild and one repaint instead of one per row.
PsListTree_BeginUpdate( hList )
PsListTree_AddHeader( hList, "Group Alpha" )
for i as integer = 0 to 20
PsListTree_AddString( hList, "Alpha item " & i, 1000 + i )
next
PsListTree_EndUpdate( hList )
' Silent -- the SelChange callback does not fire for this.
PsListTree_SetCurSel( hList, 1 )And the row painter:
sub MyList_PaintRow( byval p as PSLISTTREE_PAINTINFO ptr )
' State priority is yours to choose: selected > hot > normal is the usual one.
dim as COLORREF backclr, foreclr
if p->isSelected 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->SetForeColor( foreclr )
p->b->SetBackColor( backclr )
p->b->PaintRect( @p->rc ) ' the whole row: selection spans it
if p->isHeader then
dim as ustring wszPrefix
if p->isCollapsed then wszPrefix = "+ " else wszPrefix = "- "
p->b->SetFont( ghFont(GUIFONTBOLD_10) )
p->b->PaintText( wszPrefix & p->wszCaption, @p->rc, DT_LEFT )
else
dim as RECT rcText = p->rc
rcText.left += 16
p->b->PaintText( p->wszCaption, @rcText, DT_LEFT )
end if
end subThat is the whole minimum. To turn on report mode, add columns and show the band:
PsListTree_AddColumn( hList, "Name", 160 )
PsListTree_AddColumn( hList, "Size", 70 )
PsListTree_AddColumn( hList, "Type" ) ' the fill column (default: the last)
PsListTree_SetHeaderPaintCallback( hList, @MyHeader_PaintColumn )
PsListTree_SetHeaderBackColor( hList, theme.BackColorScrollBar )
PsListTree_SetHeaderFont( hList, ghFont(GUIFONTBOLD_10) )
PsListTree_ShowHeader( hList, true )
' Cell text: column 0 IS the row's text; columns 1..N are per-row cells.
dim as integer r = PsListTree_AddString( hList, "file0.tiko" )
PsListTree_SetCellText( hList, r, 1, "137" )
PsListTree_SetCellText( hList, r, 2, "tiko source" )Your row painter then sees p->columnCount > 0 and a p->cells array — fill the full row
background first, then draw each cell inside its own rect:
dim as integer pad = PsColumnHeader_GetPadding( PsListTree_GetHeader( hList ) )
for c as integer = 0 to p->columnCount - 1
dim as RECT rcCell = p->cells[c].rc
rcCell.left += pad : rcCell.right -= pad
p->b->PaintText( p->cells[c].wszText, @rcCell, DT_LEFT or DT_END_ELLIPSIS )
nextPsListTree_Create returns an ordinary window handle, and every PsListTree_* 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.
That handle is the container. It hosts three children: the row surface, the scrollbar and
the header band. Every function resolves them internally. Never pass a child handle to a
PsListTree_* function — results are undefined.
The children take consecutive control ids: the surface gets CtrlID, the scrollbar
CtrlID + 1, the header CtrlID + 2. Leave that much room around whatever id you pick.
PsListTree_Create gives the container the styles WS_CHILD, WS_CLIPSIBLINGS and
WS_CLIPCHILDREN. WS_VISIBLE is deliberately absent, so a newly created control shows
nothing until you size it and show it. That lets you build and configure the list — colours,
font, columns, contents — before it is ever seen.
Rows are addressed by the order they were added, independent of what is on screen. Collapsing a
group does not renumber anything, and hidden rows keep working with every row API,
selection included. Internally the control maps model rows to visible positions; that mapping
never leaks out. Everything you receive — PSLISTTREE_PAINTINFO.itemID,
PSLISTTREE_MESSAGEINFO.idx, PsListTree_GetCurSel, PsListTree_GetSelItems,
PsListTree_GetTopIndex — is a model index too.
PsListTree_GetCount is every row in the model. PsListTree_GetVisibleCount is the rows currently
on show. They differ exactly when something is collapsed.
Every row carries a tree level (0 = top-level), so the control does hierarchies of any depth —
see Tree view and in-place editing below. The classic flat group is just the depth-1 case: a
header (PsListTree_AddHeader) is a level-0 parent and the items (PsListTree_AddString) after it
are its level-1 children, hidden when the header collapses. Build deeper structure with
PsListTree_AddNode. Collapsing any node hides its entire subtree.
Headers are ordinary rows in every other respect: they are selectable, they are returned by
PsListTree_GetSelItems, and they carry itemData. Use PsListTree_IsHeader to tell them apart —
"header" is now just a styling flag, independent of whether a node has children.
Clicking a header toggles its group and leaves the selection alone — a fold gesture, not a
select. (A plain AddNode parent instead toggles only via its twisty; the label selects.)
Selection is a per-row flag in the model, not a bit inside a system listbox. That is what makes it survive collapse and expand, and it is why a hidden row can be selected. Three modes, and the two flags are mutually exclusive:
| Mode | How to get it | Mouse behaviour |
|---|---|---|
| Single | both off (the default) | Each click selects exactly one row |
| Extended | PsListTree_SetExtendedSelect( h, true ) |
Shift extends a range from the anchor, Ctrl toggles one row |
| Multiple | PsListTree_SetMultiSelect( h, true ) |
Every click toggles one row, checklist style |
Setting either mode clears the other. PsListTree_GetCurSel reports the focused row (the
caret), which is not the same as "the selection" once more than one row can be selected — use
PsListTree_GetSelItems for that.
One WM_PAINT on the surface fills the whole client with BackColor and then calls your paint
callback once per visible row, all into a single buffer. There is no per-row message and no
per-row buffer.
p->rc and the cell rects are in the surface's client coordinates, not in a row-sized
buffer starting at y = 0. Derive everything from p->rc and you never have to care.
Rows below the last one are not painted at all; the control's background already covers that strip, which is why an empty list needs no special handling from you.
If you set no paint callback, no rows are drawn — only the background. That is the one callback the control genuinely needs.
The control creates a PsVScrollBar, positions it on the right edge, keeps its range in step
with the model, and auto-hides it whenever the rows fit — at which point the row surface
reclaims the full client width. You never call SetRange or SetPos.
What is left to you is appearance: PsListTree_SetScrollBarWidth,
PsListTree_SetScrollBarColors, PsListTree_SetScrollBarPaintCallback, or
PsListTree_GetScrollBar for direct PsVScrollBar_* calls.
The mouse wheel is handled on the row surface, not by the bar: one notch scrolls
SPI_GETWHEELSCROLLLINES rows (re-read per message, and capped at one page), with the
sub-notch remainder accumulated so a precision touchpad scrolls smoothly rather than in jumps.
The thumb follows.
With no columns defined, the control paints as a single-column list and your painter sees
p->columnCount = 0.
Define columns and one thing changes: item rows arrive with a cells array giving each
column's x-span. Group-header rows never get cells — they always span the full width, so
columnCount is 0 for them regardless.
The column model lives in the embedded PsColumnHeader, which is the single store for widths,
minimums and the fill designation. The PsListTree_*Column* functions are wrappers that delegate
to it. See PsColumnHeader for the width, minimum-width and fill rules.
Column definitions and cell text are independent. Cell storage is sparse and per row, so
you can populate rows before or after defining columns; a cell never set reads back "". The
band itself starts hidden — columns still lay out and rows still paint in columns, you
simply get no interactive header strip until PsListTree_ShowHeader( h, true ).
The header band spans the full container width, over the scrollbar strip, so column geometry never shifts when the scrollbar auto-hides.
PsListTree_GetText/SetText and the cell functions with col = 0 address the same storage.
Columns 1..N live in the row's own sparse cell array. This is why a list written before columns
existed keeps working unchanged.
Inserting or deleting a column shifts every row's cell storage across that alias — insert a column at 0 and each row's text becomes its column 1 cell.
An ordinary, selectable row can be made to paint as one cell across the whole column band
instead of one cell per column — PsListTree_SetRowSpanColumns( h, row, true ). Use it for a
section note, a "no results" line, or any wide message row that lives among columned rows.
Its text stays in column 0 (PsListTree_GetText / GetCellText( h, row, 0 )). At paint time
the callback receives p->columnCount = 1 with p->isSpanned = true; cells[0].rc runs from
column 0's own left to the last column's right (the same left a normal column-0 cell gets, so
the spanned text lines up under column 0), and cells[0].wszText is the column-0 text. A painter
that already loops for c = 0 to columnCount - 1 draws it correctly with no new branch — it
simply draws one wide cell. Selection and hover still fill the full p->rc, and the row selects,
hot-tracks and keyboard-navigates like any other.
Two details worth knowing:
- With no columns defined, a spanned row is already full-width (
columnCount = 0); the flag only advertises the intent viap->isSpanned. - A spanned row is exempt from the column-shift above: inserting or deleting a column never moves its text out of column 0, so its caption cannot be silently blanked.
Group headers are not spanned rows — they have their own isHeader path and carry
isSpanned = false.
Opt in with PsListTree_SetDragReorder( h, true ) (default OFF) and the user can drag rows to
new positions inside the same list:
- What drags — press an item and move past the system drag threshold. If the pressed row is part of a multi-selection, the whole selection moves (gathered contiguously at the drop, keeping its relative order, even if it was non-contiguous); otherwise just that one row. Headers and non-selectable rows are never draggable.
- Where it drops — a horizontal insertion line shows the target gap. Dropping on a header highlights it and inserts the block as that header's first children (a collapsed header expands on drop). Because grouping is positional, a row dropped into another group simply belongs to it afterward.
-
Feedback — the insertion line / header highlight paints in
SetDragIndicatorColor(accent blue by default). Dragging near the top/bottom edge auto-scrolls so off-screen targets are reachable. Esc — or any loss of mouse capture — cancels the drag with no move.
The control moves its own model and fires two optional callbacks: CanDrop just before
(return FALSE to reject — it receives the source rows and the drop-target row's ROWINFO), and
DragDrop just after (the block's new first row and count, so you can resync parallel data
via itemData). This is the only gesture in the control that takes mouse capture, and only
while a drag is actually in progress.
PsListTree_MoveRows( h, srcRows(), insertBefore ) performs the same reorder programmatically —
silent (no DragDrop), like every other setter.
A row can be made non-selectable — PsListTree_SetRowSelectable( h, row, false ). It then
cannot be selected or focused by any path, and keyboard navigation skips over it:
- Mouse — clicking it changes nothing (no selection, no focus). The host still sees the raw click through the message callback.
- Keyboard — arrows, PageUp/Down, Home and End step past it to the nearest selectable row. A single-step arrow that would only reach non-selectable rows leaves the caret where it is; the paging and Home/End keys always land on some selectable row if one exists.
-
Programmatic —
SetCurSelreturns -1 and no-ops,SetSel( …, true )returns FALSE,SelectAllskips it, and a Shift-range selects every selectable row it spans but not this one.
The rule is a hard invariant: a non-selectable row is never selected and never focused. If you mark a row non-selectable while it is the current/selected row, its selection is cleared and the caret is dropped (silent, repaints). To select it again, clear the flag first.
The paint callback gets p->isSelectable, so a painter can grey the row — that is cosmetic; the
control does the blocking regardless of how you draw it. The flag is independent of the others: a
row can be spanned and non-selectable at once (a section note that fills the width and can't be
picked).
Every model mutator rebuilds the visible map and repaints. Wrap a bulk load in
PsListTree_BeginUpdate / PsListTree_EndUpdate and that collapses to one rebuild and one
repaint, turning an O(n²) load into O(n). The pairs nest.
PsListTree_SetCurSel, SetSel, SelectAll and Clear never fire the SelChange callback. It
reports user action only — a click, or a keyboard move. This follows Win32's own
LB_SETCURSEL / LBN_SELCHANGE split, and it means you can call the setters from inside your
own handler without recursing.
The same rule holds for column widths: PsListTree_SetColumnWidth repaints but fires no resize
callback. Only a user drag or an autosize notifies.
| Setting | Unit | Default |
|---|---|---|
Row height (PsListTree_SetRowHeight) |
Unscaled — DPI-scaled internally | 22 |
Header height (PsListTree_SetHeaderHeight) |
Unscaled — DPI-scaled internally | 24 |
Scrollbar width (PsListTree_SetScrollBarWidth) |
Unscaled — DPI-scaled at layout | 12 |
| Column widths and minimums | Pixels, used as given | 100 / 0 |
| Header caption padding | Pixels after create; the default is DPI-scaled at create | 8 |
Row height and header height are the trap in both directions: hand them a value you have already scaled and it gets scaled twice.
The row surface handles these itself and consumes them, so they never reach the message callback:
| Key | Action |
|---|---|
| Up / Down | Move the focus one visible row |
| PageUp / PageDown | Move the focus one page |
| Home / End | First / last visible row |
| Left | Collapse an expanded header; from an item, jump to its header |
| Right | Expand a collapsed header; from an expanded one, step to the first child |
| Space | Toggle the focus row's selection (multiple / extended), or select it (single) |
Shift and Ctrl modify the arrow and page moves the same way they modify a click. Because
WM_KEYDOWN never reaches the message callback, SelChangeCallback is the only way to
observe keyboard navigation.
Every move ensures the new focus row is on screen.
Exactly one row is hot at a time. Hover state is cleared both by WM_MOUSELEAVE and by a
100 ms cursor poll, because WM_MOUSELEAVE is not reliably delivered on fast exits.
Tooltips are resolved on demand: with no tooltip callback the hovered row's own text is used;
with one, whatever it returns, and "" suppresses the tip entirely. The tip is popped when the
hot row changes so the next hover re-queries.
The tip is drawn by the system tooltip unless you move the instance onto the owner-drawn
PsTooltip with PsListTree_SetTooltipMode. That choice changes only the drawing and the
timing — never the text, which comes from the rule above either way. See Tooltips under the
API reference.
A left click calls SetFocus on the row surface before your message callback runs. If the
list must not hold focus — an autocomplete popup over an editor, say — put the focus back from
your callback.
The control frees itself, its tooltip and all three children when its window is destroyed.
Fonts you pass in stay yours: the control stores the HFONT and never destroys it.

The same control, driven as a treeview: unlimited depth, control-drawn twisties, per-depth
indent, and label editing. tree_demo.exe (the tree_demo target of the demo's tiko.toml)
is a focused harness for exactly these features; main.exe (the main target) exercises
everything else.
Every row carries a tree level (0 = top-level), and the tree lives in ordinary array order: a node's whole subtree is the contiguous run of deeper-level rows right after it, so parent, child count and "has children" are all derived — there is no separate tree object to keep in sync. Collapsing a node hides its entire subtree, at any depth. The old flat header/item model is just the depth-1 case of this, so nothing about the existing API changed.
PsListTree_AddNode( h, parentRow, Text ) appends a child under parentRow (-1 = a new
top-level node); PsListTree_InsertNode places one at a specific child position. Query the shape
with PsListTree_GetLevel / GetParent / GetChildCount / HasChildren.
dim as integer proj = PsListTree_AddNode( hTree, -1, "MyProject" )
dim as integer src = PsListTree_AddNode( hTree, proj, "src" )
PsListTree_AddNode( hTree, src, "main.tiko" )
dim as integer eng = PsListTree_AddNode( hTree, src, "engine" )
PsListTree_AddNode( hTree, eng, "render.tiko" )Both default OFF, so a control that only builds flat lists or legacy groups is visually unchanged. Turn them on for a tree:
PsListTree_SetTreeIndent( hTree, true ) ' indent column 0 by depth
PsListTree_ShowTwisty( hTree, true ) ' control draws the chevron + owns its hit-rect
PsListTree_SetTwistyFont( hTree, hSymbolFont ) ' a Segoe Fluent Icons HFONTWith the twisty on, clicking it toggles the node without touching the selection; clicking the
label selects. (A legacy AddHeader row keeps its whole-row toggle.) In your row painter, draw
your column-0 text at p->rc.left + p->indent (that offset already includes the reserved twisty
band, so parents and leaves at the same depth line up). Only column 0 is indented — other columns
keep their x. To draw your own glyph instead of the built-in chevron, call ShowTwisty(false)
and read p->rcTwisty / p->level / p->hasChildren / p->isExpanded.
Keyboard: Left collapses an expanded node or jumps to its parent; Right expands a collapsed node or steps to its first child — at any depth.
Opt in with PsListTree_EnableLabelEdit( h, true ). An edit starts on F2 (on the focused row),
on PsListTree_BeginEdit, or — with PsListTree_SetClickToEdit( h, true ) — on a click of the
row that is already current (explorer rename). Enter commits, Esc cancels, clicking away
commits. The editor is a single-line PsTextBox created over the caption and destroyed on
commit or cancel.
Any column is editable, but only one has a gesture. F2, Enter and click-to-edit all open
whichever column PsListTree_SetEditColumn names — column 0, the caption, by default. To edit a
different column from a click, resolve which one the click landed in with
PsListTree_GetColumnRect from your message callback and call PsListTree_BeginEdit( h, row, col ), which is never constrained by the gesture column. Enter opens an editor only when
PsListTree_SetEnterEdits( h, true ) is on; it is off by default because claiming Enter would
otherwise take it from your dialog's default button.
Theme the editor with PsListTree_SetEditColors — the control creates it lazily and owns it, so
there is no handle for you to reach, and without this it renders black on white whatever the list
around it looks like. PsListTree_SetEditTextInset lines its text up with the inset your painter
uses for cell text, and PsListTree_SetEditSelectAll( h, false ) puts the caret at the end
instead of selecting everything.
Pump obligation. A host that enables editing MUST call
PsListTree_FilterMessage( @msg )in its message loop, ahead ofIsDialogMessage— otherwiseIsDialogMessageeats Enter (as "press the default button") and Esc (as "cancel") before they reach the editor. A host that never enables editing needs no pump call.
do while GetMessage( @msg, null, 0, 0 )
if msg.message = WM_QUIT then exit do
if PsListTree_FilterMessage( @msg ) then continue do ' editing hosts: BEFORE IsDialogMessage
if IsDialogMessage( hWnd, @msg ) = 0 then
TranslateMessage( @msg ) : DispatchMessage( @msg )
end if
loopBeginLabelEdit can veto an edit before it starts; EndLabelEdit receives the new text and can
reject it (the row keeps its old caption). Register both under Callback registration; the
typedefs are in Callbacks.
Firm properties of the control, not settings:
- Nothing is drawn without a paint callback. The background is filled, and that is all.
- All rows are the same height. There is no per-row height, so a group header cannot be taller than its items.
-
Legacy group headers do not nest, though tree nodes do.
AddHeader+AddStringbuilds a depth-1 group that runs to the next header; for real depth useAddNode/InsertNode. -
The data setters do not repaint; the text setters do.
PsListTree_SetTextandSetCellTextinvalidate (coalesced byBeginUpdate/EndUpdate, so a wrapped bulk load still costs one repaint).SetItemDataandSetItemDataExtrachange the model and return — they draw nothing, which is right, since neither is on screen unless your painter puts it there. CallPsListTree_Refreshif yours does. -
PsListTree_SetBackColordoes not repaint either. It stores the colour and returns the previous one. Set it before showing the control, or follow it withPsListTree_Refresh. -
Group-header rows never receive cells. They span the full width even in report mode, so
columnCountis 0 for them. -
The control does not clip between cells. Cell rects are handed to you as computed;
drawing text wider than its cell will spill into the next one.
DT_END_ELLIPSISis the answer. -
The control never sorts.
PsListTree_SetColumnClickCallbackis the hook; reordering the rows is yours. -
The
cellspointer is valid only during the paint callback. It points at control-owned scratch reused for every row. Copy anything you need to keep. -
Cell text survives
PsListTree_ClearColumns. Clearing column definitions does not clear row data — the two are deliberately independent. -
PsListTree_GetColumnWidthon the fill column returns 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. -
The message callback's return value has no effect for the button messages. For
WM_LBUTTONDOWN,WM_LBUTTONUP,WM_RBUTTONDOWNandWM_LBUTTONDBLCLKthe surface consumes the message either way, and for the button-down the control's own selection or collapse handling has already run by the time you are called. Returning TRUE suppressesDefWindowProconly forWM_MOUSEMOVE,WM_MOUSEHOVER,WM_MOUSELEAVEandWM_MOUSEWHEEL. -
PSLISTTREE_MESSAGEINFO.idxis filled only for the button and hover messages. It stays -1 forWM_MOUSEMOVE,WM_MOUSELEAVEandWM_MOUSEWHEEL. - Clicks that land below the last row do not reach the message callback at all, for any button message.
- The wheel is reported only when it actually scrolls. A wheel gesture against the end of the list does not call the message callback.
-
The header band's own
WidthChangedslot belongs to the control. Subscribe withPsListTree_SetColumnResizeCallback, never withPsColumnHeader_SetWidthChangedCallbackon the handle fromPsListTree_GetHeader. Every other header callback passes straight through. - No horizontal scrolling. Columns wider than the client run off the right edge and are clipped there.
- No checkbox column, no icons. Anything of that kind is drawn by your painter and driven by your message callback. (In-place editing and drag-reorder are built in, both opt-in.)
-
Only one column has an editing gesture. F2, Enter and click-to-edit all open whichever
single column
PsListTree_SetEditColumnnames (default 0). Editing any other column is programmatic: resolve the clicked column withPsListTree_GetColumnRectand callPsListTree_BeginEditwith it. - Switching tooltip backend cannot change what a tip says. Both backends resolve text through the same rule — your tooltip callback if set, otherwise the row's own text. The backend decides only how the tip is drawn and driven.
Every function takes the handle from PsListTree_Create as its first argument, written h in
the tables below.
| Function | Description |
|---|---|
PsListTree_Create( hWndParent, CtrlID ) as HWND |
Creates the control as a child of hWndParent and returns the container's handle. CtrlID becomes its GWLP_ID; the three children take CtrlID, CtrlID + 1 and CtrlID + 2. Created zero-sized and hidden — place it with SetWindowPos. |
Add* and Insert* return the new row's model index, or -1 on failure.
| Function | Description |
|---|---|
PsListTree_AddString( h, Text, itemData = 0, itemDataExtra = 0 ) as integer |
Appends an item row. |
PsListTree_AddHeader( h, Text, itemData = 0, itemDataExtra = 0 ) as integer |
Appends a group-header row. Items added after it belong to this group until the next header. |
PsListTree_InsertString( h, row, Text, itemData = 0, itemDataExtra = 0 ) as integer |
Inserts an item at row, shifting later rows down. row is clamped to [0, count] and the index actually used is returned. Inserts an item, never a header. |
PsListTree_AddNode( h, parentRow, Text, itemData = 0, itemDataExtra = 0 ) as integer |
Appends a child under parentRow (-1 = a new top-level node), after the parent's whole existing subtree, at parent.level + 1. Returns the new model index, or -1. See Tree view and in-place editing. |
PsListTree_InsertNode( h, parentRow, childIndex, Text, itemData = 0, itemDataExtra = 0 ) as integer |
Inserts among parentRow's direct children at childIndex (clamped; past the end appends). parentRow = -1 inserts a top-level node. Returns the new model index, or -1. |
PsListTree_DeleteString( h, row ) as boolean |
Deletes the one row. FALSE for an invalid index. To remove a whole subtree, delete the parent's descendants first (see GetChildCount / GetLevel). |
PsListTree_Clear( h ) |
Removes every row. Also resets the focus and anchor rows to -1, so a stale caret cannot survive into the next list. Silent. |
PsListTree_GetCount( h ) as integer |
Every row in the model. |
PsListTree_GetVisibleCount( h ) as integer |
Rows currently on show — headers plus the items of expanded groups. |
PsListTree_GetText( h, row ) as ustring |
The row's text, which is also its column 0 cell. "" for an invalid row. |
PsListTree_SetText( h, row, Text ) as boolean |
Sets it. FALSE for an invalid row. Repaints just that row — no map rebuild. Wrap a bulk loop in BeginUpdate/EndUpdate and the rows collapse to one span. |
PsListTree_GetCellText( h, row, col ) as ustring |
Column col's text for that row. col = 0 is the row's own text. A cell never set — and any col beyond what the row stores — reads "", as does a negative col. |
PsListTree_SetCellText( h, row, col, Text ) as boolean |
Sets it, growing the row's sparse cell storage as needed. FALSE for an invalid row or a negative col. A col beyond the defined columns is legal storage: it simply paints once a matching column exists. Repaints just that row, as SetText does. |
PsListTree_GetItemData( h, row ) as integer |
The row's user value. 0 for an invalid row. |
PsListTree_SetItemData( h, row, itemData ) as boolean |
Sets it. FALSE for an invalid row. Does not repaint. |
PsListTree_GetItemDataExtra( h, row ) as integer |
The row's second user value. |
PsListTree_SetItemDataExtra( h, row, itemDataExtra ) as boolean |
Sets it. FALSE for an invalid row. Does not repaint. |
PsListTree_BeginUpdate( h ) |
Suspends the rebuild-and-repaint that every model mutator would otherwise do. Nests. |
PsListTree_EndUpdate( h ) |
Ends one nesting level. The outermost one repaints once — a full refresh, unless the batch changed nothing but row contents, in which case just those rows. |
PsListTree_Refresh( h ) |
Rebuilds the visible map, re-derives the scroll position and the scrollbar, and repaints with a background erase. |
PsListTree_InvalidateRow( h, row ) |
Marks just that row's band for repaint. Silent no-op for a row that is invalid, hidden inside a collapsed parent, or scrolled out of view. |
PsListTree_InvalidateRows( h, first, last ) |
The same over a model range, coalesced into one update rect. Bounds may be given in either order. |
PsListTree_UpdateNow( h ) |
Paints whatever is invalidated now, instead of waiting for the queue to go idle. Reaches the surface, which UpdateWindow on the control's own handle does not. No-op when nothing is pending. |
Refresh is for a change to the model's shape — rows added or removed, a node collapsed,
the order changed. It rebuilds the visible map (O(rowCount)), re-derives the scroll position,
syncs the scrollbar and repaints with a background erase.
None of that is needed when only what a row says has changed. On a list that updates continuously — a price grid, a progress table, a log tail — doing it per update is the difference between a control that idles and one that does not. Invalidate the affected rows instead, and note the erase is skipped too, so a row that repaints under the user's eyes does not flash.
What it does not save, because the name invites the wrong assumption: the surface's
WM_PAINT redraws every row currently on screen regardless of the update rect. It has to —
the buffer covers the whole client and is blitted whole, so a paint that skipped rows outside
the update region would blit an unpainted band over them. The saving is the rebuild, the
scroll re-derivation, the scrollbar sync and the erase; not the row loop, which is bounded by
the window height rather than by the model.
A range whose ends are far apart repaints the rows between them as well. That is deliberate:
one region and one WM_PAINT beats a scattered region and several.
SetText and SetCellText are already row-scoped: they repaint just the row they wrote,
because cell text cannot move a row, hide one, or change how many there are. Inside a
BeginUpdate/EndUpdate batch they widen a dirty span that EndUpdate flushes as one
invalidate.
A structural change anywhere in the batch — a row added or deleted, a node collapsed, a
reorder — outranks that span and EndUpdate does the full refresh, because the span describes
a model that no longer exists. So does a batch in which nothing notified at all, which keeps
the old contract for a host that mutates rows through a ROWINFO pointer and wraps
BeginUpdate/EndUpdate around it purely to force a repaint.
The explicit calls are for the case the control cannot see: a row whose painted appearance depends on something outside its stored text — a colour driven by host state, a value the paint callback reads from your own model.
Collapse, expand and toggle act on anything that can fold — a legacy header or any node with
children — and return FALSE for a leaf or an invalid index. The tree queries read the derived
structure and work whether the nodes were built with AddNode or with AddHeader/AddString.
| Function | Description |
|---|---|
PsListTree_IsHeader( h, row ) as boolean |
Is this row a group header (a styling flag)? |
PsListTree_IsCollapsed( h, row ) as boolean |
Is this node collapsed? |
PsListTree_CollapseRow( h, row ) as boolean |
Collapses the node, hiding its whole subtree. TRUE for any foldable node; only an actual change refreshes. |
PsListTree_ExpandRow( h, row ) as boolean |
Expands the node, same convention. |
PsListTree_ToggleRow( h, row ) as boolean |
Flips it. |
PsListTree_SetNodeCollapsed( h, row, bCollapsed ) as boolean |
Sets the collapsed state directly. FALSE for a leaf or invalid row. Silent. |
PsListTree_CollapseAll( h ) as boolean |
Collapses every foldable node in one refresh. |
PsListTree_ExpandAll( h ) as boolean |
Expands every foldable node in one refresh. |
PsListTree_GetLevel( h, row ) as integer |
The node's tree depth (0 = top-level), or -1 for an invalid row. |
PsListTree_GetParent( h, row ) as integer |
The model index of the node's parent, or -1 for a top-level node / invalid row. |
PsListTree_GetChildCount( h, row ) as integer |
Number of direct children. |
PsListTree_HasChildren( h, row ) as boolean |
Whether the node has any children. |
| Function | Description |
|---|---|
PsListTree_GetCurSel( h ) as integer |
The focused row's model index, or -1. |
PsListTree_SetCurSel( h, row ) as integer |
Focuses row, selects only it, sets the anchor there, and scrolls it into view if it is off-page. Returns row. An invalid index instead clears the whole selection and the focus and returns -1. Silent — the SelChange callback does not fire. A row hidden under a collapsed header keeps the focus but does not scroll. |
PsListTree_GetSel( h, row ) as boolean |
Is that row selected? Works for hidden rows and headers. |
PsListTree_SetSel( h, row, state ) as boolean |
Sets one row's selected flag and repaints. FALSE for an invalid row. Does not move the focus. Silent. |
PsListTree_GetSelCount( h ) as integer |
How many rows are selected, hidden ones included. |
PsListTree_GetSelItems( h, selItems() ) as integer |
Redims selItems() to the model indices of every selected row, ascending, and returns the count. With nothing selected the array is erased and 0 returned. |
PsListTree_SelectAll( h, state ) |
Sets every row's selected flag to state and repaints. Silent.
|
PsListTree_SetMultiSelect( h, enable ) as boolean |
Checklist mode: every click toggles one row. Enabling it turns extended mode off. |
PsListTree_SetExtendedSelect( h, enable ) as boolean |
Explorer mode: Shift extends a range from the anchor, Ctrl toggles. Enabling it turns multiple mode off. |
PsListTree_PreventDoubleClick( h, enable = true ) as boolean |
Opts out of double-click reporting. With CS_DBLCLKS a double-click delivers down, up, dblclk, up — the dblclk substituting for the second down — so a host acting on WM_LBUTTONUP acts twice. Enabled, the dblclk still applies the ordinary click to the model (selection, header toggle) but is never surfaced, and the trailing second up is swallowed too: you see exactly one down/up pair per double-click. |
PsListTree_GetTopIndex( h ) as integer |
The model index of the first displayed row, or -1 when nothing is visible. |
PsListTree_SetTopIndex( h, row ) as integer |
Scrolls so row is at the top. A row hidden under a collapsed header resolves to its group header, so any valid model row may be passed without checking collapse state. The scroll is clamped — never past the point where the last row sits at the bottom of the viewport — and the clamped result is returned. -1 for an invalid row. |
Opt-in; see the Drag-and-drop reordering concept above. Register the callbacks with the functions under Callback registration.
| Function | Description |
|---|---|
PsListTree_SetDragReorder( h, enable = true ) as boolean |
Turns drag-reorder on or off (default OFF). Turning it off also cancels any drag in progress. |
PsListTree_GetDragReorder( h ) as boolean |
Whether drag-reorder is enabled. |
PsListTree_MoveRows( h, srcRows() as integer, insertBefore ) as integer |
Moves the given MODEL rows so they land contiguously before insertBefore (a MODEL index; GetCount() appends). Sources may be non-contiguous and any order; they are de-duplicated, kept in original relative order, and headers among them are ignored. Selection travels with each row. The moved block becomes the focus/anchor. Returns the block's new first index, or -1 if nothing moved. Silent (no DragDrop); repaints. |
PsListTree_SetDragIndicatorColor( h, clr ) as COLORREF |
Sets the insertion-line / header-highlight colour and returns the previous one. |
PsListTree_GetDragIndicatorColor( h ) as COLORREF |
The current indicator colour. |
| Function | Description |
|---|---|
PsListTree_GetBackColor( h ) as COLORREF |
The colour behind the rows, including the strip below the last one. |
PsListTree_SetBackColor( h, clr ) as COLORREF |
Sets it and returns the previous value. Does not repaint — set it before showing, or follow with PsListTree_Refresh. |
PsListTree_GetRowHeight( h ) as integer |
The row height in unscaled units. |
PsListTree_SetRowHeight( h, height ) as integer |
Sets it and returns what you passed. Unscaled units — the control DPI-scales it, so do not pre-scale. Repaints and re-syncs the scrollbar, since rows-per-page changed. The scaled result is floored at 1 pixel. |
PsListTree_GetFont( h ) as HFONT |
The font selected into the buffer before each row callback. |
PsListTree_SetFont( h, hFont ) as boolean |
Sets it and repaints. Borrowed, never owned — keep it alive and destroy it yourself. Your paint callback may select a different font per row. |
The control ships on the system tooltip (comctl32) and stays there unless you ask
otherwise. PsListTree_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 TooltipCallback if one is set, otherwise the row's own text, with "" suppressing the
tip. Only the drawing and the driving differ.
| Function | Description |
|---|---|
PsListTree_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. |
PsListTree_GetTooltipMode( h ) as long |
The current mode. PSTIP_MODE_SYSTEM for an invalid handle. |
PsListTree_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. |
PsListTree_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. |
PsListTree_SetHoverTime( h, milliseconds ) |
The initial dwell — how long the cursor must rest on a row before a tip appears. Default 250. Double duty: the same value is TrackMouseEvent's dwHoverTime, so this also decides when the control considers a row hot. |
PsListTree_SetAutoPopTime( h, milliseconds ) |
How long a shown tip stays up before it hides itself. |
PsListTree_SetReshowTime( h, milliseconds ) |
The delay before a tip reappears when the cursor moves to another row 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.
Per-instance theming through GetPsTooltipHandle works, but it is rarely what you want. The
intended way to use PSTIP_MODE_PS is to set the process-wide defaults before any control is
created, so every tip in the application matches without the host tracking handles:
dim as PSTOOLTIP_COLORS tipColors
tipColors.BackColor = theme.BackColor
tipColors.ForeColor = theme.ForeColor
PsTooltip_SetDefaultColors( @tipColors )
PsTooltip_SetDefaultFonts( ghFont(GUIFONT_9), ghFont(GUIFONTBOLD_10), ghFont(SYMBOLFONT_10) )
PsTooltip_SetDefaultMaxWidth( 320 )
PsTooltip_SetDefaultDelays( 400, 8000, 100 )
' ... then create the control and opt it in:
PsListTree_SetTooltipMode( hList, PSTIP_MODE_PS )PsTooltip_SetDefaultStyle is available on the same footing, and PsTooltip_ClearDefaults
disarms the lot. The fonts are borrowed: you keep ownership and must outlive every tip that
uses them — the one real hazard here, since you are promising them to controls you never see.
See PsTooltip for the colour struct and the style constants.
Off by default. Switch it on to give the list the same chrome a PsTextBox has, so a list
and a text field stacked on one settings page read as the same kind of control:
PsListTree_SetBorderColor( hList, PsTextBox_GetBorderColor( hField ) )
PsListTree_SetBorderWidth( hList, 1 )The frame is drawn by the control's outer window, and the row surface, the scrollbar strip
and the column header band are all inset by the width, so nothing paints over it. Corners are
square, matching a PsTextBox at its default corner radius of 0.
The width is in raw pixels and is never DPI-scaled — a chrome hairline should stay a
hairline at every scale (the same rule PsTextBox's border and PsMenuBar's separator
follow). Do not pre-scale it.
There is no focus-border colour. A PsTextBox switches to one when its editor has the caret;
a list has no caret to signal.
| Function | Description |
|---|---|
PsListTree_GetBorderWidth( h ) as integer |
Pixels. 0 (the default) means no border and no reserved space. |
PsListTree_SetBorderWidth( h, nWidth ) |
Sets it, re-lays out the children and repaints. Negative clamps to 0. Raw pixels — not DPI-scaled. |
PsListTree_GetBorderColor( h ) as COLORREF |
Default BGR(122,122,122), the same default PsTextBox uses. |
PsListTree_SetBorderColor( h, clr ) |
Sets it and repaints. |
PsListTree_ProbeRenderedPixel( h, x, y ) as long |
Renders the outer window offscreen through the same code its WM_PAINT runs and returns the colour at one client point as a COLORREF, or -1 if the point is outside the client. For asserting that a themed border actually reached the pixels rather than merely that the setter stored it. Renders the outer window only — the rows live on a child surface and are not in that image. |
All OFF / zero-effect by default, so a flat or grouped list is untouched. Widths are unscaled units, DPI-scaled internally. See Tree view and in-place editing.
| Function | Description |
|---|---|
PsListTree_SetTreeIndent( h, enable = true ) as boolean |
Turns per-depth indentation of column 0 on/off. |
PsListTree_GetTreeIndent( h ) as boolean |
Whether tree indent is on. |
PsListTree_SetIndentWidth( h, nWidth ) as integer |
Per-level indent in unscaled units (default 16). Returns the stored value; repaints. |
PsListTree_GetIndentWidth( h ) as integer |
The per-level indent width. |
PsListTree_ShowTwisty( h, enable = true ) as boolean |
Has the control draw the expand/collapse chevron and own its hit-rect. |
PsListTree_IsTwistyShown( h ) as boolean |
Whether the twisty is shown. |
PsListTree_SetTwistyWidth( h, nWidth ) |
Reserved twisty-band width in unscaled units (default 16). |
PsListTree_SetTwistyColor( h, clr ) |
The glyph colour. |
PsListTree_SetTwistyFont( h, hFont ) |
The symbol font for the glyph, borrowed not owned — a Segoe Fluent Icons HFONT. |
PsListTree_SetTwistyGlyphs( h, wszExpanded, wszCollapsed ) |
Overrides the two glyphs (default ChevronDown / ChevronRight). |
Opt-in. Enabling it establishes a pump obligation: call PsListTree_FilterMessage in the loop,
before IsDialogMessage. See Tree view and in-place editing.
| Function | Description |
|---|---|
PsListTree_EnableLabelEdit( h, enable = true ) as boolean |
Turns label editing on/off. Turning it off ends any edit in progress (as a cancel). |
PsListTree_IsLabelEditEnabled( h ) as boolean |
Whether editing is enabled. |
PsListTree_SetClickToEdit( h, enable = true ) as boolean |
When on (default OFF), a click on the already-current row starts an edit. |
PsListTree_SetEditColumn( h, col ) as boolean |
Which single column F2, Enter and click-to-edit open. Default 0 (the caption); negatives clamp to 0. Does not constrain BeginEdit, which always names its own column. |
PsListTree_GetEditColumn( h ) as integer |
That gesture column. 0 for an invalid handle. |
PsListTree_SetEnterEdits( h, enable = true ) as boolean |
Enter starts an edit on the focused row. Default OFF, and opt-in separately from EnableLabelEdit because claiming Enter is not free — the control has to answer WM_GETDLGCODE with DLGC_WANTALLKEYS for that one message, or IsDialogMessage hands Enter to your default button first. Leave it off if Enter means something else in your dialog. It wins over SetActivateCallback: a list whose Enter opens an editor does not also activate. |
PsListTree_SetEditColors( h, clrBack, clrFore, clrBorder, clrFocusBorder ) |
Themes the in-place editor. The control creates the PsTextBox lazily and owns it, so there is no handle for you to colour and no moment to do it in — without this it renders in PsTextBox's defaults, black on white, whatever the list around it looks like. Applies immediately to an editor that already exists, so a mid-session theme change is not one edit behind. |
PsListTree_SetEditSelectAll( h, enable ) |
TRUE (the default) selects the whole text so the first keystroke replaces it — explorer rename behaviour. FALSE puts the caret at the end instead, for edits that are usually amendments. |
PsListTree_SetEditTextInset( h, nPx ) |
Where the editor's text starts, in already-scaled pixels from the edited cell's left edge. Pass the same inset your paint callback uses for its cell text, or the row visibly jumps sideways the moment an edit opens. -1 (the default) leaves PsTextBox's own margins alone. |
PsListTree_BeginEdit( h, row, col = 0 ) as boolean |
Starts an edit of row, on any column. FALSE if editing is off, the row is invalid / non-selectable / hidden, or BeginLabelEdit vetoes. The editor is placed over the named column; column 0 alone is inset past the tree indent and the twisty band. |
PsListTree_EndEdit( h, bCommit = true ) as boolean |
Ends the current edit. bCommit runs it through EndLabelEdit and writes the text; returns TRUE if the model was actually written. |
PsListTree_IsEditing( h ) as boolean |
Whether an edit is in progress. |
PsListTree_GetEditRow( h ) as integer |
The model row being edited, or -1. |
PsListTree_GetEditCol( h ) as integer |
The column the edit is on. EndLabelEdit is not handed the column, so a host that wants only one column editable reads it here and returns FALSE for the others. Unlike GetEditRow this does not reset when the edit ends — the editor is torn down before EndLabelEdit runs, so a value cleared on teardown would be unreadable at the one moment it is wanted. Outside an edit it reports the last one's column. |
PsListTree_IsCharSwallowArmed( h ) as boolean |
Probe: is a WM_CHAR swallow armed? A key claimed in WM_KEYDOWN has a WM_CHAR manufactured for it, and an unswallowed one reaches DefWindowProc and beeps. The beep has no return value, so this flag is the only way to observe the suppression rather than listen for it. |
PsListTree_FilterMessage( pMsg ) as boolean |
Pump call for editing hosts (before IsDialogMessage). Claims Enter/Esc for the active editor and forwards to its context menu. Returns TRUE if consumed; safe to call always. |
Nothing here is required; the control creates, positions, ranges and auto-hides the bar itself.
| Function | Description |
|---|---|
PsListTree_GetScrollBar( h ) as HWND |
The scrollbar child, for direct PsVScrollBar_* calls. |
PsListTree_SetScrollBarWidth( h, nWidth ) |
Track width in unscaled units, DPI-scaled at layout. Clamped to a minimum of 1. Re-lays out immediately. |
PsListTree_SetScrollBarColors( h, backclr, foreclr, foreclrhot ) |
Track background, thumb, and thumb-under-cursor. |
PsListTree_SetScrollBarDividerColor( h, clr [, bShow] ) |
A one-pixel rule down the scrollbar's left edge, separating it from the rows. Off by default. Drawn over the thumb, so the line is continuous. |
PsListTree_SetScrollBarPaintCallback( h, usersub ) |
Installs a VScrollPaintCallbackSub that draws the bar instead of its built-in painter. |
All optional. Widths and minimum widths are pixels; see PsColumnHeader for the layout rules these delegate to.
| Function | Description |
|---|---|
PsListTree_AddColumn( h, Text, nWidth = 100, nMinWidth = 0, itemData = 0 ) as integer |
Appends a column and returns its index, or -1. nMinWidth = 0 means the control's own scaled floor applies. |
PsListTree_InsertColumn( h, idx, Text, nWidth = 100, nMinWidth = 0, itemData = 0 ) as integer |
Inserts at idx (clamped to [0, count]) and returns the index used. Shifts every row's cell storage to match, across the column-0 / row-text alias. |
PsListTree_DeleteColumn( h, idx ) as boolean |
Deletes the column and shifts every row's cells down. Deleting column 0 promotes each row's column 1 cell to its text. FALSE for a bad index. |
PsListTree_ClearColumns( h ) |
Removes every column definition. Row cell text is untouched and reappears if columns are defined again. |
PsListTree_GetColumnCount( h ) as integer |
How many columns are defined. |
PsListTree_GetColumnText( h, idx ) as ustring |
The column's caption. |
PsListTree_SetColumnText( h, idx, Text ) as boolean |
Sets it and repaints that header cell only — row cells are unaffected. FALSE for a bad index. |
PsListTree_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, not the stored one. |
PsListTree_SetColumnWidth( h, idx, nWidth ) as boolean |
Stores the width, clamped up to the column's effective minimum, and repaints the rows. Silent — no resize callback. On the fill column the value is stored but has no visual effect until that column stops being the fill. |
PsListTree_GetColumnRect( h, idx, byref rc ) as boolean |
The column's laid-out rect in surface client coordinates — the same x space a mouse message delivered to your message callback carries, so this is how you answer "which column did that click land in" without re-deriving the layout from the widths (which would mean reproducing the fill column's stretch). rc is emptied first; FALSE for a bad index. PsColumnHeader_GetColumnRect on the handle from GetHeader reaches the same rect. |
PsListTree_GetColumnMinWidth( h, idx ) as integer |
The column's own stored minimum. 0 means "use the control default". |
PsListTree_SetColumnMinWidth( h, idx, nMinWidth ) as boolean |
Sets it; negatives become 0. Re-lays out and repaints. |
PsListTree_GetFillColumn( h ) as integer |
The resolved index of the column that absorbs leftover width, or -1 if none. |
PsListTree_SetFillColumn( h, idx ) as boolean |
Takes a column index, CCOLHDR_FILL_LAST (the default) or CCOLHDR_FILL_NONE. FALSE for an index that is neither valid nor one of those two constants. |
PsListTree_ShowHeader( h, bShow = true ) as boolean |
Shows or hides the interactive header strip. Hidden is the default; columns lay out and rows paint in columns either way. Changes the surface height, so it re-lays out and re-syncs the scrollbar. |
PsListTree_IsHeaderVisible( h ) as boolean |
Is the band currently shown? |
PsListTree_GetHeaderHeight( h ) as integer |
Band height in unscaled units. |
PsListTree_SetHeaderHeight( h, height ) as integer |
Sets it, clamped to a minimum of 1, and returns the clamped value. Unscaled units — do not pre-scale. |
PsListTree_GetHeader( h ) as HWND |
The header child, for direct PsColumnHeader_* calls (padding, hit-tests, theming). Do not set its WidthChanged callback — that slot belongs to this control. |
PsListTree_SetRowSpanColumns( h, row, bSpan = true ) as boolean |
Makes an ordinary (selectable) row paint as one cell spanning every column instead of one per column; its text stays in column 0. The painter gets columnCount = 1 and p->isSpanned = true. Silent, and repaints. FALSE for an invalid row. See Spanned rows above. |
PsListTree_GetRowSpanColumns( h, row ) as boolean |
Whether that row is spanned. FALSE for an invalid row. |
PsListTree_SetRowSelectable( h, row, bSelectable = true ) as boolean |
Sets whether the row can be selected/focused. false makes it non-selectable on every path (mouse, keyboard, programmatic) and keyboard nav skips it; marking a live selected/focused row non-selectable clears its selection and drops the caret. Silent, and repaints. FALSE for an invalid row. See Non-selectable rows above. |
PsListTree_GetRowSelectable( h, row ) as boolean |
Whether the row can be selected (default TRUE). FALSE for an invalid row. |
| Function | Description |
|---|---|
PsListTree_SetPaintCallback( h, usersub ) |
Installs the row painter. Required — no rows are drawn without one. |
PsListTree_SetMessageCallback( h, userfunc ) |
Installs an observer for the mouse messages. |
PsListTree_SetTooltipCallback( h, userfunc ) |
Installs the on-demand tooltip text supplier. Unset, the row's own text is used. |
PsListTree_SetSelChangeCallback( h, usersub ) |
Installs the handler told when the user moves the selection, by mouse or keyboard. |
PsListTree_SetActivateCallback( h, usersub ) |
Installs the row's default action handler — double-click, or Enter on the focused row. Setting it is what makes the control claim Enter; with no callback set, Enter is left alone and reaches your default button as before. PsListTree_SetEnterEdits wins over it. See below. |
PsListTree_SetBeginLabelEditCallback( h, userfunc ) |
Installs the pre-edit veto (return FALSE to block an edit). See below. |
PsListTree_SetEndLabelEditCallback( h, userfunc ) |
Installs the commit handler (return FALSE to reject the new text). See below. |
PsListTree_SetExpandCollapseCallback( h, usersub ) |
Installs the handler told when the user expands/collapses a node. Silent for the programmatic collapse/expand setters. See below. |
PsListTree_SetCanDropCallback( h, userfunc ) |
Installs the drag-reorder veto (fires before a drop; return FALSE to reject). See below. |
PsListTree_SetDragDropCallback( h, usersub ) |
Installs the drag-reorder notify (fires after a user drop). See below. |
PsListTree_SetColumnResizeCallback( h, usersub ) |
Installs the handler told when the user resizes a column. This is the correct door — the header's own slot is taken. |
PsListTree_SetColumnClickCallback( h, usersub ) |
Installs the completed-click-on-a-column-body handler. Passes straight through to the header. |
PsListTree_SetColumnAutoSizeCallback( h, userfunc ) |
Installs the divider-double-click best-fit measurer. Passes straight through. |
PsListTree_SetHeaderPaintCallback( h, usersub ) |
Installs the per-column header cell painter. Passes straight through. |
PsListTree_SetHeaderTooltipCallback( h, userfunc ) |
Installs the per-column tooltip text supplier. Passes straight through. |
PsListTree_SetHeaderBackColor( h, clr ) |
The band's flat background. Passes straight through. |
PsListTree_SetHeaderFont( h, hFont ) |
The band's font, borrowed not owned. Passes straight through. |
Passing 0 to any callback setter clears it.
These are public rather than hidden, so they are listed here for completeness. Only the first is something a host has a reason to call.
| Function | Description |
|---|---|
PsListTree_PositionWindows( hwnd ) as LRESULT |
Re-lays out the three children — header band, row surface, scrollbar strip — inside the container's client rect, honouring the border inset and the scrollbar's auto-hide. The control calls it from WM_SIZE and from every path that changes the layout, so you normally never need it. Reach for it only if you have changed something behind the control's back and the children have not caught up. Always returns 0. Takes the control handle, like everything else here. |
The remaining four take a PSLISTTREE_STATE ptr — the control's private state struct, which a caller
outside the implementation cannot form. They are internal machinery, public only because
PSLISTTREE_STATE.Refresh is defined ahead of them, among the declarations at the top of the
module, and has to call them:
| Function | Role |
|---|---|
PsListTree_SyncViewFromModel( pList ) |
Re-derives the top visible row from the model's topRow after any change, clamps it, and repaints. |
PsListTree_SyncScrollBar( pList ) |
Pushes the row count, page size and position into the scrollbar and applies the auto-hide decision. |
PsListTree_CaptureTopRow( pList ) |
Reads the surface's actual top row back into the model, before a rebuild, so a user's scroll survives it. |
PsListTree_ItemsPerPage( pList ) as integer |
Rows that fit in the current client height, floored at 1. |
There is no colour struct. The control paints only its own background; every row pixel is yours, drawn from whatever palette your host already has. That is deliberate — a list of this kind is skinned by its host, and a fixed struct of row colours would only get in the way.
What the control itself owns:
| Setting | Paints |
|---|---|
PsListTree_SetBackColor |
The row surface's whole client, including the strip below the last row. Filled before any row callback runs, and the only thing painted when no callback is set |
PsListTree_SetScrollBarColors — backclr
|
The scrollbar track |
PsListTree_SetScrollBarColors — foreclr
|
The scrollbar thumb, idle |
PsListTree_SetScrollBarColors — foreclrhot
|
The scrollbar thumb, cursor over it or dragging |
PsListTree_SetHeaderBackColor |
The header band's flat background, behind the column cells |
PsListTree_SetBorderColor |
The frame around the whole control, when SetBorderWidth is non-zero |
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:
isSelected > isHot > plain
with isFocused drawn as an accent on top of whichever of those applied, because the caret
row and the selection are different things once more than one row can be selected.
-
At most one row is
isHotat any moment, and it clears deterministically when the cursor leaves. -
isHotandisSelectedcan both be true — the cursor resting on a selected row. -
isFocusedmarks the caret row and is independent ofisSelected: in checklist mode the arrow keys move the caret without selecting anything. -
isHeaderandisCollapseddescribe the row, not its mood.isCollapsedis meaningful only whenisHeaderis TRUE.
| Part | Drawn by |
|---|---|
| Surface background | The control, in BackColor, every repaint |
| Rows | Your paint callback, into the same buffer |
| Header band background | The header control, in its back colour |
| Header column cells | Your header paint callback |
| Scrollbar | The scrollbar's built-in painter, or your scrollbar paint callback |
In report mode, fill the full row rect first so selection and hover span the row
listview-style, then draw each cell inside cells[i].rc.
The four PsListTree typedefs are unprefixed — PaintCallbackSub, MessageCallbackFunc,
TooltipCallbackFunc and SelChangeCallbackSub occupy those exact names. If you are combining
these files with other controls, that is a name collision to check for.
type PaintCallbackSub as sub( byval p as PSLISTTREE_PAINTINFO ptr )Draws one row. Called once per visible row on every repaint, so keep it cheap. Paint through
p->b — the control's buffer for this repaint — and never touch a screen DC. The control has
already filled the client with BackColor and selected the control font into the buffer; you
may override the font per row.
PSLISTTREE_PAINTINFO:
| Field | Meaning |
|---|---|
itemID |
The row's model index |
b |
The control's PsPaintBuffer for this repaint (borrowed, not owned) |
rc |
The row's rect, in the surface's client coordinates |
isHot |
The mouse is over this row. At most one row at a time |
isSelected |
The row is part of the selection |
isFocused |
The row holds the keyboard caret |
isHeader |
This is a group-header row |
isCollapsed |
Header rows only: its items are hidden |
isSpanned |
This row spans every column — see below and Spanned rows |
isSelectable |
FALSE if the host made the row non-selectable — grey it if you like; the control does the blocking (see Non-selectable rows) |
wszCaption |
The row's text (column 0) |
level |
Tree depth, 0 = top-level (see Tree view and in-place editing) |
hasChildren |
The node has at least one child |
isExpanded |
NOT isCollapsed, read for a parent node |
indent |
Pixel offset to apply to your column 0 text, from the tree depth (0 unless SetTreeIndent is on; already includes the reserved twisty band) |
rcTwisty |
The expand/collapse glyph's rect, in the same coords as rc; empty for a leaf or when the twisty is off. Read it to draw your own glyph after setting ShowTwisty(false)
|
columnCount |
Number of cells in cells: 0 for a full-width row, 1 for a spanned row over defined columns, else one per column |
cells |
Array of PSLISTTREE_CELLINFO, or null when columnCount is 0 |
columnCount is 0 and cells is null whenever the row should paint as one full-width cell with
no columns to fill: no columns are defined, or the row is a group header.
A spanned row (isSpanned = true) over defined columns instead arrives with
columnCount = 1 and a single cells[0] whose rect covers the whole column band — so a painter
that loops for c = 0 to columnCount - 1 draws it as one wide cell unchanged. (With no columns
defined it is already full-width, columnCount = 0, and only isSpanned marks the intent.)
Whenever you fill the row background from p->rc before drawing cells, selection and hover cover
the whole row in every case.
PSLISTTREE_CELLINFO:
| Field | Meaning |
|---|---|
iCol |
The column index |
rc |
The cell's rect — x from the header's column geometry, y spanning this row |
wszText |
The cell's text |
The
cellsarray is control-owned scratch, valid only for the duration of the callback. It is reused for every row. Copy anything you need to keep.
The control does not clip between cells. Text wider than its cell spills into the next one; use
DT_END_ELLIPSIS.
type MessageCallbackFunc as function( byval m as PSLISTTREE_MESSAGEINFO ptr ) as booleanObserves mouse messages on the row surface. Return TRUE to suppress the control's remaining handling of that message, FALSE to let it proceed.
PSLISTTREE_MESSAGEINFO:
| Field | Meaning |
|---|---|
hList |
The row surface window, not the container. GetParent gives you the control handle |
uMsg |
The message |
wParam / lParam
|
Its parameters, unmodified |
idx |
The model row index under the mouse, or -1 |
isCtrl |
Ctrl was down when the message arrived |
isShift |
Shift was down |
Which messages arrive, and what the return value actually does:
| Message | idx |
When it fires | Does TRUE do anything? |
|---|---|---|---|
WM_MOUSEMOVE |
always -1 | Every move over the surface | Yes — suppresses DefWindowProc
|
WM_MOUSEHOVER |
the hovered row | Only when the cursor is resting on a row | Yes |
WM_MOUSELEAVE |
always -1 | The cursor left | Yes |
WM_MOUSEWHEEL |
always -1 | Only when the wheel actually scrolled the list | Yes — stops the message bubbling to the parent |
WM_LBUTTONDOWN |
the pressed row | Only when the press lands on a row | No |
WM_LBUTTONUP |
the released-over row | Only when the release lands on a row | No |
WM_RBUTTONDOWN |
the row under the cursor | Only over a row | No |
WM_LBUTTONDBLCLK |
the row under the cursor | Over a row, and not while PsListTree_PreventDoubleClick is on |
No |
The button messages are consumed by the surface either way, which is why the return value
cannot change them. More importantly, the control's own handling has already happened by
the time WM_LBUTTONDOWN reaches you: focus has been taken, and the click's selection change
or header collapse has been applied. Treat this callback as an observer.
WM_KEYDOWN is not reported here — the surface consumes the navigation keys itself. Use
the SelChange callback.
type TooltipCallbackFunc as function( byval hListControl as HWND, byval row as integer, byval col as integer ) as ustringSupplies the tooltip text for a model row, on demand — called only when a tip is about to show.
Return "" for no tooltip on that row. With no callback installed, the row's own text is used.
hListControl is the control handle, not the surface. col is the column the mouse was last
over (0-based). A list with no header columns is one implicit column, so it reports 0; with
columns defined, -1 means the cursor was past the last column.
type SelChangeCallbackSub as sub( byval hListControl as HWND, byval row as integer )The user moved the selection — by clicking a row, or by moving the caret with the keyboard
(arrows, PageUp/Down, Home/End, Left to a header, Space). row is the model index of the newly
focused row, or -1 if there is none. Fires after the control's state is updated, so
PsListTree_GetCurSel already agrees.
This is the only way to observe keyboard navigation, because the control consumes
WM_KEYDOWN itself and it never reaches the message callback. A mouse-only host can keep using
WM_LBUTTONUP in the message callback instead.
It does not fire for PsListTree_SetCurSel, SetSel, SelectAll or Clear, which is what
makes those safe to call from inside this handler. Nor does it fire when the user re-selects
the row that is already current, or when a clamped arrow key does not actually move the caret —
only a real change notifies.
type ActivateCallbackSub as sub( byval hListControl as HWND, byval row as integer )The row's default action: a double-click on it, or Enter while it is the focused row. One
callback covers both because they are one concept (a listview's LVN_ITEMACTIVATE). row is
the model index.
User gestures only — never a programmatic selection change. A non-selectable row is refused for the same reason every selection path refuses it: a row the user cannot land on cannot be the one they activated.
Two things follow from installing it. Setting it is what makes the control claim Enter — with
no callback set, Enter is left alone and reaches your dialog's default button as before. And
PsListTree_SetEnterEdits wins over it: a list whose Enter opens an editor does not also
activate. Double-click still activates in that configuration.
Two things suppress the double-click half of the gesture. PsListTree_PreventDoubleClick turns
the double-click into a plain click, and a message callback that returns TRUE for
WM_LBUTTONDBLCLK claims it — activation is fired after the message callback, so a host
that handles the message itself never gets both.
type BeginLabelEditCallbackFunc as function( byval h as HWND, byval row as integer ) as boolean
type EndLabelEditCallbackFunc as function( byval h as HWND, byval row as integer, byval newText as ustring ) as boolean
type ExpandCollapseCallbackSub as sub( byval h as HWND, byval row as integer, byval bExpanded as boolean )BeginLabelEdit fires just before an in-place edit begins; return FALSE to veto it (no editor
appears). EndLabelEdit fires when the user commits (Enter or focus loss), with the edited
text; return FALSE to reject it and keep the old caption, TRUE (or leave it unset) to write it.
Neither fires on Esc-cancel. newText is byval — keep it byval ustring.
ExpandCollapse fires when the user folds a node — by clicking its twisty, by Left/Right, or
by clicking a legacy header — with the new state in bExpanded. Like the other user-action
callbacks it is silent for the programmatic CollapseRow / ExpandRow / ToggleRow /
SetNodeCollapsed / CollapseAll / ExpandAll setters, so you can drive those from inside it.
type PSLISTTREE_DROPINFO
hList as HWND
srcRows as integer ptr ' the MODEL indices being dragged (srcCount of them)
srcCount as integer
insertBefore as integer ' MODEL index the block will land before (0..rowCount)
onHeader as boolean ' TRUE = dropped on a header (block becomes its first children)
targetRow as integer ' MODEL index of the drop-target row, or -1 at the list end
targetInfo as PSLISTTREE_ROWINFO ptr ' the target row's ROWINFO (read-only), or NULL at the end
end type
type CanDropCallbackFunc as function( byval p as PSLISTTREE_DROPINFO ptr ) as boolean
type DragDropCallbackSub as sub( byval hList as HWND, byval newFirstRow as integer, byval count as integer )CanDrop fires just before a user drop commits. Return FALSE to reject it — the model is left
untouched. The DROPINFO is a snapshot valid only for the duration of the call: srcRows
points at control-owned scratch and targetInfo at a live ROWINFO, so copy anything you need to
keep and do not mutate the model from inside. Unset = every drop is allowed.
DragDrop fires just after a user drop moves rows. newFirstRow is the model index the moved
block now starts at and count is how many rows moved — walk newFirstRow .. newFirstRow+count-1
with the getters (or itemData) to resync a parallel model. It does not fire for
PsListTree_MoveRows (programmatic moves are silent, like every other setter).
The five header callbacks registered through PsListTree_Set* use the HDR_* typedefs and are
documented in full in PsColumnHeader. Two notes specific to using them
through PsListTree:
-
PsListTree_SetColumnResizeCallbackis the only correct door for resize notifications. The header's ownWidthChangedslot is taken by this control, which needs it to repaint the rows on every live drag before re-broadcasting to you. - The
hHeaderparameter these callbacks receive is the header window, not the control. UseGetParentif you need the list.
Defined in PsListTree:
| Constant | Value | Meaning |
|---|---|---|
IDT_CLISTTREE_HOTTRACK |
&hCB01 |
Timer id for the hover safety-net poll. Timer ids are per-window, so every instance shares it |
PSLISTTREE_HOTTRACK_MS |
100 | How often that poll checks whether the cursor has left |
The tooltip backend values passed to PsListTree_SetTooltipMode come from PsTipHost:
| Constant | Value | Meaning |
|---|---|---|
PSTIP_MODE_SYSTEM |
0 | The comctl32 tooltip. The default |
PSTIP_MODE_PS |
1 |
PsTooltip — owner-drawn, themeable, word-wrapping |
Defaults the control starts with:
| Setting | Default | Unit |
|---|---|---|
| Row height | 22 | Unscaled units |
| Header band height | 24 | Unscaled units |
| Scrollbar width | 12 | Unscaled units |
| Hover time | 250 | Milliseconds |
| Header band visible | false | — |
| Selection mode | single | — |
| Column width / minimum width | 100 / 0 | Pixels |
The fill-column designations passed to PsListTree_SetFillColumn come from PsColumnHeader:
| Constant | Value | Meaning |
|---|---|---|
CCOLHDR_FILL_LAST |
-2 | The last column absorbs leftover width (the default) |
CCOLHDR_FILL_NONE |
-1 | No column absorbs it; the columns end where they end |
PsListTree creates and owns two other controls. You never create either one, and both are
destroyed with the list. It also holds a PsTooltip when you opt the instance into
PSTIP_MODE_PS — see Tooltips; that one is not a child window and is reached with
PsListTree_GetPsTooltipHandle.
| Control | Role | Reached through |
|---|---|---|
| PsColumnHeader | The column model and the interactive header band. It is the single store for column widths, minimums, geometry and the fill designation — the PsListTree_*Column* functions all delegate to it. See PsColumnHeader
|
PsListTree_GetHeader |
| PsVScrollBar | The owner-drawn vertical scrollbar, ranged and auto-hidden by the list | PsListTree_GetScrollBar |
Both handles are escape hatches for appearance and queries. Two things you must not do through
them: do not set the header's WidthChanged callback (use PsListTree_SetColumnResizeCallback),
and do not call PsVScrollBar_SetRange or PsVScrollBar_SetScrollCallback — the list owns the
range and the scroll notification, and overwriting either desynchronises the thumb from the
list.
PsColumnHeader also works standalone; it knows nothing about list boxes.
The demos are in the toolchain's examples/PsControls/PsListTree/. Both draw their glyphs with
Segoe Fluent Icons, installed on Windows 11, and fall 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