LiteGrid Turbo is an ActiveX/COM data grid for VBA, VB6, Microsoft Access and Excel. It installs as a single OCX, creates the same way any ActiveX control does, and works with the data you already have: an array, a Recordset, an Excel Range, or JSON text.
This Cookbook has three parts:
Part I — Learn LiteGrid Turbo teaches the control scenario by scenario: a working grid in the next few minutes, then progressively more of what a real VBA/VB6 business application needs, ending with large datasets, registration-free deployment and diagnostics.
Part II — Complete API Reference documents every method, property and event exhaustively, for lookup once you know what you're looking for.
Part III — Troubleshooting and Limits collects the error patterns and boundaries worth knowing before you ship.
This chapter is the shortest path to a working, interactive grid. It assumes nothing about LiteGrid Turbo except that you can drop an ActiveX control on a form the way you always have.
One design-time step: in the Visual Basic / Access / Excel VBA host, insert the ActiveX control LiteGridTurbo.LiteGridTurboCtrl onto a form or UserForm from the toolbox (right-click the toolbox → Additional Controls / Insert ActiveX Control, depending on host), and name it lgGrid. That is the only thing you need to know about setup for this chapter — installation, registration-free deployment and the full lifecycle template all get their own chapters later.
> Design-time properties persist across save/reopen. Anything you set in the host's property window — or in code before the host saves the form — is written into the control's saved stream and restored when the form is reopened. The persisted set is every browsable, settable appearance/behaviour property on the control: the theme and every colour override; grid lines, alternating-row colour, selection colours and mode; header and row fonts, header height, cell padding, vertical text alignment; row height and row density; compact mode; the render style and freeze-line colour/width; and the visibility/behaviour toggles — ShowFilterButtons, ShowAutoFilterRow, ShowStatusBar, StatusBarMetrics, ShowHeaderMenu, ShowBuiltinContextMenu, ShowAggregateRow, ShowProgressLabel, ShowRowNumbers, ShowTooltips, AutoSortOnHeaderClick, AllowEditing, AllowColumnReorder, AllowColumnResize, AllowRowReorder, MultiSelect, EditAdvanceOnEnter, EnableBeforeCellPaint, MatchEntry, WheelScrollRows, TreeIndent, IconSize, EmptyStateText and the rest. Theme is applied first on load, then your explicit colour overrides on top of it, and RowDensity before RowHeight, so a control reopens looking exactly as you left it. DataMode is the one deliberate exception: it is runtime-only — hidden from the property window (nonbrowsable) and not persisted — because switching mode has store-emptiness preconditions a property sheet cannot express. Set DataMode from code (e.g. in Form_Load), never in the designer.
Everything below shares one object variable, grid, that points at the lgGrid control. Set it once, at the top of your startup event — which event, and which line, depends only on your host:
VBA
' Access: Set grid = Me.lgGrid.Object' Excel UserForm / VB6: Set grid = lgGrid
Access wraps every ActiveX control in its own object model, so Me.lgGrid alone only ever exposes Access's own generic control properties — .Object unwraps the real grid underneath it. Excel and VB6 don't have that wrapper, so the control's own name already is the grid. Once grid is set, every line in this chapter is identical on all three hosts — set it in Form_Load (Access, VB6) or UserForm_Initialize (Excel).
Step 1 — Load your data
LiteGrid Turbo takes a plain two-dimensional VBA array — the same shape you'd build for almost anything else — and shows it as a grid in one call.
Run the form. Five rows are already there, with generic column headers ("Column 1", "Column 2", ...) and a working scrollbar, resize grip and selection — all without you writing any of that.
SetColumnHeader and SetColumnWidth are ordinary display metadata — call them any time after loading, in any order, as many times as you like.
Step 3 — Let the user sort, filter and search
VBA
grid.SortEx 1, 0' sort by Customer, ascending
grid.SetColumnFilter 2, 0, "London"' show only City "Contains" London
grid.ShowFindBar ' open the built-in Ctrl+F search panel
SortEx's second argument is a sort mode (0 = ascending); SetColumnFilter's second argument is a filter mode (0 = Contains). Both take a plain column index and read naturally even before you've memorized the mode numbers — Sorting, Filtering and Finding Data covers every mode. ShowFindBar opens the same interactive search panel a user's own Ctrl+F would, so you can also wire it to a toolbar button.
Step 4 — Respond to what the user does
Events arrive exactly the way they do for any ActiveX control you've hosted before: on the control's own name, in the form's code module — that naming is unaffected by the grid variable above, since it comes from the control itself, not from how you refer to it in code.
Run the form again and click a cell. The Immediate window prints the row, column and the cell's actual typed value (GetCell returns a real Double, Date or Boolean where the column has one — not display text). Click a different cell each time and watch the values change live.
Step 5 — Let the user edit
Editing is already on by default in a freshly loaded Embedded-mode grid. Double-click a cell, type a new value, press Enter — the grid accepts it, and you can read it straight back:
VBA
Debug.Print grid.GetCell(0, 1) ' whatever the user just typed
You now know enough to build a useful LiteGrid Turbo screen
In about twenty lines of code you have loaded real data, given it readable column headers, made it sortable and filterable, added a search panel, and wired a click event — all patterns a VBA/VB6/Access developer already recognizes, because none of it needed a new mental model.
Everything above ran in Embedded mode, LiteGrid Turbo's default and general-purpose way of holding data — the grid owns the rows itself, the same way a Recordset or an array does. You'll stay in Embedded mode through every ordinary, everyday chapter ahead — loading, editing, sorting, formatting, exporting, all of it. The control has two other storage engines for large or host-owned datasets, and they get their own chapters — Choosing Embedded, Turbo or Virtual Mode — once large-data requirements give you a reason to need them.
Next:Your First Grid turns this into a real, production-shaped form: a proper lifecycle template, license activation, and the handful of details that only matter once code leaves the sandbox.
2. Your First Grid
Chapter 1 got a grid working inside a single event handler, using a plain Object reference — grid, set once per host's own rule: .Object on Access, direct by name on Excel and VB6. This chapter shows the shape a real form actually uses: the same reference, upgraded to a real type for IntelliSense, plus the error handling and cleanup a production form needs around it.
Declare the grid with a real type
Adding a reference to LiteGridTurboLib (Tools → References) and declaring a typed module-level variable buys you full IntelliSense on every member — every method and property name, every parameter, right there as you type:
VBA
Private m_grid As LiteGridTurboLib.LiteGridTurboCtrl
If you would rather not take the reference, Private m_grid As Object works identically, just without IntelliSense — everything in this Cookbook is written to work either way. This is the same variable Chapter 1 called grid; only the name and the declared type are different here.
Access still needs .Object
Access wraps every hosted ActiveX control in its own object model, so Me.lgGrid alone only ever exposes Access's own generic control properties — never the grid's members. Peel that wrapper off once, in Form_Load, with .Object, exactly as Chapter 1 did:
VBA
Option Compare Database
OptionExplicitPrivate m_grid As LiteGridTurboLib.LiteGridTurboCtrl
PrivateSub Form_Load()
OnErrorGoTo ErrH
Set m_grid = Me.lgGrid.Object' Access-specific: unwraps the real controlDim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = 1
data(1, 0) = "Beta": data(1, 1) = 2
m_grid.LoadFromArray data
m_grid.SetColumnHeader 0, "Name"
m_grid.SetColumnHeader 1, "Qty"ExitSub
ErrH:
MsgBox "Form_Load: " & Err.Number & " " & Err.Description, vbCritical
EndSubPrivateSub lgGrid_ItemClick(ByVal rowIndex AsLong, ByVal columnIndex AsLong)
' respond to clicks here — same pattern as Chapter 1EndSubPrivateSub Form_Close()
OnErrorResumeNextSet m_grid = Nothing' release the reference; Access destroys the controlEndSub
Excel UserForms and VB6 forms don't have this wrapper — lgGrid.LoadFromArray (or a typed m_grid set directly from the control) works without the .Object step. Building Real VBA/VB6 Grid Screens has the complete, copy-pasteable lifecycle template for both Access and Excel, including a shared license module — worth reading before you build your first production form. For now, the shape above is everything you need to keep going through the rest of Part I.
A grid with no license key still works
Every method in this Cookbook — loading, editing, sorting, filtering, exporting, all of it — works during evaluation with no activation call at all. Once an evaluation has expired, the grid is covered, mouse and keyboard interaction with it stops, and the file/clipboard operations listed in Chapter 20 are marked or refused; everything else, including ActivateLicense itself, keeps working, and applying a key restores the same control immediately. When you're ready to move past evaluation, one call activates the control for the life of the host process:
Licensing and Trial covers activation, trial state and where the trial marker is stored — there is no reason to think about it further until you actually have a key.
Why there is no "ready" event
Developers coming from browser or WebView-hosted controls sometimes look for a ready/loaded event to wait for before calling the grid. LiteGrid Turbo doesn't have one, because it doesn't need one: the control is fully usable as soon as it exists, so Form_Load (or UserForm_Initialize) can load data, set headers and configure behavior in a straight line, with nothing to wait for.
Next: if you're arriving from MSFlexGrid, ListView, Access continuous forms, or a UserForm list you built by hand, Where Are You Coming From? maps what you already know onto LiteGrid Turbo's API before you go further. Otherwise, skip straight to Bring in Data You Already Have.
3. Where Are You Coming From?
If you've already built VBA/VB6 grid-like screens with something else, most of what you know still applies — you're mainly trading hand-written code for a method call. This section is a set of quick mappings, not a feature-for-feature comparison; it only claims what the rest of this Cookbook documents and demonstrates elsewhere.
From MSFlexGrid / MSHFlexGrid
If you've been populating a FlexGrid cell by cell in a loop —
VBA
For r = 0To rs.RecordCount - 1
MSFlexGrid1.TextMatrix(r, 0) = rs.Fields(0).Value
' ... one line per column ...Next r
— LoadFromRecordset or LoadFromArray replace that loop entirely: hand the grid the Recordset or array once, and every row and column is populated in one call. See Bring in Data You Already Have. Sorting a FlexGrid by a clicked column header is typically code you wrote yourself; AutoSortOnHeaderClick and SortEx do it without a loop — see Sorting, Filtering and Finding Data. Fixed rows/columns become SetFrozenRows/SetFrozenColumns.
From ListView
A report-style ListView populated through ListItems.Add / .SubItems maps onto LoadFromArray the same way a FlexGrid loop does — one call instead of an item-by-item loop. Column sorting, filtering and in-place editing, which a ListView doesn't provide natively and typically means owner-draw or subclassing to add, are built into LiteGrid Turbo from the start: see Sorting, Filtering and Finding Data and Editing and Validating Data. Where a ListView shows icons through an ImageList, LiteGrid Turbo's per-cell and per-column icon system is covered in Formatting and Presenting Business Data.
From Access continuous forms
A continuous form gives you full control over each row's layout at the cost of writing that layout yourself, plus the performance considerations that come with many bound controls repeated per row. LiteGrid Turbo trades per-row layout control for a grid that already knows how to scroll, sort, filter, resize and edit a large row count without you designing each row. LoadFromRecordset accepts the same DAO or ADO Recordset a continuous form would otherwise be bound to. If your continuous form exists mainly to let users browse and edit rows quickly, Editing and Validating Data and Selection and User Interaction cover the equivalent LiteGrid Turbo patterns.
From an Excel worksheet used as a pseudo-grid
Using a worksheet range as a browsable, sortable list inside a UserForm-driven application works, but it means fighting Excel's own selection, editing and recalculation behavior to make the range act like a UI control. LoadFromRange takes an actual Range object directly — including one built from Sheet1.Range("A1:F5000") — and loads it the same way LoadFromArray would, without needing the worksheet to be visible or the user to interact with Excel itself. See Bring in Data You Already Have.
From a hand-built UserForm list (ListBox/multi-TextBox rows)
A UserForm assembled from a ListBox plus a row of TextBoxes for "editable columns" usually exists because no single built-in control did everything needed: browsing, editing and formatting together. That combination — browse, sort, filter, edit, format — is the ordinary Embedded-mode grid described across Working With Cells, Rows and Columns through Formatting and Presenting Business Data.
Most of the time, the data you want in the grid already exists somewhere else: an array you built in a loop, a range on an Excel worksheet, a Recordset from a query, or a block of JSON that came back from a web call. This chapter covers the four one-call loaders LiteGrid Turbo gives you for exactly that -- LoadFromArray, LoadFromRange, LoadFromRecordset, and LoadFromJson -- plus AutoDetectColumns, which turns a plain data load into something with real headers and real column types, and QuickBind, a single call that combines loading with the interactive features most grids end up needing anyway.
All of these work in the grid's default Embedded mode, where the grid stores every row itself. Two of them -- LoadFromArray and LoadFromRecordset -- also work if the grid has been switched into Turbo column-store mode for a large dataset; the other loaders in this chapter are Embedded-only, and the chapter notes each one as it comes up. None of them work in Virtual mode, where your application owns the data and feeds the grid through a different protocol -- that's a different way of working with LiteGrid Turbo and outside the scope of this chapter.
LoadFromArray
Reach for LoadFromArray whenever you already have a two-dimensional Variant array in memory -- built by hand, assembled in a loop, or returned from some other part of your code -- and just want it in the grid.
The array's first dimension supplies rows, its second dimension supplies columns, and there is no header row: every row you pass in becomes a data row. The array can be 0-based, 1-based, or start at any other index -- LiteGrid Turbo reads whatever bounds the array actually has. Columns are created automatically and named "Column 1", "Column 2", and so on, in order; if you want real header text you either call SetColumnHeader afterward or use AutoDetectColumns to pull it from the data.
LoadFromArray replaces whatever the grid already held, and it works in both Embedded mode and Turbo column-store mode -- whichever DataMode currently selects. Two things are worth knowing before you rely on it in production code: the array must be exactly two-dimensional, so a one-dimensional array (including the result of VBA's Array() function) is rejected with a runtime error and the grid's existing data is left untouched; and if you've placed an object reference -- a Range, another automation object -- directly into a cell, convert it to a plain value before loading. Turbo column-store mode rejects an object cell outright, but Embedded mode copies it in without checking, and that combination is not supported -- it can misbehave later. If you rely on the DataLoaded event, note that it doesn't fire after a Turbo column-store load; check GetRowCount instead.
See LoadFromArray, DataMode, Clear, and SetColumnHeader in Part II for the full set of size limits and edge cases.
LoadFromRange
Reach for LoadFromRange when your source is an Excel Range -- or anything else exposing a Value2 or Value property -- and you would otherwise just write rng.Value2 yourself and pass the result to LoadFromArray.
That is, in fact, exactly what LoadFromRange does for you: it reads Value2 if the object exposes it (raw numbers and date serials, not locale-formatted text), falls back to Value if it doesn't, and hands the result straight to LoadFromArray. So the two share the same column-creation behavior and the same errors for a bad array. If you already have data sitting on a worksheet, this is the whole thing:
The one gotcha every developer trips over eventually: a single-cell Range's Value2 is a plain scalar, not an array, so calling LoadFromRange on a one-cell range fails with the same "requires a 2-dimensional array" error LoadFromArray gives for a non-array argument. If you need one cell's value, read it directly with rng.Value2 rather than routing it through LoadFromRange. One more limit to know: LoadFromRange only works in Embedded mode. If you need a range's data in Turbo column-store mode, read rng.Value2 into your own array and call LoadFromArray with that.
See LoadFromRange in Part II for the exact error text each failure case raises.
LoadFromRecordset
Reach for LoadFromRecordset when your source is a live DAO or ADO Recordset -- a query result you're about to display, most typically. It walks the recordset field by field, row by row (using MoveFirst/MoveNext internally), and loads the result the way LoadFromArray would, using each field's Name as the column header for you -- no manual header setup required.
VBA
Dim rs AsObjectSet rs = CurrentDb.OpenRecordset( _
"SELECT CustomerID, CustomerName, City, Balance FROM Customers")
grid.LoadFromRecordset rs
Debug.Print grid.GetColumnHeader(0) ' -> "CustomerID"Debug.Print grid.GetRowCount()
rs.Close
It accepts either a DAO or an ADO Recordset -- really, anything exposing a compatible Fields collection plus MoveFirst/MoveNext -- and, like LoadFromArray, it targets whichever mode DataMode currently selects. Unlike LoadFromArray, it raises DataLoaded reliably in both modes. A few smaller points worth knowing: existing grid data is cleared first, the same as calling Clear; a field that can't be read still gets a placeholder column so later columns don't shift out of position; and passing Nothing raises a runtime error rather than silently loading zero rows.
See LoadFromRecordset, Clear, and DataMode in Part II for the complete field-mapping rules, including default column widths.
One thing to know now rather than later: because it clears first, a second LoadFromRecordset against the same grid throws away the sort, filters, frozen panes, scroll position and selection your user had set up. That is the right behaviour for loading, and the wrong behaviour for refreshing. When what you actually want is "same query, newer rows, don't disturb the user", use RefreshFromRecordset instead — it wraps this loader and puts the view back afterwards, re-finding the selected records by a business key column so they survive a reordered result set. It is a reload, not a live binding: nothing updates on its own. See Chapter 12, "Reloading a Recordset without losing the view".
AutoDetectColumns
LoadFromArray on its own gives you generic "Column 1", "Column 2" headers and no particular column typing -- every cell is whatever VBA type you happened to put in it, but the grid hasn't been told to treat a column as, say, a Currency column for sorting and formatting purposes. AutoDetectColumns, called right after a load, fixes both at once: it samples up to the first 100 rows of each column, and where more than 80% of the non-blank values agree on a type (API constant), it sets that column to Checkbox, Currency, Date, Integer, or Double -- right-aligning numeric columns as it goes.
Pass True for firstRowIsHeader and row 0 is pulled out of the data and used as header text instead, the way it is above; pass False and every loaded row stays a data row, with only type detection running. Type detection looks purely at the actual VBA type of each loaded value (Long, Double, Date, Boolean, Currency), so it has nothing to work with if everything came in as text -- which matters directly for the next loader, LoadFromJson, since JSON always loads as text. Note also that the 80% threshold (API constant) means a mostly-numeric column with a handful of stray text values (like "N/A") still gets typed as numeric; the stray values just display as-is. And it needs data to already be loaded -- calling it on an empty grid raises an error telling you to load first.
See AutoDetectColumns, SetColumnType, and SetColumnHeader in Part II for the full detection rules and every supported type.
LoadFromJson
Reach for LoadFromJson when your source is a JSON array of flat objects -- typically a REST API response -- and you'd rather not write your own parser to turn it into an array first.
It expects a string shaped like [{"Col1":"val1","Col2":"val2"}, ...]: the keys of the first object become the column headers, and every object in the array becomes a row.
VBA
Dim json AsString
json = "[{""SKU"":""W-100"",""UnitPrice"":9.5},{""SKU"":""G-200"",""UnitPrice"":12.25}]"
grid.LoadFromJson json
Debug.Print grid.GetColumnHeader(1) ' -> "UnitPrice"Debug.Print grid.GetCellValue(0, 1) ' -> "9.5" (stored as text, not the number 9.5)' Numbers arrived as text -- set the real type before sorting or formatting numerically.
grid.SetColumnType 1, 3' 3 = Double
That last line matters: LoadFromJson stores every value as text, regardless of whether it looked like a number, a date, a boolean, or was JSON null (which loads as an empty cell) -- it does no type inference of its own. Because everything is already text by the time it lands in the grid, AutoDetectColumns can't help afterward either; it needs real typed values to sample. If you need numeric sorting or formatting, call SetColumnType yourself, as above. Two other limits worth knowing: only the first object's keys become columns, so a later object with extra keys silently drops them, and a later object missing a key just leaves that cell blank; and the parser expects well-formed, flat objects, with no escaped quotes and no nested objects or arrays as cell values. LoadFromJson also only works in Embedded mode; it refuses in Turbo column-store or Virtual mode.
See LoadFromJson and ExportToJson in Part II for the full parsing rules.
Choosing a loader
Which of these four to reach for comes down to where the data already is:
VBA
PublicSub Recipe_LoadFromWherever(grid AsObject, source AsString)
SelectCase source
Case"array"Dim data(0To2, 0To1) AsVariant
data(0, 0) = "Widget": data(0, 1) = 12.5
data(1, 0) = "Gadget": data(1, 1) = 7.25
data(2, 0) = "Gizmo": data(2, 1) = 19.99
grid.LoadFromArray data
Case"database"Dim rs AsObjectSet rs = CurrentDb.OpenRecordset("SELECT * FROM Products")
grid.LoadFromRecordset rs
rs.Close
Case"web"Dim json AsString
json = "[{""Name"":""Widget"",""Price"":12.5}]"
grid.LoadFromJson json
' Price came in as a number in the JSON but is stored as text --' set the real type if you need to sort or format it numerically.
grid.SetColumnType 1, 3' 3 = DoubleEndSelectEndSub
A array you've already built or computed goes through LoadFromArray -- it's the direct path, and every other loader in this chapter ultimately builds one and hands it off internally. A live database query goes through LoadFromRecordset, which gets you column headers for free from the schema. A web response goes through LoadFromJson, with the reminder that everything comes back as text. An Excel range sits slightly apart from this decision: reach for LoadFromRange only when you genuinely have a Range object in hand and don't want to read its values yourself first.
The one distinction worth keeping straight across all of them: only LoadFromArray and LoadFromRecordset can target Turbo column-store mode for a large dataset. LoadFromRange, AutoDetectColumns, and LoadFromJson are all Embedded-only. If you're working with a dataset large enough to need Turbo column-store mode, plan on getting your data into an array or a Recordset first.
If a single load call genuinely isn't a good fit -- rows arriving a page at a time from an API, or data that already lives in several separate typed arrays rather than one combined array -- Part II documents two multi-call alternatives: a streaming pair for appending batches without a repaint after each one, and a column-array session for loading one typed column at a time. Both are worth knowing about, but for a one-shot load with the data already in hand, the loaders above are simpler and do the job in a single call.
See BeginStreaming/AppendRows/EndStreaming and BeginLoad/LoadColumn/EndLoad/CancelLoad/IsLoading in Part II for the full multi-call loading options.
The one-call shortcut: QuickBind
Everything above gets data into the grid. Most of the time you also want the grid to behave like a finished piece of UI right away -- sortable columns, a filter row, sensible column widths -- and wiring that up call by call after every load gets repetitive. QuickBind does both steps at once.
That one call loads the array, detects each column's type, auto-fits the column widths to their content, and turns on filter buttons, header-click sorting, the built-in context menu, the header menu, and the status bar. For a grid you're about to hand a user to browse and filter, this is usually a faster starting point than calling LoadFromArray and then switching each of those features on individually.
The second parameter is the same firstRowIsHeader idea you saw with AutoDetectColumns: pass True, as above, and the first row of the array supplies the column headers instead of being loaded as a data row, so it's excluded from the row count.
The one restriction to plan around: QuickBind only works in Embedded mode -- it refuses in both Virtual mode and Turbo column-store mode. If the grid might already be in one of those modes, switch DataMode back to Embedded (0) before calling QuickBind. And because it turns on several display options at once, treat the defaults as a starting point: if you don't want all of filter buttons, the header menu, the context menu, and the status bar, turn the ones you don't need back off afterward.
See QuickBind, SetMode, and GetColumnCount in Part II for the full parameter and mode-support details.
The one-call shortcut for a Recordset: QuickBindRecordset
QuickBind takes an array. If your source is a live DAO or ADO Recordset instead, QuickBindRecordset is its sibling -- the same "load it and make it usable" idea, applied to LoadFromRecordset instead of LoadFromArray:
VBA
grid.QuickBindRecordset rs
That one call loads the Recordset (headers come from each field's own Name, exactly as LoadFromRecordset already provides), detects each column's type -- the one thing a plain LoadFromRecordset doesn't already give you for free, since a Currency field otherwise displays as a bare decimal instead of a formatted currency string -- auto-fits column widths, and turns on filter buttons, header-click sorting, the built-in context menu, the header menu, and the status bar. An empty Recordset (real fields, zero rows) is a successful call: the columns are created and every display option that still makes sense is applied; only type detection is skipped, since there's no data yet to detect a type from.
The second, optional argument is an LGQuickBindOptions bitmask, for when you want less than everything:
VBA
grid.QuickBindRecordset rs, lgQuickAutoDetect Or lgQuickAutoFit ' just the two load-shaping steps, no UI toggles
Omit it entirely, as in the first example, and you get lgQuickDefault -- every step, matching what QuickBind itself already does unconditionally for an array. Pass 0 for "load only": the data lands with no type detection and no display options touched, equivalent to calling LoadFromRecordset directly. That equivalence is an Embedded-mode one. In Turbo column-store mode, QuickBindRecordset refuses by name before any option is read -- option 0 included -- so the two are not interchangeable there; LoadFromRecordset is the Recordset-loading call that works in Turbo column-store mode. See QuickBindRecordset and LGQuickBindOptions in Part II for every named bit.
A primitive call after QuickBindRecordset overrides its defaults exactly the way it does after QuickBind:
VBA
grid.QuickBindRecordset rs
grid.SetColumnFormat 3, "$#,##0.00"' wins over the locale-default currency format QuickBindRecordset applied
grid.ShowFilterButtons = False' turn off just the one default you don't want
And every existing primitive remains available if you'd rather assemble the pieces yourself:
Like QuickBind, QuickBindRecordset only works in Embedded mode -- it refuses in both Virtual mode and Turbo column-store mode, with no silent fallback. SetMode is deliberately not folded into either call; chain it afterward when you also want a named interaction preset:
The one-call shortcut for a folder: LoadDirectoryTree
Sometimes the data you already have is a folder on disk. Listing one by hand is a familiar VBA chore -- Dir() in a loop, recursion for subfolders, a row per entry, then SetTreeParent on each row to build the hierarchy, then icons, then sizes and dates formatted by hand. It is roughly fifty lines before anything appears on screen, and every one of them is code you have written before.
LoadDirectoryTree is that entire chore in one call:
VBA
grid.LoadDirectoryTree "C:\Projects"
That populates the grid with the folder's contents as a real tree: columns created and named for you, the system's own file-type icons attached, and every subfolder shown collapsed with an expand arrow. Nothing below the top level is read from disk until the user actually expands a node -- which is the part that is genuinely hard to write by hand, and the reason this is the member to reach for rather than the eager alternative.
Prefer it to DirectoryList.DirectoryList lists a folder too, and it reads the whole tree up front. On a deep folder that difference is not subtle: LoadDirectoryTree brackets each level in a single batched rebuild, while DirectoryList rebuilds the tree projection once per row as it inserts. Use DirectoryList when you deliberately want everything loaded immediately and you know the folder is small; use LoadDirectoryTree otherwise. DirectoryListOneLevel sits between them, listing exactly one level with no recursion at all.
Expansion is observable, so you can veto or instrument it:
VBA
' Registered (control named lgGrid on the form):PrivateSub lgGrid_TreeExpanding(ByVal displayRow AsLong, ByVal storageRow AsLong, _
ByVal depth AsLong, Cancel AsBoolean)
If depth >= 3Then Cancel = True' stop descending past three levelsEndSub
TreeExpanding fires before the folder is read, so cancelling prevents the disk access entirely; the node stays collapsed and stays retryable.
Note what the event gives you: depth, not a path. If you need the item's path -- to open it, to filter by name, to pass it on -- ask for the lgFolderColFullPath column when you load, and read it with GetCellValue. You can hide that column with SetColumnVisible if you want the path available to your code without showing it to the user.
What this is not: it is a folder listing, not a file manager. There are no thumbnails, no shell context menus and no drag-and-drop; hidden and system entries are never listed; recursion is capped; and the listing is a snapshot -- nothing watches the filesystem for changes, so refreshing means loading again. It is Embedded-mode only, and refuses by name in Turbo column-store and Virtual mode. See LoadDirectoryTree, DirectoryList and DirectoryListOneLevel in Part II for the full column layout, the icon behaviour and every limit.
Bring in Data You Already Have got a dataset onto the grid in one call. This chapter is about what you do next: finding out how big the grid actually is, reading and writing individual cells, adding and removing rows one at a time, and shaping the columns those rows sit in. Everything here works against a 0-based index space — row 0 is the first row on screen, column 0 is the first column — and that index space is shared by every member in this chapter, so a row index you get from one call is safe to hand directly to another.
The examples below build up one small table and keep reusing it: a Customers grid with four columns, CustomerID, CustomerName, City and Balance. Wherever an example doesn't repeat the setup, assume this is still on the grid:
The grid's overall shape: Clear, GetRowCount, GetColumnCount
Before you touch a single cell, three members answer the questions you'll ask over and over: how many rows are there, how many columns, and how do I get back to nothing.
GetRowCount and GetColumnCount report the grid's current size:
Both read from whichever engine is currently active, so they report the real total in Embedded, Virtual and Turbo column-store mode alike — there's no mode where one of them silently reports zero. Two things worth knowing: GetRowCount reflects the current view, after any filter or sort, so it's not necessarily the number of rows you originally loaded; and while a load is still in progress, it reports only the rows loaded so far, not the eventual total.
Clear empties the grid back to zero rows and zero columns — the standard way to reset before loading a new dataset:
Clear also resets sort order, filters, formatting, frozen panes and the current selection, and it cancels an in-place cell edit in progress rather than committing it. One easy-to-miss gotcha: column groups created with AddColumnGroup are not removed by Clear, even though every column they group is gone — call ClearColumnGroups separately if you need a truly blank grid. In Turbo column-store mode, Clear empties the Turbo table but leaves Turbo column-store selected as the active mode, so your next load call doesn't need to set DataMode again; in Virtual mode Clear is refused outright, because the grid doesn't own the rows there in the first place.
Reading and writing cells: GetCell and SetCell
GetCell and SetCell are the pair to reach for by default. They read and write a cell's value as a typed Variant — a Double for a numeric column, Currency for a Currency column, Date, Boolean, and so on — which is what you want whenever the result needs to be compared, calculated with, or stored back with its type intact:
VBA
Dim bal AsVariant
bal = grid.GetCell(0, 3)
Debug.Print VarType(bal) ' -> 5 (vbDouble)Debug.Print bal ' -> 1250.5
VBA
grid.SetCell 1, 3, CDbl(75.25) ' Fabrikam pays down part of its balanceDebug.Print grid.GetCell(1, 3) ' -> 75.25Debug.Print grid.GetCellValue(1, 3) ' -> "75.25"
In Embedded mode, SetCell accepts any VARTYPE as-is — the storage is loosely typed. In a Turbo column-store grid it's strict: the incoming value's type must match the column's own declared type exactly, and a mismatch — even a Long written into a Double column — is refused by name rather than silently converted. See SetCell in Part II for the exact type-matching rules, including how Null/Empty and COM object references are handled. Neither GetCell nor SetCell fires a change event — only the interactive edit flow does that.
Both are refused in Virtual mode for the write side, and GetCell has a Virtual-mode quirk worth knowing before you rely on it there: it never raises an error for a row the host hasn't supplied yet, but it also never returns that row's real value — it comes back as an empty Variant either way, supplied or not. GetCellValue (next section) is the one that actually checks whether a Virtual-mode row is resident and raises an error if it isn't, so use GetCellValue for Virtual-mode reads rather than GetCell.
See SetCellRange in Part II for writing a rectangular block of typed values in one call (Embedded mode only), and SetCellColor/SetCellFontStyle for per-cell appearance.
Text-only reads and writes: GetCellValue and SetCellValue
GetCellValue and SetCellValue look like a shortcut version of GetCell/SetCell, but they deal in text only, always — that's the whole point of them, and it's also where the gotcha lives.
GetCellValue returns exactly what the cell displays on screen, as a String, no matter what type the column holds:
VBA
Debug.Print grid.GetCellValue(0, 1) ' -> "Contoso Ltd"Debug.Print grid.GetCellValue(0, 3) ' -> "1250.5" (always text, even though Balance is numeric)
That's fine for a label or a text column. Where it gets interesting is SetCellValue, which always stores its argument as text too — including on a numeric column:
VBA
grid.SetCellValue 0, 1, "Contoso Ltd."' fine -- CustomerName is already a String column' The trap: calling SetCellValue on the numeric Balance column
grid.SetCellValue 2, 3, "999.99"Debug.Print grid.GetCellValue(2, 3) ' -> "999.99", looks completely normal on screenDebug.Print VarType(grid.GetCell(2, 3)) ' -> 8 (vbString) -- this ONE cell is now text,' while every other row in the Balance column is still vbDouble
Here's why this matters more than it looks like it should: in Embedded mode, SetCellValue doesn't just format the value for display — it silently changes that one cell's stored type to String. Nothing warns you. The cell still renders correctly, so a person looking at the grid sees nothing wrong. But the next time your code loops over that column expecting every cell to be a Double — for a total, a comparison, an export — this one cell is a String instead, and only this one cell. It's an easy mistake to make by reaching for SetCellValue out of habit (it takes a plain string, so it feels like the safe, simple choice) when what you actually needed was SetCell with a typed value. The rule of thumb: use SetCell whenever the value must remain a genuine number, date or boolean for anything downstream; reserve SetCellValue for columns that are text already, or for cases where you genuinely only need the display string to change.
Turbo column-store mode closes this loophole rather than inheriting it: SetCellValue there only accepts a value for a column whose Turbo column-store type is String, and calling it on any other column raises a runtime error naming SetCell as the correct call instead — the cell is left unchanged, not partially corrupted. SetCellValue is refused outright in Virtual mode. Like SetCell, neither GetCellValue nor SetCellValue fires a change event; those only come from the interactive edit flow (double-click, F2, Tab).
One more small gotcha shared by the read side: a Null cell reads back from GetCellValue as an empty string, indistinguishable from a genuinely blank cell — and in Virtual mode, a row that isn't currently held in the grid's cache raises a runtime error rather than answering with an empty string, so don't treat that error as "the cell is blank."
Adding, inserting and deleting rows
Rows enter and leave the grid through four members, all working in the same 0-based display-index space as GetCell and GetCellValue.
AddRow appends a new row at the end, optionally with initial values:
The values argument is a single 1-D array, one entry per column, in column order — not the 2-D array LoadFromArray expects. Extra entries are ignored and a short array simply leaves the remaining columns blank; neither is an error. Unlike adding a new column to a grid that already has rows (see AddColumn below), adding a new row never leaves any existing column unable to hold a real value for it — every column gets real, if blank, storage. AddRow is refused in Virtual mode. In Turbo column-store mode it's supported, but every supplied value is checked against its column's already-established type before anything is added: if one value doesn't match, the whole call is refused and no row is added at all. Because VBA's own literal typing doesn't always match what a column inferred from an earlier LoadFromArray call, it's worth being explicit — CDbl(0), CLng(4), CStr(...) — rather than relying on a bare numeric literal to happen to be the right VARTYPE.
InsertRow puts a new row at a specific display position, shifting everything at or after it down by one:
rowIndex may equal the current row count, which behaves exactly like AddRow. It follows the same 1-D array rules and the same per-column Turbo type-checking as AddRow. One gotcha worth remembering: rowIndex is a display position, not a storage position — if a sort is currently active in Embedded mode, the row you just inserted is immediately re-sorted into its correct place rather than staying at the index you asked for. An out-of-range index raises a runtime error. Like AddRow, InsertRow is refused in Virtual mode and supported in Turbo column-store mode.
DeleteRow removes one row by its current display position, and later rows shift up to fill the gap:
VBA
grid.DeleteRow 2' removes Northwind Traders -- the row at index 2 in the base Customers gridDebug.Print grid.GetRowCount
DeleteRow fires the BeforeRowDelete event before the row is actually removed; a handler that sets Cancel to True leaves the grid completely unchanged and DeleteRow returns without error, so don't assume the row is gone without checking whether your own handler cancelled it. An out-of-range index raises a runtime error. Refused in Virtual mode; supported in Turbo column-store mode (see DeleteRow in Part II for how the removal is implemented differently there).
DeleteSelectedRows removes every currently selected row in one call:
VBA
grid.SelectRow 0
grid.SelectRow 2
grid.DeleteSelectedRows ' removes Contoso Ltd and Northwind Traders; safe no-op if nothing was selected
It fires BeforeRowDelete once per row actually deleted, with the same cancel semantics as DeleteRow — a cancelled row is skipped and the rest continue to be processed. DeleteSelectedRows is refused in Virtual mode. If you combine this with an active filter and a Turbo column-store grid, check DeleteSelectedRows in Part II first — a selected-but-currently-hidden row is not treated identically across all three modes.
See Sorting, Filtering and Finding Data for SelectRow and the rest of the selection API, and MoveRow in Part II for reordering an existing row programmatically (Embedded mode only).
Building out columns
Where rows are cheap to add one at a time, columns are set up more deliberately — usually once, near the top of a routine, before any rows go in.
AddColumn appends a new column with a header and a width:
There's no InsertColumn — columns can only be appended, so build them up in the order you want them to appear on screen. A width of zero or less silently becomes 100; unlike SetColumnWidth, AddColumn does not clamp an oversized positive width, so an unreasonable value is stored exactly as given. The gotcha to plan around: adding a column to a grid that already has rows loaded (Embedded mode) creates a column the existing rows can't really hold values in — GetCellValue on it silently returns an empty string for every pre-existing row (indistinguishable from a genuinely blank cell), while SetCellValue on it is refused outright with "Column index is out of range," even though GetColumnCount already reports the new column. The reliable way to give already-loaded rows a working extra column is to include it, with a placeholder value, directly in the array you pass to LoadFromArray from the start, rather than calling AddColumn afterward. AddColumn is refused in both Virtual and Turbo column-store mode — in Turbo column-store, columns come from what was loaded, not from calls made afterward.
SetColumnHeader and GetColumnHeader change and read a column's header text:
A column created by a loader without an explicit header of its own defaults to "Column N" (1-based, so column index 0 defaults to "Column 1"). Both work in every mode, since header text is metadata the grid owns directly rather than host data. One gotcha worth flagging early: GetColumnLayout (covered alongside column groups and saved layouts later in this Part) does not include header text, so a saved-and-restored layout will not bring back a header rename — only width, visibility, alignment and type travel with it.
SetColumnWidth and GetColumnWidth change and read a column's pixel width:
SetColumnWidth clamps its value into the range 1 to 10,000 pixels rather than rejecting an out-of-range value — only an out-of-range columnIndex raises an error. Both members are pure display metadata and work identically across all three data modes. See AutoFitColumns in Part II for resizing every visible column to fit its content in one call, AddColumn in Part II for exactly how its own width parameter is clamped, and SetColumnVisible/GetColumnLayout for hiding a column and saving layouts.
Three more column members are worth knowing exist at this point, even though they're covered in full where formatting is the subject: SetColumnType sets a data-type hint (String, Double, Currency, and a handful of others — see SetColumnType in Part II for the full list) that drives default formatting and alignment; SetColumnFormat stores a display-format string like "#,##0.00"; and SetColumnAlignment sets left/center/right text alignment directly. SetColumnType and SetColumnFormat are Embedded-mode-only — refused in both Virtual and Turbo column-store mode, since Turbo column-store infers its column types from the loaded data itself rather than accepting them after the fact. SetColumnAlignment is different: it's pure presentation state, like column width, so it's supported in all three modes. A quick taste:
VBA
grid.SetColumnType 3, 8' Balance becomes Currency
grid.SetColumnFormat 3, "#,##0.00"Debug.Print grid.GetColumnAlignment(3) ' -> 2 (right), set automatically by the Currency type
Putting it together: onboarding a new customer
A realistic version of what most VBA code in this chapter ends up doing: a user picks "New Customer" on a form, your code appends the row, then reads back and adjusts it as the user works, using the typed pair rather than the text pair so the Balance column never drifts into text:
VBA
Sub OnboardNewCustomer(grid AsObject)
Dim newRow AsLong' The grid already holds the four-column Customers table from the top of this chapter
grid.AddRow Array(6, "Wide World Importers", "Chicago", CDbl(0))
newRow = grid.GetRowCount - 1' the row we just appended is the last one' The user enters an opening balance on the form; write it typed, not as text
grid.SetCell newRow, 3, CDbl(500)
' Confirm what actually landed, both waysDebug.Print grid.GetCell(newRow, 3) ' -> 500 (still a Double, safe to sum later)Debug.Print grid.GetCellValue(newRow, 3) ' -> "500" (same value, as text, for display)' The onboarding was entered by mistake -- remove itIf MsgBox("Remove this customer?", vbYesNo) = vbYes Then
grid.DeleteRow newRow
EndIfEndSub
Nothing here is a new member — it's the same AddRow, GetRowCount, SetCell, GetCell, GetCellValue and DeleteRow already covered above, chained the way a real form handler would chain them. The one habit worth taking away from this chapter: reach for SetCell/GetCell by default for anything that will later be summed, compared or sorted, and save SetCellValue/GetCellValue for columns that are genuinely text, or for the rare case where you only need to change what's on screen.
Building on the cell- and range-level operations from Working With Cells, Rows and Columns, this chapter turns to the row-level view: reordering what the user sees (sorting), narrowing it down (filtering), and letting the user locate a row interactively (the built-in find panel). All three operate on top of whatever data is already loaded — none of them change a single cell value, they only change which rows are visible and in what order.
The examples below use small Orders and Inventory tables loaded with LoadFromArray, the way most of this cookbook's fixtures are built. Row and column indexes are zero-based throughout, as everywhere else in this control.
Sorting rows by one or more columns
Sort reorders every row by a single column. It is the plain version — ascending or descending, nothing more — and is what you reach for when a user clicks a column header and just wants the obvious behavior.
sortOrder is not range-checked — 0 means ascending and literally any other value (including 2, -1, 99) means descending. Text columns always sort case-insensitively; there is no case-sensitive option anywhere in this control. See Sort in Part II for the same-column re-sort optimization Embedded grids apply and other engine-specific performance notes.
SortEx adds three more modes on top of Sort's plain ascending/descending, most usefully a natural sort that compares embedded numbers numerically instead of as text.
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Bin 10": data(1, 0) = "Bin 2": data(2, 0) = "Bin 1"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Location"
grid.SortEx 0, 0' plain ascending -- text orderDebug.Print grid.GetCellValue(1, 0) ' -> Bin 10 (lexicographically before "Bin 2")
grid.SortEx 0, 4' natural -- numbers compare numericallyDebug.Print grid.GetCellValue(1, 0) ' -> Bin 2 (2 sorts before 10 numerically)
sortMode is 0 for Ascending or 1 for Descending, same as Sort, plus two case-insensitive variants (2, 3) that behave identically since text sorting is already always case-insensitive, and 4 for Natural, shown above. Mode 4 is always ascending — there is no natural-descending mode on SortEx itself; use SortMulti with a natdesc token for that. A sortMode outside 0–4 raises a runtime error. Writing a SortRequested handler for a Virtual-mode grid instead? SortEx in Part II documents exactly what value your handler receives for each mode.
ClearSort removes whatever Sort, SortEx or SortMulti last established and restores the order rows were originally inserted in.
It restores insertion order specifically, not some other notion of "unsorted" if rows were reordered by other means since loading. Clear() and LoadFromArray already reset the active sort to none on their own, so ClearSort is really only needed when you want to drop a sort while the same data stays loaded.
SortMulti sorts by up to three columns at once from a single spec string, using each earlier column to break ties in the one before it.
The spec is a comma-separated list of column:direction tokens, up to three; direction is asc (also the fallback for anything unrecognized), desc, nat or natdesc. A bad token is simply skipped rather than stopping the parse, but if every token in the string is invalid, SortMulti raises a runtime error rather than silently doing nothing. See SortMulti in Part II for exactly how tokens beyond the third are counted and skipped.
AutoSortOnHeaderClick lets a header click sort the grid with no event handler at all — useful when your host has no connection-point event support.
VBA
grid.AutoSortOnHeaderClick = False' disable the built-in click-to-sort behavior' Handle ColumnClick yourself instead (registered activation, registered mode):' Private Sub LiteGrid1_ColumnClick(ByVal columnIndex As Long)' LiteGrid1.Sort columnIndex, 0' End Sub
grid.AutoSortOnHeaderClick = True' back to the built-in behavior
It defaults to True, and clicking the same header a second time toggles to descending. Turning it off does not disable the header's own click notification — only the automatic Sort call that would otherwise follow it. One reload quirk worth knowing: LoadFromArray resets this property back to True in an Embedded grid, regardless of what you had set it to — reapply your setting after loading new data if you need it off. See AutoSortOnHeaderClick in Part II for exactly which other calls do and don't reset it.
See Sort, SortEx, SortMulti, ClearSort and AutoSortOnHeaderClick in Part II for full signatures and the complete gotcha list, including the Virtual-mode SortRequested/host round-trip details.
How column filters combine
Filtering a column isn't a single setting — each column carries up to four independent filter "slots" that all combine with AND across every column at once:
a text/blank slot and a date slot, both set through SetColumnFilter (which slot you're writing to depends on which filterMode you pass),
a numeric slot, set through SetColumnNumericFilter or SetColumnNumericFilterRange (the two share one slot — whichever was called most recently wins),
a value-list slot, set through SetColumnValueListFilter.
A single column can carry a text filter, a date filter, a numeric filter and a value-list filter simultaneously, and every active slot on every column narrows the visible rows together. ClearColumnFilter drops every slot on one column in a single call; ClearAllFilters does the same across the whole grid.
Filtering by text, blanks, or date
SetColumnFilter covers text matching, blank-state checks, and date comparisons, chosen by the filterMode value you pass. The two you'll reach for most are Contains (0, shown below) and the date modes like DateAfter (21, further down) — Part II has the complete table, including Equals, StartsWith, EndsWith, Regex, the DoesNotContain/IsBlank family, and DateBefore/DateBetween/Today/ThisWeek/ThisMonth/LastNDays.
VBA
Dim data(0To3, 0To0) AsVariant
data(0, 0) = "Acme Co": data(1, 0) = "Bolt Inc"
data(2, 0) = "Crane Ltd": data(3, 0) = "Acme Corp West"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Customer"
grid.SetColumnFilter 0, 0, "Acme"' Contains "Acme"Debug.Print grid.GetFilteredRowCount ' -> 2 (Acme Co, Acme Corp West)
grid.SetColumnFilter 0, 0, ""' an empty pattern with a text mode CLEARS the slotDebug.Print grid.GetFilteredRowCount ' -> 4
Date filters use the same call, just with a mode from the 20s and a date (or day-count) string as the pattern. DateBetween (22) is the one exception worth knowing up front — its pattern is "lowerDate|upperDate" rather than a single date (API constant):
VBA
Dim orders(0To3, 0To1) AsVariant
orders(0, 0) = "Acme Co": orders(0, 1) = "2026-07-01"
orders(1, 0) = "Bolt Inc": orders(1, 1) = "2026-08-15"
orders(2, 0) = "Crane Ltd": orders(2, 1) = "2026-08-20"
orders(3, 0) = "Acme Co": orders(3, 1) = "2026-08-25"
grid.LoadFromArray orders
grid.SetColumnHeader 0, "Customer"
grid.SetColumnHeader 1, "OrderDate"
grid.SetColumnFilter 1, 21, "2026-08-01"' DateAfterDebug.Print grid.GetFilteredRowCount ' -> 3 (Aug 15, Aug 20, Aug 25)' The Customer text filter is a separate slot on a different column --' both narrow the view together (AND), not either/or
grid.SetColumnFilter 0, 0, "Acme"Debug.Print grid.GetFilteredRowCount ' -> 1 (only the Aug 25 Acme Co row matches both)
The text slot and the date slot on the same column are entirely independent, so setting one never disturbs the other. In Turbo column-store mode, SetColumnFilter implements only Contains (0) and StartsWith (2), and only against a column Turbo has stored as String — every other mode refuses by name and leaves the view unchanged, so date filtering in particular is an Embedded/Virtual-only feature.
ClearColumnFilter and ClearAllFilters remove filters — one column's worth, or the whole grid's.
VBA
grid.SetColumnFilter 0, 0, "North"
grid.ClearColumnFilter 0' drops every slot on column 0 -- text, date, numeric, value-list, all at once' ...or, to reset the whole grid at once:
grid.ClearAllFilters
ClearColumnFilter clears every family on that one column together, not just the most recently set slot. A column that was never filtered is a safe no-op in both calls. See ClearColumnFilter in Part II if you need to guard an out-of-range colIndex — it's handled differently depending on which engine is active.
GetFilteredRowCount reports how many rows the combined filter set currently leaves visible — it's what the examples above have been printing all along.
VBA
Debug.Print grid.GetFilteredRowCount ' current visible row count, given every active slot
With nothing filtered, it equals the grid's ordinary row count, and in Embedded and Turbo column-store grids the two members always agree — both simply read the current view's size. It's refused outright in a Virtual grid, since filtering there is delegated to the host's own data source and the grid has no reliable way to know the true count without asking.
See SetColumnFilter, ClearColumnFilter, ClearAllFilters and GetFilteredRowCount in Part II for the full mode list and every mode's per-engine support.
Filtering by number
SetColumnNumericFilter compares a column against a single threshold; SetColumnNumericFilterRange compares it against an inclusive range. They share one slot on the column, so whichever was called more recently wins.
mode selects the comparison: GreaterThan (5), LessThan (6), GreaterOrEqual (7, shown above), LessOrEqual (8) or NotEqual (10). Mode 9 is not Between — for a range, call SetColumnNumericFilterRange instead, as shown above.
Both bounds on SetColumnNumericFilterRange are inclusive. In Turbo column-store mode, both members only route against a column stored as Int32, Int64 or Double — a Currency-typed Amount column or a Date column refuses by name even though both look numeric-ish. Also in Turbo column-store mode, a fractional threshold against an integer-stored column is refused outright rather than silently truncated, since truncation would quietly move the comparison boundary.
See SetColumnNumericFilter and SetColumnNumericFilterRange in Part II for the full mode table and the type-support matrix.
Filtering by a list of values
SetColumnValueListFilter narrows a column to an explicit set of accepted values, and pairs naturally with GetDistinctValues for building the list of choices to offer the user.
GetDistinctValues always scans the whole column directly, ignoring any filter currently active — which is exactly what a picker list needs: the full universe of values, not just what's currently visible. It's implemented for Embedded grids only; it has no Turbo column-store or Virtual-mode implementation at all, so a Turbo-mode picker has to source its candidate list some other way (typically from the same data the host loaded the grid from).
Comparison in SetColumnValueListFilter is case-insensitive. To clear the filter, pass Empty (or Null) rather than a zero-length array — a ReDim'd empty array is rejected outright rather than treated as "clear." A second call on the same column replaces the first rather than adding to it. In Turbo column-store mode this member only routes against a column stored as String.
See SetColumnValueListFilter and GetDistinctValues in Part II, along with GetColumnValueListFilter for reading the accepted list back.
Built-in filter UI: header buttons and the auto-filter row
ShowFilterButtons and ShowAutoFilterRow turn on ready-made filtering UI so you don't have to build your own toolbar for the common cases.
VBA
grid.ShowFilterButtons = True' Each column header now shows a small filter icon; clicking it opens a' checkbox dropdown of that column's distinct values -- the same mechanism' SetColumnValueListFilter/GetDistinctValues expose programmatically.
grid.ShowAutoFilterRow = True' A text box appears below every header; typing into one (after a short' pause) applies a Contains filter on that column, via the same slot' SetColumnFilter(col, 0, text) would use.
Both properties can be read in any mode, but the setter for both is Embedded-only — a Virtual or Turbo column-store grid refuses an attempt to turn either one on. Turning ShowAutoFilterRow back off does not clear any filters its text boxes created; call ClearColumnFilter or ClearAllFilters explicitly if hiding the row should also clear what it filtered.
See ShowFilterButtons and ShowAutoFilterRow in Part II.
Letting users search interactively
ShowFindBar opens the same interactive find/search panel a user's own Ctrl+F would — useful for wiring a toolbar button or menu item without requiring anyone to know the shortcut. HideFindBar closes it again.
VBA
grid.ShowFindBar ' opens the same panel Ctrl+F would' ... user searches interactively ...
grid.HideFindBar ' close it again, e.g. from your own toolbar button
Nothing about the row data changes — this only opens and closes a UI panel. Nothing else closes it automatically, so always pair a ShowFindBar call with a HideFindBar once your own UI is done with it. There is no member that reports whether the panel is currently visible; track that yourself if your UI needs to know. Both calls open the identical panel in Embedded and Turbo column-store grids — what differs between the two modes is only what the panel's search can find, not whether it opens at all. Both are refused in a Virtual grid, since the panel would have nothing of the host's own data to search.
See ShowFindBar and HideFindBar in Part II.
Saving a filter setup for later
Once a filter setup is worth keeping — an "Overdue Orders" view a user builds once and wants back every session — you don't have to reconstruct it call by call. SetColumnFilter, SetColumnNumericFilter/SetColumnNumericFilterRange and SetColumnValueListFilter calls can all be captured and reapplied together as a single named preset; that workflow, along with SaveFilterPreset, LoadFilterPreset, DeleteFilterPreset and GetFilterPresetNames, is covered in the chapter on saving and restoring the user's grid layout later in this book. Part II also documents GetColumnFilterMode, GetColumnFilterPattern, GetColumnValueListFilter and GetFilterState/SetFilterState — the lower-level read-back and serialization members presets are built on.
The Sorting, Filtering and Finding Data chapter covered rearranging and narrowing what is already in the grid. This chapter covers changing what is in it: how a user is (or is not) allowed to start typing into a cell, how to stop a bad value from ever landing there, how to let a mistaken edit be reversed, and how to bring a block of data in from the clipboard. SetCell and SetCellValue, introduced earlier in this book, remain the way to write a value from code -- everything in this chapter is about the interactive, mouse-and-keyboard side of editing, plus the two safety nets that sit around it.
That distinction between a programmatic write and an interactive edit matters more here than almost anywhere else in the control. AllowEditing, every validation rule, and an Embedded grid's own undo history all key off it: a SetCell call from VBA sails past every one of them, while a value a user types and commits through the in-cell editor is the only thing they actually see. Keep that line in mind through the rest of this chapter -- it explains most of the gotchas below.
Reacting to an edit as it happens -- vetoing it, logging it, or reformatting it on the fly -- is the job of the BeforeCellEdit and AfterCellEdit events, covered in the next chapter, Responding to Grid Events. This chapter is about the plumbing those events sit on top of.
Turning interactive editing on or off: AllowEditing
AllowEditing is the one switch that governs every interactive way of starting an edit -- double-click, F2, and pressing Enter on a focused cell -- and setting this property to False blocks all three at once. It also, perhaps less obviously, blocks pasting: PasteFromClipboard, PasteSpecial, Ctrl+V, and the grid's own right-click Paste command all check the same flag and quietly do nothing when it is off.
VBA
Dim data(0To1, 0To2) AsVariant
data(0, 0) = "SKU-1001": data(0, 1) = "Widget A": data(0, 2) = CDbl(24)
data(1, 0) = "SKU-1002": data(1, 1) = "Widget B": data(1, 2) = CDbl(9)
grid.LoadFromArray data
grid.SetColumnHeader 0, "SKU"
grid.SetColumnHeader 1, "Product"
grid.SetColumnHeader 2, "Qty"' Lock the grid down for a "review only" screen
grid.AllowEditing = False' Code can still update it -- a nightly reconciliation job, for example
grid.SetCell 0, 2, CDbl(30)
Debug.Print grid.GetCellValue(0, 2) ' -> 30
grid.AllowEditing = True' hand editing back to the user
A grid with AllowEditing = False can look completely locked down to the user while a background routine keeps updating it freely -- SetCell and SetCellValue never check this flag either way. A double-click still fires ItemDoubleClick even when editing is off; only the editor itself is suppressed, not the click notification. See AllowEditing in Part II for the full list of paths it gates, including the built-in Find and Replace panel's Replace buttons.
Advancing down a column as you type: EditAdvanceOnEnter
For fast, keyboard-only data entry -- typing a column of quantities or prices down a list of rows -- EditAdvanceOnEnter saves a user from reaching for the mouse between rows. With it set to True, pressing Enter while an editor is open commits that edit and immediately opens a new editor one row down, in the same column. The default is False, where Enter simply commits and stops.
VBA
Dim data(0To2, 0To1) AsVariant
data(0, 0) = "SKU-1001": data(0, 1) = CDbl(24)
data(1, 0) = "SKU-1002": data(1, 1) = CDbl(9)
data(2, 0) = "SKU-1003": data(2, 1) = CDbl(0)
grid.LoadFromArray data
grid.SetColumnHeader 0, "SKU"
grid.SetColumnHeader 1, "Qty"
grid.EditAdvanceOnEnter = True' A user who double-clicks row 0's Qty cell, types a count, and presses' Enter now commits that value AND opens row 1's Qty editor immediately --' useful for entering a physical stock count column by column.
This only fires from a real Enter keypress in an editor a user actually opened (double-click, F2, or a prior Enter) -- there is no way to trigger it from code, since nothing in the object model opens a cell editor programmatically. It also stops at the last row rather than wrapping around or creating a new one. A Turbo column-store grid (grid.DataMode = 2) supports genuine interactive editing, but Enter there always just commits once and stops, regardless of how this property is set. See EditAdvanceOnEnter in Part II.
Enforcing rules on typed input: AddValidationRule, RemoveValidationRule and ClearValidationRules
AddValidationRule attaches a rule to a column that is genuinely checked -- a value that fails it is rejected, the cell keeps its previous contents, and ValidationFailed fires (see the next chapter for handling that event). The rule only ever runs at the moment an interactive edit commits: the in-cell editor, a dropdown selection, or a date-picker commit. It is never consulted for a SetCell or SetCellValue call, no matter what is attached to the column.
VBA
Dim data(0To1, 0To3) AsVariant
data(0, 0) = 1001: data(0, 1) = "Acme Corp": data(0, 2) = CDbl(1500): data(0, 3) = "Open"
data(1, 0) = 1002: data(1, 1) = "Northwind": data(1, 2) = CDbl(400): data(1, 3) = "Pending"
grid.LoadFromArray data
grid.SetColumnHeader 0, "OrderID"
grid.SetColumnHeader 1, "Customer"
grid.SetColumnHeader 2, "Amount"
grid.SetColumnHeader 3, "Status"' Customer must not be left blank (0 = Required)Dim ruleRequired AsLong
ruleRequired = grid.AddValidationRule(1, 0, Empty, 0)
' Amount must fall between 0 and 100000 (4 = NumericRange)Dim ruleRange AsLong
ruleRange = grid.AddValidationRule(2, 4, 0, 100000)
' Status can only be one of three values (5 = CustomList, semicolon-separated)Dim ruleList AsLong
ruleList = grid.AddValidationRule(3, 5, "Open;Pending;Closed", 0)
A column can carry several rules at once; they run in the order they were added, and only the first one a value fails is reported. NumericRange (as used above) lets an empty commit through on its own -- pair it with a Required rule if blanks need to be rejected too. See AddValidationRule in Part II for the full rule-type table, including 1 MinLength, 2 MaxLength and 3 Regex.
VBA
' A programmatic write still succeeds, because rules only ever check an' interactive commit -- this is true no matter how many rules are attached
grid.SetCell 1, 1, ""Debug.Print grid.GetCellValue(1, 1) ' -> "" (the Required rule was never consulted)
grid.RemoveValidationRule ruleRequired ' removes just that one rule
grid.ClearValidationRules 2' removes every rule on the Amount column
grid.ClearValidationRules -1' removes every rule on every column
RemoveValidationRule takes the id AddValidationRule returned and removes only that rule, silently doing nothing if the id was never issued or was already removed -- there is no way to enumerate existing rules, so hold onto the id if a rule may need to be removed individually. ClearValidationRules removes every rule on one column, or, passing -1 (or indeed any negative number), every rule on every column at once; an out-of-range positive colIndex is also a silent no-op. A rule can be added successfully on a Virtual grid, but since interactive editing is refused there unconditionally, it can never actually reject anything. Clear and LoadFromArray both reset validation rules back to none, so a fresh reload does not need an explicit ClearValidationRules -1 first. See AddValidationRule, RemoveValidationRule and ClearValidationRules in Part II for every gotcha.
Undoing and redoing changes: Undo, Redo, CanUndo, CanRedo and ClearUndoHistory
Undo reverses the top entry of the current undo history and returns True if it actually reversed something, False if there was nothing to undo -- False is a normal result here, not an error. What counts as "undoable" is genuinely different between the two editable data modes, and this is the single most important thing to know about this group of members. In an Embedded grid, only a programmatic SetCell/SetCellValue call is ever pushed to undo history -- an edit a user types into a cell and commits is never tracked, and neither are row operations like AddRow or DeleteRow. A Turbo column-store grid (grid.DataMode = 2) keeps a separate history that is actually the more complete of the two: it tracks a programmatic write, an interactively typed and committed edit, and a row insert or delete, all through the same mechanism.
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "SKU-1001": data(0, 1) = CDbl(24)
grid.LoadFromArray data
grid.SetColumnHeader 0, "SKU"
grid.SetColumnHeader 1, "Qty"
grid.SetCell 0, 1, CDbl(30)
Debug.Print grid.GetCellValue(0, 1) ' -> 30Debug.Print grid.CanUndo ' -> TrueDim undone AsBoolean
undone = grid.Undo()
Debug.Print undone ' -> TrueDebug.Print grid.GetCellValue(0, 1) ' -> 24Debug.Print grid.CanRedo ' -> TrueDim redone AsBoolean
redone = grid.Redo()
Debug.Print redone ' -> TrueDebug.Print grid.GetCellValue(0, 1) ' -> 30' A new edit right after an Undo throws away the redo history it created
grid.SetCell 0, 1, CDbl(45)
Debug.Print grid.CanRedo ' -> False
CanUndo and CanRedo are plain, read-only checks of whichever history the current mode uses -- useful for greying out Undo/Redo toolbar buttons without triggering an actual undo. Both stay True across multiple pending steps; they report only whether the history is non-empty, not how many steps it holds, and each history keeps only a limited, most-recent window of steps before older ones drop off (see Undo in Part II for the exact cap). Neither Undo nor Redo can be called from inside an event handler; both raise an error if attempted there. Undo/Redo/CanUndo/CanRedo all work on Embedded and Turbo column-store grids; only a Virtual grid refuses Undo/Redo outright, since the host owns that data and there is nothing local to reverse.
VBA
grid.SetCell 0, 1, CDbl(99)
grid.ClearUndoHistory ' the edit above stays applied -- only its history entry is goneDebug.Print grid.CanUndo ' -> FalseDebug.Print grid.GetCellValue(0, 1) ' -> 99
ClearUndoHistory empties both the undo and redo stacks for the current mode without touching any data already in the grid -- only the ability to step back through past changes is removed. One trap worth knowing: LoadFromArray always resets undo/redo history, and Turbo column-store's Clear does too, but Embedded's Clear does not -- an edit made just before an Embedded Clear() can leave CanUndo reading True on an empty grid, with Undo() there returning True while silently doing nothing because the row it would restore no longer exists. Call ClearUndoHistory explicitly after Clear in Embedded mode if a clean history matters. See Undo, Redo, CanUndo, CanRedo and ClearUndoHistory in Part II for further detail, including how this interacts with a Virtual-mode switch.
Treating a batch of edits as one undo step: BeginUndoGroup and EndUndoGroup
A single business action -- receiving a restock shipment, say, which updates both a quantity and a unit price together -- can mean several SetCell calls in code. Left alone, each is its own undo step, so one Undo from the user only reverses half the action. BeginUndoGroup and EndUndoGroup fold every SetCell/SetCellValue call between them into a single compound entry on Embedded's own undo history, reversed by one Undo call.
VBA
Dim data(0To0, 0To3) AsVariant
data(0, 0) = "SKU-1001": data(0, 1) = "Widget A"
data(0, 2) = CDbl(24): data(0, 3) = CDbl(4.5)
grid.LoadFromArray data
grid.SetColumnHeader 0, "SKU"
grid.SetColumnHeader 1, "Product"
grid.SetColumnHeader 2, "Qty"
grid.SetColumnHeader 3, "UnitPrice"' A restock changes Qty and UnitPrice together -- group them so one' Ctrl+Z undoes the whole receipt, not just half of it
grid.BeginUndoGroup
grid.SetCell 0, 2, CDbl(24 + 50)
grid.SetCell 0, 3, CDbl(4.75)
grid.EndUndoGroup
Debug.Print grid.GetCellValue(0, 2) & " / " & grid.GetCellValue(0, 3) ' -> 74 / 4.75
grid.Undo ' one call, both columns revert togetherDebug.Print grid.GetCellValue(0, 2) & " / " & grid.GetCellValue(0, 3) ' -> 24 / 4.5
Calling BeginUndoGroup a second time while a group is already open does nothing -- there is no nesting. An empty group (no edits happened inside it) is discarded rather than pushed as a no-op step, and calling EndUndoGroup with no group open is a safe no-op. BeginUndoGroup/EndUndoGroup are Embedded-only; a Turbo column-store grid refuses them outright, though each SetCell there is still individually undoable on its own history. See BeginUndoGroup and EndUndoGroup in Part II for the full mode-restriction details.
Pasting from the clipboard: PasteFromClipboard and PasteSpecial
PasteFromClipboard reads the clipboard's tab-and-newline-delimited text -- the same layout Excel produces on a copy -- and writes it starting at the currently focused cell, one clipboard line per grid row and one tab-delimited field per column. Every pasted cell is written as a plain string, regardless of what the target column previously held, and the paste stops rather than growing the grid if it would run past the last row or column.
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "": data(0, 1) = "Chicago"' source row -- name left blank
data(1, 0) = "Acme Corp": data(1, 1) = "(unknown)"' target row -- already has a name
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SetColumnHeader 1, "City"
grid.SelectRow 0
grid.CopyWithHeaders False' clipboard: "<TAB>Chicago" -- name field empty
grid.SelectBlock 1, 0, 1, 0' move the paste target to row 1
grid.PasteSpecial 1' 1 = SkipBlanksDebug.Print grid.GetCellValue(1, 0) ' -> Acme Corp (blank clipboard field left it alone)Debug.Print grid.GetCellValue(1, 1) ' -> Chicago (non-blank field overwrote normally)' Neither paste member is tracked by undo history -- capture the old' values first if this paste needs to be reversibleDebug.Print grid.CanUndo ' -> False
PasteSpecial behaves exactly like PasteFromClipboard, with one extra option: pasteMode0 (ValuesOnly) overwrites every cell in the pasted range, blank fields included, and is what plain PasteFromClipboard does; pasteMode1 (SkipBlanks) leaves a target cell untouched wherever the corresponding clipboard field is empty, which is the only way to merge clipboard data into existing rows without clobbering cells the clipboard has nothing for. pasteMode must be exactly 0 or 1 -- anything else raises a runtime error rather than falling back to a default.
Both members are gated by AllowEditing exactly like a typed edit -- with it False, a paste (including Ctrl+V and the right-click Paste command) silently does nothing, and the clipboard is not even read. Neither is tracked by undo history at all, so a paste that needs to be undoable has to be handled by the caller: read the target cells before pasting and write them back if the user asks to undo. Both refuse outright on a Virtual grid (the host owns the data) and a Turbo column-store grid (pasting has no Turbo column-store implementation), leaving the view completely unchanged in either case -- there is no partial paste. See PasteFromClipboard and PasteSpecial in Part II.
Validation and undo do not overlap
It is worth being explicit about a point implicit in everything above: validation and undo solve two different problems and cover two different sets of writes. A validation rule can only ever stop an interactive edit before it lands, and is blind to anything written from code. Undo can only reverse a write that already happened, and in an Embedded grid that means only SetCell/SetCellValue -- a user's own typed-and-committed edit is invisible to Embedded's undo history the same way it is invisible to validation's reach. A grid that is both validated and updated from VBA needs both halves covered deliberately; neither one substitutes for the other. The next chapter's BeforeCellEdit and AfterCellEdit events are the third piece of this picture -- they fire on the same interactive-commit path validation does, and are where custom logic beyond a fixed rule (cross-column checks, formatting, logging) belongs.
LiteGrid Turbo raises 36 events in total — 30 general-purpose ones covering clicks, editing, selection, sorting, filtering, paste, and paint; 3 further events specific to Virtual mode (NeedRows, SortRequested, FilterRequested) that let a Virtual-mode host supply data on demand instead of handing the grid an array; and 3 more that arrived with later features — TreeExpanding for lazy tree loading, and RowsInserted/RowsDeleted for tracking the row lifecycle. This chapter covers the nine events a working form or worksheet handler reaches for first: clicks, selection, the editing lifecycle, a general change notification, and the keyboard.
Every one of them is delivered the ordinary way: drop a LiteGrid Turbo control on a form (named lgGrid in the examples below), and the VBA/VBE IDE lets you pick the control and the event from the two dropdowns at the top of the code window, which stubs out a Private Sub lgGrid_<EventName>(...) for you. This is registered-mode, connection-point event delivery — it is how every event example in this book has been wired so far, it works for all 36 events with no setup call of any kind, and it is what the rest of this chapter uses throughout. A control hosted registration-free (no type library registered) uses a different event-delivery mechanism entirely; that scenario, and how to reach the same events from it, is covered in Registration-Free Deployment.
The examples in this chapter assume a grid loaded with a small Orders table:
VBA
Dim data(0To9, 0To4) AsVariantDim r AsLongFor r = 0To9
data(r, 0) = 1000 + r ' OrderID
data(r, 1) = "Customer " & (r Mod5) ' Customer
data(r, 2) = DateSerial(2026, 8, 1) + r ' OrderDate
data(r, 3) = 99.5 + r * 12.25' Amount
data(r, 4) = Choose((r Mod3) + 1, "Open", "Shipped", "Closed") ' StatusNext r
lgGrid.LoadFromArray data
lgGrid.SetColumnHeader 0, "Order ID"
lgGrid.SetColumnHeader 1, "Customer"
lgGrid.SetColumnHeader 2, "Order Date"
lgGrid.SetColumnHeader 3, "Amount"
lgGrid.SetColumnHeader 4, "Status"
Each handler below can be pasted into the same form module on its own.
ColumnClick
Fires when a user left-clicks a column header — reach for it to react to header clicks beyond the built-in sort, such as showing a custom menu.
Turning on AllowColumnReorder (off by default) silently stops this event from firing on a plain click, because every header mousedown then starts a drag gesture instead. There is also no Cancel argument, so a handler cannot suppress the built-in AutoSortOnHeaderClick sort, which runs immediately afterward if enabled.
See ColumnClick in Part II for exactly how resize-edge and filter-icon clicks are excluded before this event fires; see also ColumnReorder and AllowColumnReorder.
ItemClick
Fires on a plain left-click on an ordinary data cell — the everyday "a cell was clicked" hook for row selection, drill-down, or any other click-driven behavior.
It never fires for a checkbox-typed column's cell, a cell icon's own hit rectangle, or a column with a Button/Star/Checkbox/Progress renderer assigned — those consume the click and raise their own, more specific event instead. A real double-click also delivers ItemClick once before ItemDoubleClick fires, so a handler wired to both sees two events for the same user gesture.
See ItemClick in Part II; see also CheckboxToggle, IconClick, ButtonClick, and SetColumnRenderer for the renderers that intercept the click first.
ItemDoubleClick
Fires on a double-click on a data cell, before any built-in behavior that double-click normally triggers — a natural place to open a detail form or drill into a record rather than let the cell go into edit mode.
VBA
PrivateSub lgGrid_ItemDoubleClick(ByVal rowIndex AsLong, ByVal columnIndex AsLong)
MsgBox "Open order detail for row " & rowIndex
EndSub
There is no Cancel argument here either, so this handler cannot stop the interactive editor that opens right afterward when AllowEditing is True — use BeforeCellEdit for that. A double-click on a header's resize edge is a separate gesture (auto-fit) and never fires this event at all.
See ItemDoubleClick in Part II; see also ItemClick and BeforeCellEdit.
SelectionChanged
Fires whenever a selection-affecting action happens — a click, Ctrl+A, Shift+Arrow, or a call to SelectRow/SelectAll/ClearSelection — useful for keeping something else (a toolbar button's enabled state, a totals footer) in sync with the current selection.
It fires unconditionally on every such action, even clicking a row that is already selected — there is no comparison against the prior selection. It also carries no arguments describing what changed, so a handler that needs to know the new selection reads it back afterward with GetSelectedRows or IsRowSelected.
See SelectionChanged in Part II; see also SelectRow, SelectAll, ClearSelection, and GetSelectedRows.
BeforeCellEdit
Fires just before the interactive cell editor opens — triggered by a double-click on the cell (with AllowEditingTrue) or by pressing F2 with the cell focused — and is the only point in the editing lifecycle where starting an edit can be vetoed.
VBA
PrivateSub lgGrid_BeforeCellEdit(ByVal rowIndex AsLong, ByVal columnIndex AsLong, Cancel AsBoolean)
If columnIndex = 0Then Cancel = True' the Order ID column is never editableEndSub
It is only reachable through the interactive editor — the programmatic write methods never fire it, so they cannot be intercepted here. Setting Cancel = True leaves the cell completely untouched: no AfterCellEdit, no CellValueChanged, no editor window at all.
See BeforeCellEdit in Part II; see also AfterCellEdit, CellValueChanged, and ValidationFailed — the validation pipeline itself is covered in Editing and Validating Data.
AfterCellEdit
Fires once an interactive edit is actually committed — Enter, Tab, or clicking away from the editor — with the raw old and new text, useful for logging a change or kicking off dependent work once the user finishes typing.
Like BeforeCellEdit, it fires only from the interactive commit path — a programmatic cell write never raises it. It also does not fire if the committed text is unchanged from the original, or if validation rejected the new text (ValidationFailed fires instead and the cell reverts). oldValue/newValue are always the raw typed strings, never a reformatted or type-coerced rendering — for a parsed value, use CellValueChanged instead.
See AfterCellEdit in Part II; see also BeforeCellEdit, CellValueChanged, and ValidationFailed.
CellValueChanged
Fires alongside AfterCellEdit for the same interactive commit, but with typed Variant old/new values instead of raw text — reach for it when the code needs an actual number or date rather than a string.
It also fires on its own, with no accompanying AfterCellEdit, from a Star-rating renderer click — and on that path there is no old-vs-new comparison, so clicking a star rating to the value it already had still fires the event. Like AfterCellEdit, it never fires from a programmatic cell write.
See CellValueChanged in Part II; see also AfterCellEdit, ValidationFailed, GetCell, and SetColumnRenderer.
Putting the editing events in order
For a real interactive edit — double-click a cell, type over it, press Enter — the events above fire in a fixed sequence: ItemClick, then ItemDoubleClick, then BeforeCellEdit (the only chance to cancel), then, once the editor commits a genuine change, AfterCellEdit and CellValueChanged together. Skip any step in code — an editor opened by F2 instead of a double-click, or a commit where the text did not actually change — and the corresponding event simply does not fire; there is no placeholder or empty firing to account for. In Virtual mode, ItemClick and ItemDoubleClick still fire normally, but BeforeCellEdit, AfterCellEdit, and CellValueChanged never do, since the interactive editor itself refuses to open there.
DataChanged
A coarse "something about the dataset changed" notification, carrying a changeType of 0 (Loaded), 1 (Cleared), or 2 (RowsChanged) — reach for it when one subscription point covers loads, clears, and bulk range writes together, and the specifics don't matter.
It fires alongside the more specific events for the same change, not instead of them — loading data fires both DataLoaded and DataChanged(0). changeType 2 only comes from a bulk-range write; ordinary single-cell writes never raise DataChanged at all. In Turbo column-store mode it only ever reports changeType 1 (Cleared), and it never fires at all in Virtual mode, since the host owns the data there instead of the grid.
See DataChanged in Part II for exactly which triggers fire in each mode; see also DataLoaded and SetCellRange.
KeyDown
Fires for every keystroke the grid's window receives, before any of the grid's own built-in key handling — useful for logging keystrokes or layering a new shortcut alongside the built-in ones.
VBA
PrivateSub lgGrid_KeyDown(ByVal keyCode AsLong, ByVal shift AsLong)
Debug.Print"Key " & keyCode & ", shift bits " & shift
If keyCode = 116Then' F5: a shortcut of our own, alongside the built-in keys
RefreshOrdersGrid
EndIfEndSub
shift arrives as a bitmask — 1 for Shift, 2 for Ctrl, 4 for Alt, combined by adding bits together for more than one modifier held at once. Despite firing first, it carries no Cancel argument at all: a handler cannot suppress arrow-key navigation, F2-to-edit, Delete, or any other built-in shortcut for the same key from here. It also fires for every key the grid receives, including ones the grid does not otherwise react to.
See KeyDown in Part II; see also BeforeCellEdit.
The rest of the event list
This chapter covered the nine events used most often day to day. Part II's "Events and the event callback bridge" section documents all 33 exhaustively — the remaining click, sort, filter, paste, paint, and validation events (ColumnReorder, IconClick, ButtonClick, CheckboxToggle, BeforeSort/AfterSort, BeforePaste/AfterPaste, BeforeCellPaint, ValidationFailed, ScrollChanged, CellHover/CellMouseEnter/CellMouseLeave, LoadProgress, DataLoaded, and the Virtual-mode NeedRows/SortRequested/FilterRequested trio), the mode-by-mode firing table for each one, and the full Tier A (delivered by default) vs. Tier B (opt-in, for the small set of high-frequency events) distinction that matters once a host is running registration-free.
A grid that only shows raw values earns its keep slowly. LiteGrid Turbo separates "how the grid looks" from "how individual values are called out" into two large member groups, and this chapter is a working tour of both: the appearance basics (grid lines, row banding, the core colors, and the theme system that ties them together), and the data-driven presentation members (icons, sparklines, heatmaps, data bars, and rule-based conditional formatting). It closes with a short pointer to the grid's tree view for parent/child data. Everything in this chapter is presentation only -- none of it changes a single cell's underlying value -- so none of it needs to be undone before a sort, a filter, or an export runs.
Grid lines, row banding, and the core colors
The fastest visual changes available are GridLines, AlternateRowColor, ShowRowNumbers, BackColor, and ForeColor. All five are pure rendering state with no mode restriction anywhere -- they behave identically whether the grid is holding an in-memory array, running in Virtual mode, or backed by the Turbo column-store.
VBA
Sub CustomerGridBasics(grid AsObject)
Dim data(0To3, 0To2) AsVariant
data(0, 0) = "C-1001": data(0, 1) = "Contoso Retail": data(0, 2) = CDbl(4250.5)
data(1, 0) = "C-1002": data(1, 1) = "Fabrikam Supply": data(1, 2) = CDbl(1899)
data(2, 0) = "C-1003": data(2, 1) = "Northwind Traders": data(2, 2) = CDbl(6100.25)
data(3, 0) = "C-1004": data(3, 1) = "Adventure Works": data(3, 2) = CDbl(750)
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerID"
grid.SetColumnHeader 1, "CustomerName"
grid.SetColumnHeader 2, "Balance"
grid.GridLines = 1' horizontal separators only, no vertical rules
grid.ShowRowNumbers = True' a "#" column appears at the left edge; every' other column shifts right to make room for it
grid.BackColor = RGB(255, 255, 255) ' plain row background
grid.AlternateRowColor = RGB(240, 244, 250) ' every other row, independent of BackColor
grid.ForeColor = RGB(20, 20, 20) ' default text colorEndSub
GridLines is a bit flag rather than a plain enumeration: bit 0 (value 1) turns on horizontal separators, bit 1 (value 2) turns on vertical ones.
Value
Meaning
0
None -- no separators painted
1
Horizontal only
2
Vertical only
3
Both -- the default
ShowRowNumbers defaults to False, and its row-number column's width is computed internally with no property to read or set it.
BackColor, AlternateRowColor, and ForeColor all store and return the exact OLE_COLOR value they are given, with no translation of any kind -- pass an explicit RGB() value to every one of them. A genuine system color constant (the kind with the high bit set) round-trips unchanged but paints incorrectly, since the control treats whatever it stores as a literal RGB value. ForeColor always reports back exactly the raw value you set, but at paint time the control can substitute a more readable color on screen when contrast against the row's actual background is too low -- see ForeColor in Part II for exactly when that does and does not apply. And because all three properties (along with Theme, covered next) return a VT_UI4 Variant through a late-bound Object variable, a bare = or <> comparison against the raw property read can raise a VBA "Automation type not supported" error -- wrap the read in CDbl() first if code needs to compare it.
Once the four basics are in place, the same pattern extends to hover and header state. HoverBackColor, HoverForeColor, HeaderBackColor, HeaderForeColor, and HeaderHoverColor follow the same "store and return exactly what was given" rule as BackColor -- with one exception: HeaderBackColor is the one color property in this group that runs its input through OleTranslateColor before storing it, so a system color reference passed there (and only there) resolves to a real, paintable color instead of round-tripping as a raw reference.
VBA
grid.AlternateRowColor = RGB(240, 244, 250) ' faint blue-gray for every other row
grid.HoverBackColor = RGB(214, 228, 245) ' a stronger tint while the mouse is over a row
grid.HoverForeColor = RGB(10, 30, 60)
grid.HeaderBackColor = RGB(22, 54, 92) ' the same family, darker, for the header
grid.HeaderForeColor = RGB(255, 255, 255)
grid.HeaderHoverColor = RGB(35, 75, 120) ' a lighter step up from HeaderBackColor
None of these colors derive from each other automatically -- each is an independent override on its own role, and nothing in the control computes a hover or header-hover shade from the base colors the way a built-in Theme preset does. Choosing them from one consistent family, all as explicit RGB() values, is what makes the result read as one deliberate scheme rather than several unrelated overrides.
The fast path to a good-looking grid: Theme and ApplyThemeJson
Setting individual colors one at a time works, but Theme is the faster starting point: it selects one of four built-in presets and rebuilds all of the grid's internal color roles from that preset's own palette in one call.
SignatureProperty Theme As Long -- 0 = Light (the default), 1 = Dark, 2 = an Excel-style theme, 3 = High-Contrast. Values outside 0-3 are refused with an error.
VBA
Sub ThemeThenHeaderOverride(grid AsObject)
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Widget A": data(0, 1) = CDbl(120)
data(1, 0) = "Widget B": data(1, 1) = CDbl(75)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Product"
grid.SetColumnHeader 1, "Qty"' Pick a built-in theme first -- this rebuilds ALL color roles, including' the header's, from that theme's own palette
grid.Theme = 0' Light' NOW layer a brand color onto just the header -- this only touches the' header roles, leaving every other Light-theme color untouched
grid.HeaderBackColor = RGB(22, 54, 92) ' a dark corporate blue
grid.HeaderForeColor = RGB(255, 255, 255)
Debug.Print grid.BackColor ' -> 16777215 (still Light theme's white)Debug.Print grid.HeaderBackColor ' -> 6033430 (the override)' Calling Theme again wipes the header override right back out -- Theme' does not skip roles that were individually overridden
grid.Theme = 0Debug.Print grid.HeaderBackColor <> RGB(22, 54, 92) ' -> True (override gone)EndSub
Theme is a full rebuild, not a merge: as the example just showed, it discards any individual color override made before the call -- including ones set through BackColor, HeaderBackColor, or ApplyThemeJson -- so the safe pattern is to set Theme once, then layer overrides on top, and never call Theme again afterward unless a full reset is actually wanted.
Several color roles have no dedicated property at all (grid lines, focus indicators, footer colors, heatmap tint stops, the three status colors, and more); reach them through ApplyThemeJson:
SignatureSub ApplyThemeJson(json As String)
It is strict, and it is all-or-nothing. The string is parsed as JSON, and every entry must be a known role name with a "#RRGGBB" value. A misspelled role, a malformed color, a role named twice, or text that is not well-formed JSON raises a runtime error that names the problem -- and then nothing is applied, not even the entries that were valid, so the grid keeps exactly the colors it had. A partial string is fine: name only the roles you want to change, and every other role keeps its current color. Its counterpart, GetCurrentThemeJson, returns every role's current resolved color as one JSON string, and that output is valid input back into ApplyThemeJson:
VBA
Sub SaveAndRestorePalette(grid AsObject)
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.Theme = 0
grid.HeaderBackColor = RGB(22, 54, 92)
grid.HeaderForeColor = RGB(255, 255, 255)
' Capture every color role currently in effect as one JSON stringDim saved AsString
saved = grid.GetCurrentThemeJson()
grid.Theme = 1' switch to Dark; this discards the header overrides tooDebug.Print grid.HeaderBackColor <> RGB(22, 54, 92) ' -> True' Restore everything in one call
grid.ApplyThemeJson saved
Debug.Print grid.HeaderBackColor ' -> 6033430 (back to the saved override)' The saved string is a snapshot of resolved COLORS only -- it has no idea' Theme was ever 0, so grid.Theme still reads 1 (Dark) afterward even' though every color now matches what Theme 0 plus the overrides producedDebug.Print grid.Theme ' -> 1 (unchanged by ApplyThemeJson)EndSub
A caller that needs both the colors and the Theme numeric id back needs to save Theme's own value separately -- GetCurrentThemeJson/ApplyThemeJson only ever deal in resolved colors. See ApplyThemeJson and GetCurrentThemeJson in Part II for the exact role names. Two roles reachable through ApplyThemeJson -- SelectionBackColor and SelectionForeColor -- have their own dedicated properties covered in Selection and User Interaction rather than here.
Every color property is another name for one of these roles rather than a separate setting -- BackColor is Surface_Base, AlternateRowColor is Surface_Alt -- so when a property and an ApplyThemeJson entry both set the same role, whichever ran last decides the color.
One more property rounds out the appearance basics: RenderStyle (Property RenderStyle As Long, default 0) controls whether the column header background paints as a top-to-bottom gradient (0) or a flat fill (1); values outside 0-2 are refused with an error. See HeaderBackColor, HoverBackColor, HeaderHoverColor, RowFontName, and the rest of the appearance group in Part II for the full member list, including the font, padding, and tooltip properties this chapter does not cover.
Coloring a whole column, and status badges
A theme colors the grid by role: every row background, every header, every selection. Two further tools work on one column at a time. A column color gives every cell in a column its own background or text color, and the pill renderer draws each value in a status column as a rounded, theme-colored badge.
Signatures:
Sub SetColumnBackColor(columnIndex As Long, color As OLE_COLOR)
Sub SetColumnForeColor(columnIndex As Long, color As OLE_COLOR)
Function GetColumnStylesJson() As String
Sub ApplyColumnStylesJson(json As String)
VBA
Sub StyleAccountColumns(grid AsObject)
Dim data(0To2, 0To2) AsVariant
data(0, 0) = "C-1001": data(0, 1) = "North": data(0, 2) = CDbl(4250.5)
data(1, 0) = "C-1002": data(1, 1) = "South": data(1, 2) = CDbl(1899)
data(2, 0) = "C-1003": data(2, 1) = "East": data(2, 2) = CDbl(6100.25)
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerID"
grid.SetColumnHeader 1, "Region"
grid.SetColumnHeader 2, "Balance"
grid.SetColumnForeColor 1, RGB(27, 94, 32) ' text only -- Region still bands with its row
grid.SetColumnBackColor 2, RGB(238, 244, 250) ' a background for every Balance cellDebug.Print grid.GetColumnStylesJson()
' -> {"columns":[{"index":1,"fore":"#1B5E20"},{"index":2,"back":"#EEF4FA"}]}
grid.SetColumnBackColor 2, &HFFFFFFFF ' back to the theme's own backgroundEndSub
A column with only a text color keeps its row's background exactly, so it still shows alternate-row banding and the hover highlight along with the rest of the row. A column background replaces both the plain and the alternate-row background in that column. Either way the text is checked against the background each cell actually gets, and is lightened or darkened on screen only if it would be unreadable -- so a dark column background still gets readable text.
GetColumnStylesJson returns every column that carries a color, in exactly the shape ApplyColumnStylesJson accepts, so column styling can be saved with the rest of a user's settings and applied again later. In that document index is the 0-based column, back and fore are each optional (one that is left out keeps its current value), and the string "auto" returns a color to the theme. Like ApplyThemeJson, ApplyColumnStylesJson is all-or-nothing: one bad entry raises an error and no column changes.
Signatures:
Sub SetColumnPillMap(colIndex As Long, displayValue As String, pillStatus As Long)
Sub ClearColumnPillMap(colIndex As Long)
VBA
Sub OrderStatusBadges(grid AsObject)
Dim data(0To3, 0To1) AsVariant
data(0, 0) = "SO-5001": data(0, 1) = "Delivered"
data(1, 0) = "SO-5002": data(1, 1) = "On Hold"
data(2, 0) = "SO-5003": data(2, 1) = "Cancelled"
data(3, 0) = "SO-5004": data(3, 1) = "New"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Order"
grid.SetColumnHeader 1, "Status"
grid.SetColumnRenderer 1, 6' 6 = Pill
grid.SetColumnPillMap 1, "Delivered", 1' Good
grid.SetColumnPillMap 1, "On Hold", 2' Warn
grid.SetColumnPillMap 1, "Cancelled", 3' Bad' "New" has no mapping, so it draws the neutral badgeEndSub
The map is matched against the text the cell displays, exactly and case-sensitively, so a formatted number is matched as it appears on screen. Good, Warn and Bad take the theme's Status_Good, Status_Warn and Status_Bad roles -- the same three colors the progress renderer fills with -- and Neutral takes the header surface color, so badges follow a theme change with no further code. An empty cell draws no badge, and each label is checked against its own badge color for readability.
Which color wins. When several settings apply to the same cell, the more specific one wins. From least to most specific:
Layer
Set with
Theme role
Theme, ApplyThemeJson, or a color property such as BackColor
The first two layers are the theme's own palette, and they are shared state: a color property and the role it names are one setting, so a later Theme or an ApplyThemeJson entry for that role replaces a property you set earlier. Column colors are not roles. Theme and ApplyThemeJson leave them in place, which is what lets a form switch between a light and a dark theme at run time and keep its column styling -- so pick column colors that read under both, or keep one column document per theme and apply it after the switch.
Three limits are worth knowing before you rely on these. All six members are accepted in every data mode, but they are drawn only in Embedded and Turbo column-store grids: Virtual mode's painter does not apply column colors, and badge drawing with rows supplied by the host has not been confirmed. A column color is stored on the column and moves with it when the user drags the column to a new position, whereas a renderer assignment and a pill map stay with the column position. And Clear removes column colors and renderer assignments but not pill maps, so call ClearColumnPillMap before building a different layout on the same grid. Full details are in the Part II entries under Columns.
Status icons without writing a conditional-formatting rule
LiteGrid Turbo actually has two separate icon systems that happen to share a GetResolvedIcon lookup: an older external system that loads real image files (.ico, .bmp, .png) through LoadIconFromFile into a shared pool addressed by integer id, and a newer built-in system that draws one of eight small vector glyphs directly with no file at all. This section focuses on the external system, since it is the one most VBA/VB6 projects reach for first -- a status column built from icons already sitting on disk.
VBA
Sub OrderStatusIcons(grid AsObject)
Dim data(0To4, 0To1) AsVariant
data(0, 0) = "ORD-1001": data(0, 1) = "Shipped"
data(1, 0) = "ORD-1002": data(1, 1) = "Pending"
data(2, 0) = "ORD-1003": data(2, 1) = "Shipped"
data(3, 0) = "ORD-1004": data(3, 1) = "Cancelled"
data(4, 0) = "ORD-1005": data(4, 1) = "Pending"
grid.LoadFromArray data
grid.SetColumnHeader 0, "OrderID"
grid.SetColumnHeader 1, "Status"Dim okIcon AsLong, pendingIcon AsLong, cancelIcon AsLong
okIcon = grid.LoadIconFromFile("C:\Icons\check_green.ico")
pendingIcon = grid.LoadIconFromFile("C:\Icons\clock_amber.ico")
cancelIcon = grid.LoadIconFromFile("C:\Icons\x_red.ico")
' Exact-text value map -- one icon per distinct Status string
grid.SetColumnIconMap 1, "Shipped", okIcon
grid.SetColumnIconMap 1, "Pending", pendingIcon
grid.SetColumnIconMap 1, "Cancelled", cancelIcon
' A single cell can still override the column's own rule -- row 1's order' turned out to be a rush cancellation
grid.SetCellIcon 1, 1, cancelIcon
Dim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 1, 1, kind, extId, builtinId
Debug.Print kind & " " & extId ' -> 1 12 (the cell override wins over the Pending mapping)EndSub
SetColumnIconMap assigns an icon per distinct cell text on the whole column; SetCellIcon overrides any single cell on top of that, and the override always wins because cell-level lookups are checked before column rules. If a numeric threshold reads better than a fixed text map for a given column -- flagging a Balance column red above a cutoff rather than mapping specific strings -- AddIconThreshold covers that case and is documented in Part II alongside SetColumnDefaultIcon, the external system's fallback icon (checked last, after the map and the thresholds).
RemoveIcon is worth knowing before it is needed: it does not just stop drawing an icon, it erases every value-map entry, threshold band, and column default that named it, so a later LoadIconFromFile that reuses the same numeric id can never silently repaint an assignment nobody meant to give it that artwork. One gap survives even this: in Turbo column-store mode, a cell-level assignment made with SetCellIcon keeps the removed icon's id even after RemoveIcon runs, so a later LoadIconFromFile that reuses that id can silently resurface on that cell -- see RemoveIcon in Part II for the full mechanics.
The built-in vector system (SetColumnBuiltinIconMap, SetColumnBuiltinIcon, ClearColumnBuiltinIcon, and the rest, all in Part II) is a completely separate store -- nothing in one system references the other -- yet both can be attached to the same cell at once, so GetResolvedIcon needs a resolution order: it always checks the built-in system first, and only falls back to the external system when the built-in system has nothing to say about that cell.
VBA
Sub BuiltinIconsWinOverExternal(grid AsObject)
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Low": data(1, 0) = "Medium": data(2, 0) = "High"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Priority"' An external rule on this column, for comparison ...Dim flagIcon AsLong
flagIcon = grid.LoadIconFromFile("C:\Icons\flag.ico")
grid.SetColumnIconMap 0, "High", flagIcon
' ... and a built-in rule on the SAME column, same trigger value -- no file
grid.SetColumnBuiltinIconMap 0, "High", 0' one of eight built-in glyphsDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 2, 0, kind, extId, builtinId
Debug.Print kind ' -> 2 (Builtin) -- the built-in system is always checked firstEndSub
Within the built-in system there is a further wrinkle worth flagging: SetColumnBuiltinIcon assigns one icon to every row in a column unconditionally, and that column-wide icon outranks even the built-in value map and threshold rules until ClearColumnBuiltinIcon removes it -- easy to set while forgetting it silences an entire rule chain underneath it.
Most of both icon systems works outside Embedded mode too, but two mode-specific traps are worth knowing before relying on them. SetCellBuiltinIcon, the built-in system's own single-cell override, is refused outright in both Virtual and Turbo column-store mode; its external-system counterpart SetCellIcon is safe to use in all three modes instead. And GetResolvedIcon itself only reads the Embedded row store, so outside Embedded mode it silently reports "no icon" for every row even on a cell an external or built-in rule would otherwise resolve correctly -- see GetResolvedIcon in Part II for the details.
Sparklines, heatmaps, and data bars: presenting the shape of the data
Beyond single icons, LiteGrid Turbo can paint a small chart, a color gradient, or a proportional bar into a cell. SetColumnSparkline picks a sparkline column's chart type, and SetSparklineColors sets its colors -- both can be configured in every mode, including Virtual and Turbo column-store. The actual data points, set with SetSparklineData or SetSparklineDataCSV, are the one part of this group genuinely refused in both Virtual and Turbo column-store mode, so a Turbo column-store sparkline column can be fully styled and never draw anything. Full signatures and worked recipes are in Part II.
Heatmaps and data bars are more forgiving across modes and make a good pair to learn together, since SetColumnDataBar is a close parallel to SetColumnHeatmap.
VBA
Sub AmountHeatmapAndDataBar(grid AsObject)
grid.SetColumnHeader 3, "Amount"' Auto-ranged from the column's own min/max the moment it is enabled
grid.SetColumnHeatmap 3, True
grid.SetHeatmapColors 3, RGB(255, 230, 230), RGB(255, 255, 230), RGB(230, 255, 230)
grid.SetColumnDataBar 3, True
grid.SetDataBarColor 3, RGB(99, 142, 198)
EndSub
SetColumnHeatmap(colIndex As Long, enabled As Boolean) tints a column's background along a low/mid/high gradient set by SetHeatmapColors(colIndex, lowColor, midColor, highColor); a value at or below the range minimum always renders as lowColor exactly, and one at or above the maximum always renders as highColor exactly. SetHeatmapRange(colIndex, minVal, maxVal) locks in an explicit range instead of auto-ranging -- minVal must be strictly less than maxVal -- and once set, the only way back to auto-ranging is to disable and re-enable the heatmap with SetColumnHeatmap. SetColumnDataBar(colIndex, enabled) and its own SetDataBarColor(colIndex, barColor) / SetDataBarRange(colIndex, minVal, maxVal) work the same way, with one difference worth remembering: disabling a data bar does not just hide it, it erases the column's entire data-bar configuration, so re-enabling starts back at the theme's own accent color, not whatever color was set before. Until you set a color yourself, a heatmap takes its three colors from the theme's Heat_Low, Heat_Mid and Heat_High roles and a data bar takes Accent_Primary, and both follow any later Theme or ApplyThemeJson change; once SetHeatmapColors or SetDataBarColor has been called for a column, that column keeps the colors it was given.
Both are refused only in Virtual mode, where there is no full dataset resident to find a range in. In the Turbo column-store, both are genuinely implemented from precomputed per-column statistics rather than a scan -- but only on a column stored as Int32, Int64, or Double; a Currency, Date, or String column is refused outright, even though Embedded mode never refuses a heatmap or data bar on a non-numeric column at all (it simply finds no numbers to range over and tints nothing). If the auto-range matters after data changes, the two modes behave differently too: Embedded computes it once, at the moment the heatmap or bar is enabled, and it stays fixed until disabled and re-enabled; Turbo column-store re-reads its own statistics on every redraw, so an auto-ranged tint can shift on its own as the underlying data changes.
Conditional formatting with AddFormatRule
AddFormatRule covers the cases icons and heatmaps do not: coloring a cell based on a comparison against a threshold, or against the rest of its own column.
SignatureFunction AddFormatRule(colIndex As Long, ruleType As Long, threshold As Variant, backColor As OLE_COLOR, foreColor As OLE_COLOR) As Long
ruleType
Meaning
0
GreaterThan
1
LessThan
2
Equal (numeric or exact text)
3
Contains (case-insensitive substring; threshold must be a string)
4
Between (inclusive; needs a second call to set the upper bound)
5
Top10Pct (threshold is an optional percentage, default 10)
6
Bottom10Pct
7
Duplicate (rendered text appears more than once in the column)
8
Unique (rendered text appears exactly once)
VBA
Sub AmountFormatRules(grid AsObject)
' Flag any Amount over 500 with a warm highlightDim ruleId AsLong
ruleId = grid.AddFormatRule(3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0)) ' 0 = GreaterThan' A Between rule needs its upper bound supplied separately -- without this' second call the rule can never match anythingDim rangeRuleId AsLong
rangeRuleId = grid.AddFormatRule(3, 4, CDbl(100), RGB(220, 240, 255), RGB(0, 0, 80)) ' 4 = Between, lower = 100
grid.SetFormatRuleThreshold2 rangeRuleId, CDbl(200) ' upper = 200' Duplicate reads rendered TEXT, not a numeric range, so it works on any' column type in every mode that admits AddFormatRule at all
grid.AddFormatRule 2, 7, Empty, RGB(255, 255, 220), RGB(0, 0, 0) ' 7 = DuplicateEndSub
A column can carry any number of rules, evaluated in the order they were added; the first rule a cell matches supplies its colors (and its icon, if one was attached with SetFormatRuleIcon) -- later rules on that same column never get a look at that cell. AddFormatRule is refused only in Virtual mode. In the Turbo column-store, GreaterThan, LessThan, Equal, Contains, Between, Duplicate, and Unique all work on any column type, because each is evaluated against one cell's own value or rendered text; only Top10Pct/Bottom10Pct require an Int32/Int64/Double column, for the same reason heatmaps and data bars do -- they need an exact whole-column numeric pass. A useful habit for code that has to run correctly in every mode is checking grid.DataMode directly before adding a Top10Pct/Bottom10Pct rule on a column that might not be numeric in the Turbo column-store, and catching the refusal with On Error Resume Next if it is not.
RemoveFormatRule(ruleId) removes exactly one rule by the id AddFormatRule returned, and -- unlike RemoveIcon -- an unrecognized id is refused with an error rather than silently ignored. There is currently no working way to apply one rule to every column at once -- see AddFormatRule in Part II for how a colIndex of -1 is handled in each mode. ClearFormatRules, SetFormatRuleIcon, and SetFormatRuleThreshold2 round out the rule-management calls and are documented in full in Part II.
Hierarchical and grouped data
Everything above formats a flat grid. When the data itself is hierarchical -- a reporting rollup where each row has a manager, or an assembly where each row belongs to a parent line item -- LiteGrid Turbo has a separate tree view built on top of the ordinary row store: every row in a tree stays a completely real data row, with no synthetic header or group rows inserted anywhere. SetTreeParent is the member that builds it:
VBA
grid.SetTreeParent 1, 0' row 1 reports to row 0
grid.SetTreeParent 2, 0' row 2 reports to row 0
grid.SetTreeParent 3, 1' row 3 reports to row 1
Calling SetTreeParent even once switches the whole grid into tree display, and newly attached rows start out expanded. From there, TreeExpand, TreeCollapse, TreeToggle, GetTreeParent, GetTreeChildCount, GetTreeDepth, IsTreeExpanded, ClearTreeView, and the rest of the tree view's members handle navigating and tearing down a tree. One plain warning worth carrying in: some of those members address a row by its own permanent row number, while others address it by its current on-screen displayRow, which shifts after every expand, collapse, sort, or filter change -- so check which each member expects before mixing them. Tree view is Embedded-mode only, with no partial support in Virtual or Turbo column-store mode. The full reference, the row-number-vs-displayRow rules, and worked recipes for building, collapsing, and reparenting a tree live in Part II under "Tree view."
Selection sounds like one feature until you actually need it, and then it turns out to be three independent mechanisms that are easy to conflate. There is row selection -- the set of rows a user (or your code) has picked, read and written through SelectRow, SelectAll, ClearSelection, GetSelectedRows, GetSelectedRow and IsRowSelected. There is a separate rectangular block selection -- a copy-style cell range tracked through the grid's own focus/anchor state, read and written through SelectBlock and GetBlockSelection. And there are two purely visual preference bits, MultiSelect and SelectionMode, that only change what an interactive mouse click does and how the current selection paints -- neither one changes what the programmatic row-selection API reports or accepts.
Keeping those three apart matters because they do not automatically stay in sync with each other. A row can be selected (IsRowSelected = True) and paint with no visible highlight at all. A block selection can survive ClearSelection untouched. And SelectionMode never changes what SelectRow or GetSelectedRows operate on, no matter which mode is active. This chapter works through each mechanism in turn, plus the small set of navigation aids -- HoveredRow, ScrollToView, and briefly WheelScrollRows/MatchEntry -- that round out how a user interacts with the grid day to day.
Selecting and clearing rows
SelectRow is how you build up a selection from code -- ticking a customer's row after a background validation, restoring a saved selection, or driving a "select all overdue accounts" button. It is purely additive: it never removes a row, and calling it on an already-selected row is a harmless no-op rather than a toggle.
Because there is no per-row Deselect, dropping one specific row out of an existing selection means calling ClearSelection and then SelectRow only on the rows you still want. SelectRow also ignores MultiSelect and SelectionMode completely -- both of those only govern interactive clicks, never this call. Selection itself is tracked by each row's stable identity, not its current display position, so a sort or filter you run afterward does not silently drop what was selected; it just changes which index the row reports under.
SelectAll selects every row currently in the grid -- useful for a "select all / clear all" pair next to a batch-action button. It reaches past an active filter: it walks the whole underlying row store, so a row hidden by a filter is selected too, and reappears in GetSelectedRows once the filter is cleared.
SelectAll does not move focus or the anchor, so it does not disturb what the user was last pointing at. It succeeds without error in Virtual mode but silently selects nothing there -- treat it as non-functional against a Virtual-mode grid, not merely unreliable. See SelectAll in Part II for why.
ClearSelection empties the row-selection set -- the counterpart to SelectAll, and the first step whenever you need to rebuild a selection from scratch. Calling it with nothing selected is a safe no-op.
One thing ClearSelection deliberately does not do: it only clears row selection. An active block created by SelectBlock survives it untouched -- see the block-selection section below before you rely on ClearSelection to reset everything on screen.
Reading back what's selected
GetSelectedRows returns every currently selected row's display index as a 1-D array, always sorted ascending regardless of the order the rows were selected in. It is the member you reach for whenever a batch operation needs to walk "the rows the user picked."
With nothing selected it returns a valid zero-length array, not Empty or Null -- check UBound < LBound (or compare the array's own bounds) rather than testing for emptiness. The indices are display positions, so a sort or filter that runs after the selection was made can hand the same rows back under different numbers.
GetSelectedRow is the convenience form for the common case where you only need one row -- typically the first one a user picked before extending the selection. It always returns the lowest selected display index, scanning ascending and stopping at the first match, not the most recently selected row.
VBA
grid.SelectRow 3
grid.SelectRow 1Debug.Print grid.GetSelectedRow ' -> 1 (the lowest selected index, not the last one selected)
-1 means nothing is selected -- check for it before using the result to index into GetCellValue or GetCell.
IsRowSelected answers a yes/no question about one specific row, which is usually cheaper than pulling the whole array just to test membership.
An out-of-range index (negative, or >= GetRowCount()) raises a runtime error here rather than quietly returning False, so guard the index if it came from anywhere other than the grid itself. IsRowSelected also only reports row-selection state: with SelectionMode set to Cell or Column, a row can answer True here while painting with no row highlight at all, because the paint routine only draws the row highlight in Row mode.
RecipeSelecting by identity so a reload doesn't lose the selection
GetSelectedRows and GetSelectedRow both report display indices, and display indices are only meaningful against the current view. A reload builds a brand-new view, so capturing a business key -- here, CustomerID -- before the reload and re-selecting by matching that key afterward is the reliable pattern, as illustrated in Sorting, Filtering and Finding Data for the same reason sorted/filtered views reshuffle indices.
VBA
Sub SnapshotAndRestoreSelectionByIdentity(grid AsObject)
Dim data(0To4, 0To1) AsVariantDim r AsLong, n AsLong
data(0, 0) = CLng(101): data(0, 1) = "Acme Corp"
data(1, 0) = CLng(102): data(1, 1) = "Bright Star"
data(2, 0) = CLng(103): data(2, 1) = "Cedar Ridge"
data(3, 0) = CLng(104): data(3, 1) = "Dune Traders"
data(4, 0) = CLng(105): data(4, 1) = "Echo Supply"
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerID"
grid.SetColumnHeader 1, "CustomerName"
grid.SelectRow 1' Bright Star
grid.SelectRow 3' Dune Traders' Display indices do not survive a reload, so capture CustomerID insteadDim rows AsVariant
rows = grid.GetSelectedRows
Dim selectedIds() AsLongReDim selectedIds(LBound(rows) To UBound(rows))
For r = LBound(rows) To UBound(rows)
selectedIds(r) = CLng(grid.GetCellValue(rows(r), 0))
Next r
' ... reload the grid with fresh (possibly reordered) data ...
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerID"
grid.SetColumnHeader 1, "CustomerName"' Restore the same rows by matching CustomerID, not the old display index
grid.ClearSelection
For r = 0To grid.GetRowCount - 1For n = LBound(selectedIds) To UBound(selectedIds)
If CLng(grid.GetCellValue(r, 0)) = selectedIds(n) Then
grid.SelectRow r
ExitForEndIfNext n
Next r
Dim finalRows AsVariant
finalRows = grid.GetSelectedRows
Debug.Print finalRows(LBound(finalRows)) ' -> 1 (Bright Star is selected again)EndSub
SelectRow's additive behavior is what keeps the restore loop simple -- there is no need to clear between matches, only once before the loop starts.
Choosing what an interactive click targets
MultiSelect and SelectionMode are both preference bits that only affect the mouse, never the programmatic API above.
MultiSelect decides whether Shift+Click and Ctrl+Click extend an interactive selection instead of replacing it. Set it False on a grid where you only ever want a single row picked at a time, such as a lookup grid feeding a detail form.
VBA
grid.MultiSelect = False' Shift/Ctrl+Click no longer extend the selection' Programmatic selection is unaffected either way:
grid.SelectRow 0
grid.SelectRow 1Debug.Print grid.GetSelectedRow ' -> 0
The default is True. Setting it False only changes what a plain click, Shift+Click or Ctrl+Click does at the mouse -- SelectRow, SelectAll and the rest of the row-selection API keep working exactly the same regardless of its value.
SelectionMode decides what a plain click actually targets -- a row, a single cell, or a whole column -- and, just as importantly, whether the paint routine draws a row highlight at all.
VBA
grid.SelectionMode = 1' Cell
grid.SelectRow 0Debug.Print grid.IsRowSelected(0) ' -> True (but it won't paint as a row highlight in Cell mode)
grid.SelectionMode = 0' back to Row -- the highlight reappears
Value
Meaning
0
Row -- whole-row selection and highlighting (the default).
1
Cell -- single-cell focus with a border; Shift+Click extends a block (SelectBlock).
2
Column -- whole-column selection and highlighting.
SelectRow, GetSelectedRows, GetSelectedRow and IsRowSelected always operate in row terms no matter which of these three is active -- a row added by SelectRow while SelectionMode is Cell or Column is genuinely in the selection set, it simply will not paint until SelectionMode goes back to 0. An out-of-range value raises a runtime error and leaves the mode unchanged. Note also that Column mode (2) has no getter anywhere in this API -- nothing reports which columns a user has selected under Column mode. See MultiSelect and SelectionMode in Part II for the full interactive-click matrix.
A separate rectangular selection: SelectBlock and GetBlockSelection
SelectBlock is for a copy-style rectangular range -- the kind of selection a user makes in Excel by dragging across cells -- and it is a genuinely different mechanism from row selection, tracked through the grid's own focus/anchor state rather than the row-selection set. Reach for it when you need "these cells, as a rectangle" rather than "these rows."
SelectBlock always clears the existing row selection first -- unlike SelectRow, it is not additive -- and then, as a convenience for code that only checks row selection, it also row-selects every row the block spans, so GetSelectedRows/IsRowSelected do see those rows. GetBlockSelection reports the range back in normalized form (rowStart <= rowEnd, colStart <= colEnd) regardless of which corner you originally passed as the "start."
One gotcha worth knowing before you rely on this pair: ClearSelection does not clear an active block. After ClearSelection, GetBlockSelection keeps reporting hasBlock = True with the same coordinates until SelectBlock defines a new block or the user makes a plain interactive click elsewhere in the grid. (SelectBlock also has a narrower input-validation gap worth knowing about before you pass it computed coordinates — see SelectBlock in Part II.)
SelectBlock and GetBlockSelection both raise a runtime error in Turbo column-store mode, so a Turbo caller that needs a copy region has to track it with its own variables, typically alongside a loop of SelectRow calls; Virtual mode fares better but isn't fully clean either. See SelectBlock and GetBlockSelection in Part II for the full Turbo/Virtual mode table.
Because SelectionBackColor/SelectionForeColor paint every kind of highlight identically (more below), a block selection looks exactly like a row selection unless your UI tells the user which one they're looking at -- a status label naming the range, as in the example above, is usually enough.
Coloring the highlight
SelectionBackColor and SelectionForeColor are shared by every highlight this chapter has covered -- a plain row selection, an active SelectBlock range, and the cell/column highlight SelectionMode produces in Cell or Column mode all paint with the same two colors. There is no separate color for block selection or for the Cell/Column mode highlight.
The default for both comes from the active color theme rather than a fixed constant, so read the property first if you need to restore the original value later. SelectionBackColor also only governs the active-window appearance -- when the host window is not active, selected rows paint with a separate inactive-selection color that this property does not control (it has no property of its own and is set through the Selection_Inactive role in ApplyThemeJson). SelectionForeColor is checked against whichever of the two backgrounds is actually painted, and replaced on screen with a more readable color only if it would be unreadable there. See SelectionBackColor and SelectionForeColor in Part II for the full color-precedence details; formatting colors elsewhere in the grid (cell/row background rules, alternating row shading) are covered in Formatting and Presenting Business Data.
Hover feedback and bringing a row into view
HoveredRow is a read-only, live reflection of the mouse position -- reach for it when building a custom hover-driven status readout, since there is no way to set it and no built-in status bar. Poll it from a MouseMove-driven event or a short timer, not once at load time: at load time, before the mouse has ever crossed the grid, it simply reads -1.
VBA
Dim hovered AsLong
hovered = grid.HoveredRow
If hovered >= 0ThenDebug.Print"Hovering row " & hovered & ": " & grid.GetCellValue(hovered, 1)
ElseDebug.Print"Not hovering any row"EndIf
-1 covers several distinct situations identically -- the mouse has never been over the grid, it has left the grid, or it is currently over the header or footer rather than a data row -- so do not read more into -1 than "no data row is under the pointer right now."
ScrollToView scrolls the minimum distance needed to bring a row into the visible band -- the member to call after a search or a validation step lands focus on a row the user cannot currently see.
VBA
grid.ScrollToView 80' brings row 80 into view with the minimum scroll needed
It does nothing if the row is already fully visible, so do not assume every call causes a repaint. rowIndex is a view index -- the same space GetCellValue uses -- and a frozen row is always considered on-screen, so a call targeting a frozen row simply returns without scrolling anything. An out-of-range index raises a runtime error.
Two related preferences round out this group without needing full treatment here: WheelScrollRows controls how many rows a single mouse-wheel notch moves, and MatchEntry controls type-ahead -- jumping focus to the next row whose focused-column text starts with what the user just typed. Both are mode-agnostic to get and set, and both are worth setting on any grid with more rows than fit on screen at once: a slower wheel step for a dense list, and MatchEntry's default (Complete) for letting a user type a whole name rather than only its first letter. Type-ahead is unreliable in Virtual mode, though -- see MatchEntry in Part II for the mode table and the underlying bug.
Once data is loaded into the grid, eight members handle everything it can do with that data on its own: write it to a file, hand it back as a single in-memory string, or put it on the Windows clipboard. They split into three shapes. ExportToCSV, ExportToCSVEx, ExportToPdf and ExportToXlsx write a file and return nothing but success or an error. ExportToJson and ExportToHTML write nothing to disk — they build the whole result in memory and hand it back as one string, for a caller that wants to stream it, POST it, or pass it somewhere else without touching a file. CopySelectedToClipboard and CopyWithHeaders put text on the Windows clipboard for pasting into Excel, Notepad, or back into this same grid with PasteFromClipboard.
All eight read the grid's current view — whatever the active sort and filter currently show — not the raw order data was loaded in, with one deliberate exception you'll meet below (ExportToCSVEx with includeHidden:=True). And all eight follow the same mode rule: they are refused outright in Virtual mode, because a Virtual grid never holds a full dataset of its own for any of them to read. Embedded and Turbo column-store both support all eight, but they don't behave identically — Turbo column-store is the only mode where a call can refuse purely because the result would be too big, and that difference matters enough that the last two sections of this chapter are built around it.
Embedded
Virtual
Turbo column-store
All eight export/clipboard members
supported
refused
supported, with a size or page-count preflight
Writing a CSV file: ExportToCSV and ExportToCSVEx
ExportToCSV is the one to reach for when another system just needs the rows — a nightly extract, an import into another package, anything that reads plain delimited text. It writes the current view to a UTF-8 file (with a byte-order mark, CRLF line endings) with a header line built from the column headers, quoting a field only when it contains a comma, a double quote, or a line break, per RFC 4180.
VBA
Sub ExportInventoryToCSV(grid AsObject)
Dim data(0To1, 0To2) AsVariant
data(0, 0) = "W-100": data(0, 1) = "Widget, Large": data(0, 2) = 10
data(1, 0) = "W-200": data(1, 1) = "Gadget Mini": data(1, 2) = 25
grid.LoadFromArray data
grid.SetColumnHeader 0, "SKU"
grid.SetColumnHeader 1, "Product"
grid.SetColumnHeader 2, "Qty"
grid.ExportToCSV Environ$("TEMP") & "\inventory.csv"' inventory.csv, after a UTF-8 BOM:' SKU,Product,Qty' W-100,"Widget, Large",10' W-200,Gadget Mini,25' -- only the comma inside "Widget, Large" forces quotingEndSub
Every column comes out regardless of SetColumnVisible — there is no way to exclude a hidden column from this call. And the write is never atomic, in either Embedded or Turbo column-store mode: if writing is interrupted partway (most realistically, the disk filling up after a Turbo size check already passed), whatever was written so far is left sitting at filePath, and the call still reports success. In Turbo column-store mode a preflight check estimates whether the file will fit on the target drive and refuses up front if it plainly won't; Embedded mode runs no such check at all.
ExportToCSVEx is the same export with one extra parameter — the tool for the two cases plain ExportToCSV can't cover: bypassing a filter, or getting the data back in its original load order.
VBA
Sub ExportShippedOrdersAndEverything(grid AsObject)
Dim data(0To3, 0To1) AsVariant
data(0, 0) = 1001: data(0, 1) = "Open"
data(1, 0) = 1002: data(1, 1) = "Shipped"
data(2, 0) = 1003: data(2, 1) = "Open"
data(3, 0) = 1004: data(3, 1) = "Closed"
grid.LoadFromArray data
grid.SetColumnHeader 0, "OrderID"
grid.SetColumnHeader 1, "Status"
grid.SetColumnFilter 1, 0, "Shipped"' 0 = Contains
grid.ExportToCSVEx Environ$("TEMP") & "\shipped_only.csv", False' shipped_only.csv: header + exactly the one filtered-in row (1002)
grid.ExportToCSVEx Environ$("TEMP") & "\every_order.csv", True' every_order.csv: header + all four rows, 1001/1002/1003/1004, in' their ORIGINAL load order -- the filter no longer applies hereEndSub
With includeHidden:=False this behaves exactly like plain ExportToCSV. With includeHidden:=True, every row is written — including rows the active filter is currently hiding — but the active sort is abandoned too: rows come out in the grid's own original creation order, not the sorted order. The name is easy to misread: includeHidden refers only to rows a filter is hiding, never to columns hidden with SetColumnVisible — every column is written in both states, exactly like plain ExportToCSV. In Turbo column-store mode, includeHidden:=True is the one CSV write in this pair (and one of only a handful across this whole chapter) that is written atomically — to a temporary file, only taking the real filename once writing finishes cleanly. Every other CSV write here, including includeHidden:=False and everything in Embedded mode, writes directly to the target with no such protection. Note also that SetColumnFilter's Contains mode (0) is the one used above rather than Equals — Turbo column-store mode only implements Contains and StartsWith, so an Equals-based filter would work in Embedded but refuse in Turbo.
See ExportToCSV, ExportToCSVEx, SetColumnFilter, Sort and GetFilteredRowCount in Part II for the full parameter and refusal detail, and Selection and User Interaction for more on how a filter and a selection interact.
Copying to the clipboard: CopySelectedToClipboard and CopyWithHeaders
CopySelectedToClipboard is the shortcut for "let the user paste exactly what they've selected into Excel." It builds tab-delimited, CRLF-terminated text from every currently selected row, in the rows' current on-screen order (not the order they were selected in), always with a header line first, and puts it on the clipboard as plain Unicode text — the same layout Excel produces copying its own cells, and what PasteFromClipboard expects back.
The empty-selection trap is worth knowing before it costs you a real clipboard payload:
VBA
Sub CopySelectionSafely(grid AsObject)
' ... grid already loaded and rows 1 and 3 selected, as above ...
grid.CopySelectedToClipboard ' clipboard: real data' A later handler clears the selection, then re-copies "to be safe"
grid.ClearSelection
grid.CopySelectedToClipboard
' WRONG assumption: nothing selected means nothing happens. It does' not -- this still succeeds and still overwrites the clipboard,' silently replacing the real data above with just the' "CustomerName<TAB>Balance" header line, because the header line' alone is never empty' Safe pattern: know there's at least one selected row, or use' CopyWithHeaders(False) instead, which genuinely leaves the' clipboard alone when nothing is selectedDim anySelected AsBoolean, r AsLongFor r = 0To grid.GetRowCount - 1If grid.IsRowSelected(r) Then anySelected = True: ExitForNext r
If anySelected Then grid.CopySelectedToClipboard
EndSub
CopyWithHeaders is the same copy with the header line made optional — and it's the member that actually solves the trap above, because CopyWithHeaders(False) is the one call in this pair whose result can be genuinely empty:
VBA
Sub CopyDataOnly(grid AsObject)
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Contoso Ltd": data(1, 0) = "Fabrikam Inc"
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SelectRow 0
grid.CopyWithHeaders False' clipboard now holds only: Contoso Ltd (no header line)
grid.ClearSelection
grid.CopyWithHeaders False' nothing selected, includeHeaders is False -- the text built is' genuinely empty, so this call returns without ever touching the' clipboard; "Contoso Ltd" is still sitting there from beforeEndSub
CopyWithHeaders(True) reproduces CopySelectedToClipboard's always-include-headers behavior exactly — with nothing selected it still overwrites the clipboard with a header-only line, the same trap as above. Both clipboard members copy in current on-screen order, never selection order, and both include every column regardless of SetColumnVisible. Both are refused in Virtual mode, and in Turbo column-store mode both run a memory-based preflight before committing to the clipboard and refuse if the copy would be too large; Embedded mode runs no such check.
See CopySelectedToClipboard, CopyWithHeaders and PasteFromClipboard in Part II, and SelectRow / GetSelectedRows covered in Selection and User Interaction for how a selection gets built in the first place.
Returning data as a string: ExportToJson and ExportToHTML
ExportToJson is the one to reach for when the destination is code, not a file — handing rows to a web call, another COM component, or anything that wants structured text without a round trip through disk. It returns an array with one object per row, in current view order, each keyed by the column headers.
The one thing worth internalizing before this call surprises you: every value comes out as a JSON string, never a native JSON number or boolean — a Long, Double, Currency or Date column is written as a quoted digit string or display text, not a real JSON number. There's no parameter to change that; code reading the result back has to convert the string itself. A cell with no display text, including a genuinely Null value, comes out as the bare JSON literal null, never an empty string — the two need to be treated differently on the way back in. One more wrinkle worth knowing: a Boolean column's quoted text is "True"/"False" in Embedded mode, but "-1"/"0" in Turbo column-store mode — see ExportToJson in Part II for why, and for how to round-trip a Boolean column through JSON there. Like the CSV members, every column is included regardless of SetColumnVisible, and Virtual mode refuses the call outright; Turbo column-store mode also runs a memory preflight before returning the string and can refuse if the result would be too large.
ExportToHTML returns a single, already-styled <table>...</table> — useful for an emailed report or a quick preview pane where you want something that looks like the grid without embedding the grid itself.
VBA
Sub ExportOrdersToHTML(grid AsObject)
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Contoso Ltd": data(0, 1) = "Net 30 & prepaid"
data(1, 0) = "Fabrikam Inc": data(1, 1) = "Balance < 100"
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SetColumnHeader 1, "Terms"Dim html AsString
html = grid.ExportToHTML()
' cell text is HTML-escaped -- "&" and "<" above come out as' & and < -- and the inline styles (header colors, alternating' row color, row font) come straight from the grid's CURRENT theme' at the moment of exportEndSub
Because the inline styles are pulled from the live theme and appearance settings, calling ExportToHTML again after changing Theme, AlternateRowColor or RowFontName produces visibly different HTML from the same underlying data. One Embedded-mode-only wrinkle worth knowing: the current RowFontName is written into the table's style attribute with no escaping, so a font name containing a quote character can corrupt the exported markup — Turbo column-store mode escapes it safely, and cell data itself is never affected in either mode. ExportToHTML is refused in Virtual mode and, in Turbo column-store mode, runs a memory-based preflight like ExportToJson's. See ExportToHTML in Part II for the exact preflight sizing and the full detail on that RowFontName edge case.
See ExportToJson, ExportToHTML, Theme and AlternateRowColor in Part II for the rest.
Column visibility: two different rules, not one
This is the detail most worth writing down rather than trying to remember: SetColumnVisible is honored by some of these eight members and silently ignored by the rest, and nothing in a method's name tells you which side it falls on.
Always includes every column, hidden or not:ExportToCSV, ExportToCSVEx, ExportToJson, CopySelectedToClipboard, CopyWithHeaders.
Leaves a hidden column out entirely, the same as the screen:ExportToPdf, ExportToXlsx, ExportToHTML.
VBA
Sub CompareExportsAcrossFormats(grid AsObject)
Dim data(0To2, 0To2) AsVariant
data(0, 0) = "Contoso Ltd": data(0, 1) = "Internal": data(0, 2) = 10
data(1, 0) = "Fabrikam Inc": data(1, 1) = "Internal": data(1, 2) = 20
data(2, 0) = "Alpine Traders": data(2, 1) = "Internal": data(2, 2) = 30
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SetColumnHeader 1, "Source"
grid.SetColumnHeader 2, "Score"' "Source" is internal bookkeeping nobody wants on a printed report
grid.SetColumnVisible 1, False
grid.ExportToCSV Environ$("TEMP") & "\export_demo.csv"Dim json AsString: json = grid.ExportToJson()
Dim html AsString: html = grid.ExportToHTML()
Debug.Print InStr(json, "Source") > 0' -> True (CSV: same result)Debug.Print InStr(html, "Source") > 0' -> False -- HTML respects itEndSub
If "Source" genuinely must never leave the building in any format, hiding the column is not enough on its own for CSV, JSON or the clipboard members — build a narrower array without that column instead, or strip it from the result downstream. The same SetColumnVisible call that correctly hides a column from a printed PDF report will silently fail to hide it from a CSV a scheduled task emails out an hour later; the fix is knowing which list a given export belongs to, not adding more hiding calls. If a user's chosen column visibility needs to survive between sessions, see Saving and Restoring the User's Grid Layout.
Writing to Excel and PDF: ExportToXlsx and ExportToPdf
ExportToXlsx writes a real, valid Open XML workbook with no external library — the one to reach for when the recipient just wants to open a file in Excel, no COM automation of Excel required on your end.
VBA
Sub ExportOrdersToXlsx(grid AsObject)
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Contoso Ltd": data(0, 1) = 1250.5
data(1, 0) = "Fabrikam Inc": data(1, 1) = 0
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SetColumnHeader 1, "Balance"
grid.ExportToXlsx "C:\Exports\customers.xlsx"' customers.xlsx opens directly in Excel: row 1 is a bold header,' rows 2-3 hold the data -- but Balance opens as plain, left-aligned' TEXT, not a number Excel can SUM, until it's convertedEndSub
Only visible columns are included, in the order they appear in the grid. Every value — including one that happens to start with =, +, - or @ — is written as a literal inline string, never a formula, so nothing in an exported workbook is ever evaluated by Excel; there's no formula-injection risk from exported data by construction, but it also means a numeric or date column opens as ordinary text rather than something Excel can sum, sort or date-math on until the user runs Convert to Number or Text to Columns. None of the grid's own cell colors, data bars, heatmaps or icons carry over — formatting is limited to a bold header row. An empty file path is refused, and so is a grid with no columns at all.
ExportToPdf writes a paginated PDF, also with no external library, using a fixed US-Letter page layout — margins, data text size and header size are all fixed, with no property to change any of them.
VBA
Sub ExportCustomersToPdf(grid AsObject)
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Contoso Ltd": data(1, 0) = "Fabrikam Inc"
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.ExportToPdf "C:\Exports\customers.pdf"' customers.pdf: one page, a "CustomerName" header row, then the two' names beneath it, in a standard US-Letter layoutEndSub
Like ExportToXlsx, only visible columns are included — each proportionally scaled to fill one page's width, so a wide grid grows taller, never wider than one page, and the header row repeats at the top of every page a large row count spans. Text renders through a standard, non-embedded font limited to plain ASCII: any character outside that range — including ordinary accented letters such as e-acute or u-umlaut, not just non-Latin scripts — prints as a literal question mark. An empty file path is refused, and a grid with no columns at all is refused with its own distinct error.
Both writes are atomic in Turbo column-store mode (a temporary file is promoted to the real name only once writing finishes cleanly) and non-atomic in Embedded mode, where an interrupted write can leave a truncated file in place while still reporting success.
See ExportToXlsx and ExportToPdf in Part II for the exact page layout (margins and font sizes) and the archive's ZIP-compression details.
Treat a refusal as a real outcome, not a bug to chase
This is the behavior most worth carrying out of this chapter. ExportToXlsx and ExportToPdf each run a hard, unconditional check before anything else — one Embedded mode simply doesn't have:
ExportToXlsx refuses outright if the row count (including the header row this engine always writes) or column count would exceed Excel's own hard format ceiling, regardless of how much disk space is available. Only after that check passes does an ordinary disk-space estimate run. Embedded mode has no equivalent check at all; it will attempt to write a workbook past Excel's own limit, producing a file Excel itself then refuses to open.
ExportToPdf refuses outright above a fixed page-count ceiling, computed from the same page layout the export would actually render — independent of disk space, and checked before any disk-space estimate runs at all. Embedded mode has no equivalent ceiling and simply attempts the write, however many pages the result would need.
See ExportToXlsx and ExportToPdf in Part II for the exact row/column and page-count ceilings these checks use.
A production export routine — a scheduled task, an "Export All" button over a dataset that grows every month — needs to treat that refusal as a real, expected outcome once the data is large enough, not an edge case to special-case away:
VBA
Sub ExportDefensively(grid AsObject, basePath AsString)
Dim ok AsBooleanOnErrorResumeNextErr.Clear
grid.ExportToXlsx basePath & ".xlsx"
ok = (Err.Number = 0)
OnErrorGoTo0Debug.Print"XLSX: " & IIf(ok, "written", "refused - " & Err.Description)
OnErrorResumeNextErr.Clear
grid.ExportToPdf basePath & ".pdf"
ok = (Err.Number = 0)
OnErrorGoTo0Debug.Print"PDF: " & IIf(ok, "written", "refused - " & Err.Description)
' Both checks above are real, and only run in Turbo column-store' mode. Code that calls these exports unattended should catch the' error and decide what happens next -- shrink the request, tell the' user, log it -- rather than assume the call always succeeds,' especially since the very same call behaves differently purely' because of which DataMode the grid happens to be in.EndSub
The size and page-count checks described across this chapter exist only in Turbo column-store mode, and only for the reason named in each error message — a real ceiling (disk space, process memory, Excel's own format limit, or a fixed page count), never a guess. If a size estimate turns out wrong, it is only ever wrong pessimistic: it can refuse a request that would actually have fit, but it will never let through one that would not.
"Remember what the user had" is not one feature in LiteGrid Turbo -- it is several independent, composable ones, and picking the right one saves you from writing your own JSON around a method that already returns JSON:
GetViewState / SetViewState -- the whole view, in every data mode (Embedded, Virtual, Turbo column-store), as one versioned JSON token with a built-in guard against restoring onto the wrong grid. The mode-neutral tool to reach for first; see VIEW_STATE_WORKFLOW_DESIGN.md for the full design rationale.
GetGridState / SetGridState -- the same idea, Embedded-mode-only, unchanged since before GetViewState existed. Kept exactly as-is; GetViewState does not replace or redirect it.
GetColumnLayout / SetColumnLayout -- columns only: width, visibility, alignment, and type. Nothing about filters, sort, or colors.
SaveFilterPreset / LoadFilterPreset / DeleteFilterPreset / GetFilterPresetNames -- named, switchable filter setups kept in memory on the control.
GetCurrentThemeJson / ApplyThemeJson -- a snapshot of the grid's resolved color palette, unrelated to columns, filters, or sort.
GetColumnStylesJson / ApplyColumnStylesJson -- per-column background and text colors, which are not part of the palette snapshot.
GetRowSelectionState / SetRowSelectionState -- which records the user had selected, remembered by your own business key so the selection survives a reload that renumbers every row. Embedded and Turbo column-store; Virtual refuses.
None of these share storage, and none of them call each other except where noted below. Save only the piece you actually need to restore; there is no requirement to use more than one together.
Saving the whole view across every mode: GetViewState and SetViewState
GetGridState/SetGridState (below) only work in Embedded mode. GetViewState/SetViewState answer the same "remember what the user had" need in all three data modes, with two things GetGridState does not have: a structural check that refuses a token saved against a different logical dataset, and an optional caller-supplied key for an even stricter guard.
VBA
Dim savedView AsString
savedView = grid.GetViewState()
' ... later, possibly a different session, after the same (or a compatible) schema is loaded ...
grid.SetViewState savedView
What is saved: column order, width and visibility; sort (including multi-level and natural sort, and an explicit "unsorted" state); frozen rows/columns; scroll position; and, in Embedded and Virtual modes only, filters. In Turbo column-store, filters are left out of the token entirely (not written as an empty placeholder) -- Turbo's own filter engine has no way to read its active rules back yet.
What is deliberately never saved, in any mode: cell values, row edits, or anything else about the data itself; the current selection; the color theme; and tree/column-group expand-collapse state (none of the last three survive a reload with a stable enough identity to restore correctly, so none of them are guessed at). If your application needs any of those remembered too, that is a separate, deliberate choice you make with the members built for it (ApplyThemeJson for theme, for instance) -- GetViewState will not silently start covering them later just because it happens to touch the same grid.
Mode differences that matter:
Embedded works exactly like GetGridState/SetGridState under the hood -- every section is fully engine-owned and fully restorable.
Turbo column-store restores columns, sort and frozen/scroll the same way; filters are the one thing left out (see above).
Virtual restores columns, frozen and scroll for real, but sort and filters are a different kind of promise: the token records the last sort/filter request the host acknowledged, not a guarantee about the actual order of the host's rows. Restoring replays that request through the SAME SortRequested/FilterRequested events a live user action already fires -- if your host declined it live, it declines it here too, and SetViewState reports that section as failed rather than pretending the host's data was reordered.
Schema/viewKey mismatch: every token carries a structural fingerprint (column headers and types) captured at save time. If it doesn't recognizably match the grid you're restoring onto -- not even as "some columns were added or removed since" -- SetViewState refuses the entire restore before touching anything, rather than guessing a column-by-column mapping between two different tables. This is what stops a view saved for one grid (say, a Customers list) from being silently, plausibly, wrongly applied to an unrelated one (an Employees list) that just happens to have the same number of columns.
For an even harder guarantee than the automatic fingerprint, pass your own key:
VBA
savedView = grid.GetViewState("CustomersGrid")
' ...
grid.SetViewState savedView, "CustomersGrid"' refuses outright if this token wasn't saved with a matching key
Passing "" (the default) for either parameter skips that specific check -- the automatic structural fingerprint still applies either way.
Partial failure: past the schema/key gate, SetViewState behaves like SetGridState already does -- every section that can apply, does, independently of the others; a section that fails (a malformed sub-document, a sort column that no longer exists) is named in the error and left unchanged, and the overall call reports a real failure rather than a false success. It is not transactional: sections that succeeded before a later one failed stay applied.
See GetViewState and SetViewState in Part II for the full JSON token shape and every mismatch/failure case.
Saving the whole view, Embedded only: GetGridState and SetGridState
When a user closes a form and you want the grid to look the same the next time they open it -- same columns, same filter, same sort, same scroll position, same frozen panes -- GetGridState captures all of that as one JSON string, and SetGridState puts it back.
VBA
Sub SaveAndRestoreGridState(grid AsObject)
Dim data(0To2, 0To1) AsVariant
data(0, 0) = "Acme Corp": data(0, 1) = CDbl(1250)
data(1, 0) = "Bolt Inc": data(1, 1) = CDbl(430)
data(2, 0) = "Crane LLC": data(2, 1) = CDbl(980)
grid.LoadFromArray data
grid.SetColumnHeader 0, "CustomerName"
grid.SetColumnHeader 1, "Balance"Dim savedState AsString
savedState = grid.GetGridState()
Debug.Print savedState
' -> {"version":1,"columns":[...],"filters":[...],"sort":{...},"scroll":{...},"frozen":{...}}' ... user closes the form; later, after LoadFromArray has put the same' rows back in the same column order ...
grid.SetGridState savedState
EndSub
GetGridState and SetGridState only work against a grid in Embedded mode -- Virtual mode refuses them because that view must be driven from the host's own data source, and Turbo column-store mode refuses them because its surface is read-only. If your form can run in more than one data mode, check DataMode before you rely on either member, or reach for GetColumnLayout instead (below), which works in all three.
The row data itself is never part of the payload -- if you need the rows back too, that is your own responsibility (reload from the same query, array, or recordset) before you call SetGridState. Malformed JSON passed to SetGridState is rejected outright and changes nothing. See GetGridState and SetGridState in Part II for the exact shape of each JSON section and how a partial payload is applied.
Remembering which records the user selected: GetRowSelectionState and SetRowSelectionState
Everything above remembers how the grid looks. This pair remembers which records the user picked — by your own business key, so the selection survives a reload, a re-sort, or a re-filter that renumbers or reorders every row.
VB
Dim saved AsStringDim n AsLong
saved = grid.GetRowSelectionState(0) ' column 0 holds CustomerID
grid.QuickBindRecordset rs ' reload -- this clears the selection
n = grid.SetRowSelectionState(saved) ' n = how many records were found and re-selected
Composed with View State, when you want the whole screen back:
VB
Dim savedView AsString, savedSelection AsString
savedView = grid.GetViewState()
savedSelection = grid.GetRowSelectionState(0)
grid.QuickBindRecordset rs
grid.SetViewState savedView ' columns/sort/filters/panes FIRST ...
grid.SetRowSelectionState savedSelection ' ... then the records, resolved against the settled layout
Restore View State first. Its own restore can move columns, and the selection restore has to find your key column after any such move, not before it.
You must name a business key column. The token stores the values in that column for the selected rows — CustomerID 1003, 1042, 1107 — never row numbers. That is the whole point: row 4 after a reload is not the same record as row 4 before it, but customer 1042 always is. Pick a column that genuinely identifies the record; an unstable or computed column makes an unstable token.
Typed equality, not text. Values are compared as the types they actually are, so 123 (a Long) and "123" (a String) are different keys, and a Date or Currency key matches on its exact stored value rather than on however it happens to be formatted on screen. A Null key matches only another Null — never 0 or "".
One type per key column. If the selected rows' key values span more than one non-null type — some Long, some String — GetRowSelectionState refuses and names both types rather than guessing. Null may coexist with the one real type.
Duplicates are visible, not hidden. Each distinct key is stored once. On restore, every row matching a saved key is selected, so if n comes back larger than the number of keys you saved, your key column is not unique in the current data — a real signal you can check for, not something the grid papers over.
Partial matches are normal and reported. If some records are gone, the ones still present are selected and n tells you how many. It is never an error, and never a claim that everything came back.
Filter-hidden records still count. A saved record that the current filter hides is still selected, exactly as SelectAll already reaches past a filter. Clear the filter and it is there, selected, waiting.
Optional cross-dataset guard. Two grids sharing a schema — Customers and ArchivedCustomers, or two tenants' data — can hold the same ID values for entirely different records. If that is a risk in your application, label the token:
VB
saved = grid.GetRowSelectionState(0, "Customers")
n = grid.SetRowSelectionState(saved, "Customers") ' refuses outright if the token says otherwise
Omit the label (the default) and the check is simply not performed.
What it will refuse, changing nothing: a malformed or unsupported token, a scope label that does not match, and a key column that no longer exists or has changed type. In every one of those cases the current selection is left exactly as it was.
Mode support: Embedded and Turbo column-store. Virtual mode refuses both members by name — in Virtual mode the host owns the data and the grid only ever holds the rows near the viewport, so it cannot honestly search a key column it has never seen.
Focus is not restored — only the row selection. Restoring the focused row would move the scroll position unexpectedly; if you want that too, do it yourself with ScrollToView after restoring.
Reloading a Recordset without losing the view: RefreshFromRecordset
The four-line recipe above — save the view, save the selection, reload, restore both — is the single most common thing you do with a Recordset-backed grid, and it is exactly the sequence people get subtly wrong. RefreshFromRecordset is that recipe as one call:
VB
grid.RefreshFromRecordset rs, lgRefreshDefault, 0
That is equivalent to the composed version, with the ordering and the error handling already right. The third argument is your business key column, exactly as GetRowSelectionState means it; pass -1 (the default) to skip selection entirely:
VB
grid.RefreshFromRecordset rs ' preserve the view, don't bother with selection
This is a reload, not a binding. The grid does not keep the Recordset, does not watch it, and does not synchronize with it. Nothing updates on its own. Call this again when you want new data again — that is the whole model, and it is deliberately the whole model. There is no DataSource property.
Your user's place in the data survives a reordered result set. The selection is restored by key, so if the server hands back the same records in a different order — or with different values — the record your user had highlighted is still the highlighted one, wherever it now sits. Records that disappeared are simply not selected; that is a successful refresh, not an error.
What options selects.lgRefreshDefault (the default, 1) preserves column layout, sort, frozen panes, scroll and — where the mode owns them — filters. lgRefreshNothing (0) reloads and preserves nothing. The five section bits (lgRefreshColumns, lgRefreshSort, lgRefreshFrozen, lgRefreshScroll, lgRefreshFilters) can be OR'd together when you want only some of it back:
VB
grid.RefreshFromRecordset rs, lgRefreshSort Or lgRefreshScroll, 0
An unrecognized bit is ignored rather than treated as an error, so code written against a later version still runs here.
In Turbo column-store, filters are not preserved. Everything else is — layout, sort, frozen panes, scroll and the selection — but a filter set in column-store mode is lost across the refresh. This is inherited rather than chosen: GetViewState does not capture filters in Turbo, so there is nothing to put back. It applies to filters you set programmatically with SetColumnFilter too, not just ones a user set, and there is no UI to set one in Turbo today. In Embedded mode filters come back with everything else.
Events, and they are the same in both supported modes. A successful refresh raises exactly DataLoaded(rows, cols) and then DataChanged(0). The clear that happens in the middle does not surface — your handler is not told the grid emptied and then refilled, because from the caller's point of view it did not. Note that DataChanged(0) is raised by this member and not by a plain LoadFromRecordset. Restoring sort or filters additionally raises those sections' own AfterSort/FilterChanged, and a selection restore raises exactly one SelectionChanged.
What happens when it goes wrong matters more than the happy path, so it is defined precisely:
Outcome
What your handler sees
What the grid looks like
Success
DataLoaded then DataChanged(0)
new data, old view
Refused before it starts
nothing at all
completely unchanged
The load failed after the grid was emptied
exactly DataChanged(1)
genuinely empty
Only the refusal case is all-or-nothing. Once the loader has emptied the grid there is no going back, and the method does not pretend otherwise: it will not restore your saved view over data that failed to load, because a grid showing your old sort and your old selection over no rows is a lie. You get the loader's own error message, and an empty grid.
A materially changed schema refuses the restore, and says so. If the new result set has different columns, the saved view describes columns that no longer exist — so applying it by position would silently attach your sort or filter to the wrong data. Instead the whole restore is refused by name, the reload still stands, and the call returns an error naming what could not be restored. Handle it or ignore it; either way the new data is on screen.
Mode support: Embedded and Turbo column-store. Virtual mode refuses by name before anything is captured or changed, for the same reason the members it composes do — the host owns the data.
See RefreshFromRecordset, LGRefreshOptions in Part II for the full reference.
Saving just the columns: GetColumnLayout and SetColumnLayout
When you only care about column width, visibility, alignment, and type -- not filters, not sort, not scroll position -- GetColumnLayout and SetColumnLayout are the narrower tool, and unlike GetGridState they work in every data mode, including Virtual and Turbo column-store. That makes them the right choice for persisting a user's preferred column arrangement across a Turbo column-store reload, where GetGridState is not an option at all.
VBA
Sub BuildAndPersistColumnLayout(grid AsObject)
Dim savedLayout AsStringDim restoredWidth AsLong
grid.Clear
grid.AddColumn "SKU", 120
grid.AddColumn "UnitPrice", 90
grid.AddColumn "Notes", 200
grid.SetColumnWidth 1, 110
grid.SetColumnVisible 2, False
grid.SetColumnAlignment 1, 2' right-align UnitPrice' Save width, visible, alignment and type for every column, as JSON
savedLayout = grid.GetColumnLayout
' ... later, perhaps in a different session, after the columns exist again ...
grid.SetColumnWidth 1, 50' someone changed it
grid.SetColumnVisible 2, True' someone unhid it
grid.SetColumnLayout savedLayout
restoredWidth = grid.GetColumnWidth(1)
Debug.Print"UnitPrice column width restored to " & restoredWidth ' -> 110EndSub
Header text is deliberately excluded -- GetColumnLayout's JSON has index, width, visible, alignment, and type per column, not the header caption. If your users rename columns and you want those renames to survive a restore, save the headers yourself, separately. There is also no GetColumnVisible getter anywhere in the object model, so the last line of a layout round-trip that needs to know whether a column ended up visible has to read it back out of the GetColumnLayout JSON.
SetColumnLayout applies a saved layout back onto the same column list, matching by index -- it assumes the grid's column count and order haven't changed since the layout was captured. A column index in the JSON that doesn't match an existing column is silently skipped, not an error, and a width of exactly 0 is treated as "not specified" and never applied, so you cannot use this member to force a column to width 0. One caution: a string with no opening [ at all (including an empty string) raises a runtime error rather than being silently skipped, so don't pass an arbitrary string without expecting to handle that. See SetColumnLayout in Part II for exactly how a truncated JSON array is handled.
One asymmetry worth knowing about: SetColumnLayout can change a column's type (its "colType" field) even in Turbo column-store mode, even though calling SetColumnType directly on that same column is refused there. See SetColumnType, SetColumnWidth, SetColumnVisible, and SetColumnAlignment in Part II for the underlying per-column members GetColumnLayout/SetColumnLayout summarize.
Saving named filter setups: the filter preset family
GetGridState bundles the current filter into a full-view snapshot, but sometimes what you want is a handful of named, switchable filter setups the user can jump between -- "OverdueOnly", "HighBalance" -- without touching columns, sort, or scroll at all. That's what SaveFilterPreset, LoadFilterPreset, DeleteFilterPreset, and GetFilterPresetNames are for.
Presets live in memory on the control instance only -- nothing is written to disk or the registry, and every preset is gone the moment the control is destroyed. Neither Clear() nor LoadFromArray removes a saved preset; only DeleteFilterPreset does, which means a preset saved before a reload is still there afterward if you want it. Saving under a name that already exists silently overwrites the old preset -- no error, no confirmation -- and an empty preset name is refused outright.
SaveFilterPreset and LoadFilterPreset both refuse in Turbo column-store mode: capturing a preset there would silently record "no filter" even when a real Turbo column-store filter is active, and loading that preset back later in an Embedded grid would then wrongly clear the user's actual filters. Guard calls to SaveFilterPreset with a DataMode check if the same code path can run against a Turbo column-store grid:
VBA
If grid.DataMode <> 2Then' SaveFilterPreset is refused in Turbo column-store mode
grid.SaveFilterPreset "Temp"EndIf
grid.DeleteFilterPreset "Temp"' safe even if it was never saved, and safe to call twice
DeleteFilterPreset and GetFilterPresetNames are not subject to that restriction and work in every data mode, including Turbo column-store, since they only touch the in-memory name map. Deleting an unknown name is a silent no-op rather than an error, and GetFilterPresetNames returns "[]" -- a valid empty JSON array, not an error -- when nothing has been saved yet. Preset names come back JSON-escaped, so a name containing a quote or backslash round-trips safely, but the order they appear in is unspecified.
Keep in mind that a preset captures filters only, not sort order -- if the user's whole view needs to come back exactly as it was, save sort state separately (or use GetGridState, in Embedded mode, which bundles both together). For background on the filter mechanics these presets sit on top of -- filter modes, multi-column filters, ClearAllFilters -- see Sorting, Filtering and Finding Data. SaveFilterPreset is built directly on GetFilterState, and LoadFilterPreset forwards to SetFilterState; see both in Part II for the lower-level state these two presets wrap.
Saving a color palette: GetCurrentThemeJson and ApplyThemeJson
Color state is tracked completely separately from columns, filters, and sort -- there is no color section in GetGridState's JSON, and no mode restriction on either of these two members: both work identically in Embedded, Virtual, and Turbo column-store grids, because they touch pure rendering state.
VBA
Sub SaveAndRestorePalette(grid AsObject)
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.Theme = 0
grid.HeaderBackColor = RGB(22, 54, 92)
grid.HeaderForeColor = RGB(255, 255, 255)
' Capture every color role currently in effect -- the theme's own' starting colors plus both header overrides -- as one JSON stringDim saved AsString
saved = grid.GetCurrentThemeJson()
' ... later, after something else has changed the palette ...
grid.Theme = 1' switch to Dark; this discards the header overrides tooDebug.Print grid.HeaderBackColor <> RGB(22, 54, 92) ' -> True' Restore everything in one call
grid.ApplyThemeJson saved
Debug.Print grid.HeaderBackColor ' -> 6033430 (back to the saved override)' The saved string has no idea Theme was ever 0 -- it is a snapshot of' resolved colors only. Theme still reads 1 (Dark) after the restore,' even though every color now matches what Theme 0 plus the overrides' had produced.Debug.Print grid.Theme ' -> 1 (unchanged by ApplyThemeJson)EndSub
GetCurrentThemeJson returns all 36 of the grid's internal color roles, resolved -- the active theme's own starting color for any role that hasn't been individually overridden, and the actual override value for any role that has, whether that override came from a discrete color property or from a previous ApplyThemeJson call. It is a live snapshot, not a record of "the last theme applied": call it again after any single color property change and the new value shows up immediately.
ApplyThemeJson is the inverse. You don't have to start from a full GetCurrentThemeJson snapshot -- a hand-built string naming just one or two roles works too, and every role it does not name keeps its current color:
Every entry has to be right, or nothing is applied. An unrecognized role name, a malformed hex value, a role named twice, a value that is not a string, or text that is not well-formed JSON raises a runtime error whose description names the problem, and the palette stays exactly as it was -- including the roles in that same string that were valid. An empty string is refused too. One error handler is therefore enough to know whether a saved palette was restored:
VBA
OnErrorResumeNext
grid.ApplyThemeJson savedPalette
IfErr.Number <> 0ThenDebug.Print"Palette not restored: " & Err.Description ' every previous color is still in placeOnErrorGoTo0
Most color roles -- every grid line, focus indicator, footer color, heatmap tint stop, and the three status colors -- have no dedicated property anywhere in the object model, so GetCurrentThemeJson/ApplyThemeJson are the only way to read or write them; a handful of others (BackColor for Surface_Base, ForeColor for Text_Primary, and a few more) do.
Two traps worth remembering: setting Surface_Header through ApplyThemeJson does not also update the Header_GradientTop/Header_GradientBottom roles the way setting the HeaderBackColor property does, so the header can end up a flat color that visibly clashes with its own gradient edges unless you set all three roles together. And setting Theme at any point afterward discards everything ApplyThemeJson just applied -- Theme and the resolved-color snapshot are related but not the same state. See Theme, HeaderBackColor, and BackColor in Part II, and call GetCurrentThemeJson yourself to see the full list of role names ApplyThemeJson understands.
Per-column colors are not part of that snapshot. They are set with SetColumnBackColor, SetColumnForeColor or ApplyColumnStylesJson, stay in place through any theme change, and are saved and restored as a pair of their own:
VBA
Dim savedStyles AsString
savedStyles = grid.GetColumnStylesJson() ' e.g. {"columns":[{"index":2,"back":"#EEF4FA"}]}' ... later, once the same columns have been loaded again ...
grid.ApplyColumnStylesJson savedStyles
ApplyColumnStylesJson is all-or-nothing in the same way, and it checks each index against the columns the grid has at the moment of the call -- so restore column styles after loading the data, not before.
Which one do you actually need?
The whole view, in Embedded mode only, including filters and sort and scroll: GetGridState / SetGridState.
Just column width/visibility/alignment/type, in any data mode: GetColumnLayout / SetColumnLayout.
A few named filter setups the user can switch between, in Embedded or Virtual mode: SaveFilterPreset / LoadFilterPreset / DeleteFilterPreset / GetFilterPresetNames.
Colors only, independent of everything else, in any data mode: GetCurrentThemeJson / ApplyThemeJson.
Per-column background and text colors: GetColumnStylesJson / ApplyColumnStylesJson.
They can be combined -- save a column layout and a theme snapshot side by side in your own settings table, for instance -- but each one is responsible for exactly the slice of state described above, and none of them reach into another's territory.
Your First Grid previewed the shape a real form takes: a typed m_grid variable, Form_Load doing the wiring, native lgGrid_* event handlers, Form_Close releasing the reference. That preview quietly skipped licensing. This chapter fills that gap with the canonical Registered-mode lifecycle exactly as a production form should use it, then builds outward from it — adding a derived column, a collapsible column group, a column chooser, and a filter-aware totals footer to the same grid. Nothing here is a new concept; it is earlier chapters' members composed on one screen the way a real project actually assembles them.
The canonical Registered-mode lifecycle
A Registered-mode project needs exactly two pieces of code: one shared module that holds the activation key, and a per-form template that every screen in the project follows. The form code never sees the key itself — it calls one sub. Rotate the key once, in the shared module, and every form picks up the change the next time it runs.
modLGTLicense — the shared license module
Create this module once per project (VBA editor: Insert → Module, set (Name) to modLGTLicense). The block below is the entire module — paste it as-is:
VBA
' ===== modLGTLicense — START ================================================OptionExplicit' Edit LGT_COMPANY and LGT_KEY ONCE here. Forms call EnsureLicensed.' Leave LGT_KEY empty to run on the evaluation window signed into your copy.PrivateConst LGT_COMPANY AsString = "Your Company"PrivateConst LGT_KEY AsString = ""' "V1|..."Private m_activated AsBoolean' Activate the session license against any LiteGrid Turbo control instance.' Safe to call from every form's Form_Load — only the first call does the' work; later calls are a no-op. Activation is process-wide and session-only,' so it must run once per host-process launch (see Licensing and Trial,' Chapter 20, for the full activation and trial-state reference).PublicSub EnsureLicensed(ByVal ctrl AsObject)
If m_activated ThenExitSubIf ctrl IsNothingThenExitSubIf Len(LGT_KEY) = 0ThenExitSub
ctrl.ActivateLicense LGT_COMPANY, LGT_KEY
m_activated = TrueEndSub' ===== modLGTLicense — END ==================================================
EnsureLicensed is deliberately dumb: it checks a module-level flag, and either calls ActivateLicense (see Part II) once or does nothing. Every form in the project calls it the same way, passing its own grid instance, and the module-level m_activated flag makes every call after the first a no-op for the life of the process. Leaving LGT_KEY empty is a valid, supported state — the sub simply exits without activating anything, and the control runs on the evaluation window signed into your copy.
Every form that hosts a grid follows this shape. The comments are part of the template — keep them; they document the two things that are easy to get wrong the first time (the .Object unwrap, and the fact that there is no ready event to wait for):
VBA
' ============================================================================' Registered Mode — canonical lifecycle template (per form)' ============================================================================' Form prerequisite:' - A LiteGrid Turbo ActiveX control on the form, named lgGrid' (Design view -> Insert ActiveX Control -> LiteGridTurbo.LiteGridTurboCtrl)'' Project prerequisites:' - modLGTLicense module added to the project (holds the activation key)' - Optional, for IntelliSense: Tools -> References -> check' "LiteGridTurboLib". Without it, change the declaration below to' "Private m_grid As Object" — everything still works late-bound.'' Unlike a browser control there is NO ready event to wait for: the grid is' usable the moment Form_Load runs. License, mode, and data all go here.' ============================================================================Option Compare Database
OptionExplicit' Typed reference — gives IntelliSense on every m_grid.* call.Private m_grid As LiteGridTurboLib.LiteGridTurboCtrl
PrivateSub Form_Load()
OnErrorGoTo ErrH
' .Object peels Access's control wrapper; without it, Set raises' "Type mismatch" against the typed declaration.Set m_grid = Me.lgGrid.Object' Single call — the key lives in modLGTLicense.
EnsureLicensed m_grid
' Pick the data mode BEFORE loading (0=Embedded default, 1=Virtual,' 2=Turbo column-store). Then load.
m_grid.DataMode = 0Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = 1
data(1, 0) = "Beta": data(1, 1) = 2
m_grid.LoadFromArray data
m_grid.SetColumnHeader 0, "Name"
m_grid.SetColumnHeader 1, "Qty"ExitSub
ErrH:
MsgBox "Form_Load: " & Err.Number & " " & Err.Description, vbCritical
EndSub' Events arrive directly on the control's name in the form module —' this is the native form-event experience Registered mode exists for.PrivateSub lgGrid_ItemClick(ByVal rowIndex AsLong, ByVal columnIndex AsLong)
' respond to clicks hereEndSubPrivateSub Form_Close()
OnErrorResumeNextSet m_grid = Nothing' release the reference; Access destroys the controlEndSub
Four things are worth calling out explicitly, because every richer screen later in this chapter still does all four:
Me.lgGrid.<dot> never shows the grid's members — only Access's own generic control properties, for every hosted ActiveX control. The typed m_grid variable, set once in Form_Load via .Object, is what gives IntelliSense and access to the real API.
EnsureLicensed is the only licensing call a form ever makes. It is safe to call from every form's Form_Load — that repetition is intentional, not wasteful, since only the first call in the process actually activates anything.
Events are ordinary form events.lgGrid_ItemClick and the rest of the lgGrid_* handlers appear directly in the form module's event dropdown, the same as any other bound control — this is the point of Registered mode.
Form_Close sets m_grid to Nothing under On Error Resume Next and nothing else; Access owns and destroys the actual control instance.
Composing a richer screen
A form that only ever calls LoadFromArray and SetColumnHeader covers a lot of ground, but a screen that lives in production for a while tends to grow the same four needs: a column whose value is computed rather than typed, a way to keep a wide layout manageable, a way to let the user decide what they need to see, and a running total at the bottom. Everything below assumes a form already following the template above. Each piece is independent — add the ones a given screen needs, in any order, once the grid has data loaded.
A derived column: SetComputedColumn vs. SetColumnExpression
SetComputedColumn looks like the natural way to give a column a value computed from other columns: it registers a callback object on a column index, and the grid validates that the object exposes a matching method before accepting it.
VBA
' Class module "MyComputer" must expose:' Public Function GetComputedValue(ByVal colIndex As Long, ByVal rowIndex As Long) As VariantDim computer AsNew MyComputer
grid.SetComputedColumn 0, computer ' succeeds if MyComputer exposes GetComputedValue
Be aware before reaching for it: in the current build, registering a callback here has no visible effect — no code path invokes the stored callback, so no cell ever displays a computed value from it. Treat SetComputedColumn as configuration you can set and clear cleanly, not as a working feature yet. To remove a registered callback, call ClearComputedColumn — passing Nothing to SetComputedColumn itself raises a runtime error instead. See SetComputedColumn in Part II for its exact validation behavior:
VBA
grid.ClearComputedColumn 0' removes any callback on column 0, or does nothing if none was set
ClearComputedColumn is a safe no-op on a column that never had a callback registered, which makes it cheap to call defensively whenever a screen resets a column's configuration. SetComputedColumn, ClearComputedColumn, SetColumnExpression, and RecalculateColumn/RecalculateAll all work only in Embedded mode — Virtual and Turbo column-store refuse each of them with a runtime error, since neither mode holds the per-row data they need to read. See each member's own entry in Part II for the exact refusal wording.
The member that actually produces a visible derived value today is SetColumnExpression. It compiles a small formula language — column names in brackets, arithmetic, comparisons, and a handful of functions — and evaluates it once for every row, caching the results:
The placeholder 0 in the Total column matters: a column added to an already-loaded grid with AddColumn has no real per-row storage, so SetColumnExpression needs a genuine column that was part of the array passed to LoadFromArray from the start. Beyond arithmetic and comparisons, the expression language supports IF(condition, trueVal, falseVal), ROUND, LEFT/RIGHT/MID, UPPER/LOWER, MIN/MAX and a few more — see SetColumnExpression in Part II for the full function list. Two behaviors are worth knowing up front: an expression is evaluated once at the time you set it, so a later change to a dependent value needs an explicit RecalculateColumn (or RecalculateAll) to show up; and that dependent value must be changed with SetCell, not SetCellValue — SetCellValue always stores text, and the expression engine treats a text cell as non-numeric, which makes a dependent expression display #ERR even though the string looks like a number.
VBA
grid.SetCell 0, 2, 20' change Qty for row 0 via SetCell, not SetCellValue
grid.RecalculateColumn 4' Total picks up the new QtyDebug.Print grid.GetCellValue(0, 4) ' -> "90" (4.5 * 20)
A mixed text/number expression displays as a visible #ERR cell rather than raising a runtime error, and dividing by zero produces a blank cell — both fail quietly on the screen rather than crashing the form. See SetColumnExpression in Part II for the full behavior.
Grouping related columns: AddColumnGroup and CollapseColumnGroup
A wide grid reads better when related columns share one collapsible header band. AddColumnGroup draws that band above a set of existing columns and hands back a group ID; CollapseColumnGroup and ExpandColumnGroup toggle the member columns' visibility under it, and can be wired straight to a toolbar button:
VBA
Sub GroupOrderColumns(grid AsObject)
Dim c1 AsLong, c2 AsLongDim shipGroupId AsLong
grid.Clear
grid.AddColumn "OrderID", 70
grid.AddColumn "Customer", 140
c1 = grid.GetColumnCount: grid.AddColumn "OrderDate", 90
c2 = grid.GetColumnCount: grid.AddColumn "Status", 90
shipGroupId = grid.AddColumnGroup("Order Detail", Array(c1, c2), -1)
' A toolbar button toggles the group; the underlying data is untouched.
grid.CollapseColumnGroup shipGroupId ' hides OrderDate and Status
grid.ExpandColumnGroup shipGroupId ' shows them againEndSub
Capture each child column's index right before its AddColumn call, as above — AddColumn always appends at the current column count, so that index is only correct at that exact moment. childColumns must be a Variant array such as the result of Array(...) — a late-bound call (grid As Object) that passes a typed array variable (Dim x() As Long) directly is refused. See AddColumnGroup in Part II for why. Pass -1 for parentGroupId for a top-level group, or nest one group under another by passing its ID. A brand-new group does not itself hide anything — call CollapseColumnGroup if it should start collapsed — and RemoveColumnGroup deletes one group without forcing its columns back to visible. Column groups are structural metadata independent of where the row data lives, so all of this works in Embedded, Virtual, and Turbo column-store modes alike.
One habit worth adopting: groups outlive Clear() — clearing the grid removes the columns but leaves the group band in place — so call ClearColumnGroups whenever a screen rebuilds its grid from scratch. That call also resets the next group's ID back to 1, so do not hold onto an old group ID across it. If a screen only needs to remove one group rather than rebuild the whole grid, RemoveColumnGroup shipGroupId deletes just that group — but unlike ClearComputedColumn, an unrecognized group ID here raises a runtime error rather than being silently ignored, so wrap the call in error handling if the ID might be stale.
Letting the user pick columns: ShowColumnChooser
For a grid with more columns than fit comfortably on screen, ShowColumnChooser opens a small built-in popup — one checkbox per column — positioned wherever you tell it:
VBA
PrivateSub cmdChooseColumns_Click()
m_grid.ShowColumnChooser 0, 0' 0, 0 positions it near the current mouse cursorEndSub
Passing 0, 0 is a documented shortcut for automatic positioning near the mouse, not the literal screen origin — pass real screen coordinates only when you want the popup somewhere specific. The grid needs at least one column defined already, or the call raises a runtime error instead of opening an empty popup. The popup is its own top-level window that takes keyboard focus on its own; there is no member on the control to close it again from code, so let the user dismiss it. See SetColumnVisible in Part II if a screen needs to drive column visibility from code instead of (or in addition to) this popup.
A totals footer that tracks the active filter: SetColumnAggregate and ShowAggregateRow
A footer row that summarizes a numeric column is three calls: choose the summary with SetColumnAggregate, optionally format the number with SetAggregateFormatString, and turn the footer band on with ShowAggregateRow:
VBA
Sub AddOrderAmountSummary(grid AsObject)
' Assumes the grid already has Orders data loaded, Amount at column index 3
grid.SetColumnAggregate 3, 1' 1 = Sum
grid.SetAggregateFormatString 3, "$%.2f"
grid.ShowAggregateRow = True' The footer total narrows automatically when a filter is applied
grid.SetColumnFilter 4, 0, "Shipped"' Contains filter on Status' The Amount footer now shows the sum of only the Shipped rows
grid.ClearColumnFilter 4' The footer total returns to covering every rowEndSub
The summary mode is a small numeric table — 0 None, 1 Sum, 2 Average, 3 Count, 4 Minimum, 5 Maximum — and it is computed over whatever rows are currently visible, so applying or clearing a filter (see SetColumnFilter in Part II) changes the footer with no extra call to remember. SetColumnAggregate and SetAggregateFormatString can be called in either order, but the footer band itself only appears once ShowAggregateRow is True, and only for a column that already has a mode other than 0 set. There is no getter for either the aggregate mode or the computed number on either method — the footer is a rendered value for the person using the form, not something you read back into VBA. Every summary mode, including Count, silently skips a cell whose value is not numeric and does not parse as one, rather than counting it as zero.
SetAggregateFormatString accepts at most one numeric conversion (%d, %.2f, and similar) — a format string with no numeric conversion at all is accepted but simply displays as static text, with the computed value never shown. An invalid format string is refused without disturbing whatever format the column already had. See SetAggregateFormatString in Part II for the full conversion syntax. These calls also have mode restrictions: SetColumnAggregate and SetAggregateFormatString are refused in Virtual mode, and ShowAggregateRow has its own asymmetric restriction there — see each member in Part II for the exact mode behavior.
Putting it together
None of the four additions above depend on each other, and none of them change the lifecycle template from the start of this chapter — they are calls a form makes after LoadFromArray and SetColumnHeader, using the same m_grid reference the template already set up. A screen can add a derived column, a collapsible group, a column chooser button, and a filter-aware footer in whatever combination it needs, in Form_Load or in response to later user actions, without touching EnsureLicensed, DataMode, or Form_Close at all.
Everything so far has used Embedded mode without ever naming it — load an array or a Recordset, and the grid owns the rows the same way a VBA array or a Recordset does. Most business applications can stay in Embedded mode for their entire life and never need to think about another engine.
Embedded mode does have real edges, and it's worth knowing what they are before you hit one:
LoadFromArray rejects a row count above roughly 10 million outright.
On a 32-bit host, LoadFromArray rejects a very large array — roughly 500 MB of estimated cell storage or more (API constant) — before any memory is committed, so the grid is never left half-loaded.
EstimateMemory gives you an approximate byte count for capacity planning, and Compact releases memory the grid is holding in reserve after a large series of deletes or edits — see Performance and Diagnostics.
BeginUpdate/EndUpdate around a bulk load, and ShowLoadingOverlay/HideLoadingOverlay for user feedback while it runs, keep a large load from feeling like the UI has frozen.
If your dataset is comfortably inside those limits, nothing in this chapter changes anything about how you work — keep using LoadFromArray, LoadFromRecordset, sorting, filtering and editing exactly as you have been.
When a dataset becomes unusually large, or lives somewhere you don't want to fully copy into the grid, LiteGrid Turbo gives you two additional approaches rather than forcing Embedded mode's row store to solve every problem. Both are opt-in, both are selected with the same DataMode property, and neither changes anything about the methods you already know for reading and displaying cells.
15. Choosing Embedded, Turbo column-store or Virtual Mode
LiteGrid Turbo ships three interchangeable storage engines behind one object model. Exactly one is active at a time, selected with the DataMode property:
DataMode
Mode
Who owns the data
Reach for it when...
0
Embedded
The grid, in its own row store
You have data to hand over — an array, a Recordset, a Range — and want the grid to hold it. This is the default, and everything in Part I up to this point has used it.
2
Turbo column-store
The grid, in a column-oriented store
You need a very large dataset fully materialized inside the grid, and Embedded mode's row store is not the right shape for it.
1
Virtual
Your application
Your application or database already owns an enormous dataset, and you don't want the whole thing copied into the grid at all.
If you are unsure which mode to use, use Embedded. It is the general-purpose engine, it has the widest method support of the three, and nothing about starting there prevents a later move.
Turbo column-store: a very large dataset, fully inside the grid
Turbo column-store mode is still a grid-owned store — the same mental model as Embedded, just organized column-by-column internally so it can hold and manipulate far more rows. You select it the same way you'd select any mode, then load it with a loader Turbo column-store supports:
LoadFromArray and LoadFromRecordset both work in Turbo column-store mode. LoadFromRange, LoadFromJson, AppendRows and BeginStreaming do not — they refuse outright and leave the live table unchanged, because Turbo column-store's loading surface is intentionally narrower than Embedded's. For loading one column at a time — useful when you already have typed arrays rather than one big Variant array — open a session, supply every column once in any order, then publish it atomically:
Each column is a one-dimensional array whose type matches the column — numbers, text, dates, currency or booleans — plus an optional Boolean array marking which cells are null. EndLoad only publishes once every declared column is present; CancelLoad discards an unfinished session without touching whatever table was live before it started. Advanced Loading and Streaming has the complete reference for this loading session, including the exact array types each column kind expects, IsLoading, and error recovery.
Turbo column-store mode is not simply "Embedded mode but bigger" in every respect — a handful of methods that work in Embedded mode refuse in Turbo column-store mode, or work with narrower rules (for example, SetCell requires the incoming value's type to match the column's declared type exactly, where Embedded mode accepts anything). Part II — Complete API Reference calls this out on every affected member's own entry, with the exact rule — treat a refusal there as a real, intentional boundary rather than a bug to work around.
Virtual: your application keeps the data
Virtual mode is for when your application or database already owns the dataset — a large query result, a paged API, an in-memory structure too big to copy — and handing the whole thing to LoadFromArray doesn't fit. In Virtual mode the grid never holds more than a sliding window of rows around what's currently on screen: you tell it the total row count, and it asks your code for whatever rows it actually needs to paint.
Columns have to exist before the switch to Virtual mode (AddColumn is refused once DataMode = 1), and LoadFromArray/LoadFromRecordset are intentionally unavailable — the grid does not own the dataset, so there is nothing for them to load into. From here, your code answers a NeedRows event with SupplyRows whenever the grid needs data it doesn't have cached, and answers SortRequested/FilterRequested when the user sorts or filters, since those operations must run against your own data source rather than the grid's.
That's the full protocol in outline — enough to recognize when Virtual mode is the right tool. Advanced Loading and Streaming walks through it in full, including a way to preview Virtual mode's scrolling and paint behavior with no host code at all before you wire up real data.
Changing modes safely
Only one engine is active at a time, and the grid enforces a simple rule when you switch: whichever store you are leaving must be empty first.
VB
grid.LoadFromArray data
OnErrorResumeNext
grid.DataMode = 2Debug.PrintErr.Description ' live Embedded rows: call Clear firstErr.ClearOnErrorGoTo0
grid.Clear
grid.DataMode = 2
Clear in Turbo column-store mode removes its table, edits, undo/redo state and cell metadata but leaves DataMode set to 2, ready for another load — so reloading right after Clear without resetting DataMode stays in Turbo column-store mode. Virtual mode owns no table, so leaving it does not require Clear. DataMode never copies data between engines: each mode's store is independent, so moving from one to another always means reloading, not carrying rows across.
Next: if your data lives outside the grid entirely and you're ready to wire up the real Virtual mode protocol, or you want the full Turbo column-store loading-session reference, continue to Advanced Loading and Streaming. If you're ready to ship without registering the OCX on every machine, Registration-Free Deployment is next.
16. Registration-Free Deployment
Every chapter up to this point has assumed the OCX is registered on the machine you're running on — either through the Insert ActiveX Control dialog at design time, or CreateObject("LiteGridTurbo.LiteGridTurboCtrl") at run time. Registration itself does not require administrator rights: the supplied installer registers the OCX per-user, with no machine-wide elevation needed.
PowerShell
# Per-user registration; no machine-wide administrator rights required
powershell -File scripts\Install-LiteGridTurbo.ps1 -Architecture x64
# Remove that registration when required
powershell -File scripts\Uninstall-LiteGridTurbo.ps1 -Architecture x64
Use -Architecture x86 for a 32-bit host. A machine-wide install via regsvr32 (or an MSI that does the same) is also supported, and is a reasonable choice for a managed corporate rollout when an administrator deliberately chooses it — but it's a deliberate choice, not the only registered path, and not a requirement of registration itself.
Registration-free ("reg-free") deployment is a different kind of convenience: no COM registration at all, of either kind — nothing written to the registry, no installer step to run first, just the OCX file sitting beside the workbook or database it serves. Copy the two files into the same folder, and it runs. This chapter covers the Declare statements that make that work, the shared bootstrap module you'll want once per project, full per-form lifecycle templates, and the callback-based event bridge that delivers the grid's events when there's no COM registration for connection-point events to ride on.
Why reg-free deployment exists
Reg-free earns its place for reasons that have nothing to do with registration being hard — the per-user installer already makes registration easy. What it removes is the installation step itself, and the constraint that a machine-wide ProgID registration implies:
No installer step, of any kind. Copy the folder and it runs. That matters when you're handing a customer a single .xlsm or .accdb file and don't want them to run anything first, when the target machine is one you don't have hands-on access to, or when a locked-down environment restricts running scripts or installers even for a per-user registration.
No shared registered ProgID. With per-user registration, projects running under that user account resolve the ProgID to the same registered OCX version — awkward if two projects need different OCX builds side by side, or if policy simply forbids registry changes altogether, managed or not.
Reg-free deployment sidesteps both: the OCX file itself is the only thing you ship alongside the workbook or database, nothing outside that folder changes, and removing the folder removes every trace of the deployment. The tradeoff, covered through the rest of this chapter, is that you take on a little more code — a Declare block instead of a Reference checkbox, and a callback bridge instead of connection-point events for receiving the grid's events at all, in every data mode.
Quick start: VBA7 (Access, Excel, and other Office hosts)
Place the matching OCX beside the workbook, database, or application. This minimal example creates a data-only grid — one with the full API but no visible window:
#If Win64 picks the right binary for the host process's bitness — Office itself can be 32-bit or 64-bit regardless of the Windows OS underneath it, so this check has to run against the VBA host, not the OS. Use LiteGridTurbo_CreateControlInParent instead of LiteGridTurbo_CreateGrid when you need a visible child window hosted inside a container control rather than a data-only instance; the shared bootstrap module later in this chapter wraps both.
Registration-free creation does not write COM registration anywhere — the OCX activates its embedded manifest internally, in-process. A failed reg-free creation does not fall back silently to a registered copy; if the manifest activation or the factory call fails, LiteGridTurbo_CreateGrid simply doesn't hand you a usable object, and you should check for that rather than assume success (the bootstrap module below does this for you with Err.Raise).
Quick start: VB6
VB6 is always a 32-bit host, so it loads the x86 OCX unconditionally, and VB6's Declare syntax has no PtrSafe keyword at all — VB6 predates the 64-bit concern that keyword addresses:
If you're porting a reg-free bootstrap between VB6 and a VBA7 host (or the reverse), the PtrSafe keyword and the #If Win64 bitness branch are the two things to add or remove — the rest of the Declare statement is identical.
Deployment and mode combinations
Reg-free deployment is orthogonal to DataMode — every mode from Choosing Embedded, Turbo column-store or Virtual Mode is available whether the control came from the registry or from a factory export. What changes with reg-free is not which mode you can use, but how the grid's events reach your code — and that applies the same way in every mode, not only in Virtual mode:
Surface
Registered
Registration-free
Creating the control (Embedded, Turbo column-store, or Virtual)
CreateObject, then LoadFromArray/LoadFromRecordset/a Turbo loader
Factory export, then the same calls
Receiving events (Embedded, Turbo column-store, or Virtual)
Connection-point handlers, e.g. Private Sub lgGrid_ItemClick(...)
Callback bridge: SetEventCallback/OnGridEvent, or the macro bridge
JSON Helper
Helper ProgID
LiteGridTurbo_CreateHelper
Once a control exists, Embedded and Turbo column-store data operations work exactly the same regardless of how the control was created — LoadFromArray, GetCell and the rest don't know or care whether they're talking to a registered or reg-free instance. Event delivery is the one thing registered vs. reg-free genuinely changes, and it changes for every event in every mode: a registered grid always uses connection-point handlers, a reg-free grid always uses the callback bridge, whether that's ItemClick firing on an Embedded grid or NeedRows firing on a Virtual one.
Virtual mode simply makes this distinction matter more, because its NeedRows, SortRequested and FilterRequested events are part of the protocol that supplies the grid with data at all — a Virtual-mode grid cannot function without a host answering them, registered or reg-free. In Embedded and Turbo column-store mode, by contrast, the grid already has its data; events there exist so you can react to what the user does, and the grid keeps working perfectly well even if you never attach a single handler. The last table row matters if you use the JSON Helper object described elsewhere in this book: it has its own reg-free factory, LiteGridTurbo_CreateHelper, separate from the grid's factories.
The shared bootstrap module
Reg-free code needs the same handful of Declares, the same manifest-activation call, and the same license-key handling in every project that uses it. Rather than repeating that in every form, put it once in a new standard module named modLGTRegFree and keep your forms small. This is production code, reproduced here in full — copy it as-is into a new module and edit only the two constants at the top:
VBA
' ===== modLGTRegFree — START ================================================' ============================================================================' modLGTRegFree - LiteGridTurbo registration-free bootstrap' Part of the LiteGridTurbo registration-free deployment kit.'' Import this module, copy the LiteGridTurbo folder next to your workbook or' database, and call CreateGridIn / CreateDataGrid. No regsvr32, no COM' registration, no administrator rights.'' Edit LGT_COMPANY and LGT_KEY once, below. Leave LGT_KEY empty for trial.' ============================================================================OptionExplicitPrivateConst LGT_COMPANY AsString = "Your Company"PrivateConst LGT_KEY AsString = ""' "V1|..."' The deployment folder holds one subfolder per architecture, each containing' a neutrally-named LiteGridTurbo.ocx. A Declare's Lib string is a COMPILE-TIME' literal, so a bitness-suffixed filename would force every Declare to be' written twice; the neutral name lets the ARCHITECTURE live in the path' instead, where it can be chosen at run time.#If VBA7 Then#If Win64 ThenPrivateConst LGT_ARCH AsString = "x64"#ElsePrivateConst LGT_ARCH AsString = "x86"#End If#ElsePrivateConst LGT_ARCH AsString = "x86"' VB6 is 32-bit only#End If' ----------------------------------------------------------------------------' Declares.'' The #If here is VBA7, NOT Win64: 64-bit VBA requires PtrSafe and LongPtr,' and VB6 cannot parse either. It is not about which OCX to load - both' branches name the SAME neutral library, because the architecture is in the' folder (see LGT_ARCH above and EnsureBootstrapped below).'' WHY SetDllDirectory IS HERE AND CANNOT BE MOVED INTO THE OCX.' A Declare's Lib string is a compile-time literal resolved by the standard' Windows loader search order, and the deployment folder is not on that order' for a typical Office process. Nothing inside the OCX can run before the' loader has already found and loaded it - you cannot call into an unloaded' DLL to teach Windows where that DLL is. This one line is the irreducible' bootstrap step. See the README for its process-wide security implications.' ----------------------------------------------------------------------------#If VBA7 ThenPrivateDeclarePtrSafeFunction SetDllDirectory Lib"kernel32" _
Alias"SetDllDirectoryW" (ByVal lpPathName AsLongPtr) AsLongPrivateDeclarePtrSafeFunction LiteGridTurbo_CreateGrid _
Lib"LiteGridTurbo.ocx" () AsObjectPrivateDeclarePtrSafeFunction LiteGridTurbo_CreateControlInParent _
Lib"LiteGridTurbo.ocx" (ByVal pParent AsObject, ByRef ppDisp AsObject) AsLongPrivateDeclarePtrSafeFunction LiteGridTurbo_DestroyControlInstance _
Lib"LiteGridTurbo.ocx" (ByVal pDisp AsObject) AsLongPrivateDeclarePtrSafeFunction LiteGridTurbo_GetLastError _
Lib"LiteGridTurbo.ocx" () AsLong#ElsePrivateDeclareFunction SetDllDirectory Lib"kernel32" _
Alias"SetDllDirectoryW" (ByVal lpPathName AsLong) AsLongPrivateDeclareFunction LiteGridTurbo_CreateGrid _
Lib"LiteGridTurbo.ocx" () AsObjectPrivateDeclareFunction LiteGridTurbo_CreateControlInParent _
Lib"LiteGridTurbo.ocx" (ByVal pParent AsObject, ByRef ppDisp AsObject) AsLongPrivateDeclareFunction LiteGridTurbo_DestroyControlInstance _
Lib"LiteGridTurbo.ocx" (ByVal pDisp AsObject) AsLongPrivateDeclareFunction LiteGridTurbo_GetLastError _
Lib"LiteGridTurbo.ocx" () AsLong#End IfPrivate m_bootstrapped AsBooleanPrivate m_binFolder AsString' Point the loader at the architecture subfolder. Idempotent - the first' grid-creating call runs it and later calls skip it.'' There is deliberately NO ActivateManifest call here. Every factory export in' the OCX activates its own embedded manifest on entry, deriving the path from' its own module handle - which is more reliable than a path built by hand,' because it cannot be wrong.PrivateSub EnsureBootstrapped(ByVal baseFolder AsString)
If m_bootstrapped ThenExitSubIf Len(baseFolder) = 0ThenErr.Raise vbObjectError + 512, "modLGTRegFree", _
"No deployment folder was supplied. Pass the folder that contains the " & _
"LiteGridTurbo x86 and x64 subfolders, e.g. ThisWorkbook.Path & ""\LiteGridTurbo""."EndIf' Absolute paths only: a relative path would resolve against whatever the' host's current directory happens to be at the time.If Mid$(baseFolder, 2, 1) <> ":"And Left$(baseFolder, 2) <> "\\"ThenErr.Raise vbObjectError + 512, "modLGTRegFree", _
"The deployment folder must be an absolute path, not """ & baseFolder & """."EndIf
m_binFolder = baseFolder
If Right$(m_binFolder, 1) = "\"Then m_binFolder = Left$(m_binFolder, Len(m_binFolder) - 1)
m_binFolder = m_binFolder & "\" & LGT_ARCH
If Len(Dir$(m_binFolder, vbDirectory)) = 0ThenErr.Raise vbObjectError + 515, "modLGTRegFree", _
"LiteGridTurbo could not be loaded." & vbCrLf & _
"Folder not found: " & m_binFolder & vbCrLf & _
"This process is " & LGT_ARCH & ", so it needs the " & LGT_ARCH & " subfolder of the " & _
"deployment folder. Copy the whole LiteGridTurbo folder, not just one file."EndIfIf Len(Dir$(m_binFolder & "\LiteGridTurbo.ocx")) = 0ThenErr.Raise vbObjectError + 516, "modLGTRegFree", _
"LiteGridTurbo could not be loaded." & vbCrLf & _
"Missing file: " & m_binFolder & "\LiteGridTurbo.ocx" & vbCrLf & _
"Expected the " & LGT_ARCH & " build, because this host process is " & LGT_ARCH & "."EndIf
SetDllDirectory StrPtr(m_binFolder)
m_bootstrapped = TrueEndSubPrivateSub EnsureLicensed(ByVal ctrl AsObject)
If ctrl IsNothingThenExitSubIf Len(LGT_KEY) = 0ThenExitSubOnErrorResumeNext
ctrl.ActivateLicense LGT_COMPANY, LGT_KEY
OnErrorGoTo0EndSub' Data-only grid: the full API, no visible window.PublicFunction CreateDataGrid(ByVal baseFolder AsString) AsObjectDim g AsObject
EnsureBootstrapped baseFolder
Set g = LiteGridTurbo_CreateGrid()
If g IsNothingThenErr.Raise vbObjectError + 513, "modLGTRegFree", _
"CreateDataGrid failed." & vbCrLf & _
"Folder: " & m_binFolder & vbCrLf & _
"Process architecture: " & LGT_ARCH & vbCrLf & _
"HRESULT: &H" & Hex$(LiteGridTurbo_GetLastError())
EndIf
EnsureLicensed g
Set CreateDataGrid = g
EndFunction' Visible grid hosted over a container control. Windowless Access controls (an' Option Group "Frame") work too: the OCX reads the control's screen rectangle' via IAccessible and walks the Parent chain to the form's hWnd.PublicFunction CreateGridIn(ByVal hostContainer AsObject, _
ByVal baseFolder AsString) AsObjectDim g AsObjectDim hr AsLong
EnsureBootstrapped baseFolder
If hostContainer IsNothingThenErr.Raise vbObjectError + 517, "modLGTRegFree", _
"CreateGridIn was passed Nothing as the host container. Pass a Frame " & _
"(Excel) or an Option Group (Access) that marks where the grid should appear."EndIf
hr = LiteGridTurbo_CreateControlInParent(hostContainer, g)
If g IsNothingThenErr.Raise vbObjectError + 514, "modLGTRegFree", _
"CreateGridIn failed." & vbCrLf & _
"Folder: " & m_binFolder & vbCrLf & _
"Process architecture: " & LGT_ARCH & vbCrLf & _
"HRESULT: &H" & Hex$(hr) & " LastError: &H" & Hex$(LiteGridTurbo_GetLastError())
EndIf
EnsureLicensed g
Set CreateGridIn = g
EndFunction' OPTIONAL. Set grid = Nothing is sufficient for cleanup - the control's own' FinalRelease performs the identical teardown. This exists for DETERMINISM:' VBA's release timing in Access Form_Close is not always prompt, and some' hosts want the window gone at a known moment.PublicSub DestroyGrid(ByVal grid AsObject)
OnErrorResumeNextIfNot grid IsNothingThen LiteGridTurbo_DestroyControlInstance grid
EndSub' ===== modLGTRegFree — END ==================================================
Two functions do the actual work: CreateDataGrid for a data-only instance with no window (computation, export, background reporting), and CreateGridIn for a visible grid hosted over a container control you already placed on the form. Both call EnsureBootstrapped first, which is safe to call repeatedly — SetDllDirectory and LiteGridTurbo_ActivateManifest only need to run once per process, and the m_bootstrapped flag makes every call after the first a no-op. Both raise a real VBA error (vbObjectError + 513 / + 514) with the last-error HRESULT folded into the message when the factory call comes back empty, instead of leaving you to debug a mysterious Nothing a dozen lines later. Always pass the container — a Frame, not the form itself — to CreateGridIn; the control hosts itself inside whatever object you hand it.
DestroyGrid is the cleanup half — call it from Terminate/Unload/Close, wrapped the way the module wraps it, so teardown doesn't throw if the grid was never successfully created.
What EnsureBootstrapped is doing. It adds baseFolder to the process's DLL search order (so the OCX finds any dependency DLLs sitting beside it) and activates the OCX's embedded manifest — the step that lets LiteGridTurbo_CreateGrid/LiteGridTurbo_CreateControlInParent succeed with no registry entry behind them.
When the two Err.Raise calls fire. The usual cause behind either vbObjectError + 513 (CreateDataGrid) or + 514 (CreateGridIn) is a mismatch between what baseFolder points at and what's actually on disk — the OCX not copied beside the workbook/database, the wrong bitness OCX for the host (an x86 OCX beside a 64-bit Office install, or vice versa), or baseFolder pointing at the wrong folder entirely (a ThisWorkbook.Path/CurrentProject.Path that resolves differently than expected, for instance when a file is opened from a network path or a synced cloud folder). The Hex(LiteGridTurbo_GetLastError()) folded into the message is the fastest way to tell these apart without extra breakpoints — it's the same code path a support request would ask for.
Per-form lifecycle template (Excel UserForm)
With modLGTRegFree in the project, a form's own code shrinks to creation, one-time setup, and cleanup. This is the canonical shape for an Excel UserForm hosting a visible grid over a Frame named fraGrid:
VBA
' ============================================================================' Reg-Free Mode — canonical lifecycle template (Excel UserForm)' ============================================================================' Form prerequisite: a Frame control named fraGrid (the windowed host)' Project prerequisite: modLGTRegFree added, OCX beside the workbook' ============================================================================OptionExplicitPrivate m_grid AsObjectPrivateSub UserForm_Initialize()
OnErrorGoTo ErrH
Set m_grid = CreateGridIn(Me.fraGrid, ThisWorkbook.Path)
m_grid.DataMode = 0Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = 1
data(1, 0) = "Beta": data(1, 1) = 2
m_grid.LoadFromArray data
m_grid.SetColumnHeader 0, "Name"
m_grid.SetColumnHeader 1, "Qty"ExitSub
ErrH:
MsgBox "Initialize: " & Err.Number & " " & Err.Description, vbCritical
EndSubPrivateSub UserForm_Terminate()
OnErrorResumeNextIfNot m_grid IsNothingThen
m_grid.ClearEventCallback ' harmless if none was attached
DestroyGrid m_grid
Set m_grid = NothingEndIfEndSub
Three things worth noticing: CreateGridIn is passed ThisWorkbook.Path so the bootstrap knows where to find the OCX; ClearEventCallback is called unconditionally in Terminate even if you never attached one (it's documented as harmless when there's nothing to clear, which is what makes it safe to call unconditionally rather than tracking whether you attached a callback); and everything after creation — DataMode, LoadFromArray, SetColumnHeader — is exactly the API you already know from the rest of this book. Reg-free changes how the object comes into existence, not what you can do with it once it exists.
The Access variant, in outline
Access's own controls are windowless, which sounds like it would complicate hosting a grid, but the fix is a placeholder rather than special code: drop an empty Option Group control (Access calls it a Frame; clear its border and special effect) named fraGrid where the grid should appear, and hand that to CreateGridIn exactly as the Excel template does. The OCX reads the placeholder's screen rectangle through IAccessible and hosts the grid inside the form's own section window, so it repaints correctly through app switches, minimize/restore, and section redraws — no ActiveX Control is inserted at design time, and no Reference is set; the whole form runs from modLGTRegFree alone, with Form_Load/Form_Close standing in for UserForm_Initialize/UserForm_Terminate and CurrentProject.Path standing in for ThisWorkbook.Path.
This placeholder-hosting technique requires the 2026-08-28 OCX build or later. Earlier builds hosted the grid one window too high and went blank on app reactivation — if you're deploying reg-free to Access and see a grid that repaints correctly on load but goes blank after an Alt+Tab or a minimize/restore cycle, check your OCX build date before debugging anything else.
For computation, export, and reporting work with no window at all, the data-only grid (CreateDataGrid CurrentProject.Path) remains the lightest choice on any host, Access included.
Events in reg-free mode: the callback bridge
Connection-point events — the Private Sub grid_ItemClick(...) style handlers you'd write against a registered, referenced control — require registered COM to fire. A reg-free instance has no registration for a connection point to ride on, so its events arrive a different way: through one method on a class module you write, which the grid calls directly.
Use the generated wrapper. The deployment kit ships vba\clsLiteGridTurboEvents.cls, generated from the control's own interface definition. It turns the single flat callback into one named VBA event each, so a reg-free host writes the same handler code a registered host writes:
VBA
PrivateWithEvents mEvents As clsLiteGridTurboEvents
PrivateSub UserForm_Initialize()
Set m_grid = CreateGridIn(Me.fraGrid, ThisWorkbook.Path & "\LiteGridTurbo")
Set mEvents = New clsLiteGridTurboEvents
m_grid.SetEventCallback mEvents, 1EndSubPrivateSub mEvents_ItemClick(ByVal rowIndex AsLong, ByVal columnIndex AsLong)
MsgBox "Clicked " & rowIndex & "," & columnIndex
EndSubPrivateSub mEvents_BeforeSort(ByVal columnIndex AsLong, ByRef Cancel AsBoolean)
Cancel = True' cancelling works exactly as it does registeredEndSub
Import the class as it ships and never edit it — it is regenerated from the IDL and the event table with every release, and a hand-edit would be silently overwritten. It holds no reference to your form or to the grid, deliberately: the grid AddRefs the wrapper, so storing either one would close a COM cycle no VBA host can break.
One event is deliberately missing from it.BeforeCellPaint (event 26) hands the handler a live Windows device context that is only valid inside the paint loop, so it can never cross a late-bound callback boundary. Custom cell painting requires registered mode with WithEvents. The wrapper does not declare an event that could never fire, and SetEventCallbackEnabled 26, True is refused by name for the same reason.
The raw callback, for hosts that want it
The wrapper is a convenience, not a requirement. Any object exposing one method works, and that is all SetEventCallback ever asks for:
VBA
' ===== clsGridEvents — START ================================================OptionExplicit' The form that wants to be told about clicks. Set by the form after New.Public Target AsObjectPublicSub OnGridEvent(ByVal gridId AsLong, ByVal eventId AsLong, _
a1 AsVariant, a2 AsVariant, a3 AsVariant, _
a4 AsVariant, a5 AsVariant, a6 AsVariant, _
ByRef Cancel AsBoolean)
OnErrorResumeNext' a callback must never throw back into the gridIf eventId = 2ThenIfNot Target IsNothingThen Target.ShowClick CLng(a1), CLng(a2)
EndIfEndSub' ===== clsGridEvents — END ==================================================
Taking this route means decoding a1..a6 yourself and knowing which numeric ids exist — which is exactly the work the generated wrapper does for you. Note also that the Public Target As Object above stores the form: that is workable in this small shape, but it is the reference the generated wrapper deliberately avoids.
Every event the grid raises funnels through this single OnGridEvent method, dispatched by eventId rather than by a distinct handler per event — branch on eventId the way the example branches on 2 (ItemClick). The six Variant arguments a1..a6 carry whatever that particular event needs (for ItemClick, a1 is the row index and a2 is the column index); consult the specific event's entry in Part II for what each argument holds. For a cancellable event — BeforeCellEdit, BeforeSort, and others documented as cancellable in Part II — set Cancel = True inside OnGridEvent to cancel it, the same way you would return Cancel = True from a connection-point handler.
Attach the class once, right after creating the grid, and keep the instance alive in a module-level variable — if it goes out of scope, the callback stops firing:
VBA
Private m_events As clsGridEvents
' ... after CreateGridIn / CreateDataGrid:Set m_events = New clsGridEvents
m_grid.SetEventCallback m_events, 1' 1 = your own id, echoed back
The second argument to SetEventCallback is an id you choose yourself; it's echoed back to you as gridId in every OnGridEvent call, which is what lets one clsGridEvents instance serve more than one grid on the same form if you need that.
Two limits are worth knowing before you rely on the callback bridge for everything a registered grid can do:
BeforeCellPaint is registered-mode only. It cannot reach the callback bridge under any circumstances — there's no reg-free path to it at all.
The Tier-B events are off by default.SelectionChanged, ScrollChanged, CellHover, CellMouseEnter, CellMouseLeave, and LoadProgress don't arrive through OnGridEvent until you explicitly turn each one on with SetEventCallbackEnabled. This is a deliberate default, not an oversight — enable only the ones you're actually going to handle.
In Form_Close/UserForm_Terminate, call ClearEventCallback before DestroyGrid, exactly as both lifecycle templates above already do — it's what makes teardown order safe regardless of whether a callback was ever attached.
A data-only example: a background report with no UI
The data-only grid is worth a second look on its own, because it's the pattern that needs the least ceremony — no Frame to place, no lifecycle to manage beyond creation and destruction. This builds a small customer balance report entirely off-screen and reads a value back:
There's no On Error GoTo ErrH housekeeping here beyond what CreateDataGrid already raises on failure, and no form to attach the grid to at all — CreateDataGrid/DestroyGrid bracket the whole routine the way Open/Close would bracket a file. This is the shape to reach for whenever the grid is doing computation, validation, or export work in the background rather than presenting anything to the user.
Technical identifiers
Keep this table beside your deployment scripts and support requests — these are the first-release names to use directly in projects:
What you need
Identifier
64-bit / 32-bit binary
LiteGridTurbo_x64.ocx / LiteGridTurbo_x86.ocx
Control ProgID
LiteGridTurbo.LiteGridTurboCtrl (.1 is version-specific)
Helper ProgID
LiteGridTurbo.LiteGridTurboHelper (.1 is version-specific)
The ProgIDs and interface names matter even in reg-free projects — they're what you'll reference in support requests, and what a registered fallback path (if your deployment supports one) would use. See Part II for the complete method, property, and event reference behind ILiteGridTurboCtrl and ILiteGridTurboHelper; this chapter has covered the deployment mechanics that get you an instance of either one without COM registration.
Next:Advanced Loading and Streaming covers the Virtual mode protocol and Turbo column-store loading sessions in full — both apply identically to registered and reg-free instances once the grid exists.
17. Advanced Loading and Streaming
Choosing Embedded, Turbo or Virtual Mode introduced three ways of getting data into the grid beyond a plain LoadFromArray: streaming rows into Embedded mode in batches, loading Turbo column-store mode one typed column at a time, and letting Virtual mode ask your code for rows as it needs them. This chapter is where each of those gets a full, working treatment — the calls, the gotchas, and the order things have to happen in. It assumes you already know why you'd pick a mode; it does not re-explain DataMode switching mechanics.
Streaming rows into Embedded mode
LoadFromArray replaces the grid's data in one call. When you instead want to add a large number of rows over time — a paged import, a background fetch — without the grid re-sorting and re-filtering after every batch, BeginStreaming, AppendRows and EndStreaming form a three-call pattern for exactly that.
BeginStreaming switches the grid into a mode where newly appended rows skip sorting and filtering, and mostly skip repainting, until you close the session:
VBA
grid.BeginStreaming
' ... one or more AppendRows calls ...
grid.EndStreaming
There's no argument and nothing to configure. The only thing to watch is that only one streaming session can be open at a time — calling BeginStreaming again before EndStreaming raises an error rather than nesting — and it refuses outright if an ordinary LoadFromArray chunked load is still in progress. Repainting isn't suppressed for the entire session either: the grid still forces periodic repaints during a long import so it doesn't look frozen (see BeginStreaming in Part II for the exact interval).
AppendRows is the call that actually adds rows, once a streaming session is open. It takes the same shape of 2-D Variant array LoadFromArray does — rows in the first dimension, columns in the second — and, unlike LoadFromArray, it only ever adds to what's already there:
The very first AppendRows call after BeginStreaming on an empty grid establishes the columns for you, exactly the way LoadFromArray does; every later call in the session must supply exactly that many columns, or the call is rejected and whatever was appended earlier in the session is left in place. Appended rows show up in GetRowCount() immediately, but they can appear out of sequence with any active sort until EndStreaming runs — that's expected, not a bug.
EndStreaming closes the session: it rebuilds the row order, re-applies whatever sort and filter were active before streaming began, and repaints once.
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = 5: data(1, 0) = 1
grid.LoadFromArray data
grid.Sort 0, 0' ascending
grid.BeginStreaming
Dim newRow(0To0, 0To0) AsVariant
newRow(0, 0) = 3
grid.AppendRows newRow
grid.EndStreaming
Debug.Print grid.GetCellValue(0, 0) ' -> 1 (the sort re-ran across every row, including the streamed-in one)
This is a genuine re-sort, not a reapplication of stale state, so rows you streamed in take part in it correctly. Calling EndStreaming with no matching BeginStreaming raises an error, and there's no way to abandon a streaming session short of calling EndStreaming — CancelLoad (covered below) doesn't touch it. If you open a streaming session and never close it, any active sort or filter stays stale indefinitely.
Streaming is an Embedded-mode-only tool — BeginStreaming, AppendRows and EndStreaming all refuse in Virtual and Turbo column-store mode, because streaming only ever writes into the row store. And IsLoading (below) does not report True while a streaming session is open; it's a separate mechanism with its own state.
See BeginStreaming, AppendRows, EndStreaming, LoadFromArray and BeginUpdate in Part II for the complete member entries.
This section covers each call in that sequence in full, plus how to cancel it and check whether one is open. None of BeginLoad, LoadColumn, EndLoad or CancelLoad is available in Virtual mode — this whole column-by-column route only ever runs from Embedded or Turbo column-store mode.
BeginLoad opens the session. You declare the exact row and column count up front:
rowCount must be between 1 and 10,000,000; colCount just has to be positive, with every index from 0 to colCount - 1 supplied exactly once before the session can close. If the grid is currently in Embedded mode, its row store has to be empty first — BeginLoad raises an error naming the reason rather than discarding your data for you, so Clear first. If the grid is already in Turbo column-store mode, you can call BeginLoad again straight away to load a fresh table over the current one. Calling BeginLoad while a session is already open also raises an error, leaving that earlier session untouched — finish it with EndLoad or abandon it with CancelLoad first.
typedArray has to be a one-dimensional array of a single VBA type — Long, Double, String, Date, Currency or Boolean — with exactly as many elements as the row count you declared to BeginLoad. That array's own type is what decides how the column is interpreted; there's no separate step to declare the type, and no way to load a Long array and have it read back as Currency. nullMask is optional: pass Empty when every value in the column is valid, or a same-length Boolean array where True marks that row's cell as null. Nullness only ever comes from the mask — a blank string in a String column is a real empty string, never null.
Columns can be supplied in any order, but each index only once — a duplicate is rejected and doesn't touch anything already staged. Usefully, a LoadColumn call that itself fails (a bad array length, say) never marks that index as supplied, so you can just retry the same index once you fix the problem. Neither array needs to survive past the call; LiteGrid Turbo copies the data during LoadColumn, so it's safe to reuse or discard the arrays right afterward.
EndLoad publishes the session as the grid's live data:
It only succeeds once every declared column index has actually been supplied; otherwise it fails, names the first missing column, and — importantly — leaves the session open, so you can supply what's missing and call EndLoad again rather than starting over from BeginLoad. On success it replaces the grid's live data atomically, switches DataMode to Turbo column-store (2) even if the session started from Embedded mode, and raises exactly one DataLoaded event — there's no per-column event along the way.
Error and cancel handling.CancelLoad is your way out of an open session at any point before EndLoad — it discards everything staged so far and leaves whatever the grid was already showing completely untouched:
This is the right tool when your data already lives in separate typed arrays — one array per column, fetched independently from several queries, say — rather than as a single ready-made 2-D Variant array. LoadColumn copies each typed array directly, so you avoid building and re-copying one combined array just to hand it to LoadFromArray. CancelLoad also happens to double as the cancel path for a chunked LoadFromArray call in progress (see Working With Large Datasets), and it's a harmless no-op if neither kind of load is active — safe to call defensively, for example from a form's Unload event, even when you're not sure anything is running.
After CancelLoad discards a session, IsLoading immediately returns False, and a subsequent LoadColumn or EndLoad call is rejected exactly as if BeginLoad had never been called.
IsLoading reports whether a load is currently in progress — either an in-progress LoadFromArray chunked load, or an open BeginLoad/LoadColumn session:
It's the one flag covering both loaders, and it does not distinguish which of the two is active. It's also the one member in this section with no mode restriction at all — it can be called in Embedded, Virtual or Turbo column-store mode alike, since it's just a status flag unrelated to which engine owns the data. Two things worth knowing: it does not report True during a BeginStreaming/AppendRows/EndStreaming session, so track your own flag if you need to know one is open; and because LoadFromArray normally runs to completion before your code's next line executes, you'll only see IsLoading return True for that loader from another context calling in while it's mid-chunk — for example, a LoadProgress event handler — not from the same procedure that called LoadFromArray.
See BeginLoad, LoadColumn, EndLoad, CancelLoad, IsLoading and DataMode in Part II for the complete member entries, including every rejection case.
Virtual mode: the deferred fetch protocol
Virtual mode (DataMode = 1) is for a grid backed by a dataset your application owns — a database cursor, a paged web API, an in-memory structure too large to hand over as one array. The grid never holds more than a sliding window of rows around the current viewport: you tell it the total row count with SetVirtualRowCount, and it asks for whatever it actually needs to paint through the NeedRows event, answered with SupplyRows.
Setup follows a fixed order: call Clear first, while still in Embedded mode — it refuses once DataMode is already 1. Add every column the data needs with AddColumn before switching — it too refuses once DataMode = 1. Only then set DataMode = 1, and only after that call SetVirtualRowCount. Every Virtual-only member covered below refuses identically outside Virtual mode.
Seeing it work with no host code at all
Two members let you watch Virtual mode's paint, scroll and eviction behavior before writing a single line of fetch-handling code. FillVirtualSynthetic needs no callback wiring: it fills whatever the grid's last paint reported as missing with built-in, deterministic text ("R{row}C{col}" per cell) and repaints. SetVirtualViewportTop is the programmatic equivalent of dragging the scrollbar thumb, useful for jumping to a row far from the top.
VBA
Sub PreviewVirtualMode(grid AsObject)
' Columns have to exist before the switch to Virtual mode -- AddColumn' is refused once DataMode = 1.
grid.Clear
grid.AddColumn "Col A", 120
grid.AddColumn "Col B", 120
grid.AddColumn "Col C", 120
grid.DataMode = 1' 1 = Virtual' A million-row grid costs nothing to declare -- Virtual mode keeps only' a sliding window of rows around the viewport, never the whole count.
grid.SetVirtualRowCount 1000000' FillVirtualSynthetic fills whatever the grid's last paint reported as' missing -- no NeedRows handler, no SupplyRows call, nothing wired up.Dim filled AsLong
filled = grid.FillVirtualSynthetic()
Debug.Print filled ' -> however many rows the current viewport showsDebug.Print grid.GetCellValue(0, 0) ' -> "R0C0"' SetVirtualViewportTop is the programmatic equivalent of dragging the' scrollbar thumb -- useful for exercising the cache far from row 0.
grid.SetVirtualViewportTop 500000
filled = grid.FillVirtualSynthetic()
Debug.Print grid.GetCellValue(500000, 0) ' -> "R500000C0"EndSub
FillVirtualSynthetic only fills rows the grid's last paint actually recorded as missing — call it too early (right after SetVirtualRowCount, with no repaint or message pump yet) and it can legitimately return 0. A Do ... Loop with DoEvents between attempts, a few times over, is the practical way to give the grid a chance to paint first. SetVirtualViewportTop clamps rather than refuses: a topRow at or past the declared row count lands on the last valid row instead of raising an error, though a negative topRow is still refused. Moving the viewport doesn't by itself fill anything — the newly exposed rows still need a fetch (real or synthetic) before GetCellValue on them returns anything but placeholder behavior.
Filled rows behave like any other cached row from the grid's point of view — they count toward the cache and are evicted the same way a real SupplyRows response would be — and filling doesn't touch the generation counter, so it never invalidates anything or causes a pending NeedRows request to be reissued.
The real protocol: NeedRows and SupplyRows
A paint that finds a row missing -- or an explicit InvalidateAllRows call -- posts one deferred message. Once that message is processed, on the next message pump, NeedRows fires, once per requested range, and the host answers it.
For the common case -- you have the rows to hand, right there in the handler -- answer with SupplyRequestedRows and pass only the data:
FetchRows is your own function: a query, an API call, a slice of an array -- the grid neither knows nor cares where the rows come from. Everything else stays exactly where it was:
You still own the rows. The grid caches a window of them for painting; it never owns your data.
You still own the row count.SetVirtualRowCount is how the grid learns it, and it is the only place it comes from.
You still own sorting and filtering.SortRequested and FilterRequested are unchanged -- see "Delegated sort and filter" below.
There is no source object. Nothing is registered, nothing is held by the grid, and there is nothing to release.
What SupplyRequestedRows removes is exactly one thing: echoing requestId, generation and firstRow back. The grid already knows which request is being dispatched, so it takes those three from its own live request context. That is not just less typing -- getting any of the three wrong is the single most common way a Virtual integration silently fails, and the grid counts each variant separately (unknownRequestCount, staleSupplyCount, rangeViolationCount).
It is legal only while a NeedRows handler is running. Call it anywhere else and it says so, and tells you to use SupplyRows instead. Declining is the same one line with Empty:
VBA
IfNot MyDataIsReady Then
lgGrid.SupplyRequestedRows Empty' explicit declineExitSubEndIf
If your data is not ready until later -- a query that completes on a callback, an HTTP response, a background load -- you cannot use SupplyRequestedRows, because by then the dispatch it belongs to has closed. Keep the three tokens yourself and answer with SupplyRows, exactly as below. That is what SupplyRows is for, and it is not going anywhere.
##### The explicit form: SupplyRows
A paint that finds a row missing — or an explicit InvalidateAllRows call — posts one deferred message. Once that message is processed, on the next message pump, NeedRows fires, once per requested range, and the host answers with SupplyRows.
VBA
Sub SupplyRealData(grid AsObject)
grid.Clear
grid.AddColumn "Name", 150
grid.AddColumn "Score", 80
grid.DataMode = 1
grid.SetVirtualRowCount 10000' In a real application, a form's own event handlers, or a reg-free' callback object, would answer NeedRows like this:'' Private Sub lgGrid_NeedRows(ByVal requestId As Long, ByVal generation As Long, _' ByVal firstRow As Long, ByVal lastRow As Long)' Dim n As Long, c As Long' n = lastRow - firstRow + 1' If SomeRowsAreUnavailable Then' m_grid.SupplyRows requestId, generation, firstRow, Empty ' explicit decline' Else' Dim data(0 To n - 1, 0 To 1) As Variant' For r = 0 To n - 1' data(r, 0) = "Row " & (firstRow + r)' data(r, 1) = (firstRow + r) * 10' Next r' m_grid.SupplyRows requestId, generation, firstRow, data ' real answer' End If' End Sub'' Whichever answer arrives, it may be a SHORTER page than what was asked' for (the remainder is simply requested again later), but it must start' at firstRow and never extend past lastRow, or SupplyRows raises an' error and caches nothing.EndSub
requestId and generation must be copied verbatim from the NeedRows event's own arguments — they identify the request, they're not something you choose. data is a 2-D Variant array with exactly as many columns as the grid currently has; either a 0-based Array() or a 1-based Range.Value array works, since only the element counts are checked, not the bounds. SupplyRows can be called synchronously from inside the NeedRows handler — the common case — or later, once your own data becomes available.
This is the one place in the whole protocol worth reading twice: an unanswered request is not a decline. If your handler returns without calling SupplyRows at all, the request simply stays pending — it is retried automatically once it ages out, either by scrolling out of view or after a fixed stall with no reply. An explicit decline is passing Empty or Null as data; that's the only way to actively say "I can't answer this one." A second SupplyRows call answering a request that was already answered is accepted without raising a VBA error, but does nothing beyond that — the attempt is only visible through GetVirtualDiagnostics.
SupplyRows also raises an error, and caches nothing, if the response is longer than what was asked for — a shorter, partial page is fine and simply leaves the remainder to be requested again later.
Delegated sort and filter
Because Virtual mode doesn't own the data, calling any sort method (Sort, SortEx, ClearSort, SortMulti) or filter method (SetColumnFilter, ClearColumnFilter, ClearAllFilters, SetColumnNumericFilter, SetColumnNumericFilterRange, SetColumnValueListFilter, SetFilterState, LoadFilterPreset) while DataMode = 1 doesn't sort or filter anything by itself. It fires SortRequested or FilterRequested and waits for the handler to perform the operation against its own data and set handled = True before treating the request as satisfied.
VBA
Sub DelegateSort(grid AsObject)
grid.Clear
grid.AddColumn "Name", 150
grid.DataMode = 1
grid.SetVirtualRowCount 100' A real host handler looks like this:'' Private Sub lgGrid_SortRequested(ByVal colIndex As Long, ByVal direction As Long, _' ByRef handled As Boolean)' SortMyOwnDataSource colIndex, direction ' direction: 0 = Ascending, 1 = Descending' handled = True ' MUST be set, or the call below fails' End Sub'OnErrorResumeNext
grid.Sort 0, 0' ascending on column 0IfErr.Number <> 0Then' No handler set handled = True: SortRequested's default is declined,' Sort raises an error, and the grid's view does not change at all.Debug.Print"Sort was declined: " & Err.Description
EndIfOnErrorGoTo0EndSub
handled starts False before every firing, on both events — leaving it False is a real, deliberate decline, and the triggering Sort/SetColumnFilter/etc. call raises an error on the caller's side, with nothing about the grid's view changing. Any code that calls a sort or filter method while in Virtual mode should be wrapped to handle that error, the way the recipe above does. Both events also carry a few more mode values beyond the simple ascending/descending case shown here. SortRequested's direction forwards SortEx's case-insensitive/natural-sort modes through unvalidated, so a handler written only for the plain 0/1 case can silently mishandle them. FilterRequested's colIndex = -1 is ambiguous on its own — it means "clear every column" with one mode value but "the whole filter state is being replaced" with another — so a handler that assumes -1 always means "clear" can silently drop a filter state it should have applied instead. See SortRequested and FilterRequested in Part II for the complete mode-value tables.
Invalidating the cache
InvalidateAllRows is the one invalidation entry point Virtual mode offers. There's no way to signal that a single row, or a handful of rows, changed — one changed cell and a full requery are announced identically:
VBA
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 1000
grid.FillVirtualSynthetic
' The host's data changed underneath the grid -- there is no way to say' "just row 42 changed"; the whole visible viewport is invalidated and' re-fetched instead
grid.InvalidateAllRows
OnErrorResumeNextDebug.Print grid.GetCellValue(0, 0)
Debug.PrintErr.Description
OnErrorGoTo0' -> raises an error: row 0 is no longer resident. The earlier synthetic' fill was discarded by the invalidation, and nothing has supplied it' again yet -- Virtual mode never returns an empty string for a row it' does not actually hold, by design.
InvalidateAllRows discards the cache, every pending request, and every previously-declined range together as one atomic step, then re-fetches whatever the current viewport needs. It's rejected from inside any event handler at all, including NeedRows — there's no exception carved out for it the way there is for SupplyRows. A request that was already in flight when you call it isn't cancelled at the host; if an answer for it arrives later anyway, it's recognized as stale by its generation number and discarded rather than applied.
A note on diagnostics and prefetching
Two related members are worth knowing about here even though this chapter doesn't cover them in depth: GetVirtualDiagnostics returns a JSON snapshot of the fetch protocol's internal state, and — unlike everything else in this section — it works in everyDataMode, not just Virtual. SetPrefetchMargin/PrefetchMargin widen how many extra rows Virtual mode requests on each side of the visible viewport for smoother scrolling; they're also settable in any mode, though they only have an observable effect once DataMode = 1. Both are documented fully in Part II under GetVirtualDiagnostics, SetPrefetchMargin and PrefetchMargin — reach for GetVirtualDiagnostics first if a Virtual-mode integration is behaving unexpectedly and you need to see what the fetch protocol thinks is happening, and see Performance and Diagnostics for more on diagnosing a running grid generally.
A protocol fact worth carrying into any real integration: NeedRows never fires synchronously from the call that exposed new rows (SetVirtualRowCount, SetVirtualViewportTop, a real scroll, InvalidateAllRows); it always fires later, on the next message pump, so code expecting it immediately with no DoEvents in between will not see it yet. A large gap is also split across more than one NeedRows firing rather than requested in a single call — see NeedRows in Part II for the exact per-request row limit and how many requests can be outstanding at once.
See SetVirtualRowCount, NeedRows, SupplyRows, InvalidateAllRows, SortRequested, FilterRequested, GetVirtualDiagnostics, SetPrefetchMargin and PrefetchMargin in Part II for every parameter, error case and mode restriction this section only summarized.
Every grid eventually needs two kinds of question answered: "how do I keep this fast while I make a lot of changes at once?" and "why is this slow, or wrong, and how do I find out?" This chapter covers both. The first half is the small set of members that manage memory and repainting during bulk operations -- Compact, EstimateMemory, BeginUpdate/EndUpdate, Refresh. The second half is the diagnostics surface -- LastError, the logging switch, the JSON snapshots, the paint and cache counters, and the built-in benchmark -- that you reach for when something needs explaining, whether to yourself while developing or to a support ticket after the fact.
One piece of context worth keeping in mind throughout: LiteGrid Turbo runs one of three interchangeable storage engines at a time -- Embedded (the default, where the grid owns every row), Virtual (your application owns the data, the grid caches a rendering window), and Turbo column-store (the grid owns the rows but stores them column-by-column for large datasets). Several members in this chapter behave differently, or are refused outright, depending on which of the three is active. Each member below calls out where that matters; DataMode itself is documented in Part II.
Managing Memory: Compact and EstimateMemory
Compact releases memory the grid is holding in reserve without changing the loaded data -- it never touches row count, column count, or any cell value, it only shrinks internal buffers down to what the current data actually needs. Reach for it after a large series of deletes or edits, when you want that memory returned to the process promptly instead of waiting for it to happen on its own.
VBA
Dim data(0To2, 0To1) AsVariant
data(0, 0) = 1: data(0, 1) = "Alpha"
data(1, 0) = 2: data(1, 1) = "Beta"
data(2, 0) = 3: data(2, 1) = "Gamma"
grid.LoadFromArray data
grid.DeleteRow 0OnErrorResumeNext
grid.Compact ' releases the memory the deleted row was usingOnErrorGoTo0Debug.Print grid.GetRowCount() ' -> 2, unchanged whether or not Compact succeeded
Virtual mode refuses Compact outright, because the grid does not own the data it would be repacking -- your application does. In Turbo column-store mode Compact does real work (it folds pending edits into fresh storage and permanently discards deleted rows), but it can fail after rows have been deleted -- wrap it in error handling in Turbo column-store mode if your code depends on it succeeding, as the example above does. See Compact in Part II for the exact failure conditions.
EstimateMemory returns an approximate byte count for the memory the grid's own internal structures are using -- the row, column, sort and string buffers, added together. Treat it as a number for capacity planning, not an exact accounting of every byte the process has allocated.
The gotcha to know before you rely on this outside Embedded mode: in Virtual and Turbo column-store mode, EstimateMemory reports only a rough lower bound, not an accurate total of what your data is actually costing. See EstimateMemory in Part II for why, and for the cap on the return value.
Batching Repaints: BeginUpdate and EndUpdate
BeginUpdate tells the grid to stop repainting while you make a batch of changes, so the user does not see each individual change flicker by one at a time. EndUpdate matches it and, once the last outstanding BeginUpdate is closed, recomputes scrollbars and repaints immediately.
VBA
Dim data(0To99, 0To0) AsVariantDim j AsLongFor j = 0To99
data(j, 0) = ""Next j
grid.LoadFromArray data
grid.BeginUpdate
Dim i AsLongFor i = 0To99
grid.SetCellValue i, 0, CStr(i)
Next i
grid.EndUpdate ' one repaint here, not 100
The calls nest with a counter: two BeginUpdate calls require two EndUpdate calls before the grid resumes repainting on its own. Calling EndUpdate more times than you called BeginUpdate is not an error -- an unmatched EndUpdate is simply ignored. The data changes you make between BeginUpdate and EndUpdate still take effect immediately; only the on-screen repaint is deferred. This works identically across all three data modes, since it only ever affects painting, never the data itself.
Two details distinguish EndUpdate's repaint from a plain Refresh call. First, EndUpdate's repaint is immediate and synchronous, while Refresh schedules a repaint for the next paint cycle rather than forcing one right away. Second, calling Refresh while a BeginUpdate/EndUpdate bracket is still open has no visible effect at all -- the repaint only happens once the bracket closes.
VBA
grid.SetColumnHeader 0, "New Header"
grid.Refresh ' forces the header change to redraw immediately
Refresh never changes data and never fails because of the current data mode -- it works identically in Embedded, Virtual and Turbo column-store mode, and can be called from anywhere, including from inside an event handler.
LastError and CompactMode
LastError holds the message text from whichever call most recently failed -- a refused method, a validation error, an internal exception, whatever it was. It is sticky: it keeps returning that same text until another call fails and overwrites it, even across successful calls made in between, and it returns an empty string before any error has occurred.
VBA
Dim notAMode AsLong
notAMode = 9OnErrorResumeNext
grid.DataMode = notAMode
OnErrorGoTo0Debug.Print grid.LastError ' -> "DataMode must be 0 (Embedded), 1 (Virtual) or 2 (Turbo column-store)."
Because LastError is not cleared by a subsequent successful call, check it immediately after the call you actually care about. For a single call whose outcome you control directly, On Error Resume Next plus Err.Description is usually the more precise tool; reach for LastError when you need to inspect a failure after the fact, or from code that does not control the failing call directly -- a diagnostics routine, for instance, that runs after a batch of operations elsewhere.
CompactMode is a related property that the source groups among the small set of one-time display-configuration settings alongside EmptyStateText, ShowStatusBar, StatusBarMetrics and ShowProgressLabel. It is a separate member from the Compact method covered above -- do not confuse the two. Its full set of accepted values and effects is documented in Part II.
The diagnostics surface
Four members exist specifically to help you see what the grid is doing internally, rather than to change its behavior.
EnableDiagnosticLog turns file-based diagnostic logging on or off. When enabled, the control writes diagnostic detail -- mainly the description of every internal error it encounters -- to a log file under %LOCALAPPDATA%\LiteGrid\Logs, one file per process, named with a timestamp and process ID.
VBA
grid.EnableDiagnosticLog True' ... reproduce the issue ...
grid.EnableDiagnosticLog False
There is no getter -- you cannot ask the control whether logging is currently on, so treat this as a write-only switch you flip on before reproducing a problem and off again afterward. It is also process-wide rather than instance-wide: enabling logging on one control instance turns logging on for every LiteGrid Turbo control running in the same process, not just the one you called it on.
GetFullDiagnosticsJSON returns one JSON object combining memory, paint, and grid-state information in a single call -- a convenient one-call summary for logging or for attaching to a support ticket.
Some of its diagnostic counters are not yet wired up in this release, so don't trust every field -- notably, in Turbo column-store mode the memory section's row count reads 0 rather than the real count, so call GetRowCount() directly if you need an accurate number there. See GetFullDiagnosticsJSON in Part II for the full field list and every mode difference.
GetPaintMetrics is the member to reach for when you actually need live paint-timing numbers. It reports figures drawn from the most recent paints, up to the last 60, and unlike some of the similarly named figures elsewhere in this chapter, it reliably reflects real measurements.
CurrentDpi reports the DPI the grid last recalculated its layout for -- 96 for 100% scaling, 120 for 125%, 144 for 150%, 192 for 200% (API constant) -- updated when the control is created and whenever Windows notifies it of a DPI change.
VBA
Debug.Print grid.CurrentDpi ' -> 96 on a standard-DPI display
Before the grid's window exists, CurrentDpi reports 96 regardless of the system's actual display scaling, so read it after the control is visible if you need an accurate value.
PerformanceStats, CacheHitRatio, GeometryRebuilds and AvgPaintMs
PerformanceStats returns a flat JSON object covering row count, icon count, paint counters, cache counters, geometry rebuild count, and a few display settings, in one call that always succeeds regardless of data mode. CacheHitRatio, GeometryRebuilds and AvgPaintMs expose three of those same figures individually as properties.
Some of these diagnostic counters are not yet wired up in this release. CacheHitRatio and AvgPaintMs always read 0, as do the matching paints, avgPaintMs, lastPaintMs, cacheHits, cacheMisses and cacheHitRatio fields inside PerformanceStats; GeometryRebuilds does update, but only for part of what its name suggests. GetPaintMetrics (above) is the one that reliably reflects real paint timing today -- prefer it whenever you need a live number rather than a placeholder. In Turbo column-store mode, PerformanceStats's rows field also reads 0 rather than the real row count, so use GetRowCount() for an accurate number there. See PerformanceStats, CacheHitRatio, GeometryRebuilds and AvgPaintMs in Part II for the exact field-by-field and mode-by-mode behavior.
RunBenchmark
RunBenchmark exercises a specific part of the control -- loading, sorting, painting or memory use -- and returns timing and sizing figures as JSON. It exists for you to get a quick, repeatable read on how the control performs in your environment, without writing your own timing harness.
VBA
Dim json AsString
json = grid.RunBenchmark(3) ' Memory: does not touch loaded dataDebug.Print json ' -> {"working_set_mb":...,"string_pool_size":...,"cell_count":...}
Value
Meaning
0
Load -- loads a large generated dataset and times the first and a repeated load.
1
Sort -- sorts the grid's currently loaded data ascending, then descending, and times both.
2
Paint -- forces a full repaint and times it.
3
Memory -- reads current process memory and grid sizing figures; does not touch the loaded data.
4
All -- runs Load, Sort, Paint and Memory in sequence.
Only the Memory benchmark (3) is safe to run without disturbing your loaded data. The Load and All benchmarks replace whatever is currently in the grid with generated test data and do not restore your original rows and columns afterward -- reload your own data with LoadFromArray or LoadFromRecordset when you are done (see the previous chapter, Advanced Loading and Streaming). The Sort and All benchmarks sort whatever is currently loaded and leave it sorted, without restoring the original row order. An additional, undocumented benchmarkId also exists in this build and discards whatever is loaded; it is not part of the supported 0-4 set and should not be called in production code -- see RunBenchmark in Part II. Passing a benchmarkId of 6 or higher does not raise an error -- it simply returns a JSON payload with every timing figure at zero.
RunBenchmark only works in Embedded mode. Virtual mode refuses it because a benchmark like this has to run against the host's own data source, not the grid's; Turbo column-store mode refuses it because its surface is read-only.
ShowStatusBar and StatusBarMetrics
ShowStatusBar reserves a band at the bottom of the grid showing a row count on the left, plus, once StatusBarMetrics is configured, aggregate figures on the right.
VBA
grid.ShowStatusBar = True' A status bar band now appears below the grid, showing the row count
Toggling ShowStatusBar repaints immediately, but the scrollbar range only catches up the next time something else recalculates it, such as resizing the control, sorting, or loading data. In Turbo column-store mode, the left-hand row-count text has a known display defect: it always reads "0 rows", never the real loaded row count, regardless of how many rows are actually loaded. GetRowCount() is unaffected and remains accurate in Turbo column-store mode, so use that if you need the real number.
StatusBarMetrics is a comma-separated list of aggregate tokens -- drawn from Count, Sum, Avg, Min and Max, matched case-insensitively -- that chooses what the status bar's right section shows. It has no visible effect until ShowStatusBar is also True. It also defaults to "Count, Sum, Avg" rather than an empty string, so a grid where you have turned on ShowStatusBar but never touched StatusBarMetrics is already showing aggregates -- and, in Turbo column-store mode, is already sitting in the non-empty-StatusBarMetrics danger zone described below unless you clear it explicitly.
VBA
grid.ShowStatusBar = True
grid.StatusBarMetrics = "Sum, Min, Max"' Selecting rows in Embedded mode now totals their numeric cells on the right of the status bar
Value
Meaning
Count
Number of selected numeric cells.
Sum
Sum of selected numeric cells across all columns.
Avg
Average of selected numeric cells across all columns.
Min
Minimum of selected numeric cells across all columns.
Max
Maximum of selected numeric cells across all columns.
The aggregate is scoped to the current row selection, but not to any one column -- it totals numeric cells across every column combined. This matters for realistic data: on an Inventory grid with Qty, UnitPrice and ReorderLevel all numeric, selecting rows and setting StatusBarMetrics = "Sum" sums all three columns together into one figure, not Qty alone. StatusBarMetrics has no way to scope the aggregate to a single column -- if you need a total for just one column, compute it yourself in code rather than through the status bar.
In Virtual mode the aggregate is deliberately not computed at all, to avoid reporting a total based on only whatever rows happen to be cached; the property is still readable and settable there, it just has no visible effect on the totals shown. Token matching itself is a simple case-insensitive substring search rather than a strict comma-separated parse, so unrecognized text is silently ignored rather than raising an error.
The gotcha in this pair worth flagging above the others: in Turbo column-store mode, combining a non-empty StatusBarMetrics with an active row selection can make the control read outside its own memory and crash the host application. Until this is corrected, avoid selecting rows while StatusBarMetrics is non-empty in Turbo column-store mode -- either leave StatusBarMetrics empty there, or keep row selection off.
Putting it together
A typical bulk-update routine combines several of these members: bracket the writes in BeginUpdate/EndUpdate so the screen does not flicker, call Compact afterward if you deleted a meaningful number of rows and want the memory back promptly, and use GetPaintMetrics rather than AvgPaintMs or PerformanceStats if you want to confirm the change actually improved paint time. When something goes wrong along the way, check LastError right after the call you suspect, and turn on EnableDiagnosticLog before you try to reproduce it if a quick look at LastError is not enough. GetFullDiagnosticsJSON is the fastest way to capture a snapshot for a support ticket, keeping in mind that its cache-hit and (in Turbo column-store mode) row-count figures are not to be trusted yet.
LoadFromArray, LoadFromRecordset, GetRowCount, IconSize and the other members named above by cross-reference only are documented fully in Part II.
The control's own JSON methods and the standalone JSON Helper object both run on the same built-in JSON engine, so the JSON text one produces is exactly what the other expects to consume. The difference is what each one is for: the grid's JSON methods move data and view state in and out of a grid you already have on a form; the Helper reads, writes and reshapes arbitrary JSON text with no grid involved at all — an HTTP response, a config file, a payload you are about to send somewhere. This chapter covers both briefly. The Helper's full 18-method reference, with every path rule, mode restriction and error behavior spelled out, lives in Part II.
The grid's own JSON methods
Four members on the control itself move JSON in and out of a grid:
LoadFromJson loads a JSON array-of-objects string ([{"Col1":"val1",...}, ...]) into the grid — the keys of the first object become the column headers, and every object becomes a row. It is an Embedded-mode-only loader: Virtual and Turbo column-store mode refuse it. Every value lands as text, even a numeric-looking one, so a follow-up SetColumnType is what you reach for if you need numeric sorting afterward.
``vba Dim json As String json = "[{""CustomerID"":1,""CustomerName"":""Alpha Traders""}," & _ "{""CustomerID"":2,""CustomerName"":""Beta Supply""}]" grid.LoadFromJson json Debug.Print grid.GetColumnHeader(0) ' -> "CustomerID" Debug.Print grid.GetCellValue(1, 0) ' -> "2" (stored as text, not the number 2) ``
ExportToJson returns the grid's current view as a JSON array, one object per row, keyed by column headers. Every value comes back as a JSON string — a Double column exports as "20.5", not 20.5 — and a blank cell exports as the JSON literal null. It works in both Embedded and Turbo column-store mode (Virtual mode refuses it, since Virtual has no dataset of its own to read).
``vba Dim json As String json = grid.ExportToJson() ' -> [{"CustomerName":"Alpha Traders","Balance":"1250.5"}, ...] ``
GetGridState / SetGridState capture and restore the grid's view — column layout, filters, sort, scroll position, frozen panes — as one JSON string. They deliberately do not touch row data, so pair them with your own way of reloading the underlying rows if you want both back. Both are Embedded-mode-only.
VBA
Dim savedLayout AsString
savedLayout = grid.GetGridState()
' ... later, after the same data has been reloaded ...
grid.SetGridState savedLayout
SetGridState rejects malformed JSON outright and changes nothing; a well-formed payload missing a section (say, no "sort") leaves that part of the grid's current state untouched rather than clearing it.
See LoadFromJson, ExportToJson, GetGridState and SetGridState in Part II for parameters, full mode tables and the complete gotcha list for each.
Creating the standalone Helper
The Helper is a separate, non-visual COM object — it is not a member of the grid. You can create it either as a registered ActiveX object, or through a registration-free factory function exported by the same OCX:
VBA
' RegisteredDim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
' Registration-free. Note the real exported signature takes the output' object BYREF and returns an HRESULT as a Long, so it is called as a' statement -- not Set jh2 = LiteGridTurbo_CreateHelper() -- mirroring how' LiteGridTurbo_CreateControlInParent is called elsewhere in this Cookbook.PrivateDeclarePtrSafeFunction LiteGridTurbo_CreateHelper _
Lib"LiteGridTurbo_x64.ocx" (ByRef ppDisp AsObject) AsLongDim jh2 AsObject
LiteGridTurbo_CreateHelper jh2
Either instance behaves identically. Nothing about the Helper is affected by a grid being on the form, by any grid's DataMode, or by trial status.
One thing to internalize before writing more than a single call: the Helper is stateless. Every method takes the complete JSON text as an ordinary string parameter and, for the write methods, returns a brand-new JSON string as its result — there is no document the Helper is quietly building up for you. jh.SetValue(myJson, "Balance", 100) does not change myJson; you have to capture the result yourself (myJson = jh.SetValue(myJson, "Balance", 100)) or the change is discarded.
Reading a value by path: GetValue
GetValue is the method to reach for when you have a blob of JSON — an API response, a saved settings file — and just want one field out of it, without writing a parser. Paths use dot notation for object keys and bracket notation for array indexes, e.g. customer.orders[0].total.
VBA
Dim responseJson AsString
responseJson = "{""customer"":{""name"":""Ada Lovelace"",""balance"":1250.5}}"Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim custName AsVariant
custName = jh.GetValue(responseJson, "customer.name")
Debug.Print custName ' -> Ada LovelaceDim discount AsVariant
discount = jh.GetValue(responseJson, "customer.discountPct")
If IsNull(discount) Then discount = 0' path not present -> Null, not an error
GetValue never raises an error for a missing path — it returns Null. Code reading an optional field should check IsNull(result) (or call Exists first) rather than assume a value came back.
Writing a value by path: SetValue
SetValue creates or replaces a value at a path, and — usefully — auto-creates any object or array the path passes through that does not exist yet. Building a JSON payload from scratch is a common use:
Writing to "Customer.Name" against "{}" creates the nested Customer object for you. Because every call returns a new string, the pattern is always payload = jh.SetValue(payload, ...), reassigning the variable each time.
SetValue is one of the Helper's fail-quiet methods: given a bad path or a root that isn't a JSON object or array to begin with, it silently returns your original string unchanged rather than raising an error. If you need to know whether a write actually took effect, compare the result to the input.
Formatting JSON for display: PrettyPrint
PrettyPrint reformats a compact JSON string for readability — useful when you are about to show JSON in a message box, a log, or a text box for a developer to read.
PrettyPrint is also fail-quiet, and it caps its input at 5 MB (API constant).
The rest of the Helper
The remaining methods follow the same shape — a JSON string in, a JSON string (or a scalar result) out:
id
Method
Result / behavior
1
GetValue
Typed value at a path; missing path returns Null
2
SetValue
Creates or replaces a value
3
RemoveKey
Removes a key at a path
4
Exists
Returns whether a path exists
5
Append
Appends to an array
6
Insert
Inserts into an array at an index
7
Clear
Empties the container at a path while retaining the key
8
Merge
Merges two JSON objects, optionally deeply
9
Count
Counts members; malformed input raises an error
10
PrettyPrint
Formats JSON; input cap is 5 MB (API constant)
11
Flatten
Converts nested data to dot notation
12
Unflatten
Converts dot notation to nested data
13
IsValid
Validates syntax and does not raise for invalid JSON
14
GetArrayLength
Array length, or -1 when the target is not an array
15
GetArrayItem
Gets one array item
16
GetKeys
Returns a JSON array of object keys; an empty object returns []
17
ToArray
Converts an array of objects to a two-dimensional array; caps are 100,000 rows, 1,000 columns and 50 MB (API constant)
18
Serialize
Converts a VBA value, array or Scripting.Dictionary to JSON
Most write methods (SetValue, RemoveKey, Append, Insert, Clear, Merge, PrettyPrint, Flatten, Unflatten) are fail-quiet, the same way SetValue is above. Count, ToArray and Serialize are the documented exceptions and raise a failing HRESULT for invalid input — wrap those three in On Error when the input's shape isn't already guaranteed. See each method by name in Part II for its full signature, path rules and worked recipes.
Your First Grid mentioned in passing that LiteGrid Turbo needs no key to try: every method in this Cookbook works during evaluation. This chapter is the full treatment — where the evaluation window comes from, what changes when it ends, how to activate a real key when you have one, how to read license state without guessing which state you're in, and how to handle a key that gets rejected.
None of this is required reading to ship a working evaluation build. It matters once you're preparing a build for a paying customer, or once you want your own startup code to log or react to license state deliberately instead of just showing the default watermark.
The trial works fully, with a watermark, no key needed
Your evaluation copy of LiteGrid Turbo already contains its own trial period. When you request an evaluation you receive a personalized download, and the trial window — the date it was issued and the date it expires — is signed into the OCX files in that download. There is no setup step, nothing to call, and no key to paste. Every data method, every editing feature, every export path, every mode works exactly the same during the trial as under a paid license.
VB
' Nothing to do here -- the evaluation copy you were sent is already trialing,' on the window that was issued with it.' Load data, sort it, edit it, export it: everything works.
grid.LoadFromArray customerData
Two consequences worth planning around:
The window is absolute, and it travels with the files. Copying your evaluation OCX to a second PC, a different Windows user, or another folder does not start a second trial: every copy carries the same issued and expiry dates. That is deliberate, and it is also convenient — you can evaluate on your laptop and your test VM without asking for anything extra, and both will agree on exactly how long is left.
Use the copy you were sent. An OCX that carries no evaluation entitlement — for example one extracted from an unrelated build — will not start a trial of its own. It reports EXPIRED and behaves as an expired evaluation does (see below). If that happens, request a fresh evaluation package rather than looking for a setting to change.
You can build and demo a complete application against the trial, and the only thing that changes when you later apply a real key is that the watermark goes away and the restrictions lift. Nothing about your code needs to branch on license state unless you choose to make it.
Activating a license: one call, once per process
When you do have a real key, apply it with ActivateLicense:
A few things about this call are easy to get wrong the first time:
It's session-only. A successful call updates an in-memory license state for the current process. Nothing about the key itself is written to disk. The next time the process starts — a new Access session, a new instance of your Excel add-in — it starts back in the state its OCX carries (the evaluation window signed into that copy, or expired once that window has closed), and ActivateLicense has to be called again. The natural place to call it is your application's startup code, once, before you create (or right after you create) the first grid.
It's shared process-wide, across every control instance. You do not call ActivateLicense once per grid. A single successful call applies to every LiteGrid Turbo control already created in the process and every one created afterward — one grid or twenty, and it does not matter whether some of those instances are registered COM controls and others are reg-free, as long as they were all created from the same loaded OCX file (two different OCX file paths loaded into one process each keep their own licence state). Calling it again on a second grid is harmless but unnecessary.
company is for your own records only. Pass your customer's real name for readability in your own code and logs, but nothing on the control reads it back — it is not recorded, not validated against the key, and not what GetLicenseInfo's own company field reports later (that field comes from the name signed inside the key itself, not from what you pass here).
Never place a real signed key in shared source code, a checked-in sample project, or documentation. A real key is customer- and company-specific; every example in this Cookbook — including this chapter — uses the literal placeholder string "PLACEHOLDER-KEY-NEVER-A-REAL-ONE", which is guaranteed to fail validation. Pull a real key from a per-deployment configuration source instead: an INI file, a registry value your installer writes, or similar.
Where the trial window comes from, and what the local marker is for
The evaluation window lives inside the OCX, as a cryptographically signed entitlement carrying the issue date, the expiry date, the product and the tier. It is the only thing that decides how long your evaluation runs. It cannot be edited without invalidating its signature, and nothing on your machine can extend it.
A small local marker — a start date and a last-run date, nothing else — is still written to both HKCU\Software\LiteGridTurbo and a file under %LOCALAPPDATA%\LiteGridTurbo, but its job is now much smaller: it is evidence, not authority. It exists only to notice a system clock that has moved backwards. It cannot create, extend or shorten an evaluation.
What that means in practice:
Deleting the marker does nothing useful. It does not reset the trial and it does not start a new one. The expiry date is in the file you were sent. Clearing local state simply loses the record of when you last ran, which slightly reduces clock-rollback precision and nothing else.
If the marker cannot be read or written at all — a locked-down enterprise profile, a roaming profile, security software holding the file — the grid carries on normally on its signed window. Blocked storage is not treated as tampering and never denies you your evaluation.
Reinstalling Windows, restoring a VM snapshot, or moving to a new machine leaves the expiry date exactly where it was.
This storage is not shared with LiteView2. The two products track their state independently.
A machine that ran an older LiteGrid Turbo build before is not penalised. Installing a newly issued evaluation package makes that package's signed window authoritative; the old local state is ignored rather than treated as a problem.
Reading license state without assuming which one you're in
Because activation is session-only, you can't assume a fresh process starts in the trial — a build that already called ActivateLicense earlier in the same process is fully licensed, and an evaluation copy may already be past its signed expiry date. Read the actual state instead of guessing it:
VBA
Sub ReadLicenseState(grid AsObject)
Dim st AsString, days AsLong, stateCode AsLong
st = grid.LicenseStatus ' "TRIAL", "EXPIRED", "EVAL", "PRO" or "ENTERPRISE"
days = grid.TrialDaysRemaining ' 0 once expired or once any key is applied
stateCode = grid.LicenseState ' 0=Valid, 1=Trial, 2=Expired, 3=InvalidSelectCase stateCode
Case0Debug.Print"Licensed (" & grid.LicenseType & "), version " & grid.LicenseVersion
Case1Debug.Print"Trial, " & days & " day(s) remaining"Case2Debug.Print"Evaluation expired -- grid is covered and not interactive"EndSelectDebug.Print grid.GetLicenseInfo()
EndSub
LicenseStatus, LicenseState, LicenseType, and TrialDaysRemaining all read from the same underlying state and always agree with each other. Pick whichever shape is most convenient for your own branching logic — a string, an integer code, or the full JSON from GetLicenseInfo — rather than parsing one of the string properties to derive what another already gives you directly.
A quick tour of the seven read-only members:
LicenseStatus is the shortest way to log or display state as a human-readable string: "TRIAL", "EXPIRED", "EVAL", "PRO", or "ENTERPRISE".
VB
Debug.Print grid.LicenseStatus ' -> "TRIAL" while this copy's signed evaluation window is open
Watch for two things: the control's own IDL helpstring (and Object Browser text) lists only four values and omits "EVAL", which is real and returned after activating a time-limited evaluation key — don't hard-code a 4-way match. And an activated EVAL key's expiry is checked live, so LicenseStatus can flip from "EVAL" to "EXPIRED" partway through a long-running process with no restart involved. See LicenseStatus in Part II for the full detail.
TrialDaysRemaining gives you a whole-day countdown for your own UI or logging.
VB
Debug.Print grid.TrialDaysRemaining ' -> whole days left in this copy's signed evaluation window
It reads 0 for an expired trial, but also 0 for any activated key (EVAL, PRO, or ENTERPRISE) — 0 alone doesn't prove the trial ran out, so pair it with LicenseStatus or LicenseState if that distinction matters. It also rounds up to whole days, so any part of the current day counts as a day remaining. If you need the remaining days on an activated EVAL key specifically, this property won't give it to you — that number lives in GetLicenseInfo's daysRemaining field instead, described below.
LicenseState is the same information as LicenseStatus, as an integer, for simple Select Case branching: 0 (Valid), 1 (Trial), 2 (Expired), 3 (Invalid — a reserved fallback not produced by normal use). It collapses EVAL/PRO/ENTERPRISE into a single 0; use LicenseType when you need to tell those apart.
LicenseType reports the tier as a lowercase string — "trial", "eval", "pro", or "enterprise" — without distinguishing active from expired.
VB
Debug.Print grid.LicenseType ' -> "trial", whether running or already expired
This is the one property in the group that deliberately does not answer the "is it still valid" question — it reads "trial" for both an active and an already-expired trial, so pair it with LicenseStatus, LicenseState, or TrialDaysRemaining when validity matters. Its own IDL helpstring also omits "eval" the same way LicenseStatus's does.
LicenseExpiry returns the date this license stops being valid.
VB
Debug.Print grid.LicenseExpiry <> 0' -> True for an evaluation copy, running or expired
The one gotcha worth remembering: 0 does not mean "trial, no expiry." It means a perpetual license, or an OCX that carries no evaluation entitlement at all. An evaluation copy — running or already expired — always reports a real, non-zero date. See LicenseExpiry in Part II for the exact sentinel rules and edge cases.
LicenseVersion is a small diagnostic tag, not the product's build version: "1.0" for the trial, "1.0+" once a key has been successfully activated in this process. Use it for support logging, not for detecting which LiteGrid Turbo build is installed.
GetLicenseInfo returns the whole picture as one flat JSON string — useful when you want to log everything at once rather than reading six properties individually.
It never echoes back any credential material — the key itself is not included — so it's safe to log or display as-is. One trap: tier and state use their own casing and spelling, not what LicenseType or LicenseStatus report for the same state — don't compare these JSON fields against those properties for equality. See GetLicenseInfo in Part II for the exact field values and casing.
expiryUtc and daysRemaining are independently null-or-not, which matters if you're parsing this JSON programmatically rather than just logging it. expiryUtc is null only for a perpetual license. daysRemaining is null for a perpetual license too, but for the trial — active or already expired — it is always a real number, including 0, never null. And unlike the TrialDaysRemaining property above, this field does track an activated EVAL key's own remaining time, not just the evaluation window's.
Whichever member you read, the underlying state is process-wide and independent of any one grid's data or mode: Clear, LoadFromArray, and switching between Embedded, Virtual, and Turbo column-store mode never touch it. See LicenseStatus, TrialDaysRemaining, LicenseState, LicenseType, LicenseExpiry, LicenseVersion, and GetLicenseInfo in Part II for the complete signatures, default values, and per-mode support tables.
Handling a rejected activation call
ActivateLicense is the one member in this group that can genuinely fail. An invalid, malformed, expired, or wrong-product key raises an error, and every license property keeps reporting exactly what it reported before the call — nothing is left half-applied.
VBA
Sub ActivateOnStartup(grid AsObject, companyName AsString, signedKey AsString)
OnErrorResumeNextErr.Clear
grid.ActivateLicense companyName, signedKey
IfErr.Number <> 0ThenDebug.Print"Activation failed: " & Err.Description
' A rejected key changes nothing: the grid keeps the licence state it had' (an expired evaluation stays covered) until a valid key is supplied.ElseDebug.Print"Activated: " & grid.GetLicenseInfo()
EndIfOnErrorGoTo0EndSub
Because a failed call changes nothing, there's no cleanup path to write — a rejected key just leaves the trial (or previous state) exactly as it was. Call this once from your startup code, and if it fails, decide for yourself whether to prompt the user, log it for support, or simply leave the evaluation state as it is until a valid key is supplied later. See ActivateLicense in Part II for the exact parameter behavior and the full gotcha list, including what a wrong-product or already-expired key looks like on the wire.
Never place a real signed key in shared source code or documentation.
LiteGrid Turbo is an unbound grid on purpose: it never opens your database, never holds a connection, and never writes anything anywhere. You load records into it, the user edits them, and you decide what to save. That is the whole design, and chapter 15 explains why it is a feature rather than a gap.
It does leave you with one question the grid has to help you answer, though:
> The user sorted the grid, filtered it, edited three cells, deleted a row and added one. Which records changed?
Row numbers cannot answer that. The user sorted the grid after editing it, so "row 3" is a different record now than it was then. This chapter is about the two properties that fix it.
Give every record a key
A key is whatever your application already calls a record: a CustomerId, an order number, a GUID. You tell the grid which column holds it:
That is the whole setup. KeyColumn keys every row already loaded and every row loaded afterwards, so the reverse order works identically:
VBA
Grid.KeyColumn = 0
Grid.QuickBindRecordset rs
From then on the key belongs to the record, not to its position. Sort the grid, filter it, insert rows above it, delete rows below it — Grid.FindRowByKey("ALFKI") still finds the same customer.
Keys must be unique, must all be the same kind of value (all text, or all numbers), and cannot be blank. If the column you picked breaks any of those rules, KeyColumn refuses and tells you which value is the problem and how many rows carry it — and it refuses before changing anything, so nothing is half-keyed:
text
KeyColumn: column 'City' cannot be a key column. The value London appears in 6 rows;
a key must identify exactly one record. No key was assigned and KeyColumn is unchanged.
If your records have no natural key column, assign keys yourself with Grid.SetRowKey row, someValue.
Turn on change tracking
VBA
Grid.TrackChanges = True
It is off by default, and off it costs nothing at all — a dashboard that never saves anything should not pay for a feature it never uses. Turn it on once, after you have set KeyColumn, and the grid starts recording.
From then on:
Member
Answers
RowState(row)
0 Unchanged, 1 Modified, 2 Inserted
GetChangedRows
every edited cell: key, column index, column name, old value, new value
GetInsertedRows
every new record: its key, then its cell values
GetDeletedRows
the keys of deleted records
AcceptChanges
"I saved it" — clears the record. Grid state only; it writes nothing anywhere
RejectChanges
undoes every tracked change and clears the record
Everything is reported by key, never by row number. That is the point.
Recipethe complete CRUD round trip
This is the whole pattern. It works whatever the user did to the view in between.
VBA
Sub SaveGridChanges(Grid AsObject, cn AsObject)
Dim changes AsVariant, inserts AsVariant, deletes AsVariantDim i AsLong, k AsVariant, col AsString' ---- UPDATE: one statement per edited cell -------------------------
changes = Grid.GetChangedRows
If IsArray(changes) ThenFor i = LBound(changes, 1) To UBound(changes, 1)
k = changes(i, 0) ' the record's key
col = changes(i, 2) ' the column NAME
cn.Execute "UPDATE Customers SET [" & col & "] = '" & _
Replace(CStr(changes(i, 4)), "'", "''") & "' " & _
"WHERE CustomerId = '" & Replace(CStr(k), "'", "''") & "'"NextEndIf' ---- DELETE: keys only. A deleted row has no row number ------------
deletes = Grid.GetDeletedRows
If IsArray(deletes) ThenFor i = LBound(deletes) To UBound(deletes)
cn.Execute "DELETE FROM Customers WHERE CustomerId = '" & _
Replace(CStr(deletes(i)), "'", "''") & "'"NextEndIf' ---- INSERT: key first, then the record's cell values --------------
inserts = Grid.GetInsertedRows
If IsArray(inserts) ThenFor i = LBound(inserts, 1) To UBound(inserts, 1)
cn.Execute "INSERT INTO Customers (CustomerId, CompanyName, City) VALUES ('" & _
Replace(CStr(inserts(i, 1)), "'", "''") & "','" & _
Replace(CStr(inserts(i, 2)), "'", "''") & "','" & _
Replace(CStr(inserts(i, 3)), "'", "''") & "')"NextEndIf' ---- only now, once the database has actually accepted it ----------
Grid.AcceptChanges
EndSub
Call AcceptChangesafter your save succeeds, not before. If the save fails, leave the change set alone and the user can try again.
New records whose key does not exist yet (AutoNumber / IDENTITY)
A row the user inserts has no primary key until the database INSERT hands one back — but every tracked record needs an identity. The pattern is:
Give the new row a temporary unique key with SetRowKey. When your real keys are positive AutoNumbers, negative numbers are the conventional choice:
Save. GetInsertedRows reports the row under its temporary key.
Replace the temporary key with the real one, before AcceptChanges:
``vba Grid.SetRowKey rowIndex, rs!NewId ' the AutoNumber the INSERT returned ``
The row keeps its Inserted state and any pending edits — only its identity changes. Assigning the same key twice is harmless, so a retried save loop is safe.
Grid.AcceptChanges.
If your application supplies its own primary keys, none of this applies — key the row properly the moment you create it.
Two things that will bite you
Reloading discards everything.Clear, LoadFromArray, QuickBindRecordset and friends replace the records, so the keys and the change set go with them — they describe records that no longer exist. RefreshFromRecordset will not do that behind your back: while there are unsaved tracked changes it refuses by name and changes nothing, so you get to choose between saving and discarding rather than losing the record of what the user did.
Turbo column-store mode restores deleted rows differently.RejectChanges in Turbo column-store mode refuses if a record has been deleted, and refuses before changing anything. Use Undo there, or AcceptChanges if the deletion was intended.
What this does not do
There is no SaveChangesToRecordset. The grid does not know your schema, your connection or your business rules, and a helper that guessed at all three would be wrong more often than the eight lines above. What it gives you instead is the one thing you cannot work out for yourself: which record.
Part II
Complete API Reference
Every control member, event and Helper method, with copy-pasteable VBA. Modes: Embedded (DataMode=0, default), Virtual (DataMode=1), Turbo column-store (DataMode=2).
This part is exhaustive and organized for lookup, not for reading start to finish. If you are new to LiteGrid Turbo, Part I — Learn LiteGrid Turbo is the faster way in: it teaches the same members through realistic scenarios, in the order a working grid actually needs them, and links into the entries below wherever more depth is useful.
Once the control exists as an object (grid), every method and property call below is IDENTICAL VBA/VB6 code whether you created it the Registered way or the Registration-free way -- see Registration-Free Deployment in Part I. The only places Registered and Reg-Free genuinely differ are control creation, events (the callback bridge) and teardown; entries that differ carry a Reg-Free difference block.
Lifecycle, configuration and diagnostics
This group covers the small set of members you reach for outside of normal row-and-cell editing: putting data into the grid and taking it back out, choosing which of the three storage engines the grid uses, controlling what the grid shows while it has no data or is busy, and reading back what the grid is doing internally. If you only ever use one member from this Cookbook, it will probably be one of these.
The central concept is DataMode. LiteGrid Turbo ships three interchangeable engines behind one object model: Embedded mode (the default), where the grid owns and stores every row itself; Virtual mode, where your application owns the data and the grid only caches a rendering window of it; and Turbo column-store mode, where the grid owns the rows but stores them column-by-column for large datasets. Exactly one of the three is live at any moment, and the grid enforces a simple rule when you switch: whichever store you are leaving must be empty first. In practice this means calling Clear before you change DataMode if you have already loaded data. Reading DataMode is always safe from any mode; writing it is refused if the current store still has rows, if a column-array load session is in progress, or if you call it from a thread other than the one that created the control.
BeginUpdate and EndUpdate bracket a batch of changes so the screen does not repaint after every single one. They nest: two BeginUpdate calls require two EndUpdate calls before the grid resumes automatic repainting, and calling EndUpdate more times than you called BeginUpdate is simply ignored rather than treated as an error. Refresh, Clear and Compact are the other three members that touch the whole grid at once -- Refresh repaints, Clear empties the grid back to zero rows and columns, and Compact releases memory the grid is holding in reserve without changing what is loaded. Clear and Compact are the two members in this group that Virtual mode refuses outright, because both would require the grid to own the data it is being asked to throw away or repack.
The remaining members fall into three practical categories: display configuration you set once (EmptyStateText, ShowStatusBar, StatusBarMetrics, ShowProgressLabel, CompactMode), transient UI state you turn on and off around a long operation (ShowLoadingOverlay/HideLoadingOverlay), and instrumentation you read to understand what the grid is doing (Version and friends, LastError, the paint and cache counters, GetFullDiagnosticsJSON, PerformanceStats, RunBenchmark). A few of the numeric diagnostics -- CacheHitRatio, AvgPaintMs, and several fields inside PerformanceStats -- currently always report zero; GetPaintMetrics is the one that reliably reflects real paint timing, so prefer it when you need live numbers. GetGridState and SetGridState round out the group: they save and restore the grid's column layout, filters, sort, scroll position and frozen panes as a single JSON string, which is convenient for remembering a user's view between sessions, but they do not carry row data -- you still load that yourself.
RecipeSwitching from Embedded to Turbo column-store mode and back
VBA
PublicSub Recipe_SwitchDataMode(grid AsObject, data AsVariant)
' Start in Embedded mode (the default) and load some data normally.
grid.Clear
grid.DataMode = 0' 0 = Embedded
grid.LoadFromArray data
Debug.Print"Embedded row count: " & grid.GetRowCount()
' To move the same control into Turbo column-store mode you must' empty the Embedded store first -- DataMode refuses to switch' while it still has rows.
grid.Clear
grid.DataMode = 2' 2 = Turbo column-store
grid.LoadFromArray data
Debug.Print"Turbo column-store row count: " & grid.GetRowCount()
' Clear() in Turbo column-store mode empties the Turbo column-store table but leaves Turbo column-store' selected, so a second load right after Clear still goes to Turbo column-store.
grid.Clear
grid.LoadFromArray data
Debug.Print"Reloaded into Turbo column-store without resetting DataMode: " & grid.GetRowCount()
' Returning to Embedded mode again requires the Turbo column-store table to be' empty, so Clear before switching back.
grid.Clear
grid.DataMode = 0EndSub
DataMode never copies data between engines -- each mode's store is independent, and the grid insists the store you are leaving is empty before it will let you switch. The pattern is always the same: Clear, then set DataMode, then load. Forgetting the Clear raises a runtime error rather than silently doing the wrong thing, so wrap mode switches in this three-step sequence and you avoid the most common mistake with this property.
RecipeOne-call setup, then locking the grid down for viewing
VBA
PublicSub Recipe_QuickBindThenLock(grid AsObject, rawData AsVariant)
grid.Clear
grid.DataMode = 0' QuickBind is only available in Embedded mode' QuickBind loads the data, detects column types, auto-fits columns,' and turns on filter buttons, header-click sorting and the status bar' in a single call -- useful for a quick "drop this array in" view.
grid.QuickBind rawData, True' True = first row holds the headersDebug.Print"Rows: " & grid.GetRowCount() & " Columns: " & grid.GetColumnCount()
' Now turn the same grid into a read-only dashboard: no editing,' no selection, but keep the status bar visible.
grid.SetMode "Dashboard"Debug.Print"AllowEditing after Dashboard preset: " & grid.AllowEditing
EndSub
QuickBind and SetMode both work only in Embedded mode -- Virtual mode refuses them because the host owns the data, and Turbo column-store mode refuses them because its surface is read-only. Combining the two is a common pattern: QuickBind gets a grid from bare data to something usable in one call, and SetMode then applies a named preset (Excel, ReadOnly, Dashboard or DataEntry) on top without you having to set half a dozen properties by hand.
RecipeShowing progress during a bulk load, then saving the layout
VBA
PublicSub Recipe_LoadWithOverlayAndSaveState(grid AsObject, bigData AsVariant)
grid.Clear
grid.DataMode = 0
grid.ShowLoadingOverlay "Loading records..."
grid.BeginUpdate
grid.LoadFromArray bigData
grid.SetFrozenColumns 1
grid.Sort 0, 0' ascending by column 0
grid.EndUpdate ' repaints once, here, not after every step above
grid.HideLoadingOverlay
' Remember the user's view (columns, filter, sort, scroll, frozen panes)' so it can be restored next time -- this does NOT include the row data.Dim savedLayout AsString
savedLayout = grid.GetGridState()
' ... later, after the same data has been reloaded ...
grid.SetGridState savedLayout
EndSub
ShowLoadingOverlay/HideLoadingOverlay give the user feedback during a load without blocking input, and BeginUpdate/EndUpdate around the load and the sort/freeze calls means the grid repaints once at the end instead of after each step. GetGridState/SetGridState then let you persist the resulting view -- column widths, the freeze, the sort -- as one JSON string, independent of however you choose to persist the underlying rows.
ClearLifecycle, configuration and diagnostics
Empties the grid back to zero rows and zero columns.
Clear removes every row and column, along with sort order, filters, formatting, frozen panes and the current selection. It is the standard way to reset a grid before loading a new dataset, and it is also the required first step before switching DataMode. In Turbo column-store mode, Clear empties the Turbo column-store table but leaves Turbo column-store selected as the active mode, so the very next load call uses Turbo column-store again without you needing to set DataMode a second time.
Virtual mode refuses Clear because the grid does not own the rows there is nothing for it to empty. Embedded and Turbo column-store mode both support it; Turbo column-store keeps Turbo column-store selected afterward.
Clear cancels any in-place cell edit in progress rather than committing it.
Sort order, frozen rows/columns, filters, column groups (AddColumnGroup) and formatting rules are all reset along with the data -- only the value change (an empty grid) is what fires the DataChanged event. A LoadFromArray that replaces the dataset resets the same state, whether or not DataMode changes as part of that call.
In Turbo column-store mode, Clear does not change DataMode; the grid stays in Turbo column-store, ready for another load.
See alsoCompact, DataMode, GetRowCount, GetColumnCount
GetRowCountLifecycle, configuration and diagnostics
Returns the number of rows currently visible in the grid.
GetRowCount reports the number of rows in the current view, after any filter or sort has been applied. It reads from whichever engine is active, so it reports the real total in Embedded, Virtual and Turbo column-store mode alike -- it never silently falls back to reporting zero just because a different mode is selected.
Works identically in all three modes; in Virtual mode it reports the row count the host declared, and while a partial load is still in progress it reports the count loaded so far.
While a load is still in progress, GetRowCount reports only the rows loaded so far, not the eventual total.
See alsoGetColumnCount, Clear, DataMode
GetColumnCountLifecycle, configuration and diagnostics
Returns the number of columns currently defined on the grid.
GetColumnCount reports how many columns are defined, regardless of which data mode created them. Columns share one internal list across Embedded, Virtual and Turbo column-store mode, so this count is always accurate no matter which engine loaded the data.
Signaturegrid.GetColumnCount() As Long
ReturnsLong: the current column count (0 or more).
Releases memory the grid is holding in reserve, without changing the loaded data.
Compact shrinks the grid's internal buffers down to what the current data actually needs. It does not change the row count, column count or any cell value -- it only affects how much memory the grid is holding onto. In Turbo column-store mode, Compact also folds any pending edits into fresh storage and permanently discards rows that have been deleted; in Embedded mode it simply releases spare capacity.
Virtual mode refuses Compact because the grid does not own the data it would be repacking. Turbo column-store mode performs a real, more substantial compaction than Embedded mode's simple buffer shrink when it succeeds, but success is not guaranteed -- see the gotcha below.
Example
VBA
Dim data(0To2, 0To1) AsVariant
data(0, 0) = 1: data(0, 1) = "Alpha"
data(1, 0) = 2: data(1, 1) = "Beta"
data(2, 0) = 3: data(2, 1) = "Gamma"
grid.LoadFromArray data
grid.DeleteRow 0OnErrorResumeNext
grid.Compact ' releases the memory the deleted row was using -- can fail in Turbo column-store mode, see gotchasOnErrorGoTo0Debug.Print grid.GetRowCount() ' -> 2, unchanged whether or not Compact succeeded
Gotchas
Compact never changes row or column counts -- it is purely a memory-management call.
In Turbo column-store mode, once started, Compact cannot be canceled; it runs to completion.
Call Compact after a large series of deletes or edits if you want memory returned promptly rather than waiting for it to happen on its own.
In Turbo column-store mode, Compact can genuinely fail with the error "Compact: the Turbo column-store seam refused compaction" after rows have been deleted -- observed on a small table (3 rows, 1 deleted). The exact trigger was not fully isolated; do not assume a Turbo column-store-mode Compact call is guaranteed to succeed, and wrap it in error handling if your code depends on the outcome.
See alsoEstimateMemory, Clear, BeginUpdate
EstimateMemoryLifecycle, configuration and diagnostics
Returns an approximate byte count for the memory the grid's own data structures are using.
EstimateMemory adds up the capacity of the grid's internal row, column, sort and string buffers and returns the total in bytes. It is an approximation intended for capacity planning, not an exact accounting of every byte the process has allocated. The call does not check DataMode at all: in Virtual and Turbo column-store mode it still reports the size of the same Embedded-mode row/column/string-pool scaffolding, which stays largely empty while either of those two modes is active, so treat the figure as a rough lower bound in those two modes rather than an accurate total.
Signaturegrid.EstimateMemory() As Long
ReturnsLong: an approximate byte count, capped at 2,147,483,647.
EstimateMemory ignores DataMode entirely -- it always reports the size of the same embedded-mode scaffolding, regardless of which engine is actually active. In Turbo column-store mode this under-counts the Turbo column-store table's own memory. In Virtual mode the number does not reflect cached/visible-row memory or your application's full dataset either -- it is the same largely-empty scaffolding size that Turbo column-store mode reports.
EstimateMemory does not check DataMode, so in both Virtual and Turbo column-store mode it reports the size of the mostly-empty Embedded-mode scaffolding, not the memory your data actually occupies there. Treat it as a rough lower bound in those two modes rather than an accurate total.
The value is capped at 2,147,483,647 bytes (constant) even if actual usage is higher.
See alsoCompact, GetFullDiagnosticsJSON, PerformanceStats
BeginUpdateLifecycle, configuration and diagnostics
Suspends screen updates until a matching EndUpdate call.
BeginUpdate tells the grid to stop repainting while you make a batch of changes, so the user does not see each individual change flicker by. Calls nest: each BeginUpdate needs a matching EndUpdate before the grid resumes repainting, and the data changes you make in between still take effect immediately -- only the on-screen repaint is deferred.
Works the same way in all three modes; it only affects repainting, never the data itself.
Example
VBA
Dim data(0To99, 0To0) AsVariantDim j AsLongFor j = 0To99
data(j, 0) = ""Next j
grid.LoadFromArray data ' a text column -- SetCellValue below only accepts text on a Turbo column-store String column
grid.BeginUpdate
Dim i AsLongFor i = 0To99
grid.SetCellValue i, 0, CStr(i)
Next i
grid.EndUpdate ' one repaint here, not 100
Gotchas
Calls nest with a counter: two BeginUpdate calls require two EndUpdate calls before repainting resumes.
Calling Refresh while inside a BeginUpdate/EndUpdate bracket has no visible effect until the matching EndUpdate runs.
See alsoEndUpdate, Refresh
EndUpdateLifecycle, configuration and diagnostics
Ends a batch of changes started with BeginUpdate and refreshes the display.
EndUpdate matches a prior BeginUpdate call. Only when the last outstanding BeginUpdate is matched does the grid recompute its scrollbars, repaint, and force that repaint to happen immediately rather than waiting for the next idle moment. Calling EndUpdate without a matching BeginUpdate is harmless -- it is simply ignored.
grid.BeginUpdate
Dim data(0To2, 0To1) AsVariant
data(0, 0) = 1: data(0, 1) = "Alpha"
data(1, 0) = 2: data(1, 1) = "Beta"
data(2, 0) = 3: data(2, 1) = "Gamma"
grid.LoadFromArray data
grid.EndUpdate ' repaints immediately once loading is done
Gotchas
An unmatched EndUpdate call (with no prior BeginUpdate) does not raise an error; it is simply a no-op.
The repaint EndUpdate triggers is immediate and synchronous, unlike Refresh, which schedules a repaint for the next paint cycle.
See alsoBeginUpdate, Refresh
RefreshLifecycle, configuration and diagnostics
Repaints the grid.
Refresh schedules a full repaint of the grid on the next paint cycle. It never changes data, never fails because of the current data mode, and can be called from anywhere, including from inside an event handler.
grid.SetColumnHeader 0, "New Header"
grid.Refresh ' forces the header change to redraw immediately
Gotchas
Refresh has no visible effect while a BeginUpdate/EndUpdate bracket is still open; the repaint only happens once the bracket closes.
See alsoBeginUpdate, EndUpdate
LastErrorLifecycle, configuration and diagnostics
Returns the description of the most recent failed call.
LastError holds the message text from whichever call most recently failed, whether that was a refused method, a validation error, or an internal exception. It is sticky: it keeps returning that same text until another call fails and overwrites it, even across successful calls made in between. Before any error has occurred, it returns an empty string.
Signaturegrid.LastError As String (get)
ReturnsString: the most recent error description, or an empty string if nothing has failed yet.
Behaves identically in all three modes; it simply reports the last failure, regardless of what caused it.
Example
VBA
' Only 0, 1 and 2 are modes; anything else is refused and nothing changesDim notAMode AsLong
notAMode = 9OnErrorResumeNext
grid.DataMode = notAMode
OnErrorGoTo0Debug.Print grid.LastError ' -> "DataMode must be 0 (Embedded), 1 (Virtual) or 2 (Turbo column-store)."
Gotchas
LastError is sticky and is not cleared by a subsequent successful call -- check it immediately after the call you care about.
Prefer On Error Resume Next plus Err.Description for a specific call; use LastError when you need to inspect the failure after the fact, or from code that does not control the failing call directly.
See alsoDataMode, CompactMode
CompactModeLifecycle, configuration and diagnostics
When enabled, keeps only display text for cells instead of full typed values, to reduce memory use.
CompactMode trades some flexibility for a smaller memory footprint: when it is on, the grid keeps only the text needed to display each cell rather than the fully typed value. It can only be changed while the grid has no rows loaded, so set it once right after Clear and before you load your data.
CompactMode can only be changed while the grid is empty; call Clear first, then set it, then load your data.
Attempting to change it after data is loaded raises an error and leaves the setting unchanged.
See alsoClear, DataMode
VersionLifecycle, configuration and diagnostics
Returns the control's version string.
Version returns a simple Major.Minor.Patch string identifying the build of the control, matching the VersionMajor, VersionMinor and VersionPatch properties. Use it for support requests and compatibility checks in your application.
See alsoVersionMajor, VersionMinor, VersionPatch, BuildDate
BuildDateLifecycle, configuration and diagnostics
Returns the date this copy of the control was compiled.
BuildDate returns the compilation date of the specific binary you have loaded, as a plain date string. It reflects when the control was built, not when it was installed on your machine.
Selects which of the three storage engines the grid uses: Embedded, Virtual or Turbo column-store.
DataMode chooses how the grid stores its rows. Embedded (0) is the default, general-purpose store the grid owns itself. Virtual (1) hands ownership to your application, with the grid only caching a rendering window. Turbo column-store (2) is a grid-owned, column-oriented store built for large datasets. Reading DataMode is always safe; writing it requires the store you are currently in to be empty, so call Clear first if you have data loaded. Writing it is also refused while a column-array load session (BeginLoad without a matching EndLoad or CancelLoad) is still open, from inside one of the grid's own event handlers, or while a paint or a load is in progress. Switching modes clears the current selection and fires SelectionChanged -- there is no separate "mode changed" event.
Signaturegrid.DataMode As Long (get/put)
Parameters
pVal (Long) - 0 for Embedded, 1 for Virtual, 2 for Turbo column-store.
ReturnsLong: the active data mode.
Default0 (Embedded)
Value
Meaning
0
Embedded -- the grid owns the rows in its own row store.
1
Virtual -- your application owns the data; the grid caches a rendering window of it.
2
Turbo column-store -- the grid owns the rows, stored column-by-column for large datasets.
Writing DataMode while the current store still has rows raises an error; call Clear first.
Writing DataMode is also refused while a column-array load session (BeginLoad without a matching EndLoad or CancelLoad) is still open -- call EndLoad or CancelLoad first.
DataMode cannot be changed from inside one of the grid's own event handlers, or while a paint or a load is in progress.
DataMode must be set on the same thread that created the control.
Values other than 0, 1 or 2 are rejected with an error; the property is never silently clamped.
Switching modes clears the current selection and fires SelectionChanged; there is no dedicated event for the mode change itself.
See alsoClear, QuickBind, LoadFromArray
QuickBindLifecycle, configuration and diagnostics
Loads an array in one call and turns on the common interactive features: type detection, auto-fit columns, filter buttons, header-click sorting and the status bar.
QuickBind is a convenience method for getting from a raw 2-D array to a usable, interactive grid in one call. It loads the data, detects each column's type, auto-fits column widths, and enables filter buttons, header-click sorting, the built-in context menu, the header menu and the status bar. If firstRowIsHeader is True, the first row's values become the column headers and that row is excluded from the row count.
Signaturegrid.QuickBind data As Variant, firstRowIsHeader As Boolean
Parameters
data (Variant) - A 2-D array of values, laid out the same way as LoadFromArray expects.
firstRowIsHeader (Boolean) - If True, the first row of data supplies the column headers instead of being loaded as a data row.
QuickBind only works in Embedded mode. Virtual mode refuses it because the host owns the data; Turbo column-store mode refuses it because its surface does not accept new loads through this call.
QuickBind only works in Embedded mode -- switch DataMode to 0 first if the grid was previously in Virtual or Turbo column-store mode.
With firstRowIsHeader:=True, the header row is excluded from the visible row count.
QuickBind turns several display options on (filter buttons, header menu, context menu, status bar); if you do not want all of them, turn the ones you do not need back off afterward.
See alsoSetMode, LoadFromArray, GetColumnCount
QuickBindRecordsetLifecycle, configuration and diagnostics
Loads a DAO or ADO Recordset in one call and turns on the common interactive features: type detection, auto-fit columns, filter buttons, header-click sorting and the status bar.
The Recordset-source sibling to QuickBind: orchestrates LoadFromRecordset followed by the same optional steps QuickBind applies after LoadFromArray -- AutoDetectColumns, AutoFitColumns, and the filter/sort/context-menu/header-menu/status-bar display flags -- each one now individually controlled by the options bitmask rather than always applied unconditionally. Column headers come from each field's own Name (exactly as a direct LoadFromRecordset call already provides); type detection is what a plain LoadFromRecordset does not already give you, and is the one step that visibly changes a Currency field's display, from a bare decimal to a formatted currency string. An empty Recordset (real fields, zero rows) is a successful call, not an error: the columns are created and every display option that still applies is turned on; type detection is silently skipped rather than attempted against zero rows, since AutoDetectColumns itself has nothing to sample.
Unlike QuickBind, a failure in a required step (the Recordset load itself, or -- defensively, since neither can realistically fail once rows have loaded -- type detection or auto-fit) is never reported as success: the call returns that failure, though any data already loaded is left in place rather than rolled back.
Signaturegrid.QuickBindRecordset pRecordset As Object, [options As Long = lgQuickDefault]
Parameters
pRecordset (Object) - A DAO or ADO Recordset, exactly as accepted by LoadFromRecordset. A Nothing reference is rejected with the same specific error LoadFromRecordset itself raises for one.
options (Long, optional) - An LGQuickBindOptions bitmask. Omit it for lgQuickDefault (every step, matching what QuickBind always does); pass 0 for load-only (no type detection, no display changes); OR together individual lgQuick* constants for anything in between. A bit outside the currently-defined set is silently ignored, never an error.
ReturnsNo return value; raises a runtime error if the Recordset load fails, or (defensively) if a later required step fails.
QuickBindRecordset only works in Embedded mode. Virtual mode refuses it because the host owns the data; Turbo column-store mode refuses it because AutoDetectColumns -- the step that gives this call its value over calling LoadFromRecordset directly -- has no Turbo column-store equivalent, so there is no truthful Turbo behavior to offer instead of an explicit refusal.
Example
VBA
Dim rs AsObjectSet rs = CreateObject("ADODB.Recordset")
' ... rs.Open a query returning CustomerID, Balance (Currency), SignupDate ...
grid.QuickBindRecordset rs
Debug.Print grid.GetRowCount() ' -> however many rows the Recordset returnedDebug.Print grid.GetColumnHeader(0) ' -> "CustomerID" (from Field.Name)Debug.Print grid.GetCellValue(0, 1) ' -> a formatted currency string, e.g. "$1,234.56"Debug.Print grid.ShowStatusBar ' -> True
Gotchas
QuickBindRecordset only works in Embedded mode -- switch DataMode to 0 first if the grid was previously in Virtual or Turbo column-store mode.
Only Currency fields visibly need the type-detection step this call provides; Boolean, Date, Double, Integer and String fields already display correctly straight out of LoadFromRecordset with no extra call. Type detection additionally right-aligns numeric columns.
An empty Recordset (zero rows, real fields) is not an error -- columns are created, and type detection is skipped rather than attempted against no data.
QuickBindRecordset turns several display options on by default (filter buttons, header menu, context menu, status bar, header-click sort, type detection, auto-fit); pass a narrower options bitmask, or turn individual ones back off afterward, if you don't want all of them.
A primitive call after QuickBindRecordset (SetColumnFormat, ShowFilterButtons, ...) always wins over what QuickBindRecordset itself set, the same as after QuickBind.
SetMode is not part of this call's options; chain grid.SetMode "..." afterward if you also want a named interaction preset.
See alsoQuickBind, LoadFromRecordset, AutoDetectColumns, AutoFitColumns, SetMode
LGQuickBindOptionsLifecycle, configuration and diagnostics
The bitmask enum QuickBindRecordset's options parameter accepts. OR named constants together; omit the parameter entirely for lgQuickDefault.
Constant
Value
Effect
lgQuickAutoDetect
&H1
Runs AutoDetectColumns(False) (skipped automatically on zero loaded rows).
lgQuickAutoFit
&H2
Runs AutoFitColumns().
lgQuickFilterButtons
&H4
Turns on ShowFilterButtons.
lgQuickHeaderSort
&H8
Turns on AutoSortOnHeaderClick.
lgQuickStatusBar
&H10
Turns on ShowStatusBar.
lgQuickContextMenu
&H20
Turns on ShowBuiltinContextMenu.
lgQuickHeaderMenu
&H40
Turns on ShowHeaderMenu.
lgQuickDefault
&H7F
Every bit above -- the default when options is omitted, matching what QuickBind itself always does for an array.
lgQuickAll
&H7F
Identical to lgQuickDefault today; a separate name reserved for a future bit that might be available without being on by default.
Gotchas
0 is a valid, meaningful value: load the Recordset and change nothing else -- no type detection, no display options.
A bit this enum does not name is silently ignored rather than rejected, so passing a numeric literal from a newer version of this control against an older build degrades gracefully instead of erroring.
lgQuickDefault and lgQuickAll are the same value today; use whichever name reads better at the call site.
See alsoQuickBindRecordset
SetModeLifecycle, configuration and diagnostics
Applies a named preset of interaction settings: Excel, ReadOnly, Dashboard or DataEntry.
SetMode is a shortcut for configuring several interaction-related properties at once, matched to a common scenario. The match on modeName is case-insensitive. An unrecognized or empty name raises an error and leaves every property untouched.
Signaturegrid.SetMode modeName As String
Parameters
modeName (String) - One of "Excel", "ReadOnly", "Dashboard" or "DataEntry" (case-insensitive).
ReturnsNo return value.
Value
Meaning
"Excel"
Full interactive editing: editing, multi-select, filter buttons, header-click sorting, status bar, context menu, header menu and tooltips all on.
"ReadOnly"
Same as Excel but with editing turned off.
"Dashboard"
No editing, no multi-select, no filter buttons, no header-click sorting, no context menu, no header menu; status bar and tooltips stay on.
"DataEntry"
Editing on, multi-select and filter buttons off, header-click sorting off, header menu off, the built-in context menu on, tooltips off, and Enter advances to the next cell (the only preset that changes this).
SetMode only works in Embedded mode. Virtual mode refuses it because it must be performed against the host's own data source; Turbo column-store mode refuses it because its surface is read-only.
Only the DataEntry preset touches the Enter-advances-to-next-cell setting; switching away from DataEntry to another preset does not automatically turn that behavior back off.
An unrecognized preset name (or an empty string) raises an error and changes nothing -- it is not silently ignored.
See alsoQuickBind, AllowEditing
EmptyStateTextLifecycle, configuration and diagnostics
The message shown centered in the grid when there are no rows to display.
EmptyStateText is drawn in place of the grid's rows whenever the row count is zero. Setting it takes effect immediately, and once set, it always takes priority over the grid's own built-in "no rows match the filter" and "loading" messages -- you will only see your custom text.
The empty-state check correctly reflects the row count from whichever engine is active, so this works the same in all three modes.
Example
VBA
grid.Clear
grid.EmptyStateText = "No records match your search"' The grid now displays that text instead of the default
Gotchas
A non-empty EmptyStateText always wins over the grid's own filtered-to-zero-rows or loading messages -- set it back to an empty string if you want those built-in messages to show instead.
See alsoShowStatusBar, Clear
ShowLoadingOverlayLifecycle, configuration and diagnostics
Shows a semi-transparent overlay with centered text on top of the grid, without blocking the UI.
ShowLoadingOverlay draws a dimmed panel with your message centered over the grid. It returns immediately -- there is no pump loop or timer involved, and the overlay is purely visual. Calling it again while it is already showing simply replaces the text.
Signaturegrid.ShowLoadingOverlay text As String
Parameters
text (String) - The message to center on the overlay. This argument is required -- the type library does not mark it optional, so VBA cannot omit it (doing so raises "Argument not optional"). Pass an empty string and "Loading..." is shown instead.
Purely a paint-layer effect; not affected by data mode.
Example
VBA
grid.ShowLoadingOverlay "Loading records..."' ... perform a long-running load ...
grid.HideLoadingOverlay
Gotchas
The overlay is drawn on top of the grid but does not block mouse clicks or keystrokes underneath it; set AllowEditing = False yourself if you also need to stop interaction during the operation.
Calling ShowLoadingOverlay again while it is already visible replaces the text; there is no stacking or call counter.
The text argument is required in VBA -- pass an empty string, not a missing argument, if you want the default "Loading..." text.
See alsoHideLoadingOverlay, BeginUpdate
HideLoadingOverlayLifecycle, configuration and diagnostics
Hides the loading overlay shown by ShowLoadingOverlay.
HideLoadingOverlay clears the overlay and repaints. It is safe to call even if no overlay is currently showing.
grid.ShowLoadingOverlay "Please wait..."' ... work ...
grid.HideLoadingOverlay
Gotchas
Safe to call even when no overlay is showing -- it is not an error to call it without a prior ShowLoadingOverlay.
See alsoShowLoadingOverlay
ShowProgressLabelLifecycle, configuration and diagnostics
Shows or hides the percentage text drawn on Progress-type cells.
ShowProgressLabel controls whether the percentage value is drawn as text centered on top of a progress-bar cell. It only affects columns whose type is set to Progress; it has no effect on any other column type.
Signaturegrid.ShowProgressLabel As Boolean (get/put)
Read purely at paint time for Progress-type cells; not affected by data mode. In Turbo column-store mode the property itself is readable and settable, but SetColumnType is refused there, so a column can never actually be made Progress-type -- ShowProgressLabel has no cell left to apply to.
Example
VBA
grid.SetColumnType 4, 7' 7 = Progress
grid.ShowProgressLabel = False' bars show without the percentage text
Gotchas
Only affects columns whose type is set to Progress -- it has no effect on any other column type.
In Turbo column-store mode, SetColumnType is refused outright, so no column can ever be Progress-type there -- ShowProgressLabel is effectively inert in Turbo column-store mode even though setting it does not error.
See alsoShowStatusBar
EnableDiagnosticLogLifecycle, configuration and diagnostics
Turns diagnostic file logging on or off for troubleshooting.
When enabled, the control writes diagnostic detail -- mainly the description of every internal error it encounters -- to a log file under %LOCALAPPDATA%\LiteGrid\Logs, one file per process, named with a timestamp and process ID. There is no corresponding property to read the current setting back; treat this as a write-only switch you turn on before reproducing a problem and off again afterward.
Signaturegrid.EnableDiagnosticLog enabled As Boolean
Parameters
enabled (Boolean) - True to start logging, False to stop.
grid.EnableDiagnosticLog True' ... reproduce the issue ...
grid.EnableDiagnosticLog False
Gotchas
There is no getter -- you cannot ask the control whether logging is currently on.
Enabling logging on one control instance turns logging on for every LiteGrid Turbo control in the same process, not just the one you called it on.
The log file is written under %LOCALAPPDATA%\LiteGrid\Logs, not to a debugger output stream.
See alsoLastError, GetFullDiagnosticsJSON
GetFullDiagnosticsJSONLifecycle, configuration and diagnostics
Returns a single JSON snapshot combining memory, paint and grid-state diagnostics.
GetFullDiagnosticsJSON returns one JSON object with three sections: memory (estimated bytes, row and column counts), paint (average and maximum paint time, paint count, cache hit rate), and state (sort column, whether filters are active, frozen row/column counts, theme, and whether editing is allowed). It is a convenient one-call summary for logging or support tickets.
Signaturegrid.GetFullDiagnosticsJSON() As String
ReturnsString: a JSON object with "memory", "paint" and "state" sections.
The byte-estimate figures in the memory section ignore DataMode entirely, the same way EstimateMemory does: in Virtual mode the number does not reflect cached/visible-row memory, it is the same largely-empty Embedded-mode scaffolding size Turbo column-store mode reports. In Turbo column-store mode, the memory section's "rowCount" field specifically reads exactly 0 -- not merely under-reported -- for as long as Turbo column-store mode is active, because it comes from the same non-mode-aware row-store accessor PerformanceStats uses rather than the mode-aware one GetRowCount() uses.
The "cacheHitRate" field inside the paint section currently always reads 0 in this release.
In Turbo column-store mode, the memory section's "rowCount" field always reads exactly 0, not the real row count -- use GetRowCount() instead. The memory section's byte estimate also under-counts real usage there, the same way EstimateMemory does.
See alsoEstimateMemory, GetPaintMetrics, PerformanceStats
GetPaintMetricsLifecycle, configuration and diagnostics
Returns average and maximum paint time, and how many recent paints were measured, as JSON.
GetPaintMetrics reports live paint-timing figures drawn from the most recent paints (up to the last 60). It is the most reliable source of paint-timing information in this group -- some of the similarly named figures elsewhere always read zero in this release, but this one reflects real measurements.
Signaturegrid.GetPaintMetrics() As String
ReturnsString: a JSON object with "avgPaintMs", "maxPaintMs" and "frameCount".
frameCount never exceeds 60 -- only the most recent 60 paints are kept.
Unlike GetFullDiagnosticsJSON, this JSON does not include a cache-hit-rate field at all.
See alsoAvgPaintMs, GetFullDiagnosticsJSON, Refresh
CurrentDpiLifecycle, configuration and diagnostics
Returns the effective DPI the grid is currently rendering at.
CurrentDpi reports the DPI the grid last recalculated its layout for, updated when the control is created and whenever Windows notifies it of a DPI change. Standard Windows values (constant) are 96 (100%), 120 (125%), 144 (150%) and 192 (200%).
Signaturegrid.CurrentDpi As Long (get)
ReturnsLong: the effective DPI.
Default96 (before the control's window is created)
Debug.Print grid.CurrentDpi ' -> 96 on a standard-DPI display
Gotchas
Before the grid's window exists, CurrentDpi reports 96 regardless of the system's actual display scaling.
See alsoGeometryRebuilds
PerformanceStatsLifecycle, configuration and diagnostics
Returns a JSON summary of row, icon, paint and cache figures.
PerformanceStats reports a flat JSON object covering row count, icon count, paint counters, icon cache counters, geometry rebuild count, formatting rule counts, icon size, theme and DPI. Several of the paint- and cache-related fields always read 0 in the current release; the row, icon, rule, theme, DPI and geometryRebuilds fields are otherwise live.
Signaturegrid.PerformanceStats As String (get)
ReturnsString: a flat JSON object of engine statistics.
Not gated by data mode -- the call always succeeds in every mode. The "rows" field, however, reads the same non-mode-aware row-store accessor that GetRowCount() specifically avoids, so it does not reflect the real count in Turbo column-store mode (see gotchas).
The "paints", "avgPaintMs", "lastPaintMs", "cacheHits", "cacheMisses" and "cacheHitRatio" fields always read 0 in this release -- use GetPaintMetrics for live paint timing instead.
In Turbo column-store mode, the "rows" field always reads 0, not the real row count. GetRowCount() is unaffected by this and remains accurate in every mode.
"icons", "geometryRebuilds", "iconSize", "theme" and "dpi" are live figures in every mode.
See alsoGetFullDiagnosticsJSON, GetPaintMetrics, CacheHitRatio, GeometryRebuilds
CacheHitRatioLifecycle, configuration and diagnostics
Reports the icon geometry cache's hit ratio, from 0.0 to 1.0.
CacheHitRatio is meant to show how often the icon geometry cache serves a request without rebuilding. In the current release it always reads 0.0, regardless of how many paints occur, because the underlying counters are not yet being incremented.
Currently always reads 0.0 in this release, regardless of activity -- do not use it as a live performance signal yet.
See alsoGeometryRebuilds, PerformanceStats
GeometryRebuildsLifecycle, configuration and diagnostics
Counts how many times the icon geometry cache has been rebuilt.
GeometryRebuilds increases each time the icon geometry cache is rebuilt. In the current release the only thing that increments it is changing the IconSize property; it does not increase in response to a DPI change, even though rebuilding on a DPI change is part of the cache's intended design. It never resets to zero on its own.
Signaturegrid.GeometryRebuilds As Long (get)
ReturnsLong: the number of geometry rebuilds since the control was created.
Dim before AsLong: before = grid.GeometryRebuilds
grid.IconSize = 24Debug.Print grid.GeometryRebuilds > before ' -> True
Gotchas
Only IconSize changes increment this counter in the current release; DPI changes do not, even though the cache is documented to rebuild on both.
It only ever increases for the lifetime of the control -- there is no way to reset it from VBA.
See alsoCurrentDpi, CacheHitRatio
AvgPaintMsLifecycle, configuration and diagnostics
Intended to report a smoothed average paint time in milliseconds.
AvgPaintMs is documented as an exponentially-smoothed average paint time. In the current release it always reads 0.0, regardless of how many paints have occurred. For a live paint-timing average, read the "avgPaintMs" field from GetPaintMetrics instead, which is a plain average over the most recent paints and does update.
Currently always reads 0.0 in this release; use GetPaintMetrics's avgPaintMs field for a live figure instead.
See alsoGetPaintMetrics, CacheHitRatio
RunBenchmarkLifecycle, configuration and diagnostics
Runs one of the control's built-in timing tests and returns the results as JSON.
RunBenchmark exercises a specific part of the control -- loading, sorting, painting or memory use -- and returns timing and sizing figures as JSON. The Load, Sort and All benchmarks work against real (in the Load/All case, generated) data and change what is currently loaded into the grid; only the Memory benchmark is read-only. Run it on a grid you do not mind losing the current contents of, or reload your own data afterward.
Signaturegrid.RunBenchmark(benchmarkId As Long) As String
Parameters
benchmarkId (Long) - Which benchmark to run: 0=Load, 1=Sort, 2=Paint, 3=Memory, 4=All.
ReturnsString: a JSON object with the timing/sizing figures for the benchmark(s) run.
Value
Meaning
0
Load -- loads a large generated dataset and times the first and a repeated load.
1
Sort -- sorts the grid's currently loaded data ascending, then descending, and times both.
2
Paint -- forces a full repaint and times it.
3
Memory -- reads current process memory and grid sizing figures; does not touch the loaded data.
4
All -- runs Load, Sort, Paint and Memory in sequence.
5
Undocumented internal benchmark ("AddRow50k"): appends 50,000 generated rows to the grid in timed chunks and then clears them. It exists in this build but is not part of the supported 0-4 set -- destructive, and not for use in production code.
RunBenchmark only works in Embedded mode. Virtual mode refuses it because it must be performed against the host's own data source; Turbo column-store mode refuses it because its surface is read-only.
Example
VBA
Dim json AsString
json = grid.RunBenchmark(3) ' Memory: does not touch loaded dataDebug.Print json ' -> {"working_set_mb":...,"string_pool_size":...,"cell_count":...}
Gotchas
Running the Load or All benchmark replaces the grid's current data with generated test data and does not restore your original rows and columns afterward -- reload your own data with LoadFromArray or LoadFromRecordset when you are done.
Running the Sort or All benchmark sorts whatever data is currently in the grid and leaves it sorted; the original row order is not restored.
benchmarkId 5 ("AddRow50k") exists in this build but is undocumented and not part of the supported 0-4 set -- it appends 50,000 rows and then clears them, discarding whatever was already loaded. Do not call it in production code.
A benchmarkId of 6 or higher does not raise an error -- it returns a JSON payload with every timing figure at zero.
Only the Memory benchmark (3) is safe to run without disturbing your loaded data.
See alsoGetPaintMetrics, EstimateMemory, GetFullDiagnosticsJSON
GetRowSelectionStateLifecycle, configuration and diagnostics
Captures the current row selection as a versioned JSON token of business-key values read from a caller-named column.
The token stores the typed values in keyColumn for every selected row -- never storage or display row indices, neither of which survives a reload. Rows an active filter currently hides are captured too, exactly as SelectAll already reaches past a filter. See Saving and Restoring the User's Grid Layout for the full task-oriented walkthrough.
Signaturegrid.GetRowSelectionState(keyColumn As Long, [scope As String = ""]) As String
Parameters
keyColumn (Long) - the 0-based column whose value identifies a record (for example a CustomerID column).
scope (String, optional, default "") - an opaque caller-chosen label naming the logical record domain, stored in the token for SetRowSelectionState to check later. Omit it to leave the token unlabelled.
ReturnsString: a JSON object with "version", "keyColumn", "type" and "selected", plus "scope" when one was supplied. Each DISTINCT key value appears once.
Refuses if the selected rows' key values span more than one non-null type (some Long, some String), naming both types; Null may coexist with the one real type.
Refuses a key value that cannot form a durable identity -- an object, a Decimal, or a non-finite number.
Integer and Currency keys are written as exact decimal strings, deliberately, so a 64-bit id or a raw scaled Currency value cannot be degraded by a JSON number's floating-point range.
Nothing selected is not an error: you get a valid token with an empty selected array, and restoring it clears the selection.
See alsoSetRowSelectionState, GetViewState, GetSelectedRows
SetRowSelectionStateLifecycle, configuration and diagnostics
Restores a row selection from a GetRowSelectionState token, matching records by business key and replacing the current selection.
Selects every row whose keyColumn value matches a saved key -- including rows the current filter hides -- in one pass, firing exactly one SelectionChanged and one repaint no matter how many rows match. Does not restore focus.
Signaturegrid.SetRowSelectionState(state As String, [expectedScope As String = ""]) As Long
Parameters
state (String) - a token previously returned by GetRowSelectionState.
expectedScope (String, optional, default "") - if non-empty, the entire restore is refused unless it exactly matches the token's own scope. Leave as "" to skip the check.
ReturnsLong: the number of rows actually selected. Fewer than the token's key count means some records are gone; more means the key column is not unique in the current data.
Dim n AsLong
n = grid.SetRowSelectionState(saved)
Debug.Print n & " of the previously selected records are still present"
Gotchas
Replaces the current selection; it never merges. An empty token therefore clears the selection and returns 0.
Refuses -- leaving the current selection completely unchanged -- on malformed JSON, a missing or unsupported version, a scope mismatch, or a key column that can no longer be resolved or whose type has changed.
The key column is resolved by stable column id and header together, falling back to header plus column type; a renamed key column header is refused rather than silently resolved by id, because a fresh load restarts column ids.
A partial match is a success, not an error -- check the return count.
Never touches cell data, focus, or the scroll position.
See alsoGetRowSelectionState, SetViewState, SelectRow
GetViewStateLifecycle, configuration and diagnostics
Serializes the grid's view -- column layout, sort, frozen panes, scroll, and (Embedded/Virtual only) filters -- to a versioned JSON token, in every data mode.
Unlike GetGridState, GetViewState works in Embedded, Virtual and Turbo column-store alike, always including a structural schema fingerprint (column headers and types) that SetViewState uses to refuse restoring the token onto a different, incompatible grid. See Saving and Restoring the User's Grid Layout for the full task-oriented walkthrough (what is and is not saved, mode differences, and the Virtual-mode replay semantics for sort/filters) and VIEW_STATE_WORKFLOW_DESIGN.md for the design rationale.
Signaturegrid.GetViewState([viewKey As String = ""]) As String
Parameters
viewKey (String, optional, default "") - An opaque caller-chosen identifier stored verbatim in the token. Omit it (or pass "") to leave the token unkeyed. Purely a later SetViewState self-check convenience; the engine does not interpret it.
ReturnsString: a JSON object with "version", "mode", "schema", "columns", "sort", "frozen" and "scroll" sections, plus "viewKey" when one was supplied and "filters" in Embedded and Virtual modes.
Dim state AsString
state = grid.GetViewState()
Debug.Print state ' -> {"version":1,"mode":"Embedded","schema":{...},"columns":[...],"sort":{...},"frozen":{...},"scroll":{...},"filters":[...]}
Gotchas
The token never includes row/cell data, selection, theme, or tree/column-group expand state -- see the task-oriented section for exactly why each is excluded.
In Turbo column-store, the token has no "filters" key at all (omitted, not an empty array) -- Turbo's own filter engine cannot be read back yet.
In Virtual, "sort"/"filters" describe the last request the host acknowledged, not a guarantee about the actual row order -- see the Virtual-mode replay explanation in the task-oriented section.
See alsoSetViewState, GetGridState, GetColumnLayout
SetViewStateLifecycle, configuration and diagnostics
Restores a JSON token produced by GetViewState, in every data mode, with a built-in guard against restoring onto an incompatible grid.
Past its identity gates, SetViewState applies each section independently and reports any that failed by name -- the same non-transactional, best-effort contract SetGridState already uses -- rather than either guessing a wrong mapping or discarding everything a partial failure did not touch.
Signaturegrid.SetViewState state As String, [expectedViewKey As String = ""]
Parameters
state (String) - A JSON token previously returned by GetViewState. A payload previously returned by GetGridState is also accepted.
expectedViewKey (String, optional, default "") - If non-empty, the ENTIRE restore is refused, before anything is touched, unless it exactly matches the token's own viewKey. Leave as "" to skip this check (the automatic structural fingerprint check below still applies regardless).
ReturnsNo return value; raises an error (Err.Description names the reason) if the identity gate refuses the restore, or if any applicable section fails to apply.
Dim savedView AsString
savedView = grid.GetViewState()
' ... later ...
grid.SetViewState savedView
Gotchas
Invalid JSON, a missing "version" field, or an unsupported version are all refused outright, changing nothing.
If the token's structural schema (column headers/types) is not recognizably the same as, a superset of, or a subset of the current grid's columns, the ENTIRE restore is refused before anything is touched -- this is what stops a view saved for one table from being silently applied to a different one that just happens to share a column count. Columns that were only added or only removed since the token was saved still pass this check and restore tolerantly by identity; a column swapped for an unrelated one does not.
A section present in the token but not applicable to the current mode (for example, "filters" restored onto Turbo column-store) is silently skipped, not an error.
A section that IS applicable but fails (malformed sub-document, a sort column that no longer exists, a Virtual host declining the replayed request) is named in the error and left unchanged; every other applicable section still applies.
Never touches row/cell data, selection, theme, or tree/column-group state.
See alsoGetViewState, SetGridState, SetColumnLayout
GetGridStateLifecycle, configuration and diagnostics
Serializes the grid's column layout, filters, sort, scroll position and frozen panes to JSON.
GetGridState captures the grid's view-level configuration as one JSON string: column layout, filter rules, sort column and order, scroll position, and frozen row/column counts. It does not include the row data itself -- pair it with your own way of reloading the underlying rows if you need to restore both.
Signaturegrid.GetGridState() As String
ReturnsString: a JSON object with "version", "columns", "filters", "sort", "scroll" and "frozen" sections.
GetGridState only works in Embedded mode. Virtual mode refuses it because it must be performed against the host's own data source; Turbo column-store mode refuses it because its surface is read-only.
GetGridState does not include row data -- only column layout, filters, sort, scroll position and frozen panes.
See alsoSetGridState
SetGridStateLifecycle, configuration and diagnostics
Restores column layout, filters, sort, scroll position and frozen panes from JSON previously returned by GetGridState.
SetGridState applies a JSON payload produced by GetGridState back onto the grid. Malformed JSON is rejected with an error and nothing is changed. A well-formed payload that is missing a section leaves that part of the grid's current state untouched, so you can apply a partial state deliberately -- for example a payload with only a "frozen" section changes frozen panes and leaves everything else as it was.
Signaturegrid.SetGridState json As String
Parameters
json (String) - A JSON payload previously returned by GetGridState (or a hand-built subset of the same sections).
SetGridState only works in Embedded mode. Virtual mode refuses it because it must be performed against the host's own data source; Turbo column-store mode refuses it because its surface is read-only.
Example
VBA
Dim savedLayout AsString
savedLayout = grid.GetGridState()
' ... later, after reloading the same data ...
grid.SetGridState savedLayout
Gotchas
Malformed JSON raises an error and changes nothing.
A payload missing a section (for example, no "sort") leaves that part of the grid's current state untouched rather than clearing it.
SetGridState only affects layout/view state -- it never touches row or cell data.
See alsoGetGridState
ShowStatusBarLifecycle, configuration and diagnostics
Shows or hides a status bar band below the grid's data area.
ShowStatusBar reserves a band at the bottom of the grid showing a row count on the left (and, once configured, aggregate figures on the right via StatusBarMetrics). Toggling it repaints immediately, but the scrollbar range only catches up the next time something else recalculates it, such as resizing the control, sorting, or loading data.
The property itself works in every mode. In Turbo column-store mode, the left-hand row-count text is wrong outright (see gotchas) -- it always reads "0 rows" regardless of how many rows are actually loaded.
Example
VBA
grid.ShowStatusBar = True' A status bar band now appears below the grid, showing the row count
Gotchas
In Turbo column-store mode the status bar's left-hand row-count text always reads "0 rows", never the real loaded row count -- this is a known display defect, not an occasional undercount. GetRowCount() is unaffected and remains accurate in Turbo column-store mode.
Toggling ShowStatusBar repaints immediately, but the scrollbar range only updates the next time something else recalculates it (resize, sort, load).
See alsoStatusBarMetrics, EmptyStateText
StatusBarMetricsLifecycle, configuration and diagnostics
A comma-separated list of aggregate tokens (Count, Sum, Avg, Min, Max) shown on the right side of the status bar.
StatusBarMetrics chooses which aggregate figures the status bar's right section shows, drawn from Count, Sum, Avg, Min and Max (matched case-insensitively; unrecognized tokens are silently ignored). The aggregate totals numeric cells across every column, but only for rows currently selected -- not the whole grid, and not one particular column. It has no visible effect until ShowStatusBar is also True.
Signaturegrid.StatusBarMetrics As String (get/put)
ReturnsString: the current comma-separated token list.
Default"Count, Sum, Avg"
Value
Meaning
Count
Number of selected numeric cells.
Sum
Sum of selected numeric cells across all columns.
Avg
Average of selected numeric cells across all columns.
Min
Minimum of selected numeric cells across all columns.
Max
Maximum of selected numeric cells across all columns.
In Virtual mode the aggregate is deliberately not computed, to avoid reporting a total based on only whatever happens to be cached; the property itself is still readable and settable. In Turbo column-store mode, do not combine a non-empty StatusBarMetrics with an active row selection until this is corrected -- see the gotcha below.
Example
VBA
grid.ShowStatusBar = True
grid.StatusBarMetrics = "Sum, Min, Max"' Selecting rows in Embedded mode now totals their numeric cells on the right of the status bar
Gotchas
In Turbo column-store mode, combining a non-empty StatusBarMetrics with an active row selection can make the control read outside its own memory and crash the host application. Until this is corrected, avoid selecting rows while StatusBarMetrics is non-empty in Turbo column-store mode.
The aggregate is computed over every numeric cell in the current selection across all columns combined, not scoped to a single column.
Setting StatusBarMetrics has no visible effect until ShowStatusBar is also True.
Token matching is a simple case-insensitive substring search, not a strict comma-separated parse; unrecognized text is silently ignored rather than raising an error.
See alsoShowStatusBar
Loading data
This group covers everything involved in getting data into the grid. LiteGrid Turbo gives you four ways to load a complete dataset in a single call -- LoadFromArray, LoadFromRange, LoadFromRecordset, and LoadFromJson -- plus two multi-call patterns for building up a dataset over several calls: a streaming pair (BeginStreaming/AppendRows/EndStreaming) for appending batches of rows without repainting or re-sorting after each one, and a column-array loading session (BeginLoad/LoadColumn/EndLoad/CancelLoad, with IsLoading to check on either loader) for supplying one typed column at a time instead of building a single large 2-D array.
Which loaders are available, and where the data ends up, depends on the DataMode property. LiteGrid Turbo has three data modes: Embedded (the default), where the grid owns and stores every row itself; Virtual, where your application owns the data and the grid only caches a rendering window of it; and Turbo column-store mode, where the grid owns the rows but stores them column-by-column for large datasets. None of the members in this group ever accept data while Virtual mode is selected -- in Virtual mode the host supplies rows itself, through the Virtual-mode data protocol, and every loader here refuses outright. Within Embedded and Turbo column-store mode, support varies member by member: LoadFromArray, LoadFromRecordset, and the column-array loading session (BeginLoad/LoadColumn/EndLoad/CancelLoad) all work in both modes, while LoadFromRange, AutoDetectColumns, LoadFromJson, and the streaming API only work in Embedded mode. A few of those Embedded-only restrictions are not obvious from what each method claims to do, so check each member's own notes rather than assuming Turbo column-store mode support carries over from LoadFromArray.
For a one-shot load, pick the loader that matches where your data already lives. Use LoadFromArray when you have already built (or computed) a 2-D Variant array in memory -- it is a direct path and the one every other loader ultimately builds on. Use LoadFromRange when your source is an Excel Range or similar cell-like object; it reads the values out for you and hands them to LoadFromArray. Use LoadFromRecordset when your source is a live DAO or ADO Recordset -- it reads column names from the recordset's fields and can target either Embedded or Turbo column-store mode, just like LoadFromArray. Use LoadFromJson when your source is a JSON array of flat objects, such as a web API response -- keep in mind that every value it loads becomes text, even numbers and dates, so you may need SetColumnType afterward if you want numeric sorting or formatting.
The streaming pair and the column-array loading session both exist for cases where a single LoadFromArray call is not a good fit. Reach for BeginStreaming/AppendRows/EndStreaming when rows arrive incrementally -- for example, a page at a time from a web API or a long-running query -- and you want to add them to the grid as they arrive without the cost of a full repaint, sort, and filter after every batch; EndStreaming does that work once, at the end. Reach for BeginLoad/LoadColumn/EndLoad when your data already lives in separate typed arrays -- for instance, one array per database column fetched independently -- since LoadColumn accepts each typed array directly and copies it once, without you first having to assemble everything into a single Variant array. That session always finishes by selecting Turbo column-store mode, whichever mode the grid was in when you called BeginLoad. CancelLoad and IsLoading work across both of these multi-call patterns' underlying mechanisms as far as they can: CancelLoad safely discards a column-array session or stops a chunked array load early, and IsLoading tells you whether either one is in progress -- though notably not whether a streaming session is open, which is a separate flag entirely.
RecipeChoosing a loader for your data source
VBA
PublicSub Recipe_LoadFromWherever(grid AsObject, source AsString)
SelectCase source
Case"array"Dim data(0To2, 0To1) AsVariant
data(0, 0) = "Widget": data(0, 1) = 12.5
data(1, 0) = "Gadget": data(1, 1) = 7.25
data(2, 0) = "Gizmo": data(2, 1) = 19.99
grid.LoadFromArray data
Case"database"Dim rs AsObjectSet rs = CurrentDb.OpenRecordset("SELECT * FROM Products")
grid.LoadFromRecordset rs
rs.Close
Case"web"Dim json AsString
json = "[{""Name"":""Widget"",""Price"":12.5}]"
grid.LoadFromJson json
' Price came in as a number in the JSON but is stored as text --' set the real type if you need to sort or format it numerically.
grid.SetColumnType 1, 3' 3 = DoubleEndSelectEndSub
Each loader matches a different starting point: LoadFromArray for data you have already computed or assembled yourself, LoadFromRecordset for a live database query (where column names come from the schema for free), and LoadFromJson for API responses. The one thing to remember across all three is that only LoadFromArray and LoadFromRecordset can target Turbo column-store mode -- LoadFromJson is Embedded-only, and it always stores values as text regardless of how they looked in the JSON, so numeric columns typically need an explicit SetColumnType call afterward if you plan to sort or format them as numbers.
RecipeAppending a large dataset in batches without stalling the UI
VBA
PublicSub Recipe_LoadInBatches(grid AsObject, allRows() AsVariant, batchSize AsLong)
grid.ClearDim total AsLong, r AsLong, lo AsLong, hi AsLong
lo = LBound(allRows, 1): hi = UBound(allRows, 1)
grid.BeginStreaming
r = lo
DoWhile r <= hi
Dim thisBatch AsLong
thisBatch = batchSize
If r + thisBatch - 1 > hi Then thisBatch = hi - r + 1Dim chunk() AsVariantReDim chunk(0To thisBatch - 1, LBound(allRows, 2) To UBound(allRows, 2))
Dim i AsLong, c AsLongFor i = 0To thisBatch - 1For c = LBound(allRows, 2) To UBound(allRows, 2)
chunk(i, c) = allRows(r + i, c)
Next c
Next i
grid.AppendRows chunk
r = r + thisBatch
Loop
grid.EndStreaming
Debug.Print grid.GetRowCount()
EndSub
This pattern is worth the extra ceremony over a single LoadFromArray call whenever rows genuinely arrive in pieces -- a paged API, a long export, or a background process handing you chunks as they finish -- because BeginStreaming suppresses sorting, filtering, and repainting until EndStreaming runs, so the cost of those steps is paid once at the end instead of after every batch. If you already have the whole dataset in one array, LoadFromArray alone is simpler and does the same job in a single call; reach for streaming only when the data truly shows up over time. Remember that a streaming session must always be closed with EndStreaming -- there is no cancel for it -- so wrap the loop in error handling that still reaches EndStreaming if something goes wrong partway through.
RecipeLoading one column at a time, with a cancel option
This is the right tool when your data already lives in separate typed arrays -- for example, one array per column fetched independently from several queries -- rather than as a single ready-made 2-D Variant array. LoadColumn accepts each typed array directly and copies it once, so you avoid building and re-copying a combined array just to hand it to LoadFromArray. The session always ends up in Turbo column-store mode once EndLoad succeeds, regardless of which mode the grid started in. CancelLoad is the safe way out at any point before EndLoad: it discards everything staged so far and leaves whatever the grid was already showing completely untouched, which is exactly what you want behind a Cancel button on a long import.
LoadFromArrayLoading data
Loads a full 2-D array of values into the grid in one call.
LoadFromArray hands the control a block of data you already have in memory directly: a plain two-dimensional Variant array, with dimension 1 supplying rows and dimension 2 supplying columns, and no header row. The array can be 0-based, 1-based, or any other base -- LiteGrid Turbo reads whatever bounds you give it. Columns are created automatically and named "Column 1", "Column 2", and so on; call SetColumnHeader afterward for real names, or use AutoDetectColumns to pull the first row in as headers. Which engine receives the data depends on the DataMode property at the moment you call LoadFromArray: with DataMode left at its default (Embedded), the array is copied into the grid's own row store; with DataMode set to Turbo column-store mode, the same call routes into the column-store engine instead. Either way, existing data is fully replaced.
Signaturegrid.LoadFromArray data As Variant
Parameters
data (Variant (2-D array)) - A two-dimensional array; dimension 1 supplies rows, dimension 2 supplies columns. Must contain at least one row and one column.
ReturnsNo return value; raises a runtime error if the array is rejected.
Embedded and Turbo column-store mode: supported -- the array is copied into whichever engine DataMode currently selects. Virtual mode: not available (the host owns the data).
A LoadFromArray call made while Turbo column-store mode is selected does not raise the DataLoaded event -- only LoadFromRecordset and the column-array loading session (BeginLoad/LoadColumn/EndLoad) raise it while Turbo column-store mode is selected. In Embedded mode, LoadFromArray always raises DataLoaded. Do not rely on DataLoaded firing after a Turbo column-store load from an array; check GetRowCount instead.
The array must be exactly two-dimensional. A one-dimensional array (for example, the result of VBA's Array() function) is rejected with a runtime error, and the grid's existing data is left exactly as it was.
Row counts above roughly 10 million are rejected outright.
On a 32-bit host in Embedded mode, a very large array (roughly 500 MB (constant) of estimated cell storage or more) is rejected before any memory is committed, so the grid is never left half-loaded. Turbo column-store mode has no equivalent up-front size estimate; it only fails if the actual allocation cannot be satisfied.
A cell holding an object reference (for example, a Range or another automation object placed directly into the array) is rejected only when Turbo column-store mode is selected. In Embedded mode, LoadFromArray does not check for this at all -- an object-typed cell is copied into the grid as-is, which is not a supported combination and can behave unpredictably later (for example when the cell is displayed, sorted, or exported). Convert object cells to plain values before calling LoadFromArray regardless of mode.
See alsoLoadFromRange, LoadFromRecordset, LoadFromJson, BeginLoad, Clear, DataMode, IsLoading, CancelLoad
LoadFromRangeLoading data
Loads data directly from an Excel Range object.
LoadFromRange reads the values out of an Excel Range -- or any object that exposes a Value2 or Value property -- and loads them the same way LoadFromArray does. It tries Value2 first, because Value2 returns raw numbers and date serials rather than locale-formatted text, then falls back to Value for objects that do not expose Value2. Internally it passes whatever that property returns straight to LoadFromArray, so the two share identical column-creation and error-reporting behavior for the resulting array. A multi-cell Range's Value2 is a 2-D array and loads normally; a single-cell Range's Value2 is a plain scalar, not an array, so calling LoadFromRange on a single cell fails with the same error LoadFromArray gives for a non-array argument. Read a single cell's value directly (for example with rng.Value2) rather than through LoadFromRange.
Signaturegrid.LoadFromRange pRange As Object
Parameters
pRange (Object (a Range or similar value-bearing object)) - The source object. Must expose a Value2 or Value property; a Nothing reference is rejected.
ReturnsNo return value; raises a runtime error if the object cannot be read.
Embedded mode: supported. Virtual and Turbo column-store mode: not available -- unlike LoadFromArray, LoadFromRange refuses outright in Turbo column-store mode even though the array it builds internally is one LoadFromArray itself accepts in that mode. There is no setting that makes LoadFromRange populate a Turbo column-store grid directly; read the range's values into your own array first (for example with rng.Value2) and pass that array to LoadFromArray instead.
LoadFromRange refuses outright in Turbo column-store mode, even though LoadFromArray (which it calls internally) supports that mode. This is a real difference between the two methods, not something you can switch on -- if you need a Range's data in Turbo column-store mode, read rng.Value2 yourself and pass the resulting array to LoadFromArray.
Passing Nothing raises a runtime error identifying the range as missing.
An object with neither a Value2 nor a Value property (so is not range-like at all) is rejected with a specific error rather than a generic type mismatch.
A single-cell Range returns its Value2/Value as a plain scalar, not an array, so LoadFromRange on a single cell raises the same 'requires a 2-dimensional array' error LoadFromArray gives for a non-array argument. LoadFromRange only works on a multi-cell range.
See alsoLoadFromArray, AutoDetectColumns
AutoDetectColumnsLoading data
Infers column headers and data types from data you have already loaded.
Call AutoDetectColumns right after LoadFromArray (or another Embedded-mode loader) to have the grid guess sensible column types from the data itself: it samples up to the first 100 rows (constant) of each column, and when more than 80% of the non-blank values agree, it sets that column's type to Checkbox, Currency, Date, Integer, or Double, right-aligning numeric columns as it goes. When firstRowIsHeader is True, row 0 is also removed from the visible rows and its text is used as the column headers instead. AutoDetectColumns requires data to already be present -- calling it on an empty grid raises an error telling you to call LoadFromArray first.
Signaturegrid.AutoDetectColumns firstRowIsHeader As Boolean
Parameters
firstRowIsHeader (Boolean) - True to treat row 0 as header text (and remove it from the data rows); False to leave every loaded row as data and detect types only.
Embedded mode: supported. Virtual and Turbo column-store mode: not available -- AutoDetectColumns refuses outright as soon as either mode is selected, before it ever looks at loaded data.
Type detection only looks at the actual VBA type of each loaded value (Long, Double, Date, Boolean, Currency) -- if you loaded everything as text (for example, with LoadFromJson, which always stores text), AutoDetectColumns has nothing typed to recognize and columns are left as-is.
Calling it on a grid with no data raises an error rather than silently doing nothing.
The 80% (constant) agreement threshold means a mostly-numeric column with a few stray text values (like "N/A") is still typed as numeric; the stray values are simply displayed as-is.
See alsoLoadFromArray, SetColumnType, SetColumnHeader, QuickBindRecordset
LoadFromRecordsetLoading data
Loads data straight from a DAO or ADO Recordset.
LoadFromRecordset walks a Recordset field by field and row by row (using MoveFirst/MoveNext) and loads the result the same way LoadFromArray would, using each field's name as the column header. It works with either DAO or ADO Recordset objects, or anything else that exposes a compatible Fields collection with Name/Value plus MoveFirst/MoveNext. Like LoadFromArray, which engine receives the data depends on DataMode at the time of the call: Embedded mode loads into the row store, Turbo column-store mode loads into the column-store engine. An empty recordset (already at EOF) still creates the columns; it simply adds no rows.
Signaturegrid.LoadFromRecordset pRecordset As Object
Parameters
pRecordset (Object (a DAO.Recordset, ADODB.Recordset, or compatible object)) - The source recordset. A Nothing reference is rejected.
Embedded and Turbo column-store mode: supported. Virtual mode: not available (the host owns the data). Unlike LoadFromArray, LoadFromRecordset raises the DataLoaded event in both Embedded and Turbo column-store mode -- there is no asymmetry here.
Example
VBA
Dim rs AsObjectSet rs = CreateObject("ADODB.Recordset")
rs.CursorLocation = 3' adUseClient -- required to Open a disconnected Recordset built from Fields.Append
rs.Fields.Append "ProductID", 3' adInteger
rs.Fields.Append "ProductName", 200, 50' adVarChar
rs.Open
rs.AddNew Array("ProductID", "ProductName"), Array(1, "Widget")
rs.Update
rs.AddNew Array("ProductID", "ProductName"), Array(2, "Gadget")
rs.Update
rs.MoveFirst
grid.LoadFromRecordset rs
Debug.Print grid.GetColumnHeader(0) ' -> "ProductID"Debug.Print grid.GetRowCount() ' -> 2
rs.Close
Gotchas
Existing data is cleared before the recordset is read, the same as calling Clear first.
A field whose value cannot be read is still given a placeholder column, so every later column keeps its correct position -- its own values simply come out blank rather than shifting the whole row left.
In Embedded mode, columns default to a width of 80 pixels here, versus 100 pixels for LoadFromArray and AppendRows -- a small but real difference if you compare the two loaders' output pixel-for-pixel. In Turbo column-store mode this difference disappears: LoadFromRecordset defaults to 100 pixels there too, matching LoadFromArray.
Passing Nothing raises a runtime error rather than loading zero rows.
The loader enforces the same 10-million-row ceiling LoadFromArray does. A Recordset that never reports EOF -- which a broken or hostile provider can do while still answering every call successfully -- is stopped there and refused by name rather than read forever.
Every call that controls the read loop is checked: if MoveNext fails, or EOF cannot be read or is not a Boolean, the load stops and reports that specific reason. MoveNext failing is NOT treated as reaching the end of the data.
If the load fails partway through, the grid is left genuinely empty rather than showing the rows read so far under a schema from a load that did not finish. In Embedded mode that is unavoidable -- the loader clears before it reads -- so a failed load raises DataChanged(Cleared) and no DataLoaded. In Turbo column-store mode the new data is published only after a successful read, so a refused load leaves the previous contents untouched.
See alsoLoadFromArray, Clear, DataMode, AutoDetectColumns, QuickBindRecordset, RefreshFromRecordset
RefreshFromRecordsetLoading data
Reloads the grid from a Recordset while putting the user's view back afterwards.
This is LoadFromRecordset plus the save/restore work you would otherwise write by hand: it captures the view state and (optionally) the row selection, hands the Recordset to LoadFromRecordset unchanged, then restores what it captured. It is a reload, not a binding — no reference to the Recordset is kept, nothing is observed, nothing synchronizes on its own, and there is deliberately no DataSource property.
The selection is restored by business key, not by row index, so the record your user had highlighted stays highlighted even when the new result set arrives in a different order.
Signaturegrid.RefreshFromRecordset rs As Object, [options As Long = lgRefreshDefault], [selectionKeyColumn As Long = -1]
Parameters
rs (Object) - the DAO or ADO Recordset to reload from; anything LoadFromRecordset accepts.
options (Long) - which sections of the view to preserve; see LGRefreshOptions. Defaults to lgRefreshDefault (1).
selectionKeyColumn (Long) - the VISUAL index of the column whose typed value identifies a record, exactly as GetRowSelectionState's own keyColumn parameter means it. -1 (the default) skips selection restoration entirely.
ReturnsNothing. Raises an error if the load fails, or if the reload succeeded but a section of the view could not be restored (the message names which).
Turbo column-store is partial for one reason only: filters are not preserved there. Layout, sort, frozen panes, scroll and selection all are. GetViewState does not capture filters in Turbo, so there is nothing for the restore to put back -- including filters set programmatically with SetColumnFilter. Virtual mode refuses by name before anything is captured or changed, matching every member this one composes.
Events: on success, exactly DataLoaded(rows, cols) then DataChanged(0); the internal clear's DataChanged(1) does not escape. If the call is refused before it starts, no events at all and the grid is unchanged. If the load fails after the grid was already emptied, exactly DataChanged(1), no DataLoaded, no restoration, and the loader's own named error. Note DataChanged(0) is raised by this member and not by a plain LoadFromRecordset. Restoring sort or filters raises those sections' own AfterSort/FilterChanged; a selection restore raises exactly one SelectionChanged.
Example
VBA
' The user has sorted, filtered and picked a row. Bring in fresh data' without taking any of that away.
grid.RefreshFromRecordset rs, lgRefreshDefault, 0' column 0 = CustomerID' Preserve only the sort and scroll position, and don't touch the selection:
grid.RefreshFromRecordset rs, lgRefreshSort Or lgRefreshScroll, -1' Reload and preserve nothing -- equivalent to LoadFromRecordset, but still' raising the single DataLoaded + DataChanged(0) pair:
grid.RefreshFromRecordset rs, lgRefreshNothing, -1
Gotchas
Only the refusal case is all-or-nothing. Once the loader has emptied the grid the method is explicitly not transactional: it leaves the grid empty rather than restoring a saved view over data that never loaded.
A materially changed schema (different column count, headers or types) refuses the ENTIRE view restore rather than applying a saved sort or filter to whatever column now occupies that position. The reload still stands and the error names what could not be restored.
Restoring fewer rows than you selected is normal, not an error -- those records are simply not in the new result set. More rows than keys means your key column is not unique.
It does not restore focus or scroll-to-selection, only selection; use ScrollToView yourself if you want that.
An unrecognized options bit is ignored rather than rejected, so code written against a later version still runs.
See alsoLoadFromRecordset, LGRefreshOptions, GetViewState, SetViewState, GetRowSelectionState, SetRowSelectionState, QuickBindRecordset
LGRefreshOptionsLoading data
The options bitmask RefreshFromRecordset takes. OR values together.
Constant
Value
Preserves
lgRefreshNothing
0
nothing -- reload only
lgRefreshDefault
1
the five sections below (the declared default)
lgRefreshColumns
2
column order, width, visibility, alignment
lgRefreshSort
4
sort column/order and multi-level sort
lgRefreshFrozen
8
frozen column and row counts
lgRefreshScroll
16
scroll top/left
lgRefreshFilters
32
filters, where the mode owns them
lgRefreshAll
62
the explicit OR of the five section bits
lgRefreshDefault is bit 0 rather than the OR of the section bits, so the declared default value of 1 means what the documentation says it means. Combining it with a section bit simply re-selects a section it already covers.
A section the current mode does not own is silently omitted rather than treated as an error -- in Turbo column-store that always includes filters. An unrecognized or future bit is likewise ignored, never an error, the same forward-compatible convention LGQuickBindOptions uses.
See alsoRefreshFromRecordset, LGQuickBindOptions, GetViewState
LoadFromJsonLoading data
Loads data from a JSON array-of-objects string.
LoadFromJson parses a JSON string shaped like [{"Col1":"val1","Col2":"val2"}, ...] and loads it into the grid: the keys of the first object become the column headers, and every object in the array becomes a row. It is a convenient way to load data received from a web service or REST API without writing your own parser. Every value is stored as text regardless of whether it looked like a number in the JSON -- LoadFromJson does not attempt type inference the way AutoDetectColumns does, so a numeric-looking value like 42 is loaded as the text "42".
Signaturegrid.LoadFromJson json As String
Parameters
json (String) - A JSON array of flat objects. Nested objects or arrays inside a row are not supported. A JSON syntax error is rejected.
Embedded mode: supported. Virtual and Turbo column-store mode: not available -- this holds even though the description of LoadFromJson does not mention any mode restriction; in practice the method always builds the grid's row-store data structures directly and refuses outright whenever Turbo column-store or Virtual mode is selected.
Example
VBA
Dim json AsString
json = "[{""ID"":1,""Name"":""Alpha""},{""ID"":2,""Name"":""Beta""}]"
grid.LoadFromJson json
Debug.Print grid.GetColumnHeader(0) ' -> "ID"Debug.Print grid.GetCellValue(1, 0) ' -> "2" (stored as text, not the number 2)
Gotchas
LoadFromJson does not populate a Turbo column-store grid, even though nothing in its description calls that out -- treat it as an Embedded-only loader.
Every value becomes a text cell, even numbers, dates, booleans, and JSON null (which loads as an empty cell) -- AutoDetectColumns cannot help afterward because it needs real typed values; use SetColumnType yourself if you need numeric sorting or formatting.
Only the FIRST object's keys become columns. If later objects in the array have extra keys the first object did not have, those extra values are silently ignored; if a later object is missing a key the first object had, that cell is left blank.
The parser expects well-formed, flat objects; it does not support escaped quotes inside string values or nested objects/arrays as cell values.
See alsoLoadFromArray, ExportToJson
BeginStreamingLoading data
Opens a streaming session that suppresses sorting and filtering, and most repainting, until EndStreaming.
BeginStreaming, together with AppendRows and EndStreaming, forms a three-call pattern for adding a large number of rows without paying the cost of re-sorting and re-filtering after every batch. Once you call BeginStreaming, the grid stops applying sort and filter to newly appended rows until you call EndStreaming. Repainting is mostly suppressed the same way, but not entirely: AppendRows still forces one brief automatic repaint every 100 rows appended in a single session, so a very long session does not look frozen while it runs. There is no argument and no return value -- it simply switches the grid into streaming mode.
Embedded mode: supported. Virtual and Turbo column-store mode: not available -- streaming only ever writes into the row store, so it is refused outright in the other two modes.
Example
VBA
grid.BeginStreaming
' ... one or more AppendRows calls ...
grid.EndStreaming
Gotchas
Calling BeginStreaming a second time before EndStreaming raises an error rather than nesting; there is only ever one streaming session at a time.
There is no way to abandon a streaming session other than calling EndStreaming -- CancelLoad does not touch it. If you begin streaming and never call EndStreaming, any active sort or filter stays stale indefinitely and the view never gets its final rebuild.
IsLoading does not report True while a streaming session is open -- it only reports on LoadFromArray's chunked load or an open column-array loading session, never on streaming.
BeginStreaming also refuses while an ordinary LoadFromArray chunked load is still in progress.
Repainting is not fully suppressed for the whole session: every 100 rows appended in a single session, the grid automatically repaints once (an undocumented fixed batch size) before suspending again. Only sorting and filtering are withheld in full until EndStreaming.
See alsoAppendRows, EndStreaming, LoadFromArray, BeginUpdate
AppendRowsLoading data
Appends a 2-D array of rows during a streaming session.
AppendRows adds the rows in a 2-D Variant array (the same shape LoadFromArray takes) onto the end of the grid's existing rows. It can only be called between BeginStreaming and EndStreaming, and -- unlike LoadFromArray -- it never replaces existing data, it only adds to it. The very first AppendRows call after BeginStreaming on an empty grid creates the columns for you (100 pixels wide, named "Column 1", "Column 2", and so on), just as LoadFromArray does; every later call in the same session must supply exactly that many columns.
Signaturegrid.AppendRows data As Variant
Parameters
data (Variant (2-D array)) - Rows to append; dimension 1 is rows, dimension 2 is columns. Its column count must match the grid's existing column count once one has been established.
AppendRows raises an error if BeginStreaming was not called first -- there is no implicit session.
Appended rows are added to the visible row order immediately (GetRowCount reflects them right away), but no sort or filter is applied to them until EndStreaming runs -- a newly appended row can appear out of sequence with an active sort until the session ends.
A column-count mismatch against the grid's already-established columns is rejected outright, and the rows appended earlier in the session are left in place.
See alsoBeginStreaming, EndStreaming, LoadFromArray
EndStreamingLoading data
Closes a streaming session, applying any active sort or filter and repainting once.
EndStreaming closes the session opened by BeginStreaming: it rebuilds the grid's row order from scratch, re-applies whatever sort and filters were active before streaming began, and repaints exactly once. This is the point where all of the rows added through AppendRows finally take their place in a sorted or filtered view, so a sequence of appends followed by one EndStreaming call avoids re-sorting and re-filtering after every batch while still producing a fully correct final view.
Embedded mode: supported. Virtual and Turbo column-store mode: not available.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = 5: data(1, 0) = 1
grid.LoadFromArray data
grid.Sort 0, 0' ascending
grid.BeginStreaming
Dim newRow(0To0, 0To0) AsVariant
newRow(0, 0) = 3
grid.AppendRows newRow
grid.EndStreaming
Debug.Print grid.GetCellValue(0, 0) ' -> 1 (sort is re-applied across all rows, including the one streamed in)
Gotchas
Calling EndStreaming without a matching BeginStreaming raises an error.
Sort re-application forces a genuine re-sort rather than trusting whatever sort state was left over from before streaming, so rows added during the session actually take part in the sort, not just the rows that existed before BeginStreaming.
See alsoBeginStreaming, AppendRows, Sort
BeginLoadLoading data
Starts a column-by-column loading session that publishes into Turbo column-store mode.
BeginLoad opens a new loading session for building a table one typed column at a time, instead of assembling one big 2-D Variant array. You declare the final row and column count up front; then you call LoadColumn once for each column index (in any order), and finally EndLoad to publish the whole table atomically. This route always ends in Turbo column-store mode -- a successful EndLoad selects Turbo column-store mode automatically, even if the grid was in Embedded mode when the session began. If the grid is currently in Embedded mode, its row store must be empty before you call BeginLoad (call Clear first); if it is already in Turbo column-store mode, BeginLoad can be called again to load a fresh table over the current one.
Signaturegrid.BeginLoad rowCount As Long, colCount As Long
Parameters
rowCount (Long) - The exact number of rows the finished table will have. Must be between 1 and 10,000,000.
colCount (Long) - The exact number of columns the finished table will have. Must be positive; each index from 0 to colCount-1 must later be supplied exactly once via LoadColumn.
Embedded mode: supported only when the row store is currently empty -- otherwise call Clear first. Turbo column-store mode: supported, including starting a fresh session while a Turbo column-store table is already loaded. Virtual mode: not available (the host owns the data).
Example
VBA
grid.Clear
grid.BeginLoad 3, 2Debug.Print grid.IsLoading ' -> TrueDim col0(0To2) AsLong
col0(0) = 1: col0(1) = 2: col0(2) = 3
grid.LoadColumn 0, col0, EmptyDim col1(0To2) AsString
col1(0) = "A": col1(1) = "B": col1(2) = "C"
grid.LoadColumn 1, col1, Empty
grid.EndLoad ' every session must end in EndLoad or CancelLoad
Gotchas
Calling BeginLoad while the Embedded row store still has rows raises an error naming the reason; it never discards your existing data for you.
Calling BeginLoad while a loading session is already open raises an error and leaves the earlier session completely untouched -- you must finish it (EndLoad) or abandon it (CancelLoad) first.
The row and column count declared here is fixed for the whole session; every column supplied via LoadColumn must have exactly this many elements.
IsLoading returns True as soon as BeginLoad succeeds, and stays True until EndLoad or CancelLoad closes the session.
See alsoLoadColumn, EndLoad, CancelLoad, IsLoading, Clear, DataMode, LoadFromArray
LoadColumnLoading data
Supplies one column's worth of data for an open BeginLoad session.
LoadColumn hands over one column of a table you are building with BeginLoad and EndLoad. typedArray must be a one-dimensional array of a single VBA type -- Long, Double, String, Date, Currency, or Boolean -- with exactly as many elements as the row count you declared to BeginLoad. Column indexes can be supplied in any order, but each index must be supplied exactly once; a duplicate is rejected, and a rejected column changes nothing already staged. The optional nullMask lets you mark individual cells as null: pass Empty to mean "every value in this column is valid", or pass a one-dimensional Boolean array of the same length where True marks that row's cell as null. Nullness comes only from this mask -- a blank string in a String column is a genuine empty string, never a null.
Signaturegrid.LoadColumn colIndex As Long, typedArray As Variant, nullMask As Variant
Parameters
colIndex (Long) - 0-based column index; must be less than the colCount declared to BeginLoad, and not already supplied.
typedArray (Variant (1-D array of a single type)) - The column's values as a 1-D array of Long, Double, String, Date, Currency, or Boolean. Element count must equal BeginLoad's declared row count.
nullMask (Variant (Empty, or a 1-D Boolean array)) - Empty means no nulls in this column. A Boolean array of the same length as typedArray marks True positions as null.
Available whenever a BeginLoad session is open, whether the grid was in Embedded or Turbo column-store mode at the moment BeginLoad was called -- the live DataMode does not actually change until EndLoad publishes. It can never be reached in Virtual mode, because BeginLoad itself refuses there, so no session is ever open to supply a column to.
Supplying the same column index twice is rejected; the fix is to finish or cancel the session, since a column already accepted cannot be overwritten in place -- but a call that itself FAILED never marks that index as supplied, so retrying the same index after a failure is fine.
The array's element type decides how the column is interpreted; there is no separate 'declare the type' step. A Long array is always read as whole numbers -- there is no way to load a Long array as Currency, for instance.
The null mask's element count must exactly equal the declared row count, just like typedArray; a mismatched mask is rejected and the column is not staged.
An out-of-range column index is rejected immediately, before either array is touched.
Neither array needs to survive past the call -- LiteGrid Turbo copies the data during LoadColumn, so you can destroy or reuse the arrays immediately afterward.
See alsoBeginLoad, EndLoad, CancelLoad, IsLoading
EndLoadLoading data
Publishes a completed column-array loading session as the grid's live data.
EndLoad finishes a BeginLoad/LoadColumn session. It only succeeds once every column index declared to BeginLoad has been supplied exactly once; otherwise it fails, names the first missing column, and leaves the session open so you can supply what is missing and call EndLoad again. On success it replaces the grid's live data in one atomic step, selects Turbo column-store mode (even if the grid was in Embedded mode when the session began), configures the columns, and raises exactly one DataLoaded event.
Signaturegrid.EndLoad
ReturnsNo return value; raises an error if the session is incomplete or missing.
Available whenever a BeginLoad session is open; unreachable in Virtual mode for the same reason LoadColumn is (no session can ever be open there). A successful call always leaves the grid in Turbo column-store mode.
Calling EndLoad with no BeginLoad session open raises a distinct error from calling it with an incomplete one -- check IsLoading first if you are not sure a session is open.
An incomplete session (a missing column) is not discarded by a failed EndLoad -- the session stays open, so you can supply the missing column and call EndLoad again rather than starting over from BeginLoad.
A successful EndLoad raises exactly one DataLoaded event -- there is no per-column event during LoadColumn calls.
EndLoad always switches the grid to Turbo column-store mode on success, even if BeginLoad was called while the grid was in Embedded mode.
See alsoBeginLoad, LoadColumn, CancelLoad, IsLoading, DataMode
CancelLoadLoading data
Cancels an in-progress array load, or discards an open column-array loading session, without changing the grid's current data.
CancelLoad serves two loaders at once. If a chunked LoadFromArray call is currently running -- which in practice only matters if you are calling it from a timer or another asynchronous context, since LoadFromArray itself blocks the calling code while it runs -- CancelLoad marks it to stop at the next internal chunk boundary, keeping the rows loaded so far. If a BeginLoad/LoadColumn session is open, CancelLoad discards the staged table completely, and whatever was already showing in the grid before BeginLoad was called stays exactly as it was -- nothing is published. Calling CancelLoad when neither is happening is a harmless no-op.
Embedded and Turbo column-store mode: supported, and always succeeds (it is a no-op when there is nothing to cancel). Virtual mode: not available.
Example
VBA
grid.Clear
grid.BeginLoad 100, 3' ... something goes wrong before all 3 columns are supplied ...
grid.CancelLoad
Debug.Print grid.IsLoading ' -> False
Gotchas
Cancelling a BeginLoad/LoadColumn session never touches the grid's previously published data -- only the staged, not-yet-published table is thrown away.
After CancelLoad discards a session, IsLoading immediately returns False, and a subsequent LoadColumn or EndLoad call is rejected with 'no column-array load is in progress', exactly as if BeginLoad had never been called.
CancelLoad is safe to call defensively (for example, from a form's Unload event) even when you are not sure whether a load or a loading session is active.
See alsoBeginLoad, LoadColumn, EndLoad, IsLoading, LoadFromArray
IsLoadingLoading data
Reports whether a load or a column-array loading session is currently in progress.
IsLoading returns True while either of two independent operations is under way: a LoadFromArray call that is still processing its chunks, or an open BeginLoad/LoadColumn session that has not yet reached EndLoad or CancelLoad. It returns False the rest of the time, including while a streaming session (BeginStreaming/AppendRows/EndStreaming) is open -- streaming is a separate mechanism that IsLoading does not report on.
Signaturegrid.IsLoading As Boolean (get)
ReturnsBoolean: True if a load or an open column-array loading session is in progress; False otherwise.
DefaultFalse
Value
Meaning
True
A LoadFromArray call is still processing, or a BeginLoad session is open and has not yet reached EndLoad or CancelLoad.
False
No load and no column-array loading session is in progress. This includes an open streaming session -- IsLoading does not report on streaming.
Supported in all three data modes, including Virtual mode -- it is a simple status flag unrelated to which engine owns the data, so it is never restricted by DataMode.
IsLoading does not report True during a BeginStreaming/AppendRows/EndStreaming session -- track your own flag if you need to know a streaming session is open.
Because LoadFromArray normally runs to completion before your code's next line executes, you will only see IsLoading return True for that loader from another context calling in while it is mid-chunk (for example, a LoadProgress event handler) -- not from the same procedure that called LoadFromArray.
IsLoading is the one flag that reports on both the array loader and the column-array loading session; there is no way to tell from IsLoading alone which of the two is active.
See alsoBeginLoad, LoadColumn, EndLoad, CancelLoad, LoadFromArray
DirectoryListLoading data
Lists a folder's files and subfolders as grid rows, each with its real Windows shell icon.
DirectoryList turns the grid into a directory listing, the way Windows File Explorer shows one. It enumerates rootPath and adds one row per file and folder; it does not read or import the contents of those files. Every row carries the same icon Explorer would show for that item, taken from the Windows shell itself, so an .xlsx row gets the real Excel icon and an .exe gets that program's own icon. Icons are extracted at the size the IconSize property is set to, from the matching Windows icon set, so they stay sharp rather than being scaled up from 16 pixels. Name is always column 0. columnFlags is a bitwise OR of LiteGridFolderColumn constants choosing which further columns appear, always in the order Size, Type, Date Modified, Full Path, Attributes. Hidden and system items are skipped, as they are in Explorer's default view. Directory junctions and symbolic links are listed but never followed, so a link that points back up its own tree cannot send the listing into an endless loop. A subfolder that cannot be read is skipped and the rest of the listing still succeeds. Rows come back in a fixed order: within each folder, subfolders first and then files, each sorted by name ignoring case; when recursing, a folder's contents follow immediately after that folder's own row. Existing rows and columns are replaced exactly as LoadFromArray replaces them. An existing but empty folder is not an error: the columns are created and the method returns 0.
Signaturegrid.DirectoryList(rootPath As String, recurseSubfolders As Boolean, columnFlags As Long) As Long
Parameters
rootPath (String) - The folder to list. May be relative (it is resolved against the host's current directory), a UNC share, or longer than 260 characters.
recurseSubfolders (Boolean) - True also lists everything inside each subfolder. False lists only the folder's own immediate contents.
columnFlags (Long) - Bitwise OR of LiteGridFolderColumn values selecting the optional columns. Pass 0 for the Name column alone. An unrecognised flag is rejected.
ReturnsThe number of rows listed. Returns 0 for an existing but empty folder; raises a runtime error if the path does not exist, names a file rather than a folder, or cannot be read.
Value
Meaning
lgFolderColSize (1)
File size in bytes. Blank for folders.
lgFolderColType (2)
The shell's own type description, e.g. "Microsoft Excel Worksheet".
lgFolderColDateModified (4)
Last modified, in local time.
lgFolderColFullPath (8)
The item's full path.
lgFolderColAttributes (16)
Attribute letters: D directory, R read-only, A archive, L reparse point.
Embedded mode: supported. Virtual mode: refused -- the host owns the data there, so a listing has nothing to replace. Turbo column-store mode: refused by name. Turbo keeps cell icons in a separate store that this listing does not write, so allowing it would load the rows with every icon silently missing; call Clear and set DataMode = 0 first.
Example
VBA
' List a folder the way Explorer shows it, with real Windows icons.Dim n AsLong
n = grid.DirectoryList("C:\Reports", False, _
lgFolderColSize Or lgFolderColType Or lgFolderColDateModified)
MsgBox n & " items"' Recursive listing. Leaf names repeat across subfolders, so ask for the' full path when you need to tell two same-named files apart.
n = grid.DirectoryList("C:\Reports", True, _
lgFolderColSize Or lgFolderColFullPath)
Gotchas
Set IconSize before calling DirectoryList, not after. Changing IconSize rebuilds the control's icon pool and clears the icons already on screen; the next DirectoryList call re-extracts them at the new size.
A recursive listing can contain several rows with the same Name, because the name shown is the leaf name only. Include lgFolderColFullPath when you need to tell them apart -- it is never added implicitly.
Hidden and system files are not listed. A folder that contains nothing else therefore lists as empty and returns 0.
Folder rows leave the Size cell blank rather than showing 0, which is what Explorer does.
See alsoLoadDirectoryTree, LoadFromArray, Clear, DataMode, IconSize, SetCellIcon, GetCellIcon, SetColumnHeader, AutoFitColumns
LoadDirectoryTreeLoading data
Loads a folder as an expandable tree that reads each subfolder from disk only when the user actually opens it.
LoadDirectoryTree is the one-call way to put a File-Explorer-style folder tree on screen. It enumerates only rootPath's own immediate contents, shows them as tree rows with their real Windows shell icons, and marks every subfolder as expandable without reading it. The moment the user expands one of those folders -- and not before -- that folder's own immediate children are read from disk and inserted underneath it. A subtree the user never opens is never walked, so pointing this at a folder with a hundred thousand items deep inside it costs no more than the top level. This is the recommended way to build a folder tree; the hand-built pattern documented under "Lazy directory trees by hand" below is the same idea spelled out member by member, kept for callers who need to control every row themselves.
There are no placeholder rows. Nothing is ever added to the grid to represent "not loaded yet", so GetRowCount, GetStorageRowCount, GetDataArray, SelectAll and every export see only real filesystem entries at every moment -- a collapsed folder that has never been read contributes exactly one row, its own. A folder is loaded once: collapsing and re-expanding it does not re-read the disk.
Rows and columns are replaced exactly as DirectoryList and LoadFromArray replace them. Name is always column 0; columnFlags picks which further columns appear, in the same fixed order and with the same meanings as DirectoryList (see its table above). The item's full path is always tracked internally -- expansion does not depend on any column being present -- so ask for lgFolderColFullPath only when you want the path visible to the user or to your own code.
Signaturegrid.LoadDirectoryTree(rootPath As String, [columnFlags As Long = 5], [options As Long = 0]) As Long
Parameters
rootPath (String) - The folder to show. May be relative (resolved against the host's current directory), a UNC share, or longer than 260 characters.
columnFlags (Long) - Bitwise OR of LiteGridFolderColumn values selecting the optional columns, exactly as in DirectoryList. Defaults to lgFolderColSize Or lgFolderColDateModified (5). Pass 0 for the Name column alone. An unrecognised flag is rejected.
options (Long) - Bitwise OR of LGDirTreeOptions values (below). Defaults to 0. An unrecognised bit is ignored, not an error.
ReturnsThe number of immediate filesystem entries found directly beneath rootPath. This is a count of what is on disk, not of what ended up on screen: it is the same number with or without lgDirTreeIncludeRoot, and it is the full child count even when lgDirTreeFoldersOnly filtered the files out of the display. An existing but empty folder is not an error -- it returns 0.
Value
Meaning
lgDirTreeFoldersOnly (1)
Show folders only; files are not added as rows. The return value still counts every entry found.
lgDirTreeNoIcons (2)
Skip shell icon resolution entirely. Slightly faster, and nothing is added to the icon pool.
lgDirTreeIncludeRoot (4)
Add one row for rootPath itself, expanded, with everything else beneath it. Without this, the top level's entries are the top-level rows.
Embedded mode: supported. Virtual mode: refused -- the host owns the data there, so there is nothing for the grid to replace. Turbo column-store mode: refused by name, for the same reason DirectoryList refuses it. Both refusals happen before anything is changed: the grid you already had is untouched. Call Clear and set DataMode = 0 first.
Example
VBA
' The whole thing. Subfolders load when the user opens them.Dim n AsLong
n = grid.LoadDirectoryTree("C:\Projects")
MsgBox n & " items in the top level"
VBA
' Folders only, with the root folder shown as the single top row,' and the full path in a visible column.
n = grid.LoadDirectoryTree("C:\Projects", _
lgFolderColDateModified Or lgFolderColFullPath, _
lgDirTreeFoldersOnly Or lgDirTreeIncludeRoot)
VBA
' Refuse to let the user open one particular branch.PrivateSub grid_TreeExpanding(ByVal displayRow AsLong, ByVal storageRow AsLong, _
ByVal depth AsLong, Cancel AsBoolean)
If depth >= 3Then Cancel = True' no directory is read at allEndSub
Gotchas
Hidden and system items are skipped, exactly as in Explorer's default view and in DirectoryList. A folder containing nothing else therefore shows as empty.
Directory junctions and symbolic links are listed but never descended into, so a link pointing back up its own tree cannot cause a loop. Expanding one adds no rows and turns it into a leaf.
A subfolder that cannot be read -- access denied, or deleted between being listed and being expanded -- quietly becomes a leaf when expanded. The expand does not raise an error, and there is no notification that it was the filesystem rather than an empty folder that produced no children. If you need to tell those two cases apart, check the path yourself in TreeExpanding.
TreeExpanding fires before the folder is read, so setting Cancel = True prevents the disk access entirely; the node stays collapsed and stays retryable -- lift the veto and the next expand loads it normally.
Set IconSize before calling, not after, for the same reason as DirectoryList: changing IconSize rebuilds the icon pool.
Icons are cached internally -- one folder icon plus one per distinct file extension, reused across every expand for the life of the control -- so a tree with thousands of files still resolves only a handful of shell icons. This cache is separate from the pool AcquireSystemIconId allocates from; the tree does not consume that pool.
Sorting and filtering are not disabled while a lazy tree is on screen, and neither one destroys the tree. A sort reorders siblings within the tree layout as usual.
Clear, a fresh load, or any other call that replaces the grid's contents also discards the tree's internal path and load-state bookkeeping. There is no way to reload a single node, and no API for saving or restoring which nodes were expanded.
See alsoDirectoryList, TreeExpanding, TreeExpand, TreeCollapseAll, GetTreeChildCount, IconSize, DataMode
Lazy directory trees by handLoading data
Use LoadDirectoryTree (above) unless you have a specific reason not to. It does everything this section describes, in one call, without placeholder rows. What follows is the same pattern spelled out from its individual parts -- worth reading only when you need to build the tree from something that is not a plain folder on disk (an archive, a remote listing, a database hierarchy), when each row has to carry data the folder columns do not cover, or when you want the child rows to arrive under your own rules rather than the built-in ones. It is not a better folder tree; it is a longer one.
The pattern is: show the top level immediately, seed every subfolder with a collapsed placeholder child so it displays an expand glyph, and enumerate a folder's real children only when the user actually expands it. None of the members below are DirectoryList itself, and none of them recurse on their own -- the caller decides, one expand at a time, how far to go. Note the placeholder: a hand-built tree needs a real, visible child row to make the glyph appear, and that row is counted by GetRowCount, returned by GetDataArray, selected by SelectAll and written to every export until you replace it. LoadDirectoryTree has no equivalent row, which is the main practical difference between the two approaches.
The pieces fit together like this: DirectoryListOneLevel lists exactly one folder's immediate contents without touching the grid at all, so you stay in control of exactly which rows get added and how. AcquireSystemIconId resolves the same real Windows shell icon DirectoryList itself would show for a given path, for use with the already-public SetCellIcon. Because this is all built on ordinary tree view (SetTreeParent and friends -- see "Tree view" above), it inherits that feature's own row-numbering split and its Embedded-mode-only restriction: nothing in this group works in Virtual or Turbo column-store mode. GetStorageRowCount and SetCellByStorage exist because a freshly-added row's permanent storage index is what you have in hand right after AddRow or SetTreeParent -- not a displayRow, which only exists once tree view has flattened things into an on-screen order -- so writing to that row's cells needs a storage-indexed setter rather than the ordinary view-indexed SetCellValue/SetCell. SetTreeNodeExpandedByStorage seeds a node's collapsed/expanded flag the same way, before that node has ever been part of a display order. BeginTreeBatch/EndTreeBatch bracket a multi-row expand so the many SetTreeParent calls it makes collapse into one tree-projection rebuild instead of one per call -- nestable, so it is safe to wrap an outer call that itself wraps this pattern.
The TreeExpanding event is the hook that ties it to user interaction: it fires only when a currently-collapsed node is about to become expanded -- never on collapse, never on a no-op re-expand of an already-expanded row -- carrying both the node's current displayRow and its permanent storageRow. A handler seeded with a placeholder child replaces that placeholder (via SetCellByStorage, on the placeholder's own storage index) with the folder's real first child, appends any further children with AddRow/SetTreeParent, seeds a further placeholder under any of those that are themselves folders, and sets Cancel = True to refuse the expand outright (for example, on a folder DirectoryListOneLevel reports as access-denied).
Signatures:
VBA
Function DirectoryListOneLevel(folderPath AsString, maxEntries AsLong, _
status AsLong, hitCap AsBoolean, _
names AsVariant, sizes AsVariant, lastModified AsVariant) AsVariant' returns isDirectoryFunction AcquireSystemIconId(path AsString, fileAttributes AsLong) AsLongFunction GetStorageRowCount() AsLongSub SetTreeNodeExpandedByStorage(storageIdx AsLong, expanded AsBoolean)
Sub SetCellByStorage(storageIdx AsLong, col AsLong, value AsVariant)
Sub BeginTreeBatch()
Sub EndTreeBatch()
Event TreeExpanding(displayRow AsLong, storageRow AsLong, depth AsLong, Cancel AsBoolean)
Parameters and returns:
DirectoryListOneLevel lists folderPath's immediate contents only -- never recursive -- capped at maxEntries (0 = unlimited); hitCap comes back True if the cap stopped the walk early. names, sizes, lastModified and the return value (isDirectory) are parallel 1-based arrays, one element per entry, in the same folders-first/case-insensitive-name order DirectoryList itself uses -- deliberately four parallel 1-D arrays rather than one 2-D array, since a 2-D SAFEARRAY's dimension order reverses silently between C++ and VB. sizes/lastModified are Empty for a folder, exactly matching DirectoryList's own Size/Date Modified columns. status reports 0=OK, 1=Empty, 2=NotFound, 3=NotDirectory, 4=AccessDenied, 5=Failed. Unlike DirectoryList, there is no Type column here -- resolving a real type name costs one extra shell call per distinct extension, left to the caller's discretion via AcquireSystemIconId or otherwise.
AcquireSystemIconId queries by path string and fileAttributes (the FILE_ATTRIBUTE_DIRECTORY bit is what actually matters) -- it never touches disk, so a synthetic path is enough to ask "what icon does a folder, or a .xlsx file, get." Returns -1 (not an error) when the shell has no icon for that combination; pass the result straight to SetCellIcon.
GetStorageRowCount returns the true row count, unaffected by any active sort or filter -- unlike GetRowCount, which reflects the current view.
SetTreeNodeExpandedByStorage and SetCellByStorage both address a row by the permanent storage index AddRow/SetTreeParent returned or expect, not a displayRow -- see the "Tree view" group introduction above for why the two numbering schemes are not interchangeable.
BeginTreeBatch/EndTreeBatch nest freely; only the outermost EndTreeBatch triggers the rebuild, and calling EndTreeBatch when nothing changed is safe.
TreeExpanding's depth is the node's depth in the tree (root = 0). Setting Cancel = True aborts the expand -- the node stays collapsed and no rebuild happens.
Mode support: Embedded only, same as the rest of tree view -- refused outright in Virtual and Turbo column-store mode.
Example (seeding one root level, then loading a folder's real children the first time it is actually expanded):
VBA
' -- Form_Load or wherever the tree is first built --
grid.Clear
grid.AddColumn "Name", 300
grid.AddColumn "Size", 100
grid.AddColumn "Date Modified", 140Dim names AsVariant, sizes AsVariant, dates AsVariant, dirs AsVariantDim status AsLong, hitCap AsBoolean
dirs = grid.DirectoryListOneLevel("C:\Reports", 5000, status, hitCap, names, sizes, dates)
grid.BeginTreeBatch
Dim i AsLongFor i = LBound(names) To UBound(names)
Dim storIdx AsLong
storIdx = grid.GetStorageRowCount
grid.AddRow Array(names(i), sizes(i), dates(i))
If dirs(i) Then
grid.SetCellIcon storIdx, 0, grid.AcquireSystemIconId(names(i), 16) ' FILE_ATTRIBUTE_DIRECTORYDim placeholderIdx AsLong
placeholderIdx = grid.GetStorageRowCount
grid.AddRow Array("(loading...)")
grid.SetTreeParent placeholderIdx, storIdx
grid.SetTreeNodeExpandedByStorage storIdx, FalseEndIfNext i
grid.EndTreeBatch
' -- grid_TreeExpanding, wired to the control's TreeExpanding event --PrivateSub grid_TreeExpanding(ByVal displayRow AsLong, ByVal storageRow AsLong, _
ByVal depth AsLong, Cancel AsBoolean)
' Look up which real folder path and placeholder storage index this' node was seeded with (your own bookkeeping -- e.g. a Collection keyed' by CStr(storageRow), populated at seed time above), then:Dim names AsVariant, sizes AsVariant, dates AsVariant, dirs AsVariantDim status AsLong, hitCap AsBoolean
dirs = grid.DirectoryListOneLevel(folderPath, 5000, status, hitCap, names, sizes, dates)
If status = 4Then' AccessDenied
Cancel = TrueExitSubEndIf
grid.BeginTreeBatch
' Replace the placeholder with real child #1 (SetCellByStorage on the' placeholder's own storage index), then AddRow/SetTreeParent the rest' under storageRow, seeding a further placeholder under any of those' that are themselves folders. Cache AcquireSystemIconId per extension' rather than per row, and add your own row if hitCap came back True.
grid.EndTreeBatch
EndSub
Gotchas
DirectoryListOneLevel never touches the grid and never recurses -- calling it does not, by itself, put anything on screen. Every row shown here comes from your own AddRow/SetCellByStorage calls.
Writing to a row via SetCellValue/SetCell (the ordinary, view-indexed setters) right after AddRow works only by coincidence, while nothing has been sorted or filtered yet -- once a tree is in tree-flattened display order, use SetCellByStorage for any row you're addressing by the storage index AddRow or SetTreeParent handed you.
TreeExpanding fires once per node, the first time it is actually expanded -- track "already loaded" yourself (e.g. a Collection keyed by storage index) if you don't want a second expand-after-collapse to re-enumerate the folder.
AcquireSystemIconId result should be cached per file extension (and once for "folder") rather than re-resolved per row -- it is a shell round trip each time it hasn't been seen before.
See alsoLoadDirectoryTree, DirectoryList, SetTreeParent, GetTreeChildCount, TreeExpand, SetCellIcon, AddRow
Columns
Columns are the one part of the grid's shape that you configure independently of the rows sitting in them. A column has a header, a width, a visibility flag, an alignment, a data-type hint, a display format, and (optionally) a place in a frozen band, a group, or a rendering scheme — and every one of those is addressed by a 0-based column index, the same index space GetCellValue and SetCellValue use. AddColumn appends a new column at the end of the list; there is no InsertColumn, so if you need a specific order, add columns in that order or move data around afterward. Most columns, though, never go through AddColumn at all — LoadFromArray, LoadFromRecordset and similar loaders create columns for you (named "Column 1", "Column 2", ... — 1-based, unless you called SetColumnHeader first), and you simply configure the ones that already exist.
Column behavior splits cleanly along the control's three data modes. In Embedded mode (the default), every member in this group works exactly as described. In Virtual mode, where the host application owns the row data and the grid only caches a rendering window of it, presentation-only settings — width, visibility, header text, alignment, freeze counts, auto-size, drag-reorder and drag-resize opt-in, layout save/restore, renderers, tooltips, the read-only flag, column grouping, and reading whether the aggregate footer is shown — all keep working, because they describe the grid's own configuration rather than the host's data. What Virtual mode refuses is anything that needs the grid to actually hold or evaluate row values: adding a new column, setting a type hint or a format string, wiring up a computed or expression column, configuring a dropdown or date-picker editor, and turning the aggregate footer on (though reading its current state still works). Each of those raises a runtime error identifying the reason; nothing is silently ignored. Turbo column-store mode is more permissive: the entire column-grouping workflow and the entire aggregate-row workflow work there, because Turbo column-store keeps its own value-reading path for both. What Turbo column-store still refuses is the handful of members that depend on the row-based Embedded store specifically — AddColumn, the type and format setters, computed and expression columns, and the dropdown/date-picker editors — because that store sits empty once data has been loaded into the column store instead.
Column grouping lets you place a shared header over a run of columns — for example, spanning "Q1", "Q2", "Q3" and "Q4" columns under one "Sales" band. AddColumnGroup takes the group's title and an array of the column indices it covers, and returns a group ID; groups may nest by passing a parent group's ID. CollapseColumnGroup hides the group's member columns (without deleting them — their data survives, only their visibility changes) and ExpandColumnGroup reveals them again. ClearColumnGroups removes every group and resets ID numbering back to 1 the next time you start adding groups, so don't assume an ID from a previous session is still meaningful after a clear.
The aggregate-row workflow adds a summary band beneath the data: SetColumnAggregate turns on a specific summary (sum, average, count, minimum or maximum) for one column, SetAggregateFormatString controls how that column's computed number is displayed, and ShowAggregateRow reveals or hides the footer band itself. The summary is computed over whatever rows are currently visible, so an active filter narrows it the same way it narrows the grid. There is currently no method that reads the computed summary value back into VBA — it is a rendered, visual feature, not a queryable one — so treat it as something your users see on the form rather than something your code reads back.
A few members in this group set up state that has no way to feed back into what you can inspect from VBA: SetComputedColumn validates and stores a callback object but does not currently alter any cell's displayed value, and neither SetColumnRenderer, SetColumnDropdownItems, SetColumnDatePicker, SetColumnTooltip nor SetColumnReadOnly have a matching getter. Where that is the case it is called out explicitly below, along with the one legitimately useful side effect of SetColumnReadOnly: it blocks only interactive editing (double-click, Enter, F2, Tab) — code that calls SetCellValue or SetCell directly on a read-only column still succeeds.
Per-column colors and status badges are presentation settings as well. SetColumnBackColor, SetColumnForeColor and ApplyColumnStylesJson give a column a background or text color of its own, GetColumnStylesJson reads those back as one document, and SetColumnRenderer type 6 together with SetColumnPillMap turns a status column into rounded, theme-colored badges. All of them are accepted in every mode, but they are drawn only in Embedded and Turbo column-store grids: Virtual mode's painter does not apply column colors, and badge drawing with rows supplied by the host has not been confirmed. The two kinds of setting also belong to different things. A column color is stored on the column itself and moves with it when the user drags the column to a new position; a renderer assignment and a pill map are stored against the column position and stay where they were.
RecipeBuild a column layout once, then save and restore it
VBA
Sub BuildAndPersistLayout(grid AsObject)
Dim savedLayout AsStringDim restoredWidth AsLong
grid.Clear' Build three columns from scratch
grid.AddColumn "Region", 120
grid.AddColumn "Sales", 90
grid.AddColumn "Notes", 200' Adjust one of them and hide another
grid.SetColumnWidth 1, 110
grid.SetColumnVisible 2, False
grid.SetColumnAlignment 1, 2' right-align the Sales column' Save the whole layout as JSON text (width, visible, alignment, type per column)
savedLayout = grid.GetColumnLayout
' ... later, perhaps in a different session, after the columns exist again ...
grid.SetColumnWidth 1, 50' someone changed it
grid.SetColumnVisible 2, True' someone unhid it' Restore exactly what was saved
grid.SetColumnLayout savedLayout
restoredWidth = grid.GetColumnWidth(1)
Debug.Print"Sales column width restored to " & restoredWidth ' -> 110' Visibility has no dedicated getter: read it back out of the layout JSONDebug.Print"Layout after restore: " & grid.GetColumnLayout
EndSub
GetColumnLayout captures width, visibility, alignment and type hint for every column as a JSON string (not the header text — headers are not part of the saved layout). SetColumnLayout applies a previously saved layout back onto the SAME column list: it matches by column index, so the layout only makes sense for a grid whose column count and order have not changed since the layout was captured. Both members work in every mode, so you can safely save a layout while the user is looking at Turbo column-store-loaded data and restore it after a later reload. There is no GetColumnVisible getter — if you need to know whether a column is currently visible, keep track of what you set, or read it back out of GetColumnLayout's JSON as the last line above does.
RecipeGroup quarterly columns under one header the user can collapse
AddColumnGroup takes the group title, an array of the child column indices (captured here right before each AddColumn call, since AddColumn always appends at the current column count), and a parent group ID (-1 for a top-level group). CollapseColumnGroup and ExpandColumnGroup only change the child columns' visibility — the underlying column data is untouched, so collapsing a group is cheap and reversible. If you rebuild the grid from scratch, call ClearColumnGroups first; group IDs reset to 1 afterward, so don't hold onto an old ID across a clear.
RecipeAdd a summary footer that follows the active filter
VBA
Sub AddAmountSummary(grid AsObject)
' Assumes the grid already has data loaded with an Amount column at index 3
grid.SetColumnAggregate 3, 1' 1 = Sum
grid.SetAggregateFormatString 3, "$%.2f"
grid.ShowAggregateRow = True' The footer total narrows automatically when a filter is applied
grid.SetColumnFilter 2, 0, "North"' Contains filter on Region' The Amount footer now shows the sum of only the North rows
grid.ClearColumnFilter 2' The footer total returns to covering every rowEndSub
SetColumnAggregate and SetAggregateFormatString can be called in any order, but the footer only appears once ShowAggregateRow is set to True. Because the summary is computed over the current view, applying or clearing a filter changes what the footer reports without any extra call — there's no separate "recompute" step to remember. There's no method to read the computed number back into VBA; treat the footer as a visual aid for the person using the form. In Virtual mode, ShowAggregateRow's put side is refused (the grid can't summarize data it doesn't hold), but you can still read its current True/False state.
AddColumnColumns
Append a new column with a header and width.
Adds one column to the end of the column list; there is no way to insert a column in the middle, so build up columns in the order you want them to appear. The new column starts visible, left-aligned, with an Auto type hint. If width is zero or negative the column gets a default width of 100 pixels instead — unlike SetColumnWidth, AddColumn does not clamp a very large positive width, so an unreasonable value passed here is stored as-is.
SignatureSub AddColumn(header As String, width As Long)
Parameters
header (String) - Text shown in the column's header cell. An empty string is allowed.
width (Long) - Initial column width in pixels. Values <= 0 become 100; positive values are used exactly as given, with no upper limit.
Refused in both Virtual mode and Turbo column-store mode with a runtime error explaining the column set can't be changed that way; the grid's column count is left exactly as it was.
Example
VBA
Dim newIndex AsLong
grid.Clear
newIndex = grid.GetColumnCount ' 0, since the grid is empty
grid.AddColumn "Customer", 150Debug.Print grid.GetColumnCount ' -> 1Debug.Print grid.GetColumnHeader(newIndex) ' -> "Customer"
Gotchas
Columns can only be appended, never inserted at a specific position — plan the AddColumn order to match the order you want on screen.
A width of 0 or less silently becomes 100; there is no error for an invalid width, only for an invalid column index elsewhere.
Large positive widths are not clamped here the way SetColumnWidth clamps to 10,000 — an accidental huge width will be applied literally.
Adding a column to a grid that already has rows loaded (Embedded mode) creates a column that cannot hold real values for those existing rows: GetCellValue on it silently returns an empty string for every pre-existing row (no error — indistinguishable from a genuinely blank cell), and SetCellValue on it is refused outright with "Column index is out of range", even though GetColumnCount already reports the new column. The only reliable way to give already-loaded rows a working extra column is to include it (with a placeholder value) directly in the array you pass to LoadFromArray from the start, rather than calling AddColumn afterward.
See alsoSetColumnWidth, SetColumnHeader, GetColumnLayout
SetColumnWidthColumns
Change an existing column's pixel width.
Sets the width of a column that already exists. The value is clamped into the range 1 to 10,000 pixels — anything outside that range is silently pulled back to the nearest end rather than rejected. Because this only changes display metadata, it works the same way regardless of which data mode the grid is in.
SignatureSub SetColumnWidth(columnIndex As Long, width As Long)
Parameters
columnIndex (Long) - 0-based index of an existing column.
width (Long) - Desired width in pixels; clamped to 1-10,000.
An out-of-range columnIndex raises a runtime error; an out-of-range width does not — it is clamped instead.
Unlike AddColumn's own width parameter, SetColumnWidth always clamps, so the two members can disagree on the same column if you AddColumn with an oversized width and never call SetColumnWidth afterward.
See alsoGetColumnWidth, AddColumn, AutoFitColumns, SetColumnAutoSize
GetColumnWidthColumns
Read a column's current width in pixels.
Returns the current width of a column, whatever set it — AddColumn's initial width, a later SetColumnWidth call, AutoFitColumns, SetColumnAutoSize, or a restored SetColumnLayout. A newly created column that never had a width specified defaults to 80 pixels.
SignatureFunction GetColumnWidth(columnIndex As Long) As Long
Parameters
columnIndex (Long) - 0-based index of an existing column.
ReturnsThe column's width in pixels.
Default80 for a column created without an explicit width (100 if created via AddColumn with width <= 0).
A pure read of display metadata; identical in every mode.
Example
VBA
Dim w AsLong
w = grid.GetColumnWidth(3) ' Amount column from the standard layoutDebug.Print w
Gotchas
An out-of-range columnIndex raises a runtime error rather than returning 0.
See alsoSetColumnWidth, AddColumn
SetColumnVisibleColumns
Show or hide a column without deleting it.
Toggles whether a column is drawn and takes up horizontal space. The column's data, type, format and every other setting are untouched — hiding a column is purely cosmetic and fully reversible. There is no dedicated getter for this flag; if you need to know a column's current visibility from code, read it back from GetColumnLayout's JSON.
SignatureSub SetColumnVisible(columnIndex As Long, visible As Boolean)
Parameters
columnIndex (Long) - 0-based index of an existing column.
visible (Boolean) - True to show the column, False to hide it.
ReturnsNone.
DefaultTrue (visible) for every column when created.
Presentation-only, so it behaves the same in every mode.
Example
VBA
grid.SetColumnVisible 8, False' hide the Notes column' ... later ...
grid.SetColumnVisible 8, True' show it again
Gotchas
No GetColumnVisible member exists; use GetColumnLayout's JSON to inspect the current visibility of every column at once.
CollapseColumnGroup and ExpandColumnGroup also write this same flag for a group's member columns, so a column that belongs to a collapsed group will be forced invisible again the next time that group's state is applied.
See alsoGetColumnLayout, CollapseColumnGroup, ExpandColumnGroup
AutoFitColumnsColumns
Resize every visible column to fit its header and content.
Measures the header text and every row's displayed text for each visible column (using the row font for content and the header font for the header) and sets each column's width to the larger of the two, subject to the grid's configured minimum column width. Hidden columns are skipped. This is a one-shot sizing pass, not a persistent mode — call it again after data changes if you want the widths refreshed.
Works in every mode, but in Virtual mode it only measures the rows currently cached for display — rows the host hasn't supplied yet aren't considered, so a column can turn out too narrow once more of the host's data scrolls into view. This is a cosmetic limitation, not an error.
Example
VBA
grid.AutoFitColumns
Debug.Print grid.GetColumnWidth(1) ' widened to fit "Item 01".. "Item 60" and the header "Name"
Gotchas
Only affects visible columns; a hidden column's width is untouched until it's shown again.
In Virtual mode the measurement only covers rows currently held in the grid's cache, so widths can need a second AutoFitColumns call after scrolling reveals wider values.
See alsoSetColumnWidth, SetColumnAutoSize
SetColumnHeaderColumns
Change an existing column's header text.
Replaces the text shown in a column's header cell. This does not affect the column's data, type, or any other setting, and it works the same in every mode since header text is metadata the grid owns directly, not host data.
SignatureSub SetColumnHeader(columnIndex As Long, header As String)
Parameters
columnIndex (Long) - 0-based index of an existing column.
header (String) - New header text. An empty string clears the header to blank.
GetColumnLayout does not include header text, so a saved-and-restored layout will not bring back a header rename — only width, visibility, alignment and type.
To show a small icon next to the header text, see SetColumnHeaderIcon (external pool icon) and SetColumnHeaderBuiltinIcon (built-in vector icon); both are screen-only and are likewise not part of a saved layout.
See alsoGetColumnHeader, AddColumn, SetColumnHeaderIcon, SetColumnHeaderBuiltinIcon
GetColumnHeaderColumns
Read a column's current header text.
Returns whatever text is currently displayed in a column's header, whether it came from AddColumn, SetColumnHeader, or an automatic "Column N" name assigned by a loader that created columns without headers of its own.
SignatureFunction GetColumnHeader(columnIndex As Long) As String
Parameters
columnIndex (Long) - 0-based index of an existing column.
ReturnsThe column's header text.
Default"Column N" for columns created by a loader that supplies no header of its own, where N is the column's 1-based position (index + 1) — e.g. the column at index 0 defaults to "Column 1", not "Column 0".
Debug.Print grid.GetColumnHeader(0) ' -> "Id" on the standard fixture
Gotchas
An out-of-range columnIndex raises a runtime error rather than returning an empty string.
See alsoSetColumnHeader
SetColumnTypeColumns
Set a column's data-type hint, which controls formatting and default alignment.
Tells the grid how to interpret and display a column's values. Setting a Checkbox type centers the column if it was still left-aligned; setting a Currency type right-aligns it (if still left-aligned) and immediately reformats the column's displayed text as currency; setting the Rating type center-aligns it and switches the column to a star-rating cell renderer. The valid range accepted by the control is wider than the original documentation: 0 through 9, with 9 meaning a star Rating type in addition to the eight types listed below.
SignatureSub SetColumnType(colIndex As Long, colType As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
colType (Long) - 0-9; see the value table.
ReturnsNone.
Default0 (automatic -- inferred from the value) for every column when created.
Value
Meaning
0
Automatic — infer display formatting from the value itself.
1
String
2
Integer
3
Double
4
Date
5
Boolean
6
Checkbox — also centers the column if it was left-aligned.
7
Progress (a progress-bar cell).
8
Currency — also right-aligns the column and reformats its text immediately.
9
Rating — a star-rating renderer; also centers the column.
Refused in Virtual mode and in Turbo column-store mode with a runtime error. In Turbo column-store mode column types are inferred automatically from the loaded data's own Variant types at load time instead of being set afterward.
The accepted range (0-9, including 9=Rating) is wider than the type list some documentation shows (0-8) — Rating is a real, working ninth type.
Assigning Checkbox, Currency or Rating overrides the column's alignment, but only if it was still at the default left alignment — a column you already right- or center-aligned keeps your choice.
In Turbo column-store mode, calling SetColumnLayout can still write a colType value onto a column (layout restore is not restricted by mode), even though calling SetColumnType directly on the same column is refused — the two paths are not symmetric.
See alsoGetColumnType, SetColumnFormat, SetColumnAlignment, SetColumnLayout
GetColumnTypeColumns
Read a column's current data-type hint.
Returns the type value most recently set on a column, whether by SetColumnType, an automatic type detection during a load, or the automatic (0) default.
SignatureFunction GetColumnType(colIndex As Long) As Long
Parameters
colIndex (Long) - 0-based index of an existing column.
ReturnsThe column's type value (0-9; see SetColumnType's value table).
Read-only, so it works in every mode, including Turbo column-store, where the type reflects what was inferred from the loaded data rather than anything set explicitly.
Example
VBA
Debug.Print grid.GetColumnType(6) ' -> 4 (Date) on the standard fixture
Gotchas
An out-of-range colIndex raises a runtime error rather than returning 0.
See alsoSetColumnType
SetColumnFormatColumns
Set a display-format string for a column's values.
Stores a format string that controls how the column's values are rendered as text, and clears any cached display text so cells re-render using the new format on the next paint. Passing an empty string removes the column's format string entirely, reverting to the type-based default formatting.
SignatureSub SetColumnFormat(colIndex As Long, formatString As String)
Parameters
colIndex (Long) - 0-based index of an existing column.
formatString (String) - A format string; an empty string clears any format previously set.
ReturnsNone.
DefaultNo format string (empty) for every column when created.
Refused in Virtual mode and in Turbo column-store mode with a runtime error, for the same reason as SetColumnType: it changes how stored values are interpreted for display, and neither mode lets you rewrite that after the fact from this member.
Refused identically to SetColumnType in both non-Embedded modes — if your code sets both together, expect both calls to fail together outside Embedded mode.
See alsoGetColumnFormat, SetColumnType
GetColumnFormatColumns
Read a column's current display-format string.
Returns the format string currently stored for a column. In Embedded mode this is an empty string until you call SetColumnFormat; in Turbo column-store mode, a numeric column already carries a default format string inferred from its data (e.g. "0.00"), because SetColumnFormat itself is refused there and Turbo column-store has to have something to show.
SignatureFunction GetColumnFormat(colIndex As Long) As String
Parameters
colIndex (Long) - 0-based index of an existing column.
ReturnsThe column's format string, or an empty string if none is set.
DefaultEmpty string in Embedded mode. In Turbo column-store mode, an inferred default such as "0.00" for a numeric column, not an empty string.
A read-only lookup, so it works in every mode even though the setter is refused outside Embedded mode.
Example
VBA
Debug.Print grid.GetColumnFormat(3) ' -> "" until you call SetColumnFormat
Gotchas
Returns an empty string both when no format has ever been set and when one was set and then cleared — the two cases are indistinguishable from this call alone.
In Turbo column-store mode the default is NOT an empty string for a numeric column — it is an inferred format such as "0.00". Code that assumes GetColumnFormat starts empty in every mode will see a mismatch the first time it runs against Turbo column-store data.
See alsoSetColumnFormat
SetColumnAlignmentColumns
Set a column's horizontal text alignment.
Sets whether a column's text is drawn left, center or right aligned. This is purely a presentation setting and is independent of the column's type, though SetColumnType and SetColumnRenderer both auto-adjust a column's alignment as a side effect when the column was still left-aligned.
SignatureSub SetColumnAlignment(columnIndex As Long, alignment As Long)
Parameters
columnIndex (Long) - 0-based index of an existing column.
Debug.Print grid.GetColumnAlignment(0) ' -> 0 (Left) by default
See alsoSetColumnAlignment
SetFrozenColumnsColumns
Pin a number of leading columns so they don't scroll horizontally.
Freezes the first count columns in place, similar to a spreadsheet's frozen panes: those columns stay visible while the remaining columns scroll underneath them. A count larger than the current column count is silently reduced to the column count instead of raising an error.
SignatureSub SetFrozenColumns(count As Long)
Parameters
count (Long) - Number of leading columns to freeze; must be >= 0.
Debug.Print grid.GetFrozenColumns ' -> 0 by default
See alsoSetFrozenColumns
SetFrozenRowsColumns
Pin a number of leading rows so they don't scroll vertically.
Freezes the first count rows in place at the top of the grid, similar to SetFrozenColumns but for rows. Unlike SetFrozenColumns, an oversized count is not clamped to the current row count — it is stored as given.
SignatureSub SetFrozenRows(count As Long)
Parameters
count (Long) - Number of leading rows to freeze; must be >= 0.
Presentation-only; works identically in every mode.
Example
VBA
grid.SetFrozenRows 1' keep the first row visible while scrollingDebug.Print grid.GetFrozenRows ' -> 1
Gotchas
Only negative values raise an error; a count larger than the row count is accepted without complaint, unlike SetFrozenColumns' equivalent clamp against the column count.
See alsoGetFrozenRows, SetFrozenColumns
GetFrozenRowsColumns
Read the current number of frozen leading rows.
Returns the count most recently set by SetFrozenRows.
Color of the divider line drawn at the frozen row/column boundary.
Controls the color of the separator line the grid draws where frozen rows or columns meet the scrollable area. Has a visible effect only once SetFrozenRows or SetFrozenColumns has frozen at least one row or column.
Values must be 1-5; anything outside that range raises a runtime error instead of being clamped.
See alsoFreezeLineColor, SetFrozenRows, SetFrozenColumns
GetColumnLayoutColumns
Export every column's width, visibility, alignment and type as a JSON string.
Serializes the full column list into a JSON array — one object per column with its index, width, visible flag, alignment and type — so you can save a user's column arrangement (for example, into a settings table) and restore it later with SetColumnLayout. Header text is intentionally not included.
SignatureFunction GetColumnLayout() As String
ReturnsA JSON array string such as [{"index":0,"width":100,"visible":true,"alignment":0,"colType":1}, ...].
Header text is not part of the exported layout — only width, visibility, alignment and type. If you need to preserve renamed headers across a save/restore cycle, store them separately.
See alsoSetColumnLayout
SetColumnLayoutColumns
Restore a column layout previously captured by GetColumnLayout.
Parses a JSON layout string and applies the width, visibility, alignment and type it contains back onto the matching columns by index. Malformed entries for a field are simply skipped for that field (a missing or negative value leaves that column's current setting alone). Column objects are read left to right and only applied after parsing finishes, but parsing stops at the first object that isn't fully closed — so a JSON string truncated partway through the array still applies every column that was completely described before the cut, and only the cut-off object (and anything after it) is left unchanged.
SignatureSub SetColumnLayout(layout As String)
Parameters
layout (String) - A JSON array string in the shape produced by GetColumnLayout.
Works in every mode, including Turbo column-store — notably, this means SetColumnLayout can change a column's type in Turbo column-store mode even though calling SetColumnType directly on the same column is refused there.
Example
VBA
Dim saved AsString
saved = grid.GetColumnLayout
grid.SetColumnWidth 0, 40' change something
grid.SetColumnLayout saved ' put it backDebug.Print grid.GetColumnWidth(0) ' -> the width recorded in "saved"
Gotchas
Column indices in the layout that don't match an existing column are silently skipped rather than raising an error.
A layout string with no '[' character raises a runtime error; anything that parses at all is applied only for the columns and fields it actually contains.
A JSON array truncated partway through does not leave the layout untouched — every column object that was fully closed before the cut point is still applied; only the incomplete trailing object (and anything after it) is dropped.
A width of exactly 0 in the JSON is treated the same as "not specified" and is not applied — you cannot use SetColumnLayout to force a column's width to 0.
This member can write a column's colType even in Turbo column-store mode, unlike SetColumnType, which Turbo column-store refuses outright — the two do not behave symmetrically.
See alsoGetColumnLayout
SetComputedColumnColumns
Register a callback object to supply a column's values.
Validates that the supplied object exposes a Function GetComputedValue(ByVal colIndex As Long, ByVal rowIndex As Long) As Variant method and, if so, stores it against a column index for later use. Must be called from the same thread that created the control. Note: in the current build, storing a computed-column callback here does not cause any cell to display a computed value — no code path currently invokes the stored callback. Treat this as configuration you can set and clear cleanly, not as a feature with a visible effect yet.
SignatureSub SetComputedColumn(colIndex As Long, callback As Object)
Parameters
colIndex (Long) - 0-based index of an existing column.
callback (Object) - An object exposing GetComputedValue(colIndex, rowIndex) As Variant. Cannot be Nothing.
Refused in Virtual mode and Turbo column-store mode with a runtime error; the previously installed callback, if any, is left untouched by a refusal.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = 1
grid.LoadFromArray data
' Class module "MyComputer" must expose:' Public Function GetComputedValue(ByVal colIndex As Long, ByVal rowIndex As Long) As VariantDim computer AsNew MyComputer
grid.SetComputedColumn 0, computer ' succeeds if MyComputer exposes GetComputedValue
Gotchas
Passing Nothing raises a runtime error asking you to use ClearComputedColumn instead.
An object that doesn't expose GetComputedValue with that exact name is rejected, and any callback already installed for the column is left in place.
As shipped, registering a callback here has no visible effect on the grid's displayed values — there is no getter or paint behavior currently wired to it.
See alsoClearComputedColumn, SetColumnExpression
ClearComputedColumnColumns
Remove a computed-column callback previously registered with SetComputedColumn.
Removes any callback registered for the given column index. Safe to call even if no callback was registered — it simply has no effect in that case.
SignatureSub ClearComputedColumn(colIndex As Long)
Parameters
colIndex (Long) - 0-based column index whose callback should be removed.
Refused in Virtual mode and Turbo column-store mode with a runtime error, matching SetComputedColumn.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = 1
grid.LoadFromArray data
grid.ClearComputedColumn 0' removes any callback on column 0, or does nothing if none was set
Gotchas
Calling this on a column with no registered callback does not raise an error — it is a safe no-op.
See alsoSetComputedColumn
SetColumnExpressionColumns
Give a column a formula that is evaluated from other columns' values.
Compiles a lightweight expression (not a full Excel formula language) that references other columns by name in brackets, such as "[Price]*[Qty]" or an IF(...) conditional, and evaluates it once immediately for every row, then caches the results. Column names are resolved at compile time and are case-insensitive; a reference to a column that doesn't exist fails immediately, before anything is stored. Passing an empty string clears the column's expression and any cached results for it.
SignatureSub SetColumnExpression(colIndex As Long, expression As String)
Parameters
colIndex (Long) - 0-based index of the column that should display computed results.
expression (String) - An expression referencing other columns in brackets, e.g. '[Price]*[Qty]'. An empty string clears the expression.
Refused in Virtual mode and Turbo column-store mode with a runtime error — expressions are evaluated against the grid's own stored row values, which don't exist to read in either mode.
AddColumn cannot be used to give an already-loaded grid a new working column: a column added after LoadFromArray/LoadFixture has no real per-row storage, so GetCellValue on it silently returns an empty string for every existing row and SetCellValue refuses it outright ("Column index is out of range"). The extra column must be baked into the array passed to LoadFromArray from the start (with a placeholder value), as the example does.
A compile error (for example, referencing a column name that doesn't exist) is reported immediately and leaves any previous expression on the column untouched.
The expression is evaluated once when set; if the underlying data changes afterward, call RecalculateColumn or RecalculateAll to refresh it — it is not automatically live.
Beyond arithmetic (+ - /) and comparisons (= <> < > <= >=), the expression language also supports functions: IF(condition, trueVal, falseVal), ABS, ROUND(value, decimals), LEN, UPPER, LOWER, TRIM, LEFT(text, n), RIGHT(text, n), MID(text, start, length), MIN, and MAX. A column can also be referenced by its 0-based position instead of its header text, e.g. '[#3]2'.
Mixing text and numbers in the same expression (other than string concatenation with +, which requires both sides to already be text) produces an error cell that displays as "#ERR" rather than raising a runtime error. Dividing by zero produces a blank (NULL) cell instead of an error.
See alsoRecalculateColumn, RecalculateAll, SetComputedColumn
RecalculateColumnColumns
Re-run one column's expression against the current data.
Re-evaluates the expression previously set with SetColumnExpression for a single column, refreshing its cached results and display text. Has no effect (and no error) if the column has no expression.
SignatureSub RecalculateColumn(colIndex As Long)
Parameters
colIndex (Long) - 0-based index of a column with an expression set via SetColumnExpression.
Refused in Virtual mode and Turbo column-store mode with a runtime error, matching SetColumnExpression.
Example
VBA
Dim data(0To0, 0To2) AsVariant
data(0, 0) = 10.5: data(0, 1) = 1: data(0, 2) = 0' Amount, Qty, Total (placeholder)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Amount"
grid.SetColumnHeader 1, "Qty"
grid.SetColumnHeader 2, "Total"
grid.SetColumnExpression 2, "[Amount]*[Qty]"
grid.SetCell 0, 1, 9' change Qty for row 0 - use SetCell, not SetCellValue, to keep the value numeric
grid.RecalculateColumn 2' Total picks up the new QtyDebug.Print grid.GetCellValue(0, 2) ' -> "94.5" (10.5 * 9)
Gotchas
AddColumn cannot be used to give an already-loaded grid a new working column: a column added after LoadFromArray/LoadFixture has no real per-row storage, so GetCellValue on it silently returns an empty string for every existing row and SetCellValue refuses it outright ("Column index is out of range"). The extra column must be baked into the array passed to LoadFromArray from the start (with a placeholder value), as the example does.
Calling this on a column with no expression set is a silent no-op, not an error.
Changing a value that an expression depends on: use SetCell (the typed VARIANT setter), not SetCellValue. SetCellValue always stores a text value, and the expression engine treats any text cell as non-numeric — so a value changed via SetCellValue makes an expression referencing it evaluate to "#ERR" even though the string looks like a number.
See alsoSetColumnExpression, RecalculateAll
RecalculateAllColumns
Re-run every column's expression against the current data.
Refreshes every column that has an expression set via SetColumnExpression, in one batched update. Equivalent to calling RecalculateColumn for each expression column, but issues a single repaint afterward instead of one per column.
Refused in Virtual mode and Turbo column-store mode with a runtime error, matching SetColumnExpression.
Example
VBA
Dim data(0To1, 0To2) AsVariant
data(0, 0) = 10.5: data(0, 1) = 1: data(0, 2) = 0' Amount, Qty, Total (placeholder)
data(1, 0) = 21#: data(1, 1) = 2: data(1, 2) = 0
grid.LoadFromArray data
grid.SetColumnHeader 0, "Amount"
grid.SetColumnHeader 1, "Qty"
grid.SetColumnHeader 2, "Total"
grid.SetColumnExpression 2, "[Amount]*[Qty]"
grid.SetCell 1, 1, 9' change Qty for row 1 - use SetCell, not SetCellValue, to keep the value numeric
grid.RecalculateAll ' refresh every expression column at onceDebug.Print grid.GetCellValue(1, 2) ' -> "189" (21 * 9)
Gotchas
AddColumn cannot be used to give an already-loaded grid a new working column: a column added after LoadFromArray/LoadFixture has no real per-row storage, so GetCellValue on it silently returns an empty string for every existing row and SetCellValue refuses it outright ("Column index is out of range"). The extra column must be baked into the array passed to LoadFromArray from the start (with a placeholder value), as the example does.
If no column has an expression set, this is a safe no-op.
Changing a value that an expression depends on: use SetCell (the typed VARIANT setter), not SetCellValue. SetCellValue always stores a text value, and the expression engine treats any text cell as non-numeric — so a value changed via SetCellValue makes an expression referencing it evaluate to "#ERR" even though the string looks like a number.
See alsoRecalculateColumn, SetColumnExpression
SetColumnAutoSizeColumns
Resize a single column once, using content, header, both, or fill sizing.
Applies one of several sizing rules to a single column and resizes it immediately: fit to the widest displayed value, fit to the header text, whichever of the two is wider, or expand to fill the grid's remaining horizontal space. This is a one-time resize, not a live behavior — the grid does not re-run it automatically afterward, not even when the control is resized (Fill mode) or when rows are added, edited, sorted, filtered, or reloaded. The chosen mode is remembered against the column, but nothing reapplies it on its own; call SetColumnAutoSize again (or AutoFitColumns) whenever you want the column resized to match the current data or the current window size. Setting mode to 0 (None) simply forgets the column's remembered mode; it does not undo a resize already applied.
SignatureSub SetColumnAutoSize(colIndex As Long, mode As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
mode (Long) - 0-4; see the value table.
ReturnsNone.
Default0 (None) for every column.
Value
Meaning
0
None — turn off auto-sizing for this column.
1
Content — size to fit the widest displayed value.
2
Header — size to fit the header text.
3
Both — size to fit whichever of header or content is wider.
4
Fill — expand to consume remaining horizontal space.
Works in every mode without raising an error. In Virtual mode, content-based sizing (modes 1 and 3) only considers the rows currently cached for display, the same cosmetic limitation AutoFitColumns has. In Turbo column-store mode, content-based sizing cannot see the loaded data at all, so a mode-1 column stays at its built-in minimum width and a mode-3 column ends up sized by its header text alone; Header-only (mode 2) and Fill (mode 4) are unaffected and size correctly in every mode.
Example
VBA
grid.SetColumnAutoSize 1, 3' Both: resizes the Name column once, to fit whichever of header or content is widerDebug.Print grid.GetColumnWidth(1)
Gotchas
A mode value outside 0-4 raises a runtime error rather than being clamped.
This is a one-shot resize, not a live auto-size: it is never re-applied automatically afterward, even by adding rows, editing cells, reloading data, or resizing the control (so Fill mode does not keep the column filling the available space after the host form is resized). Call SetColumnAutoSize again whenever you want the size refreshed.
In Turbo column-store mode, content-based sizing (modes 1 and 3) does not measure the grid's actual data — it behaves as though every cell were empty, so a mode-1 column stays at the minimum width and a mode-3 column is sized by its header text alone.
See alsoAutoFitColumns, SetColumnWidth
AllowColumnReorderColumns
Allow or block the user from dragging columns to reorder them in the header.
Controls whether the user can drag a column header to move that column to a new position. This does not affect any programmatic way of reordering columns from code (there is none in this group) — it only controls the interactive header-drag gesture.
Add a shared header spanning a set of columns, returning a group ID.
Creates a column group: a header band drawn above the given columns' own headers, labeled with title. Pass -1 for parentGroupId to create a top-level group, or the ID of an existing group to nest this one beneath it. Group IDs are assigned sequentially starting at 1 and are not reused after RemoveColumnGroup, though ClearColumnGroups resets the counter back to 1 for the next group you add. Clear() (and a LoadFromArray that replaces the dataset) removes every group along with the columns and resets that same counter back to 1, exactly as ClearColumnGroups does -- you don't need to call ClearColumnGroups yourself just to reset a grid. The group band is drawn above the column headers and is not included in HeaderHeight.
SignatureFunction AddColumnGroup(title As String, childColumns As Variant, parentGroupId As Long) As Long
Parameters
title (String) - Text shown on the group's header band.
childColumns (Variant) - A 1-D array of 0-based column indices the group spans. Must be non-empty; an out-of-range index is not rejected here — it is simply ignored later when the group's visibility and header span are computed.
parentGroupId (Long) - -1 for a top-level group, or an existing group's ID to nest under it.
Column grouping is grid-side structural metadata layered over the existing columns, independent of where the row data lives, so it works in every mode, including Turbo column-store.
Example
VBA
Dim groupId AsLong
groupId = grid.AddColumnGroup("Money", Array(3, 5), -1) ' Amount and PriceDebug.Print groupId ' -> 1 for the first group added
Gotchas
childColumns must be a non-empty array of column indices; an empty array or the wrong Variant shape raises a runtime error.
A brand-new group does not itself change any column's visibility — call CollapseColumnGroup afterward if you want it to start collapsed.
Group IDs are not reused after RemoveColumnGroup, but they do reset to 1 the next time you add a group after calling ClearColumnGroups, Clear(), or a LoadFromArray that replaces the dataset.
An out-of-range column index inside childColumns is not validated or rejected — it is stored as-is and simply has no effect when the group's visibility or header span is computed, so a group made entirely of bad indices behaves like an empty group without ever raising an error.
parentGroupId is not validated either: passing an ID that doesn't match any existing group is accepted silently, and the new group is just treated as top-level for header-row layout purposes.
childColumns must be passed as a Variant array (e.g. the result of VBA's Array() function), not a typed array variable (Dim x() As Long) referenced directly. Late-bound calls (grid As Object) pass a typed array variable by reference, which this method does not recognize and refuses with "childColumns must be an array of column indices" — Array(...) avoids that because it returns a Variant array by value.
See alsoRemoveColumnGroup, CollapseColumnGroup, ExpandColumnGroup, ClearColumnGroups
RemoveColumnGroupColumns
Delete a single column group by ID.
Removes one group, leaving its member columns exactly as visible or hidden as they were at the moment of removal (removing a group does not force its columns back to visible). Other groups' IDs are unaffected.
SignatureSub RemoveColumnGroup(groupId As Long)
Parameters
groupId (Long) - ID returned by a previous AddColumnGroup call.
Dim groupId AsLong
groupId = grid.AddColumnGroup("Temp", Array(3), -1)
grid.RemoveColumnGroup groupId
Gotchas
An unknown groupId raises a runtime error rather than being silently ignored — unlike ClearComputedColumn, this is not a safe no-op for a bad ID.
See alsoAddColumnGroup, ClearColumnGroups
CollapseColumnGroupColumns
Hide a column group's member columns.
Sets the group's collapsed state and immediately hides every column listed in that group (as if SetColumnVisible had been called False on each). The columns' data is untouched — this is purely a visibility change, fully reversed by ExpandColumnGroup.
SignatureSub CollapseColumnGroup(groupId As Long)
Parameters
groupId (Long) - ID returned by a previous AddColumnGroup call.
Structural/visibility metadata; works in every mode.
Example
VBA
Dim groupId AsLong
groupId = grid.AddColumnGroup("Money", Array(3, 5), -1)
grid.CollapseColumnGroup groupId ' hides every column in that groupDebug.Print grid.GetColumnLayout ' shows "visible":false for those columns
Gotchas
An unknown groupId raises a runtime error.
See alsoExpandColumnGroup, AddColumnGroup, SetColumnVisible
ExpandColumnGroupColumns
Show a previously collapsed column group's member columns again.
Clears the group's collapsed state and shows every column listed in that group. If the group was already expanded, this has no visible effect.
SignatureSub ExpandColumnGroup(groupId As Long)
Parameters
groupId (Long) - ID returned by a previous AddColumnGroup call.
Structural/visibility metadata; works in every mode.
Example
VBA
Dim groupId AsLong
groupId = grid.AddColumnGroup("Money", Array(3, 5), -1)
grid.CollapseColumnGroup groupId
grid.ExpandColumnGroup groupId ' shows the group's columns again
Gotchas
An unknown groupId raises a runtime error.
See alsoCollapseColumnGroup, AddColumnGroup
ClearColumnGroupsColumns
Remove every column group.
Deletes all groups at once. Member columns' current visibility is left exactly as it was — clearing groups does not force any column visible or hidden. The next group added after this call gets ID 1 again.
grid.ClearColumnGroups ' drop every group at onceDebug.Print grid.AddColumnGroup("First", Array(0), -1) ' -> 1 again
Gotchas
Group ID numbering restarts at 1 after this call — don't rely on IDs from before a ClearColumnGroups still being unique afterward.
See alsoAddColumnGroup, RemoveColumnGroup
SetColumnRendererColumns
Assign a built-in cell renderer to a column.
Switches a column between plain text display and a built-in renderer such as checkboxes, a progress bar, a button, a star rating, or a status badge (pill). Assigning Checkbox, Star or Pill auto-centers the column if it was still left-aligned. There is no getter to read a column's current renderer back — track what you set from code if you need to know it later.
SignatureSub SetColumnRenderer(colIndex As Long, rendererType As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
rendererType (Long) - 0-6; see the value table.
ReturnsNone.
Default0 (Default — plain text).
Value
Meaning
0
Default — plain text display.
1
Checkbox — also centers the column if it was left-aligned.
2
Progress — a progress-bar cell.
3
Sparkline — currently a no-op here; use the dedicated sparkline members to configure sparklines instead.
4
Button — a clickable cell button.
5
Star — a star-rating renderer; also centers the column if it was left-aligned.
6
Pill — draws each non-empty value as a rounded status badge, colored through SetColumnPillMap; also centers the column if it was left-aligned.
Presentation-only; accepted in every mode. For type 6, badge drawing in Virtual mode with rows supplied by the host has not been confirmed -- see SetColumnPillMap.
Example
VBA
grid.SetColumnAlignment 7, 0' start left-aligned
grid.SetColumnRenderer 7, 1' Checkbox for the Active columnDebug.Print grid.GetColumnAlignment(7) ' -> 1 (auto-centered)
Gotchas
rendererType 3 (Sparkline) does nothing when passed here — sparkline configuration is done through the dedicated sparkline members, not this one, even though 3 is listed as an accepted value.
rendererType 5 (Star) and 6 (Pill) are accepted even though some documentation only lists renderer values up to 4.
A rendererType outside 0-6 raises a runtime error.
The assignment belongs to the column position, not to the column: if the user drags the column to a new position, the renderer stays at the old position and applies to whichever column moves into it. Clear removes every renderer assignment.
Switching a pill column back to 0 removes the badges but keeps its SetColumnPillMap mappings; assigning 6 again brings the same badges back.
See alsoSetColumnAlignment, SetColumnType, SetColumnPillMap
SetColumnBackColorColumns
Set the background color for every cell in one column.
Gives a column a background of its own, painted in place of both the plain and the alternate-row background in that column. It sits above the theme and below everything more specific: a SetCellColor override, a conditional-format rule and a selection all still paint over it. The text in the column is checked against this background and replaced on screen with a more readable color only if it would be unreadable. Write &HFFFFFFFF to return the column to the theme's background.
SignatureSub SetColumnBackColor(columnIndex As Long, color As OLE_COLOR)
Parameters
columnIndex (Long) - 0-based index of an existing column.
color (OLE_COLOR) - The background color as an explicit RGB() value, or &HFFFFFFFF to restore the themed background.
ReturnsNone.
Default&HFFFFFFFF (no column background of its own) for every column.
EmbeddedsupportedVirtualaccepted, not drawnTurbo column-storesupported
Accepted in every mode. Embedded and Turbo column-store grids paint it. Virtual mode stores it, and GetColumnStylesJson reads it back there, but Virtual mode's painter does not apply column colors.
Example
VBA
grid.SetColumnBackColor 2, RGB(238, 244, 250) ' every Balance cell, plain and alternate rows alikeDebug.Print grid.GetColumnStylesJson() ' -> {"columns":[{"index":2,"back":"#EEF4FA"}]}
grid.SetColumnBackColor 2, &HFFFFFFFF ' back to the theme's backgroundDebug.Print grid.GetColumnStylesJson() ' -> {"columns":[]}
Gotchas
There is no per-column getter; GetColumnStylesJson reports every column that carries a color.
Not a theme role: Theme and ApplyThemeJson leave it in place. Choose a color that reads under every theme the grid can switch to, or apply different column styles after a switch.
Stored on the column, so it moves with the column when the user drags it to a new position. Clear removes it along with the columns.
On the hovered row the column background is tinted toward the hover color rather than replaced, so the row still reads as hovered.
The color is used exactly as given, with no system-color translation -- pass an explicit RGB() value.
An out-of-range columnIndex raises a runtime error.
See alsoSetColumnForeColor, ApplyColumnStylesJson, GetColumnStylesJson, SetCellColor
SetColumnForeColorColumns
Set the text color for every cell in one column.
Gives a column a text color of its own while leaving its background alone: a column with only a text color keeps its row's plain or alternate-row background and hover highlight exactly. The color is checked against whatever background each cell actually gets -- the row's, a column background, a cell color or a heatmap tint -- and lightened or darkened on screen only if it would be unreadable there. A SetCellColor override, a conditional-format rule and a selection all paint their own text color over it. Write &HFFFFFFFF to return the column to the theme's text color.
SignatureSub SetColumnForeColor(columnIndex As Long, color As OLE_COLOR)
Parameters
columnIndex (Long) - 0-based index of an existing column.
color (OLE_COLOR) - The text color as an explicit RGB() value, or &HFFFFFFFF to restore the themed text color.
ReturnsNone.
Default&HFFFFFFFF (no column text color of its own) for every column.
EmbeddedsupportedVirtualaccepted, not drawnTurbo column-storesupported
Accepted in every mode. Embedded and Turbo column-store grids paint it. Virtual mode stores it, and GetColumnStylesJson reads it back there, but Virtual mode's painter does not apply column colors.
Example
VBA
grid.SetColumnForeColor 1, RGB(27, 94, 32) ' green Region text; the column still bands with its rowDebug.Print grid.GetColumnStylesJson() ' -> {"columns":[{"index":1,"fore":"#1B5E20"}]}
Gotchas
GetColumnStylesJson reports the color you set, not a more readable color the grid may have substituted on screen.
Independent of SetColumnBackColor: restoring one of the two with &HFFFFFFFF leaves the other in place.
Not a theme role: Theme and ApplyThemeJson leave it in place.
Stored on the column, so it moves with the column when the user drags it to a new position. Clear removes it along with the columns.
An out-of-range columnIndex raises a runtime error.
See alsoSetColumnBackColor, ApplyColumnStylesJson, GetColumnStylesJson, ForeColor
ApplyColumnStylesJsonColumns
Apply background and text colors to several columns at once from one JSON document -- all of it or none.
Takes a document of the form {"columns":[{"index":2,"back":"#FFF8E1","fore":"#5D4037"},{"index":4,"fore":"#1B5E20"}]} -- the same shape GetColumnStylesJson returns. Each element names one column by its 0-based index and sets back, fore, or both. A color an element leaves out keeps its current value, and the string "auto" returns that color to the theme, exactly as &HFFFFFFFF does through SetColumnBackColor and SetColumnForeColor. Columns the document does not mention are untouched. The whole document is checked before anything is applied: if any part of it is wrong, the call raises a runtime error that names the problem and ends "No column styling was changed.", and no column is touched.
SignatureSub ApplyColumnStylesJson(json As String)
Parameters
json (String) - A JSON object with exactly one key, "columns", holding an array of objects. Each object must have "index" (a whole number naming an existing column) and at least one of "back" and "fore", each either "#RRGGBB" or "auto". Whitespace anywhere and either hex case are accepted, as is a leading byte-order mark such as a Windows text editor writes.
ReturnsNone.
EmbeddedsupportedVirtualaccepted, not drawnTurbo column-storesupported
Accepted in every mode, with the same drawing limit in Virtual mode as SetColumnBackColor.
Example
VBA
' Save the current column colors with the rest of the user's settings ...Dim savedStyles AsString
savedStyles = grid.GetColumnStylesJson()
' ... and restore them later, once the same columns have been loaded againOnErrorResumeNext
grid.ApplyColumnStylesJson savedStyles
IfErr.Number <> 0ThenDebug.Print"Column styles not restored: " & Err.Description ' no column changedOnErrorGoTo0
Gotchas
All or nothing: an index that is out of range or not a whole number, a column listed twice, a key other than index, back or fore, an element with neither back nor fore, a malformed color, or text that is not well-formed JSON raises an error and changes no column.
Each index is checked against the columns the grid has at the moment of the call, so apply column styles after the columns exist -- after loading data, not before.
Columns are matched by position only. Applying a document saved from a different column layout colors whichever columns now sit at those positions.
"auto" and an omitted color are different: "auto" clears that color, while leaving the key out keeps the color it already had.
An empty string is refused. {"columns":[]} is valid and changes nothing.
Not part of GetCurrentThemeJson or ApplyThemeJson, and Theme changes leave these colors in place.
See alsoGetColumnStylesJson, SetColumnBackColor, SetColumnForeColor, ApplyThemeJson
GetColumnStylesJsonColumns
Return every column's background and text color overrides as one JSON document.
Returns the per-column colors set through SetColumnBackColor, SetColumnForeColor or ApplyColumnStylesJson, in exactly the shape ApplyColumnStylesJson accepts, so the string can be stored and applied again later. Only columns that carry at least one color appear, each with only the color or colors it carries, in column order; a grid with none returns {"columns":[]}.
SignatureFunction GetColumnStylesJson() As String
ReturnsA JSON object string with one key, "columns", holding an array of {"index":n,"back":"#RRGGBB","fore":"#RRGGBB"} objects, colors in uppercase hex.
Reads stored column settings only, so it works the same way in every mode.
Example
VBA
' On a grid with at least five columns
grid.SetColumnBackColor 2, RGB(255, 248, 225)
grid.SetColumnForeColor 2, RGB(93, 64, 55)
grid.SetColumnForeColor 4, RGB(27, 94, 32)
Debug.Print grid.GetColumnStylesJson()
' -> {"columns":[{"index":2,"back":"#FFF8E1","fore":"#5D4037"},{"index":4,"fore":"#1B5E20"}]}
Gotchas
Reports the colors you set, not a more readable text color the grid may have substituted on screen.
Each index is the column's position at the time of the call. Column colors move with a dragged column, so after the user reorders columns the same colors can come back under different indexes.
Theme colors are not included; use GetCurrentThemeJson for those.
See alsoApplyColumnStylesJson, SetColumnBackColor, SetColumnForeColor, GetCurrentThemeJson
SetColumnPillMapColumns
Choose which status color a value draws with in a pill (status badge) column.
Maps one exact display value in colIndex to a status: 0 = Neutral, 1 = Good, 2 = Warn, 3 = Bad. The column draws badges once it has the pill renderer (SetColumnRenderer colIndex, 6): each non-empty cell becomes a rounded chip with its text as the label, filled with the theme's Status_Good, Status_Warn or Status_Bad color for a mapped value -- the same three colors the progress renderer uses -- or with the neutral header surface color for a value that has no mapping. Call it once per value; mapping a value again replaces its status. The map can be built before or after the renderer is assigned.
SignatureSub SetColumnPillMap(colIndex As Long, displayValue As String, pillStatus As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
displayValue (String) - The cell text exactly as the grid displays it; compared case-sensitively.
pillStatus (Long) - 0-3; see the value table.
ReturnsNone.
DefaultNo mappings -- every non-empty value in a pill column draws the neutral badge.
Value
Meaning
0
Neutral -- the theme's header surface color.
1
Good -- the Status_Good role.
2
Warn -- the Status_Warn role.
3
Bad -- the Status_Bad role.
EmbeddedsupportedVirtualaccepted, drawing not confirmedTurbo column-storesupported
Accepted in every mode. Embedded and Turbo column-store grids draw the badges. In Virtual mode the renderer and the map are both accepted, but badge drawing with rows supplied by the host has not been confirmed.
Example
VBA
grid.SetColumnRenderer 1, 6' 6 = Pill
grid.SetColumnPillMap 1, "Delivered", 1' Good
grid.SetColumnPillMap 1, "On Hold", 2' Warn
grid.SetColumnPillMap 1, "Cancelled", 3' Bad' Any other non-empty Status value draws the neutral badge
Gotchas
Matching is on the displayed text, exact and case-sensitive: "on hold" does not match "On Hold", and a formatted number matches as it appears on screen, not as its stored value.
Has no visible effect until the column's renderer is 6. SetColumnRenderer colIndex, 0 removes the badges but keeps the map.
An empty cell draws no badge at all; an unmapped non-empty value draws the neutral badge.
A label longer than its badge is cut off with an ellipsis at the badge edge.
There is no per-value custom color. Badges use the theme's three status roles, so recolor every badge at once with ApplyThemeJson (Status_Good, Status_Warn, Status_Bad).
The map belongs to the column position, not to the column: if the user drags the column elsewhere, the map stays at the old position. It also survives Clear, so call ClearColumnPillMap before building a different layout.
There is no getter for the map.
An out-of-range colIndex, or a pillStatus outside 0-3, raises a runtime error.
See alsoClearColumnPillMap, SetColumnRenderer, SetColumnBuiltinIconMap
ClearColumnPillMapColumns
Remove every value-to-status mapping from a column.
Drops all of colIndex's SetColumnPillMap mappings at once. The pill renderer stays attached, so every non-empty value in the column then draws the neutral badge; use SetColumnRenderer colIndex, 0 to remove the badges themselves. Clearing a column that has no mappings is a safe no-op.
SignatureSub ClearColumnPillMap(colIndex As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
Accepted in every mode; it only discards stored mappings.
Example
VBA
grid.ClearColumnPillMap 1' every Status value now draws the neutral badge
grid.SetColumnPillMap 1, "Delivered", 1' start a new map
Gotchas
There is no call to remove a single mapping. Map that value to 0 (Neutral), which draws the same badge as an unmapped value, or clear the column and add the other mappings again.
Clear does not remove pill maps, so this is the call that discards a map left behind from an earlier layout.
An out-of-range colIndex raises a runtime error.
See alsoSetColumnPillMap, SetColumnRenderer
ShowColumnChooserColumns
Open the built-in column-visibility picker popup at a screen position.
Opens a small popup window listing every column with a checkbox for its visibility, positioned at the given screen coordinates. Passing 0, 0 positions it automatically near the current mouse cursor instead of at the literal screen origin. The grid must already have at least one column defined.
SignatureSub ShowColumnChooser(x As Long, y As Long)
Parameters
x (Long) - Screen X position for the popup, or 0 (with y also 0) for automatic positioning.
y (Long) - Screen Y position for the popup, or 0 (with x also 0) for automatic positioning.
Opens a UI element rather than touching row data, so it works in every mode.
Example
VBA
grid.ShowColumnChooser 0, 0' opens near the current mouse position
Gotchas
Calling this with no columns defined raises a runtime error rather than opening an empty popup.
The popup is a separate top-level window that takes its own keyboard focus; there is no member on the control to close it again from code once it is open.
See alsoSetColumnVisible
SetColumnReadOnlyColumns
Block interactive editing for a column without blocking programmatic writes.
Marks a column so the user cannot start editing it by double-clicking, pressing Enter or F2, and Tab skips over it while moving between cells. This only affects interactive, UI-driven editing — code that calls SetCellValue or SetCell against a read-only column still succeeds normally. There is no getter for this flag.
SignatureSub SetColumnReadOnly(colIndex As Long, readOnly As Boolean)
Parameters
colIndex (Long) - 0-based index of an existing column.
readOnly (Boolean) - True to block interactive editing, False to allow it again.
This flag is not restricted by mode at all — it works everywhere, including Turbo column-store.
Example
VBA
grid.SetColumnReadOnly 1, True' Name column can't be edited by hand
grid.SetCellValue 0, 1, "Overwritten"' still works from codeDebug.Print grid.GetCellValue(0, 1) ' -> "Overwritten"
Gotchas
This does not make a column read-only against code — SetCellValue/SetCell still write to a read-only column without error. It only blocks the interactive editors (double-click, Enter, F2, Tab-into).
No GetColumnReadOnly member exists; track the flag's state in your own code if you need to query it.
In Turbo column-store mode, SetCellValue only accepts a value for a String-typed column; writing to a numeric column that way raises an error (use SetCell, the typed VARIANT setter, for numeric columns instead). This is a Turbo column-store-wide SetCellValue limitation, not specific to a read-only column.
See alsoSetColumnTooltip, SetCellValue
SetColumnTooltipColumns
Set hover tooltip text for a column header.
Stores text shown when the user hovers the mouse over a column's header. Passing an empty string removes the tooltip. There is no getter for this value.
SignatureSub SetColumnTooltip(colIndex As Long, tooltipText As String)
Parameters
colIndex (Long) - 0-based index of an existing column.
tooltipText (String) - Tooltip text; an empty string clears any tooltip previously set.
grid.SetColumnTooltip 8, "Free-form notes entered by the sales rep"' Hovering the Notes header now shows that text.
Gotchas
No GetColumnTooltip member exists to read the value back; keep your own record if your code needs to know it.
See alsoSetColumnHeader
AllowColumnResizeColumns
Allow or block the user from dragging a header edge to resize a column.
Controls only the interactive header-edge drag gesture and its cursor. SetColumnWidth and AutoFitColumns keep working from code regardless of this setting.
A UI-gesture toggle with no data dependency; works identically in every mode.
Example
VBA
grid.AllowColumnResize = False' users can no longer drag column edgesDebug.Print grid.AllowColumnResize ' -> False
grid.SetColumnWidth 0, 150' still works from code
Gotchas
Turning this off does not disable SetColumnWidth or AutoFitColumns from code — only the mouse-driven resize gesture.
See alsoAllowColumnReorder, SetColumnWidth
SetColumnDropdownItemsColumns
Give a column a fixed list of choices for interactive editing.
Configures a column so that starting an edit on one of its cells shows a dropdown list of the given items instead of a plain text box. Pass an array of strings; passing Empty or Null removes the dropdown and returns the column to plain text editing. There is no getter for the configured item list.
SignatureSub SetColumnDropdownItems(colIndex As Long, items As Variant)
Parameters
colIndex (Long) - 0-based index of an existing column.
items (Variant) - A 1-D array of strings to offer as choices, or Empty/Null to clear the dropdown.
Refused in Virtual mode and Turbo column-store mode with a runtime error. This is a blanket refusal on the configuration call itself — it never reads any cell value. Virtual mode refuses it because the grid doesn't own the data; Turbo column-store mode refuses it because the Turbo column-store column store is read-only for this kind of per-column configuration.
Example
VBA
grid.SetColumnDropdownItems 2, Array("North", "South", "East", "West")
' Editing a Region cell now offers those four choices instead of free text.
Gotchas
Passing an array of a type other than strings or Variants (or a non-array value other than Empty/Null) raises a type-mismatch error.
SetColumnDropdownEditable is not restricted by mode on its own, so you can call it in Virtual or Turbo column-store mode without error, but since SetColumnDropdownItems is refused there, the editable flag has no visible effect until you switch back to Embedded mode.
If the column's cells already show icons (SetColumnIconMap / SetColumnBuiltinIconMap), SetColumnDropdownIcons makes this dropdown draw each item with the icon its value resolves to; the items and their order still come from here.
See alsoSetColumnDropdownEditable, SetColumnDropdownIcons, SetColumnDatePicker
SetColumnDatePickerColumns
Give a column a calendar date-picker editor for interactive editing.
Configures a column so that starting an edit on one of its cells shows a calendar popup instead of a plain text box. There is no getter for this flag.
SignatureSub SetColumnDatePicker(colIndex As Long, enable As Boolean)
Parameters
colIndex (Long) - 0-based index of an existing column.
enable (Boolean) - True to use the date-picker editor, False to return to plain text editing.
Refused in Virtual mode and Turbo column-store mode with a runtime error — the same blanket refusal as SetColumnDropdownItems. This configuration call itself never reads a cell value; only the interactive calendar popup does that, and it is refused separately (on its own path) when it tries to open in Turbo column-store mode.
Example
VBA
grid.SetColumnDatePicker 6, True' OrderDate column now edits via a calendar popup
Gotchas
Enabling this on a non-date column is accepted without error; the picker will still open, seeded from whatever the cell's current text parses as.
See alsoSetColumnDropdownItems
SetColumnDropdownEditableColumns
Allow free-text entry alongside a column's dropdown list.
When a column has dropdown items configured via SetColumnDropdownItems, this switches its editor between a locked list (the user must pick an existing item) and an editable combo box (the user can type a value not in the list, with autocomplete against the existing items). This flag is not restricted by mode, so it can be called in any mode, but it has no visible effect unless SetColumnDropdownItems has also been applied to the same column — which is refused outside Embedded mode.
SignatureSub SetColumnDropdownEditable(colIndex As Long, editable As Boolean)
Parameters
colIndex (Long) - 0-based index of an existing column.
editable (Boolean) - True for a free-text-capable combo box, False for a locked list.
This flag itself is not restricted by mode and can be set in every mode, but it only matters once SetColumnDropdownItems has actually populated a dropdown, which Virtual and Turbo column-store mode both refuse.
Example
VBA
OnErrorResumeNext
grid.SetColumnDropdownItems 2, Array("North", "South", "East", "West") ' refused in Turbo column-store mode - the dropdown itself is Embedded/Virtual onlyOnErrorGoTo0
grid.SetColumnDropdownEditable 2, True' users may now also type a region not in the list (no visible effect in Turbo column-store, since there is no dropdown there)
Gotchas
Calling this in Virtual or Turbo column-store mode succeeds silently but has no observable effect, since the dropdown it modifies can't be configured there — don't mistake the lack of an error for the feature actually working.
See alsoSetColumnDropdownItems
SetColumnDropdownIconsColumns
Show each choice's icon in a column's dropdown cell editor.
When a column already shows icons for its values (SetColumnIconMap / SetColumnBuiltinIconMap, including their thresholds and default icon) and offers a fixed list of choices for editing (SetColumnDropdownItems), this makes the dropdown editor draw each choice with the icon that value resolves to. Picking a choice writes the value into the cell exactly as before, and because the cell resolves its own icon from the same rules, the cell's icon changes to match. Items and their order still come from SetColumnDropdownItems; nothing about icons is configured a second time. There is no getter.
SignatureSub SetColumnDropdownIcons(colIndex As Long, showIcons As Boolean)
Parameters
colIndex (Long) - 0-based index of an existing column.
showIcons (Boolean) - True to draw each item's icon in the dropdown; False to return to a plain text dropdown.
Refused in Virtual mode and Turbo column-store mode with a runtime error -- exactly the same blanket refusal as SetColumnDropdownItems, its prerequisite (and deliberately not the silent success of SetColumnDropdownEditable, which can have no effect outside Embedded mode).
Example
VBA
Dim i AsLongFor i = 0To UBound(countries)
grid.SetColumnIconMap 0, countries(i), flagIcons(i) ' the cells already show flagsNext
grid.AllowEditing = True
grid.SetColumnDropdownItems 0, countries ' which values may be chosen, in this order
grid.SetColumnDropdownIcons 0, True' ...and the dropdown now shows each flag' Double-click a Country cell: the list shows every country with its flag;' pick one and both the cell's text and its flag change.
Gotchas
Each item's icon is resolved through the same rules the column's cells use -- exact value map, then numeric threshold bands, then the column default icon, with built-in icons checked before external ones -- so a dropdown item and the cell it commits into can never show different icons. An item with no matching rule shows no icon, but its slot is still reserved so the text column stays aligned.
A cell-level override (SetCellIcon / SetCellBuiltinIcon) is deliberately not consulted for the items: a dropdown item is a candidate value, not a cell. If the cell being edited carries such an override, it keeps that override after the commit, exactly as it always has.
Turning this on for a column with no dropdown items is harmless: no dropdown opens until SetColumnDropdownItems has been called, and the flag simply waits.
Works together with SetColumnDropdownEditable: a free-text value the user types commits as before and shows whatever icon the rules give it (possibly none).
Changes only how the next dropdown editor is created; an editor already open when this is called keeps its current appearance.
External icons are drawn at the pool's IconSize but clipped to the item's icon slot, so a large IconSize never paints across neighbouring items. Removing an icon (RemoveIcon) while a dropdown is open simply makes that item's icon disappear on its next repaint.
The Excel-style filter dropdown (ShowFilterButtons) is a separate, unrelated list and is not affected by this setting.
The dropdown editor's commit fires AfterCellEdit (and ValidationFailed when a rule rejects the value) but, exactly like the plain text dropdown before it, does not fire CellValueChanged and does not record an undo entry.
See alsoSetColumnDropdownItems, SetColumnDropdownEditable, SetColumnIconMap, SetColumnBuiltinIconMap, SetColumnDefaultIcon, AddIconThreshold
SetColumnAggregateColumns
Turn on a summary calculation for a column, shown in the aggregate footer.
Chooses which summary (if any) is computed for a column and shown in the footer row once ShowAggregateRow is turned on. The summary is computed over whatever rows are currently visible, so an active filter changes it. Passing mode 0 turns the summary off for that column again. There is no getter for either the mode or the computed result.
SignatureSub SetColumnAggregate(colIndex As Long, mode As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
mode (Long) - 0-5; see the value table.
ReturnsNone.
Default0 (None) for every column.
Value
Meaning
0
None — no summary for this column.
1
Sum
2
Average
3
Count — number of cells in the column whose value is numeric, or is text that parses as a number
Refused in Virtual mode (the grid can't summarize data it doesn't hold), but fully supported in Turbo column-store mode, which has its own value-reading path for computing summaries.
Example
VBA
grid.SetColumnAggregate 3, 1' Sum of Amount
grid.ShowAggregateRow = True' The footer under the Amount column now shows the sum of the visible rows.
Gotchas
There is no method that reads the computed summary value back into VBA — it's a rendered footer value, not a queryable one.
The footer only recomputes automatically while the control has a real window; a headless grid instance may not refresh the number until it's actually shown.
Every summary mode, including Count, silently skips cells whose value isn't numeric and doesn't parse as one — a blank cell or ordinary text is excluded from the calculation rather than counted as zero.
See alsoShowAggregateRow, SetAggregateFormatString
SetAggregateFormatStringColumns
Control how a column's computed aggregate number is displayed.
Sets a printf-style format string used to render the single numeric value SetColumnAggregate computes for a column. At most one numeric conversion (like %f, %d or %.2f) is allowed; a doubled percent (%%) is treated as a literal percent sign, and a format string with no numeric conversion at all is also accepted — it is rendered as static literal text and the computed value isn't shown. The format is validated before it is stored, so an invalid string leaves the column's previous format (if any) untouched. There is no getter for the stored format string.
SignatureSub SetAggregateFormatString(colIndex As Long, formatString As String)
Parameters
colIndex (Long) - 0-based index of an existing column.
formatString (String) - A format string with at most one numeric conversion, e.g. "$%.2f" or "%d units".
ReturnsNone.
DefaultNo format string set (a built-in default rendering is used).
Refused in Virtual mode; supported in Turbo column-store mode along with the rest of the aggregate-row workflow.
Example
VBA
grid.SetColumnAggregate 3, 1' Sum
grid.SetAggregateFormatString 3, "$%.2f"' The Amount footer now renders as e.g. "$19215.00" instead of a plain number.
Gotchas
At most one numeric conversion is allowed (%d, %i, %u, %o, %x, %X, %f, %F, %e, %E, %g, %G, optionally with flags/width/precision). String, character, pointer or count conversions, and a second numeric conversion, are all rejected outright — the aggregate value is always passed as a single number.
A format string with no numeric conversion at all (plain text, or only %% literals) is accepted rather than refused; the footer then shows that literal text unchanged on every recompute, and the computed aggregate value is never displayed.
An invalid format string is refused without disturbing whatever format string (or lack of one) the column had before the call.
See alsoSetColumnAggregate, ShowAggregateRow
ShowAggregateRowColumns
Show or hide the aggregate summary footer band.
Reveals or hides the footer row where each column's SetColumnAggregate summary is drawn, once at least one column has an aggregate configured. Reading the property works in every mode; setting it is refused specifically in Virtual mode.
In Virtual mode, reading ShowAggregateRow works, but setting it is refused — the one asymmetric case in this group where the getter and setter have different mode support.
Turning this on before any column has an aggregate configured is accepted but has nothing to show; the footer band only actually appears once SetColumnAggregate has set at least one column's mode to something other than None.
See alsoSetColumnAggregate, SetAggregateFormatString
Rows and cells
Rows enter and leave the grid through four members: AddRow appends a row at the end, InsertRow puts one at any display position (including the very end, which behaves exactly like AddRow), DeleteRow removes one row by its current display position, and DeleteSelectedRows removes every currently selected row in one call (a safe no-op if nothing is selected). All four take or use 0-based display indices -- the same index space GetCellValue, GetCell and every other member here shares. AddRow and InsertRow both take their values as a single 1-D array, one value per column in column order; extra values are ignored and a short array leaves the remaining columns blank, never an error, and the new row's cells are created for every existing column, not just the ones you happened to supply -- appending or inserting a row never leaves a column that can't hold a real value for that row the way adding a new COLUMN to already-loaded data can.
Cell access splits into two families that behave very differently and are easy to mix up. GetCellValue and SetCellValue always deal in text: GetCellValue returns whatever the cell displays as a String, and SetCellValue always stores its argument as text, no matter what type the column holds. In an Embedded-mode grid this means calling SetCellValue on a numeric column silently replaces that one cell's stored value with a String, so a later GetCell on that same cell returns a String where every other row in the column still returns a Double, Currency, or whatever the column's real type is -- nothing warns you this happened. GetCell and SetCell are the typed pair: GetCell returns a Variant carrying the column's own type (Double, Currency, Date, Boolean, and so on) and SetCell writes a Variant whose own VARTYPE decides how the value is stored. In a Turbo column-store grid, both members enforce this distinction directly: SetCellValue only accepts a value for a column whose Turbo column-store type is String and raises a runtime error naming SetCell as the alternative for every other column; SetCell requires the incoming VARIANT's VARTYPE to match the column's own declared type exactly (an Int32 column takes VT_I4/VT_I2/VT_UI1, a Double column takes VT_R8/VT_R4, a Currency column takes only VT_CY, a Date column only VT_DATE, a Boolean column only VT_BOOL, a String column only VT_BSTR) -- a mismatch, including a numeric type that merely isn't the exact one the column stores, is refused by name rather than silently converted. Neither SetCellValue nor SetCell fires a change event; those only fire from the interactive edit flow.
Mode support in this group splits cleanly. AddRow, InsertRow, DeleteRow, DeleteSelectedRows, GetCellValue, SetCellValue, GetCell, SetCell, SetCellColor, ClearCellColor, SetCellFontStyle, SetRowEnabled and GetDataArray all work in both Embedded and Turbo column-store mode, and are refused only in Virtual mode, where the host owns the row data. Virtual mode narrows GetCellValue and GetCell further still: a row that isn't currently held in the grid's cache raises a runtime error rather than answering with an empty or default value, so a caller must supply the row and retry rather than trust a suspiciously blank result. Four members in this group are, for now, Embedded-only, with no Turbo column-store equivalent at all: SetCellRange (a bulk typed write across a rectangular block), MoveRow (a programmatic row reorder), and CheckAllVisible/UncheckAllVisible (bulk checkbox toggles) all raise a runtime error if called against a Turbo column-store grid, exactly as they do in Virtual mode.
A handful of members describe a row rather than its cells. RowHeight and RowDensity are both single, global settings -- one height applies to every row in the grid, there is no per-row override -- and they share the same underlying pixel value: setting RowDensity recalculates and overwrites RowHeight's stored value from a DPI-scaled preset (Compact/Normal/Comfortable), while setting RowHeight directly leaves RowDensity's own reported value untouched, so the two can end up describing different things if you mix them. AllowRowReorder and MoveRow look related but only share a UI gesture: AllowRowReorder is a plain on/off switch that lets the user Alt+drag a row to a new position, while MoveRow is the separate, purely programmatic call that gesture ends up invoking -- turning AllowRowReorder on works in every mode, including Turbo column-store, but dragging a row in a Turbo column-store grid has no visible effect, because the drag gesture's own call to MoveRow is refused there. Finally, SetRowEnabled and ClearAllRowStates control only how a row paints (the theme's disabled background and text colors, and a dimmed icon tint) -- disabling a row does not block selecting it, editing it interactively, or writing to its cells with SetCellValue or SetCell; there is currently no enforcement behind the name.
RecipeFlag rows that fail a rule, then lock them from further changes
VBA
Sub FlagAndLockFailingRows(grid AsObject)
Dim r AsLongDim amount AsDouble
grid.Clear
grid.AddColumn "Region", 100
grid.AddColumn "Amount", 90
grid.AddRow Array("North", CDbl(120.5))
grid.AddRow Array("South", CDbl(9.99))
grid.AddRow Array("East", CDbl(455))
grid.AddRow Array("West", CDbl(3.25))
' Highlight and lock every row whose Amount is below a thresholdFor r = 0To grid.GetRowCount - 1
amount = grid.GetCell(r, 1)
If amount < 10Then
grid.SetCellColor r, 1, RGB(255, 220, 220), RGB(150, 0, 0)
grid.SetCellFontStyle r, 1, 1' Bold
grid.SetRowEnabled r, False' visual only -- see the note belowEndIfNext r
' ... later, once the data has been reviewed ...
grid.ClearAllRowStates ' every row goes back to Normal renderingEndSub
SetCellColor and SetCellFontStyle both take effect immediately and have no getters, so keep track of which cells you touched if you need to undo the highlight later on specific cells (ClearCellColor/SetCellFontStyle 0 do that one cell at a time). SetRowEnabled is included here for realism, but it is worth being explicit that it is cosmetic only: a 'disabled' row's cells can still be read and written by code, and the row can still be selected by the user -- it only changes the color the row paints with. If you need to actually prevent edits to a row, this group does not provide that; SetRowEnabled communicates it visually, nothing more. ClearAllRowStates resets every row's enabled flag in one call, in every data mode.
RecipeLet the user reorder a short list by dragging, then read back the final order
VBA
Sub ReorderableList(grid AsObject)
grid.Clear
grid.AddColumn "Priority Item", 200
grid.AddRow Array("Renew support contract")
grid.AddRow Array("Migrate reporting database")
grid.AddRow Array("Retire the old file share")
' Let the user Alt+drag a row's header to move it (Embedded mode only --' see the note below for what happens in a Turbo column-store grid)
grid.AllowRowReorder = True' A toolbar button offering the same move without dragging:' promote the currently focused row to the top of the list' grid.MoveRow someFocusedRow, 0' ... after the user has finished reordering (or clicked the button) ...Dim finalOrder AsVariantDim r AsLong
finalOrder = grid.GetDataArray
For r = LBound(finalOrder, 1) To UBound(finalOrder, 1)
Debug.Print r & ": " & finalOrder(r, LBound(finalOrder, 2))
Next r
EndSub
AllowRowReorder only arms the interactive Alt+drag gesture; MoveRow is the separate call that both the drag gesture and a 'move to top' button would use, and it works the same regardless of AllowRowReorder's current value. Both are Embedded-only in the sense that matters here: MoveRow itself is refused in a Turbo column-store grid, so turning AllowRowReorder on there compiles and reads back True, but dragging a row visibly does nothing, because the drag's own MoveRow call is refused. GetDataArray comes back as a 1-based 2D array (1 To rowCount, 1 To colCount), which is why the loop above starts at LBound rather than assuming 0 or 1.
RecipeBulk-load a block of values, then edit safely with the typed vs. text writers
VBA
Sub BulkEditRange(grid AsObject)
Dim block(0To1, 0To2) AsVariant
grid.Clear
grid.AddColumn "Region", 100
grid.AddColumn "Q1", 70
grid.AddColumn "Q2", 70
grid.AddColumn "Q3", 70
grid.AddRow Array("North", 0, 0, 0)
grid.AddRow Array("South", 0, 0, 0)
block(0, 0) = 100: block(0, 1) = 150: block(0, 2) = 200
block(1, 0) = 300: block(1, 1) = 350: block(1, 2) = 400' One call fills Q1..Q3 for both rows -- Embedded mode only
grid.SetCellRange 0, 1, block
' A single follow-up correction: SetCell keeps the value numeric
grid.SetCell 0, 2, CDbl(999)
' SetCellValue would instead overwrite that cell with the TEXT "999",' which is fine for display but no longer a number for later arithmetic:' grid.SetCellValue 0, 2, "999"Debug.Print grid.GetCell(0, 2) ' -> 999 (still a Double)Debug.Print grid.GetCellValue(1, 3) ' -> "400"EndSub
SetCellRange writes each array element with its own VARTYPE, exactly like SetCell -- values are not coerced to text the way SetCellValue coerces them. It has no Turbo column-store equivalent yet: calling it there raises a runtime error and writes nothing, so a Turbo column-store-mode caller has to fall back to a loop of individual SetCell calls for a bulk update. The array's own bounds decide its size; startRow/startCol only say where the array's own first element lands in the grid, so this works the same whether your VBA array is 0-based or 1-based.
AddRowRows and cells
Append a new row at the end of the grid, optionally with initial values.
Adds one row after the last existing row. values is a single 1-D array with one entry per column, in column order; extra entries beyond the column count are ignored and a shorter array leaves the remaining columns blank -- neither case is an error. The new row's cells are created for every existing column, not only the ones a short array supplies, so a column you didn't mention still holds a real (blank) value rather than one that silently can't be read or written. In a Turbo column-store grid, every supplied value is checked against its column's already-established type before anything is added: if one value doesn't match, the whole call raises a runtime error and no row is added at all, never a partially written one.
SignatureSub AddRow(values As Variant)
Parameters
values (Variant) - A 1-D array of values, one per column, in column order. Pass an empty array or Empty for an all-blank new row.
Refused in Virtual mode, where the host owns the row data. Supported in Turbo column-store mode, but each value's VARTYPE must match its column's own declared type (the same rule SetCell enforces) -- a mismatch refuses the entire call rather than adding a row with a wrong-typed cell.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = CDbl(90)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"
grid.AddRow Array("Bob", CDbl(75)) ' match the Score column's inferred type exactlyDebug.Print grid.GetRowCount ' -> 2Debug.Print grid.GetCellValue(1, 0) ' -> "Bob"
Gotchas
values is a single 1-D array (one value per column), not the 2-D array LoadFromArray expects -- passing a 2-D array raises a runtime error asking for LoadFromArray instead.
A short values array is not an error: the columns it doesn't cover are simply left blank.
In a Turbo column-store grid, a value whose VARTYPE doesn't match its column's declared type refuses the whole call before anything is added -- there is no partial add.
Unlike adding a new COLUMN to a grid that already has rows, adding a new ROW never leaves any existing column unable to hold a real value for it.
In Turbo column-store mode, a new row's values must match each column's already-inferred type exactly (a bare Integer literal like 75 is not the same VARTYPE as a Double column inferred from an earlier 90 loaded via LoadFromArray) - use CDbl()/CLng()/CStr() etc. to make the type explicit and match the column, rather than relying on VBA's default literal typing.
See alsoInsertRow, DeleteRow, SetCell, GetRowCount
InsertRowRows and cells
Insert a new row at a specific display position, optionally with initial values.
Inserts one row so that it becomes row rowIndex, shifting every row that was at or after that position down by one. rowIndex may equal the current row count, which inserts at the very end -- the same effect as AddRow. values follows the exact same rules AddRow's values does: a single 1-D array, one entry per column, extra entries ignored, a short array leaves the rest blank, and every existing column gets real (if blank) storage for the new row.
SignatureSub InsertRow(rowIndex As Long, values As Variant)
Parameters
rowIndex (Long) - 0-based display position for the new row; 0 inserts at the top, GetRowCount() inserts at the end.
values (Variant) - A 1-D array of values, one per column, in column order. Pass an empty array or Empty for an all-blank new row.
Refused in Virtual mode. Supported in Turbo column-store mode with the same per-column type check as AddRow's Turbo column-store behavior -- a mismatched value refuses the whole call.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Cara"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.InsertRow 1, Array("Bob") ' insert between Alice and CaraDebug.Print grid.GetCellValue(0, 0) ' -> "Alice"Debug.Print grid.GetCellValue(1, 0) ' -> "Bob"Debug.Print grid.GetCellValue(2, 0) ' -> "Cara"
Gotchas
rowIndex is a display position, not a storage position; if a sort is currently active in Embedded mode, the newly inserted row is immediately re-sorted into its correct place rather than staying at the index you requested.
rowIndex out of the 0..GetRowCount() range (inclusive of GetRowCount()) raises a runtime error.
Same 1-D array shape and same per-column type checking in Turbo column-store mode as AddRow -- see AddRow's own notes.
See alsoAddRow, DeleteRow, MoveRow
DeleteRowRows and cells
Delete one row by its current display position.
Removes the row currently at rowIndex; every later row shifts up by one to fill the gap. Fires the BeforeRowDelete event before the row is actually removed; a handler that sets Cancel to True leaves the grid completely unchanged and DeleteRow returns without error.
SignatureSub DeleteRow(rowIndex As Long)
Parameters
rowIndex (Long) - 0-based display position of the row to delete.
Refused in Virtual mode. Supported in Turbo column-store mode; the row is tombstoned in the Turbo column-store store rather than physically shifted, but the visible effect (the row disappears and later rows shift up) is the same.
rowIndex out of range raises a runtime error rather than doing nothing.
BeforeRowDelete is cancellable -- always check whether your own handler (if any) sets Cancel before assuming the row is gone.
The index is re-validated after BeforeRowDelete fires, since a handler could itself have changed the row count.
See alsoDeleteSelectedRows, AddRow, InsertRow
DeleteSelectedRowsRows and cells
Delete every currently selected row in one call.
Removes every row the selection model currently considers selected. If nothing is selected, this is a safe no-op: it succeeds and changes nothing. Fires BeforeRowDelete once per row actually deleted, with the same cancel semantics as DeleteRow -- a cancelled row is skipped and the rest continue to be processed.
Refused in Virtual mode. Supported in Turbo column-store mode, where it reaches every selected row regardless of an active filter, including rows the filter currently hides -- in Embedded mode it only reaches rows that are part of the current filtered view, so a selected row that a later filter hid is left behind.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Bob": data(2, 0) = "Cara"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SelectRow 0
grid.SelectRow 2
grid.DeleteSelectedRows ' removes Alice and CaraDebug.Print grid.GetRowCount ' -> 1Debug.Print grid.GetCellValue(0, 0) ' -> "Bob"
Gotchas
Calling this with nothing selected is safe and simply does nothing -- it is not an error.
In Embedded mode, a selected row that an active filter is currently hiding is NOT deleted (the sweep only walks the current filtered view); in a Turbo column-store grid, the same selected-but-hidden row IS deleted, because Turbo column-store's sweep walks every physical row regardless of the filter.
Each row's BeforeRowDelete can individually cancel that one row without stopping the rest from being deleted.
See alsoDeleteRow, SelectRow, GetSelectedRows
GetCellValueRows and cells
Read a cell's value as display text.
Returns the cell's value exactly as it would render on screen, as a String -- a numeric or currency cell comes back formatted, not as a number you can use in arithmetic. Use GetCell instead when you need the underlying typed value.
SignatureFunction GetCellValue(row As Long, col As Long) As String
Works in Embedded and Turbo column-store mode. In Virtual mode the grid only holds a cached window of the host's data: a row that is not currently resident in that cache raises a runtime error rather than returning an empty string standing in for an unknown value -- the host must supply the row and the caller must retry.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = 90
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"Debug.Print grid.GetCellValue(0, 0) ' -> "Alice"Debug.Print grid.GetCellValue(0, 1) ' -> "90" (always text, even for a numeric cell)
Gotchas
Always text -- even for a numeric, currency, date or boolean column. Use GetCell for the typed value.
A Null cell reads back as an empty string, indistinguishable from a genuinely blank cell.
An out-of-range row or column index raises a runtime error.
In Virtual mode, a non-resident row raises a runtime error rather than returning an empty string -- do not treat an error here as 'the cell is blank'.
See alsoGetCell, SetCellValue, SetCell
SetCellValueRows and cells
Write a cell's value as text, regardless of the column's own type.
Stores value as the cell's content. In Embedded mode this ALWAYS stores text: calling it on a numeric, currency, date or boolean column silently replaces that one cell's stored VARIANT with a String, so a later GetCell on that exact cell returns a String where every other row in the same column still returns the column's real type -- nothing warns you this happened, and the cell still displays correctly via GetCellValue either way. In a Turbo column-store grid this ambiguity cannot occur, because SetCellValue only accepts a value for a column whose Turbo column-store type is String; calling it on any other column raises a runtime error naming SetCell as the correct call instead. Use SetCell whenever the value must remain a genuine number, date or boolean for later use (sorting, arithmetic, export, and so on).
SignatureSub SetCellValue(row As Long, col As Long, value As String)
Refused in Virtual mode. In Turbo column-store mode, supported ONLY for a column whose stored type is String; calling it on any other Turbo column-store column raises "SetCellValue only accepts text on a Turbo column-store String column; use SetCell for other column types." and leaves the cell unchanged.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = 90
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"
grid.SetCellValue 0, 0, "Alicia"Debug.Print grid.GetCellValue(0, 0) ' -> "Alicia"' SetCellValue always writes text. On a Turbo column-store grid whose Score' column was inferred as numeric, this next line raises a runtime error:' "SetCellValue only accepts text on a Turbo column-store String column; use SetCell for' other column types." Use SetCell (above) to write a number that must stay' a number.' grid.SetCellValue 0, 1, "95"
Gotchas
In Embedded mode, SetCellValue always stores text, even onto a column that is otherwise entirely numeric, currency, date or boolean -- the affected cell's own stored type silently changes to String, and only that one cell.
In a Turbo column-store grid, calling SetCellValue on a non-String column raises "SetCellValue only accepts text on a Turbo column-store String column; use SetCell for other column types." -- the cell is left completely unchanged.
Never fires AfterCellEdit or any other change event -- only the interactive edit flow (double-click, F2, Tab) fires those.
Refused entirely in Virtual mode.
See alsoSetCell, GetCellValue, GetCell
GetCellRows and cells
Read a cell's value as a typed Variant.
Returns the cell's value as a Variant carrying the column's own type -- a Double for a numeric column, Currency for a Currency column, Date for a Date column, Boolean for a Boolean column, and so on -- rather than the display text GetCellValue returns. This is the call to use whenever the result needs to be compared, calculated with, or stored back with its type intact.
SignatureFunction GetCell(row As Long, col As Long) As Variant
Works in Embedded and Turbo column-store mode, reflecting whatever sorting, filtering, edits, inserts or deletes are currently in effect. In Virtual mode, GetCell does not check whether the requested row has actually been supplied by the host: for any row inside the declared row count it returns successfully with an empty Variant, whether or not that row's data has ever arrived. Use GetCellValue for Virtual-mode reads instead -- it is the one that checks residency and raises a runtime error for a row the host has not yet supplied.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = CDbl(90)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"Dim v AsVariant
v = grid.GetCell(0, 1)
Debug.Print VarType(v) ' -> 5 (vbDouble)Debug.Print v ' -> 90
Gotchas
Returns a genuinely typed Variant -- check VarType if your code needs to branch on the column's type.
In Virtual mode, GetCell never raises an error for a row that hasn't arrived yet, and it never returns the host-supplied value either -- it always comes back as an empty Variant, even for a row that has already been supplied. Use GetCellValue for Virtual-mode reads; it is the call that actually checks residency and raises a runtime error for a row that is not yet available.
An out-of-range row or column index raises a runtime error.
See alsoGetCellValue, SetCell, SetCellValue
SetCellRows and cells
Write a cell's value as a typed Variant.
Stores value, keeping its own type. In Embedded mode any VARTYPE is accepted as-is -- a cell's storage is loosely typed there, so writing a String into what was a numeric cell works exactly like SetCellValue would. In a Turbo column-store grid the incoming VARIANT's VARTYPE must match the column's own declared type exactly: an Int32 column accepts VT_I4, VT_I2 or VT_UI1; an Int64 column accepts VT_I8 or VT_I4; a Double column accepts VT_R8 or VT_R4; a Currency column accepts only VT_CY; a Date column accepts only VT_DATE; a String column accepts only VT_BSTR; a Boolean column accepts only VT_BOOL. Anything else -- including a numeric type that isn't the specific one the column stores, such as a Long written into a Double column -- is refused by name rather than converted. VT_NULL and VT_EMPTY are always accepted on any column and store as Null; COM object values (VT_DISPATCH/VT_UNKNOWN) are always refused, in every mode.
SignatureSub SetCell(row As Long, col As Long, value As Variant)
Parameters
row (Long) - 0-based display row index.
col (Long) - 0-based column index.
value (Variant) - The typed value to store. Its VARTYPE must match the column's own type in a Turbo column-store grid.
Refused in Virtual mode. Supported in Turbo column-store mode, but with the strict per-column VARTYPE matching described above -- a value of the wrong VARTYPE for that column is refused, never silently converted.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = CDbl(90)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"
grid.SetCell 0, 1, CDbl(95.5) ' a typed numeric write -- Score stays a numberDebug.Print grid.GetCell(0, 1) ' -> 95.5Debug.Print grid.GetCellValue(0, 1) ' -> "95.5" (the same value, as text)
Gotchas
In a Turbo column-store grid the VARTYPE must be exactly the one the column stores -- a Long into a Double column, or a Single into an Int32 column, is refused even though both are 'numeric'.
VT_NULL/VT_EMPTY always succeed, on any column, and store as Null.
COM object values (an object reference) are refused in every mode, not just Turbo column-store.
Never fires AfterCellEdit or any other change event, the same as SetCellValue.
Refused entirely in Virtual mode.
See alsoSetCellValue, GetCell, GetCellValue, CellValue
CellValueRows and cells
The two-dimensional array syntax, as a property.
CellValue(row, col) is GetCell and SetCell wearing VBA's own idiom. It is a parameterized property, so it reads and assigns like an array element:
This is syntax, not a second way for the grid to behave. Both directions are a single call to GetCell/SetCell, so every rule below is inherited rather than re-implemented — including the ones you might hope a friendlier name would relax. If GetCell refuses something, CellValue refuses it with the same error text.
SignatureProperty CellValue(row As Long, col As Long) As Variant
Parameters
row (Long) - a VISUAL row index, the same space every other row at this boundary uses.
col (Long) - a VISUAL column index (translated to the physical column internally in Turbo column-store, exactly as GetCell does).
Returnsthe cell's value as a Variant on read; assigns it on write.
Embedded accepts any VARTYPE. Turbo column-store enforces the column's stored type and refuses a mismatch by name — a Long into a Double column is refused even though both are "numeric". In Virtual mode a read of a row that is not currently cached returns LG_E_ROW_NOT_RESIDENT rather than an empty value standing in for an unknown one, and writes are refused because the host owns the data.
Example
VBA
' Fill a block the way you would fill an arrayDim r AsLong, c AsLongFor r = 0To grid.GetRowCount() - 1For c = 0To grid.GetColumnCount() - 1
grid.CellValue(r, c) = r & "," & c
Next c
Next r
Gotchas
Reading or writing through CellValue fires no event, exactly as GetCell/SetCell fire none. Programmatic writes are not edits.
It is not the default member, so grid(3, 2) does not work and is not planned. A default member changes how VB6/VBA resolves bare object references and could silently change the meaning of code that already exists.
There is deliberately no DataSource property on this control. If you are looking for one, you want LoadFromRecordset, QuickBindRecordset or RefreshFromRecordset - the grid loads from a Recordset but never binds to one, and naming a member DataSource would promise synchronisation that does not happen.
See alsoGetCell, SetCell, GetCellValue, SetCellValue
SetCellColorRows and cells
Override one cell's background and text color.
Sets a per-cell color override that takes effect immediately and persists through sorting, filtering and scrolling, because it is keyed to the row's own identity rather than its current display position.
SignatureSub SetCellColor(row As Long, col As Long, backColor As OLE_COLOR, foreColor As OLE_COLOR)
Parameters
row (Long) - 0-based display row index.
col (Long) - 0-based column index.
backColor (OLE_COLOR) - Background color, e.g. RGB(255, 255, 0).
There is no getter for a cell's color override -- keep track of what you set if your code needs to know it later.
The override is keyed to the row's identity, so it survives a sort or filter that changes the row's display position.
The background is painted exactly as given, but foreColor is checked against it and replaced on screen with a more readable color if it would be unreadable.
Wins over a column color from SetColumnBackColor or SetColumnForeColor in the same cell; a conditional-format rule and a selection both paint over it. On the hovered row the cell's background is tinted toward the hover color.
An out-of-range row or column index raises a runtime error.
Refused entirely in Virtual mode.
See alsoClearCellColor, SetCellFontStyle, SetColumnBackColor
ClearCellColorRows and cells
Remove a cell's color override, if any.
Removes any color override SetCellColor previously applied to this cell, returning it to the grid's normal coloring. Calling it on a cell that never had an override is a safe no-op.
SignatureSub ClearCellColor(row As Long, col As Long)
Clearing a cell that never had a color override is a safe no-op, not an error.
An out-of-range row or column index is handled differently by mode: Embedded mode raises a runtime error (the same as SetCellColor), but in a Turbo column-store grid it is silently ignored -- the call returns without error and nothing changes.
Refused entirely in Virtual mode.
See alsoSetCellColor
SetCellFontStyleRows and cells
Override one cell's font style (bold/italic).
Sets or clears a per-cell font-style override. Passing style 0 (Normal) clears any override on that cell -- there is no separate clear method.
SignatureSub SetCellFontStyle(row As Long, col As Long, style As Long)
Parameters
row (Long) - 0-based display row index.
col (Long) - 0-based column index.
style (Long) - 0-3; see the value table.
ReturnsNone.
Value
Meaning
0
Normal (also clears any existing override on this cell).
Refused in Virtual mode. Supported in Turbo column-store mode.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Alice"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetCellFontStyle 0, 0, 1' Bold' ... later, back to normal ...
grid.SetCellFontStyle 0, 0, 0
Gotchas
style 0 clears the override rather than raising an error -- there is no ClearCellFontStyle.
There is no getter for a cell's current font-style override.
A style value outside 0-3 raises a runtime error.
Refused entirely in Virtual mode.
See alsoSetCellColor
SetCellRangeRows and cells
Write a rectangular block of cells from a 2-D array in one call.
Writes data, a 2-D Variant array, into the grid starting at (startRow, startCol): data's own [LBound,LBound] corner lands at (startRow, startCol), and it extends across as many rows and columns as the array itself has, in either direction. Each value keeps its own VARTYPE, exactly like SetCell -- values are not coerced to text the way SetCellValue coerces them. The whole range is validated before anything is written: if it would run past the last existing row or column, the entire call is refused and nothing changes.
SignatureSub SetCellRange(startRow As Long, startCol As Long, data As Variant)
Parameters
startRow (Long) - 0-based display row where data's first row lands.
startCol (Long) - 0-based column where data's first column lands.
data (Variant) - A 2-D Variant array of values to write.
Refused in both Virtual mode and Turbo column-store mode -- there is currently no Turbo column-store equivalent of a bulk typed range write; use a loop of SetCell calls there instead.
Unlike SetCell and SetCellValue, this member has no Turbo column-store implementation at all: it is refused there exactly as it is in Virtual mode.
data's own array bounds (0-based, 1-based, or anything else) decide its size -- only its first element's position is anchored to (startRow, startCol), so a 1-based VBA array works the same as a 0-based one.
A range that would extend past the last existing row or column is refused entirely, and nothing in the range is written -- there is no partial write.
Fires DataChanged once for the whole call, unlike SetCell and SetCellValue, which fire nothing.
See alsoSetCell, SetCellValue, GetDataArray
AllowRowReorderRows and cells
Let the user drag a row's header to reorder it.
Arms an interactive gesture: when True, holding Alt and dragging a row's row header moves that row to a new position. This property only controls whether the drag gesture is available -- it has no effect on MoveRow, the separate programmatic call the drag gesture itself ends up invoking. The property can be read and set in every data mode, since until a drag actually starts it is nothing more than a stored preference bit.
SignatureProperty AllowRowReorder As Boolean
ReturnsTrue if the drag-to-reorder gesture is currently enabled.
Works in every mode, since setting it only stores a preference bit rather than touching row data. The gesture it arms, however, ends in a call to MoveRow, which is itself refused in Turbo column-store mode -- see MoveRow's own notes.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Bob"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.AllowRowReorder = True' lets the user Alt+drag a row to a new positionDebug.Print grid.AllowRowReorder ' -> True
Gotchas
Setting this to True in a Turbo column-store grid succeeds and reads back True, but dragging a row there has no visible effect, because the drag gesture's own underlying MoveRow call is refused in Turbo column-store mode.
This only enables the drag gesture -- MoveRow itself works regardless of this property's value.
See alsoMoveRow
MoveRowRows and cells
Programmatically move a row from one display position to another.
Removes the row currently at fromDisplayIndex and reinserts it at toDisplayIndex, shifting every row between the two positions by one -- a move, not a swap with whatever previously occupied toDisplayIndex. Works regardless of AllowRowReorder's current value; that property only enables the interactive drag gesture, not this call.
SignatureSub MoveRow(fromDisplayIndex As Long, toDisplayIndex As Long)
Parameters
fromDisplayIndex (Long) - 0-based current display position of the row to move.
toDisplayIndex (Long) - 0-based display position the row should end up at.
Refused in both Virtual mode and Turbo column-store mode -- there is currently no Turbo column-store equivalent for a programmatic row reorder.
Example
VBA
grid.Clear
grid.AddColumn "Name", 120
grid.AddRow Array("Alice")
grid.AddRow Array("Bob")
grid.AddRow Array("Cara")
grid.MoveRow 2, 0' promote Cara to the topDebug.Print grid.GetCellValue(0, 0) ' -> "Cara"Debug.Print grid.GetCellValue(1, 0) ' -> "Alice"
Gotchas
Refused in a Turbo column-store grid exactly as in Virtual mode, even though AllowRowReorder itself can be turned on there -- see AllowRowReorder's own notes.
Both indices are display positions, not row identities; every row strictly between fromDisplayIndex and toDisplayIndex shifts by one to make room.
Calling it with fromDisplayIndex equal to toDisplayIndex is a safe no-op.
An out-of-range index (either parameter) raises a runtime error.
See alsoAllowRowReorder, GetDataArray
SetRowEnabledRows and cells
Mark a row as enabled or disabled for rendering purposes.
Sets whether a row paints using its normal colors or a disabled appearance: the theme's disabled background (the Surface_Disabled role), its subdued disabled text color (Text_Disabled), and a dimmed icon tint. This is purely a rendering flag: it does not block selecting the row, does not block interactive editing of the row's cells, and does not block a direct SetCellValue or SetCell call against that row -- there is currently no enforcement behind the name, only the visual cue.
SignatureSub SetRowEnabled(row As Long, enabled As Boolean)
Parameters
row (Long) - 0-based display row index.
enabled (Boolean) - False renders the row as disabled; True renders it normally.
Refused in Virtual mode. Supported in Turbo column-store mode.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Bob"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetRowEnabled 1, False' Bob's row renders as disabled' Note: this only changes how the row paints -- Bob's cells can still be' read and written normally.Debug.Print grid.GetCellValue(1, 0) ' -> "Bob"
Gotchas
This only changes how the row paints. It does NOT block selection, interactive editing, or a direct SetCellValue/SetCell call on that row's cells -- treat it as a visual cue for the user, not an access control.
The disabled colors come from the theme; change them with ApplyThemeJson (Surface_Disabled and Text_Disabled). Disabled text is deliberately subdued and is not raised to full reading contrast. A selected disabled row paints with the selection colors.
There is no getter for a row's current enabled state.
An out-of-range row index raises a runtime error.
Refused entirely in Virtual mode.
See alsoClearAllRowStates
ClearAllRowStatesRows and cells
Reset every row's enabled/disabled flag to Normal in one call.
Clears whatever SetRowEnabled has set on every row, in one call, so every row renders as Normal again. Unlike most mutators in this group, it is not refused in Virtual mode -- it is mode-agnostic.
Works in every data mode, including Virtual, since it only clears the grid's own rendering flags rather than touching row data.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Bob"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetRowEnabled 0, False
grid.SetRowEnabled 1, False
grid.ClearAllRowStates ' both rows render as Normal again
Gotchas
There is no way to read a single row's enabled state back from VBA -- SetRowEnabled has no matching getter, and this method does not report which rows it changed.
Safe to call even when no row is currently disabled.
See alsoSetRowEnabled
CheckAllVisibleRows and cells
Documented to set every currently visible row's value in a column to True - currently has no observable effect.
The API accepts colIndex and returns success, and is documented (by its own name and IDL helpstring) to write a Boolean True into every row currently in the grid's filtered view order. In real testing against the live control this write was not observed to happen: reading the same cells back afterward with GetCellValue or GetCell shows them unchanged, whether the grid is in its default (unsorted) view or a genuinely re-ordered sorted view. Treat this as a currently non-functional member rather than a working bulk-check operation.
SignatureSub CheckAllVisible(colIndex As Long)
Parameters
colIndex (Long) - 0-based column index whose visible cells should be set to True.
Refused in both Virtual mode and Turbo column-store mode -- currently an Embedded-only call.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = False
data(1, 0) = "Bob": data(1, 1) = False
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Selected"
grid.CheckAllVisible 1' As of this build, CheckAllVisible does not actually change any cell's' value (verified against the live control) - the line below still prints' "False" for both rows. Loop the rows and call SetCell yourself instead:Dim r AsLongFor r = 0To grid.GetRowCount - 1
grid.SetCell r, 1, TrueNext r
Debug.Print grid.GetCellValue(0, 1) ' -> "True"Debug.Print grid.GetCellValue(1, 1) ' -> "True"
Gotchas
Verified against the live control (both with the grid's default identity view and with a real sort applied, forcing a genuine reordered view): this call currently has NO observable effect on any cell. GetCellValue, GetCell and the underlying value all remain exactly as they were before the call, for every row tested, in every mode where the call is accepted without error. Do not rely on this member to actually change cell values; if you need to write a Boolean value into every visible row of a column today, loop the rows yourself and call SetCell (row, colIndex, True/False) for each one instead.
'Visible' is documented to mean every row in the grid's current filtered view order, not literally scrolled into the viewport - but this cannot currently be observed to matter, since no cell value change was observed at all.
Refused in both Virtual mode and Turbo column-store mode; in Embedded mode it returns success without raising an error, which makes the lack of any real effect easy to miss.
An out-of-range colIndex still raises a runtime error, so that part of its contract does work.
See alsoUncheckAllVisible
UncheckAllVisibleRows and cells
Documented to set every currently visible row's value in a column to False - currently has no observable effect.
The mirror image of CheckAllVisible: the API accepts colIndex and returns success, documented to write a Boolean False into every row currently in the grid's filtered view order. In real testing against the live control this write was not observed to happen either, in the same way and for the same reason as CheckAllVisible - reading the cells back afterward shows them unchanged.
SignatureSub UncheckAllVisible(colIndex As Long)
Parameters
colIndex (Long) - 0-based column index whose visible cells should be set to False.
Refused in both Virtual mode and Turbo column-store mode -- currently an Embedded-only call, exactly like CheckAllVisible.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = True
data(1, 0) = "Bob": data(1, 1) = True
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Selected"
grid.UncheckAllVisible 1' As of this build, UncheckAllVisible does not actually change any cell's' value (verified against the live control) - the line below still prints' "True" for both rows. Loop the rows and call SetCell yourself instead:Dim r AsLongFor r = 0To grid.GetRowCount - 1
grid.SetCell r, 1, FalseNext r
Debug.Print grid.GetCellValue(0, 1) ' -> "False"Debug.Print grid.GetCellValue(1, 1) ' -> "False"
Gotchas
Verified against the live control (both with the grid's default identity view and with a real sort applied, forcing a genuine reordered view): this call currently has NO observable effect on any cell. GetCellValue, GetCell and the underlying value all remain exactly as they were before the call, for every row tested, in every mode where the call is accepted without error. Do not rely on this member to actually change cell values; if you need to write a Boolean value into every visible row of a column today, loop the rows yourself and call SetCell (row, colIndex, True/False) for each one instead.
'Visible' is documented to mean every row in the grid's current filtered view order, not literally scrolled into the viewport - but this cannot currently be observed to matter, since no cell value change was observed at all.
Refused in both Virtual mode and Turbo column-store mode; in Embedded mode it returns success without raising an error, which makes the lack of any real effect easy to miss.
An out-of-range colIndex still raises a runtime error, so that part of its contract does work.
See alsoCheckAllVisible
GetDataArrayRows and cells
Read the entire grid into a 2-D Variant array in one call.
Returns every column (including hidden ones, not just the ones currently visible on screen) for every row, as a 2-D Variant array dimensioned (1 To rowCount, 1 To colCount), addressed data(row, col). Rows follow the grid's current view order -- reflecting any active sort or filter -- rather than raw load order. This is a different convention from LoadFromArray's own input array, which is 0-based and carries no header row, so do not assume you can feed GetDataArray's result straight back into LoadFromArray without adjusting the bounds. On a completely empty grid (no rows or no columns) it returns an Empty Variant rather than a zero-length array.
SignatureFunction GetDataArray() As Variant
ReturnsA 1-based 2-D Variant array (1 To rowCount, 1 To colCount), or an Empty Variant if the grid has no rows or no columns.
Refused in Virtual mode, since it would otherwise have to force the host to materialize its entire dataset. Supported in both Embedded and Turbo column-store mode, reading through the same live view (sort/filter/edit/insert/delete all reflected) every other reader in this group uses.
The array is 1-based (1 To rowCount, 1 To colCount) -- do not assume 0-based indices the way you would with LoadFromArray's input array.
Includes every column, including hidden ones -- not only the ones currently visible on screen.
Rows follow the current view order (after any active sort or filter), not raw storage order.
An empty grid returns an Empty Variant, not a zero-length array -- check with IsArray or VarType before indexing the result.
Refused entirely in Virtual mode.
See alsoGetCell, GetCellValue, LoadFromArray
RowHeightRows and cells
The pixel height applied to every row in the grid.
RowHeight is the DEFAULT height every row starts at. Individual rows can override it with SetRowHeight (Embedded mode only) -- and because setting RowHeight rebuilds every row at the new default, it CLEARS any per-row overrides that were in force, so set the global first and the individual rows afterward. Values are clamped to 16-100 pixels; a value outside that range is silently pulled back to the nearest end rather than rejected. RowHeight and RowDensity share the same underlying pixel value: setting RowDensity recalculates and overwrites RowHeight's value from a DPI-scaled preset, but setting RowHeight directly does not change RowDensity's own stored value, so the two can end up describing different things if you mix them.
SignatureProperty RowHeight As Long
ReturnsThe current row height in pixels.
Default20 pixels (constant) at 100% display scaling (scaled proportionally at higher DPI). This is a separate constant from RowDensity's own Normal preset (22 pixels) -- the default was never run through RowDensity's own calculation, so the two happen to differ until RowDensity is set explicitly.
Setting this property CLEARS every per-row override set with SetRowHeight: it rebuilds all rows at the new default. Set RowHeight (or RowDensity) first, then the individual rows -- never the other way round.
Clamped to 16-100 pixels rather than rejected when out of range.
Setting RowDensity overwrites RowHeight's stored value; setting RowHeight afterward leaves RowDensity reporting whatever density you last set even though the actual height no longer matches that density's preset.
The default height (20px) does not equal RowDensity's own Normal preset height (22px) -- they come from two different constants until you call RowDensity's setter yourself.
See alsoRowDensity, SetRowHeight, GetRowHeight
RowDensityRows and cells
Apply a Compact/Normal/Comfortable row-height preset to the whole grid.
Sets RowHeight from one of three DPI-scaled presets in a single call, applied to every row in the grid. Setting RowDensity immediately recalculates and overwrites RowHeight's underlying pixel value; RowHeight's own setter does not update RowDensity's stored value in return, so the two can disagree if you set RowHeight directly afterward.
SignatureProperty RowDensity As Long
ReturnsThe current density value (0, 1 or 2).
Default1 (Normal) as the reported preference, though the actual row height starts from a separate default constant (see RowHeight's own notes) until RowDensity's setter is called explicitly.
Works in every data mode; row density is presentation state, not row data.
Example
VBA
grid.RowDensity = 2' ComfortableDebug.Print grid.RowDensity ' -> 2Debug.Print grid.RowHeight ' -> a taller row height than the Compact preset
Gotchas
Only 0, 1 or 2 are accepted; anything else raises a runtime error and leaves the density unchanged.
Setting this immediately recalculates RowHeight's own pixel value from a DPI-scaled preset -- reading RowHeight right afterward reflects the preset, not whatever value it held before.
The reported default (1, Normal) does not by itself guarantee the row height equals the Normal preset's own pixel value -- see RowHeight's own notes for why the two can start out slightly different.
See alsoRowHeight, SetRowHeight
SetRowHeightRows and cells
Give one individual row its own height, overriding the grid-wide RowHeight.
The override belongs to the ROW'S RECORD, not to a screen position: sort or filter the grid afterwards and the tall row is still the same tall row, wherever it has moved to. Pass a height of 0 to remove the override and put the row back on the grid-wide default. Heights are clamped to the same 16-100 pixel range RowHeight uses -- except 0, which is not clamped because it is how you clear an override.
Setting the grid-wide RowHeight or RowDensity afterwards CLEARS every per-row override, because both rebuild all rows at the new default. Set the global first, the individual rows second.
SignatureSub SetRowHeight(row As Long, height As Long)
Parametersrow -- a VISUAL row index (the position the row currently occupies on screen, matching every other row-taking member). height -- 16 to 100 pixels, or 0 to clear the override.
EmbeddedsupportedVirtualrefused by nameTurbo column-storerefused by name
Turbo column-store has no per-row object to carry the override -- the whole point of the mode is that rows are not objects -- and in Virtual mode the host owns the rows. Both raise a runtime error naming the mode rather than silently ignoring the call. It is also refused while row grouping or the tree view is active, because those projections insert header rows that have no record behind them.
Example
VBA
grid.RowHeight = 20' set the global default FIRST
grid.SetRowHeight 2, 44' row 2 is now taller than its neighbours
grid.Sort 0, 1' the same RECORD is still the tall one
grid.SetRowHeight 2, 0' clear the override -> back to 20
Gotchas
Setting RowHeight or RowDensity afterwards wipes every override -- this is the single most common surprise. Global first, per-row second.
height = 0 clears; it is not clamped up to 16.
The override travels with the record, so after a sort the row index you passed is no longer the row that is tall -- look the record up again rather than reusing the old index.
Refused in Turbo column-store and Virtual, and while grouping or the tree view is active.
Each call rebuilds the grid's row-position table, which is one pass over the rows. Setting heights on many rows one call at a time is therefore slow on a large grid, and dramatically so once the grid is large - wrap the loop in BeginUpdate / EndUpdate and the rebuild happens once, at EndUpdate, instead of once per call.
See alsoGetRowHeight, RowHeight, RowDensity
GetRowHeightRows and cells
Read one row's current height in pixels.
Returns that row's own override if it has one, otherwise the height every other row is using. Like SetRowHeight, row is a VISUAL row index and the value resolves through the row's record, so it stays correct across sorting and filtering.
SignatureFunction GetRowHeight(row As Long) As Long
ReturnsThe row's height in pixels.
EmbeddedsupportedVirtualrefused by nameTurbo column-storerefused by name
Refused in Turbo column-store and Virtual, matching SetRowHeight -- use the global RowHeight property there.
An out-of-range row index raises a runtime error rather than returning 0.
See alsoSetRowHeight, RowHeight
Row identity and change tracking
The grid never opens your database. It loads records, the user edits them, and you decide what to save — which leaves exactly one question the grid has to help with: which records changed? A row index cannot answer it, because the user can sort or filter the grid after editing it and every index then means something else.
This group answers it. A key is whatever your application already calls a record, and it belongs to the RECORD rather than to a screen position: it survives sorting, filtering, grouping, insertion and deletion. KeyColumn is the one-line way to key an entire result set; SetRowKey is the per-row way. TrackChanges then records what happened, and GetChangedRows/GetInsertedRows/GetDeletedRows report it entirely in keys — never in row numbers.
Keys must be unique, must all belong to one type family, and cannot be Empty, Null or blank. That is stricter than GetRowSelectionState's own keyColumn parameter, which tolerates duplicates deliberately: matching several rows is a legitimate outcome for SELECTION, but here a key ends up in your UPDATE ... WHERE key = ? and a duplicate would silently write the wrong record. The column coordinates and the type rules are the same as that member's; the tolerance for duplicates is not.
Change tracking is off by default and free when off. Everything in this group is supported in Embedded and Turbo column-store mode; Virtual mode refuses every member by name, because there the host owns the data and therefore already owns identity — a key the grid assigned over a window it does not own would go stale as soon as the host scrolled.
See Part I chapter 21 for the complete CRUD round trip, including what to do about AutoNumber keys that do not exist until the INSERT returns.
SetRowKeyRow identity and change tracking
Give one record a stable key.
row is a VISUAL row index; the key binds to the record currently at that position. Assigning a record the key it already has succeeds and changes nothing — which is what makes replacing a temporary key with a database-generated one safe to retry.
SignatureSub SetRowKey(row As Long, key As Variant)
EmbeddedsupportedVirtualrefused by nameTurbo column-storesupported
Refuses a key another record already owns — keys are persistence identity, and a duplicate would make FindRowByKey ambiguous and let a write reach the wrong record.
Refuses Empty, Null, an empty string, NaN, Infinity, Decimal and objects: none of them can be looked up again.
Every key in one grid must be the same kind of value. Mixing text and numeric keys is refused.
A reload or Clear discards every key.
See alsoGetRowKey, FindRowByKey, KeyColumn
GetRowKeyRow identity and change tracking
Return the key of the record at a visual row index, or Empty if it has none.
SignatureFunction GetRowKey(row As Long) As Variant
EmbeddedsupportedVirtualrefused by nameTurbo column-storesupported
Gotchas
An unkeyed row returns Empty rather than raising an error — use IsEmpty on the result.
An out-of-range row index does raise an error.
See alsoSetRowKey, KeyColumn
FindRowByKeyRow identity and change tracking
Return the VISUAL row index of the record carrying a key, or -1.
SignatureFunction FindRowByKey(key As Variant) As Long
EmbeddedsupportedVirtualrefused by nameTurbo column-storesupported
Gotchas
Returns -1 both for a key that does not exist and for a record currently hidden by a filter. Use RowKeyExists to tell those apart — the difference matters when you are deciding between INSERT and UPDATE.
Because keys are unique there is never more than one candidate, so there is no "first match" rule.
See alsoRowKeyExists, SetRowKey
RowKeyExistsRow identity and change tracking
True if a record with this key is loaded, whether or not it is currently visible.
SignatureFunction RowKeyExists(key As Variant) As Boolean
EmbeddedsupportedVirtualrefused by nameTurbo column-storesupported
See alsoFindRowByKey
KeyColumnRow identity and change tracking
The visual column index whose cell value is each record's business key, or -1 for none.
Setting it keys every row already loaded and also every subsequent load, so both of these work:
EmbeddedsupportedVirtualrefused by name (settable); readable in every modeTurbo column-storesupported
Gotchas
The build is all-or-nothing. If the column contains a duplicate, a blank, a value that cannot be a durable identity, or values of more than one type, the whole assignment refuses — naming the offending value and how many rows carry it — and KeyColumn and every existing key are left exactly as they were.
A key set explicitly with SetRowKey is never overwritten by a column build. Setting KeyColumn back to -1 removes the column-derived keys and leaves the explicit ones.
On a one-million-row grid the build costs roughly ten times a single sort of that column. Set it once, not in a loop.
See alsoSetRowKey, TrackChanges
TrackChangesRow identity and change tracking
Whether the grid is recording which records were modified, inserted and deleted.
SignatureProperty TrackChanges As Boolean
DefaultFalse.
EmbeddedsupportedVirtualrefused by nameTurbo column-storesupported
Gotchas
Set KeyColumn (or call SetRowKey) first. A change to an unkeyed record cannot be reported under any identity you would recognise.
Turning it on records from that moment: edits made earlier are not recovered, because their original values are gone.
Turning it off discards the change set, exactly as AcceptChanges does.
See alsoRowState, GetChangedRows, AcceptChanges
RowStateRow identity and change tracking
What has happened to the record at a visual row index: 0 Unchanged, 1 Modified, 2 Inserted.
SignatureFunction RowState(row As Long) As Long
Gotchas
A deleted record has no visual row, so it is not reachable here — use GetDeletedRows.
Always 0 while TrackChanges is False.
See alsoTrackChanges, GetDeletedRows
GetChangedRowsRow identity and change tracking
Every edited cell since tracking began, as a two-dimensional array with one row per changed cell and five columns: key, columnIndex, columnName, oldValue, newValue.
SignatureFunction GetChangedRows() As Variant
Gotchas
Returns an empty Variant when nothing has changed — check with IsArray before indexing.
Editing the same cell twice still produces ONE entry, whose oldValue is the value from when tracking began, not the previous edit.
Records that were inserted appear in GetInsertedRows instead, not here.
See alsoGetInsertedRows, GetDeletedRows
GetInsertedRowsRow identity and change tracking
Every record inserted since tracking began: first column the key, remaining columns the record's current cell values.
SignatureFunction GetInsertedRows() As Variant
Gotchas
Returns an empty Variant when none.
A record that was inserted and then deleted again appears in neither this list nor GetDeletedRows — your database never saw it.
An inserted record with no key yet is reported with an Empty key. Give it a temporary key with SetRowKey if you need to loop them.
See alsoGetDeletedRows, SetRowKey
GetDeletedRowsRow identity and change tracking
The keys of every record deleted since tracking began, as a one-dimensional array.
SignatureFunction GetDeletedRows() As Variant
Gotchas
Keys only. A deleted record has no row index, so an index would be dead on arrival.
A record deleted before it was ever keyed cannot be reported and is not included.
See alsoGetChangedRows, RejectChanges
AcceptChangesRow identity and change tracking
Clear the change set and reset every record to Unchanged.
SignatureSub AcceptChanges()
Gotchas
Grid state only. It writes nothing to any database and knows nothing about one. Call it after your own save has succeeded.
It alters no cell values and removes no rows.
See alsoRejectChanges, TrackChanges
RejectChangesRow identity and change tracking
Undo every tracked change: edited cells go back to the values they had when tracking began, inserted records are removed, deleted records are restored. Then the change set is cleared.
SignatureSub RejectChanges()
EmbeddedsupportedVirtualrefused by nameTurbo column-storepartial — refuses if a record was deleted
Gotchas
In Turbo column-store mode a deleted record cannot be restored this way. If any record has been deleted the whole call refuses by name and nothing is changed — use Undo there, or AcceptChanges if the deletions were intended.
Restored records are appended rather than put back at their original position.
See alsoAcceptChanges, Undo
Selection and navigation
Selection in this group splits into three independent mechanisms that are easy to conflate: row selection (SelectRow, SelectAll, ClearSelection, GetSelectedRows, GetSelectedRow, IsRowSelected), a rectangular block/cell-range selection (SelectBlock, GetBlockSelection), and two purely visual preferences (MultiSelect, SelectionMode) that only change what an interactive mouse click does and how the current selection paints -- neither one changes what the programmatic row-selection API does. SelectRow is purely additive: it never toggles a row off, and there is no per-row Deselect method anywhere in the public API -- the only way to remove specific rows from the selection is to call ClearSelection and then SelectRow only the ones you still want selected. Selection state is tracked by each row's own stable identity rather than its current display position, so sorting or filtering after a SelectRow call does not silently drop the selection; GetSelectedRows, GetSelectedRow and IsRowSelected simply report the same physical rows under whatever display index they occupy after the reorder. GetSelectedRow always returns the LOWEST currently-selected display index, not the most recently selected one, and GetSelectedRows always returns its array sorted in ascending display order, regardless of the order the rows were selected in.
MultiSelect and SelectionMode are two orthogonal preference bits, both stored the same way in every data mode: MultiSelect only decides whether an interactive Shift+Click or Ctrl+Click extends the selection instead of replacing it; SelectionMode (Row/Cell/Column) decides what a plain click targets and, just as importantly, decides whether the paint routine draws a row highlight at all -- a row added to the selection set by SelectRow while SelectionMode is Cell or Column is genuinely selected (IsRowSelected and GetSelectedRows both report it) but paints with no visible highlight at all until SelectionMode goes back to Row (0). Neither property has any effect on SelectRow, SelectAll, ClearSelection, GetSelectedRows, GetSelectedRow or IsRowSelected, which always operate in row terms no matter what SelectionMode is currently set to.
SelectBlock and GetBlockSelection describe a separate rectangular cell range, tracked through the grid's own focus/anchor state rather than the row-selection set, and are implemented in Embedded mode only -- both raise a runtime error naming block selection as unsupported in a Turbo column-store grid. Calling SelectBlock always clears the current row selection first and then row-selects every row the block spans (so GetSelectedRows and IsRowSelected still see those rows as selected), but ClearSelection only undoes half of that: it empties the row-selection set without clearing an active block, so GetBlockSelection keeps reporting hasBlock = True with the same coordinates until SelectBlock defines a new block or the user makes a plain interactive click elsewhere in the grid. SelectBlock's own range validation also has a real gap worth knowing about: it checks that rowStart and colStart are not negative and that rowEnd and colEnd are within range, but it never checks that rowStart/colStart are themselves within range, nor that rowEnd/colEnd are non-negative -- a call like SelectBlock(GetRowCount(), 0, 0, 0) is accepted without error even though its first argument is clearly out of range, and GetBlockSelection will report that same out-of-range value straight back.
HoveredRow, ScrollToView, WheelScrollRows and MatchEntry round out the group as navigation and discovery aids -- all mode-agnostic presentation state rather than row data. HoveredRow is a read-only live reflection of the mouse position (-1 when nothing is hovered). ScrollToView scrolls the minimum distance needed to bring a view row into the visible band and does nothing if the row is already fully on screen. WheelScrollRows controls how many rows a single mouse-wheel notch moves (-1 automatic, 0 disabled, or an exact row count). MatchEntry controls type-ahead: typing a printable character while the grid has focus jumps to the next row (searching forward from the row after the current focus, wrapping around) whose value in the focused column starts with what has been typed; Off disables this entirely, FirstLetter resets the search buffer on every keystroke (so repeating a letter cycles through every row starting with it), and Complete (the default) accumulates consecutive keystrokes into one growing prefix as long as they arrive within a second of each other. Finally, SelectionBackColor and SelectionForeColor are shared by every highlight this group can produce -- plain row selection, an active SelectBlock range, and the cell/column highlight SelectionMode produces in Cell or Column mode all draw with the same two colors; there is no separate color for block selection or for the Cell/Column mode highlight.
RecipeSnapshot a selection by identity, reload the grid, and restore it
VBA
Sub SnapshotAndRestoreSelectionByIdentity(grid AsObject)
Dim data(0To4, 0To1) AsVariantDim r AsLong, n AsLong
data(0, 0) = CLng(101): data(0, 1) = "Alpha"
data(1, 0) = CLng(102): data(1, 1) = "Bravo"
data(2, 0) = CLng(103): data(2, 1) = "Charlie"
data(3, 0) = CLng(104): data(3, 1) = "Delta"
data(4, 0) = CLng(105): data(4, 1) = "Echo"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Id"
grid.SetColumnHeader 1, "Name"
grid.SelectRow 1' Bravo
grid.SelectRow 3' Delta' Display indices do not survive a reload, so capture the Id column insteadDim rows AsVariant
rows = grid.GetSelectedRows
Dim selectedIds() AsLongReDim selectedIds(LBound(rows) To UBound(rows))
For r = LBound(rows) To UBound(rows)
selectedIds(r) = CLng(grid.GetCellValue(rows(r), 0))
Next r
' ... reload the grid with fresh (possibly reordered) data ...
grid.LoadFromArray data
grid.SetColumnHeader 0, "Id"
grid.SetColumnHeader 1, "Name"' Restore the same rows by matching Id, not by their old display index
grid.ClearSelection
For r = 0To grid.GetRowCount - 1For n = LBound(selectedIds) To UBound(selectedIds)
If CLng(grid.GetCellValue(r, 0)) = selectedIds(n) Then
grid.SelectRow r
ExitForEndIfNext n
Next r
Dim finalRows AsVariant
finalRows = grid.GetSelectedRows
Debug.Print finalRows(LBound(finalRows)) ' -> 1 (Bravo is selected again)EndSub
GetSelectedRows and GetSelectedRow both report DISPLAY indices, which are only meaningful against the current view -- a reload, sort, insert or delete can change which row occupies a given index, or remove that index's row entirely. Capturing a stable business key (here, the Id column) rather than the raw index, and re-selecting by matching that key after the reload, is the pattern to use whenever a selection needs to survive anything that could reshuffle the view. SelectRow's additive behavior is what makes the restore loop simple: there is no need to clear between each match, only once before the loop starts.
RecipeOffer a rectangular copy region that is clearly not the same thing as row selection
VBA
Sub HighlightACopyRegionSeparateFromRowSelection(grid AsObject)
Dim data(0To3, 0To2) AsVariant
data(0, 0) = "North": data(0, 1) = CDbl(120): data(0, 2) = CDbl(15)
data(1, 0) = "South": data(1, 1) = CDbl(95): data(1, 2) = CDbl(22)
data(2, 0) = "East": data(2, 1) = CDbl(210): data(2, 2) = CDbl(8)
data(3, 0) = "West": data(3, 1) = CDbl(60): data(3, 2) = CDbl(31)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetColumnHeader 1, "Revenue"
grid.SetColumnHeader 2, "Units"' Make the highlight distinct so users don't mistake a copy region for a' normal row selection -- both share these same two colors
grid.SelectionBackColor = RGB(255, 244, 180)
grid.SelectionForeColor = RGB(60, 40, 0)
' Select Revenue and Units for South and East as a rectangular copy region.' SelectBlock is Embedded-only -- see the note below for Turbo column-store.
grid.SelectBlock 1, 1, 2, 2Dim hasBlock AsBoolean, r1 AsLong, c1 AsLong, r2 AsLong, c2 AsLong
hasBlock = grid.GetBlockSelection(r1, c1, r2, c2)
If hasBlock ThenDebug.Print"Copy region: rows " & r1 & "-" & r2 & ", columns " & c1 & "-" & c2
EndIf' A plain click elsewhere clears the block automatically; ClearSelection' does NOT -- call SelectBlock again (or click a cell) if the block itself' needs to go away, not ClearSelection.' In a Turbo column-store grid, SelectBlock raises a runtime error instead' of doing any of this; fall back to a loop of SelectRow calls plus your' own remembered column range there.EndSub
SelectBlock and GetBlockSelection track a rectangular range through the grid's focus state, entirely separate from the row-selection set SelectRow/GetSelectedRows read and write -- though SelectBlock does also row-select every row it spans, as a convenience for code that only checks row selection. Because SelectionBackColor/SelectionForeColor paint every kind of highlight in this group identically, a block selection looks exactly like a row selection unless the surrounding UI (a status label, a different border) tells the user which one they are looking at. SelectBlock has no Turbo column-store equivalent at all, unlike most other members in this cookbook that have a Turbo column-store alternative -- Turbo column-store callers who need a copy region have to track it themselves.
RecipeA hover status readout plus type-to-jump navigation
VBA
Sub HoverStatusAndTypeToJumpNavigation(grid AsObject)
Dim data(0To9, 0To0) AsVariantDim r AsLongFor r = 0To9
data(r, 0) = "Customer " & (r + 1)
Next r
grid.LoadFromArray data
grid.SetColumnHeader 0, "Customer"' Type-ahead: click into the grid, then type a customer's name to jump to it
grid.MatchEntry = 2' Complete (the default) -- accumulate the whole prefix' Slow the wheel down for a long, dense list
grid.WheelScrollRows = 3' Bring a specific row into view without the user scrolling manually
grid.ScrollToView 7' A Timer, or the grid's own MouseMove event, would call something like' this on a short interval to drive a "Row 4 of 10" status readout:Dim hovered AsLong
hovered = grid.HoveredRow
If hovered >= 0ThenDebug.Print"Hovering row " & hovered & ": " & grid.GetCellValue(hovered, 0)
ElseDebug.Print"Not hovering any row"EndIfEndSub
HoveredRow, WheelScrollRows, MatchEntry and ScrollToView are all mode-agnostic presentation state -- none of them touch row data, so this recipe works identically in Embedded and Turbo column-store grids. HoveredRow only has a meaningful value while the mouse is actually over the control; poll it from a UI event rather than once at load time, since at load time it will simply read -1. MatchEntry's default (Complete) is usually the right choice for a customer-facing grid because it lets a user type a whole name rather than only its first letter; FirstLetter is better suited to a fast, single-key cycling shortcut.
SelectRowSelection and navigation
Add one row to the current selection.
Adds rowIndex to the set of selected rows. This is purely additive: it never removes a row from the selection, and calling it again on a row that is already selected is a harmless no-op, not a toggle. Neither MultiSelect nor SelectionMode has any effect on this call -- it always adds the row in row terms, even while SelectionMode is Cell or Column, though the row will not paint with a visible highlight until SelectionMode is back to Row (0).
SignatureSub SelectRow(rowIndex As Long)
Parameters
rowIndex (Long) - 0-based display row index to add to the selection.
Works the same way in every data mode -- selection is grid-owned presentation state, not row data the host or the Turbo column-store store has to supply, so it is not restricted the way most row-data mutators are.
Purely additive: there is no per-row Deselect method in the public API. To remove one specific row from the selection, call ClearSelection and then SelectRow only the rows you still want selected.
Selecting an already-selected row is a safe no-op -- it does not toggle it off the way an interactive Ctrl+Click does.
Ignores both MultiSelect and SelectionMode entirely -- those two properties only affect interactive mouse clicks, never this call.
An out-of-range row index (negative, or >= GetRowCount()) raises a runtime error and adds nothing.
Selection is tracked by the row's own stable identity, not its display position, so a later sort or filter does not lose a row selected here -- GetSelectedRows/IsRowSelected simply report it under its new display index afterward.
See alsoClearSelection, GetSelectedRows, IsRowSelected, SelectAll
SelectAllSelection and navigation
Select every row currently in the grid.
Selects every row, including rows a currently active filter is hiding -- in both Embedded and Turbo column-store mode this walks the whole underlying store, not just the current filtered view, so a hidden row reappears in GetSelectedRows if the filter is cleared afterward. It does not touch focus or the anchor, so it does not change what the user was last pointing at.
Selects every row in Embedded mode (walking the full row store, filtered-out rows included) and every physical row plus every appended row in Turbo column-store mode. In Virtual mode SelectAll succeeds without error but selects nothing: it shares the same internal path Embedded mode uses, which walks the grid's embedded row store rather than the host-declared virtual row count. Entering Virtual mode requires that embedded store to be empty, and the only two calls that could ever add rows to it -- AddRow and InsertRow -- both refuse outright while Virtual mode is active, so the store SelectAll walks is guaranteed empty for as long as Virtual mode stays selected, no matter how many rows SetVirtualRowCount has declared. This has not been exercised against a live Virtual-mode control (this group's test run only covers Embedded and Turbo column-store), but the mechanism is structural rather than incidental, so treat SelectAll as non-functional in Virtual mode rather than merely unreliable there.
Selects filtered-out rows too, in both Embedded and Turbo column-store mode -- it is not limited to the rows currently visible in a filtered view.
Does not move focus or the anchor -- selecting everything does not change which cell the keyboard would act on next.
In Virtual mode, SelectAll silently selects nothing: it shares Embedded mode's code path, which selects rows out of the grid's embedded row store -- a store Virtual mode requires to be empty on entry, and one that AddRow/InsertRow (the only calls that could populate it) both refuse to touch while Virtual mode is active. No error is raised, and GetSelectedRows returns a zero-length array afterward regardless of how many rows SetVirtualRowCount declared. This has not been run against a live Virtual-mode control, but the code path leaves no way for it to behave otherwise.
GetSelectedRows immediately afterward returns every row's display index in ascending order (Embedded and Turbo column-store).
See alsoClearSelection, SelectRow, GetSelectedRows
ClearSelectionSelection and navigation
Deselect every currently selected row.
Empties the row-selection set. Calling it when nothing is selected is a safe no-op. It only clears row selection -- it does NOT clear an active block created by SelectBlock; GetBlockSelection keeps reporting the same block and coordinates until SelectBlock defines a new one or a plain interactive click clears it as a side effect.
Safe to call when nothing is selected -- not an error.
Clears row selection only. An active block from SelectBlock survives ClearSelection untouched; GetBlockSelection will still report hasBlock = True with the same coordinates afterward.
GetSelectedRows returns a zero-length array (not Empty, not Null) immediately after this call, and GetSelectedRow returns -1.
See alsoSelectRow, SelectAll, GetSelectedRow
GetSelectedRowsSelection and navigation
Read every currently selected row's display index as an array.
Returns a 1-D array of 0-based display indices, one per selected row, sorted in ascending order regardless of the order the rows were selected in. When nothing is selected, it returns a valid zero-length array rather than Empty, Null, or an error -- check with UBound < LBound (or the array's own bounds) rather than testing for Empty.
SignatureFunction GetSelectedRows() As Variant
ReturnsA 1-D array of Long display indices in ascending order, or a zero-length array if nothing is selected.
Always sorted ascending by display index, independent of the order the rows were selected in.
A zero-length array, not Empty or Null, when nothing is selected -- test with LBound/UBound rather than IsEmpty or VarType.
Indices are display positions: after a sort or filter changes the view, the same set of rows can come back under different index values.
See alsoGetSelectedRow, IsRowSelected, SelectRow
GetSelectedRowSelection and navigation
Read the lowest-indexed currently selected row.
Returns the display index of the lowest currently selected row, scanning in ascending order and stopping at the first match -- not the most recently selected row, and not necessarily the row the user last clicked. Returns -1 when nothing is selected.
SignatureFunction GetSelectedRow() As Long
ReturnsThe lowest selected display index, or -1 if no row is selected.
Reports the LOWEST selected display index, not the most recently selected row -- with several rows selected, selecting a new, lower-indexed row changes what this returns even though the previous selection is still there.
-1 means nothing is selected; it is not itself a valid row index and must be checked before using the result to index into GetCellValue/GetCell.
See alsoGetSelectedRows, IsRowSelected, SelectRow
IsRowSelectedSelection and navigation
Check whether a specific row is currently selected.
Returns True if rowIndex is in the current row-selection set, False otherwise. This checks row selection only -- it does not report whether a row happens to fall inside an active SelectBlock range unless that row was also individually added to the row-selection set (which SelectBlock does for the rows it spans).
SignatureFunction IsRowSelected(rowIndex As Long) As Boolean
Parameters
rowIndex (Long) - 0-based display row index to check.
ReturnsTrue if the row is currently selected, False otherwise.
An out-of-range row index (negative, or >= GetRowCount()) raises a runtime error rather than returning False.
Reports row-selection state only -- with SelectionMode set to Cell or Column, a row can be IsRowSelected = True yet paint with no row highlight at all, because the paint routine only draws the row highlight in Row mode.
See alsoSelectRow, GetSelectedRows, GetSelectedRow
MultiSelectSelection and navigation
Allow Shift+Click and Ctrl+Click to extend an interactive selection.
When True, an interactive Shift+Click extends the selection to a range and Ctrl+Click toggles an individual row (or column, under SelectionMode = Column) in or out of the selection. When False, a click always replaces the selection with just the row clicked, regardless of held modifier keys. This property only affects interactive mouse clicks -- it has no effect whatsoever on SelectRow, SelectAll, ClearSelection or any other programmatic member in this group, all of which keep working exactly the same way with MultiSelect set to either value.
SignatureProperty MultiSelect As Boolean
ReturnsTrue if Shift/Ctrl+Click extend the selection interactively.
Works in every data mode -- it is a stored preference bit consulted only by the interactive mouse-click handler, not by any row-data path.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Item A"
data(1, 0) = "Item B"
data(2, 0) = "Item C"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.MultiSelect = False' Shift/Ctrl+Click no longer extend the selectionDebug.Print grid.MultiSelect ' -> False' Programmatic selection still works exactly the same either way:
grid.SelectRow 0
grid.SelectRow 1Debug.Print grid.GetSelectedRow ' -> 0
Gotchas
Only changes interactive Shift/Ctrl+Click behavior -- SelectRow, SelectAll and the rest of this group's programmatic API are completely unaffected by its value.
Independent of SelectionMode: MultiSelect decides whether a modifier key extends a click; SelectionMode decides what the click targets (row, cell, or column) in the first place.
See alsoSelectionMode, SelectRow
SelectionModeSelection and navigation
Choose whether an interactive click targets a row, a cell, or a column.
Decides what an interactive click selects and how the grid paints its focus/selection cues: Row mode highlights whole rows and is the only mode in which SelectRow's selections actually paint a visible row highlight; Cell mode draws a border around the focused cell and lets Shift+Click extend a block (see SelectBlock); Column mode highlights whole columns and lets Ctrl+Click/Shift+Click toggle or extend a set of selected columns. Changing this property never changes what SelectRow, GetSelectedRows, GetSelectedRow or IsRowSelected report -- those four always operate in row terms regardless of SelectionMode's value.
SignatureProperty SelectionMode As Long
ReturnsThe current selection mode (0, 1 or 2).
Default0 (Row).
Value
Meaning
0
Row -- whole-row selection and highlighting (the default).
1
Cell -- single-cell focus with a border; Shift+Click extends a block (SelectBlock).
2
Column -- whole-column selection and highlighting.
Works in every data mode -- it only changes how interactive input is routed and how the highlight paints, not any row data.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Item A"
data(1, 0) = "Item B"
data(2, 0) = "Item C"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SelectionMode = 1' CellDebug.Print grid.SelectionMode ' -> 1' The row-selection API keeps working in row terms regardless of SelectionMode
grid.SelectRow 0Debug.Print grid.IsRowSelected(0) ' -> True (though it will not paint as a row highlight in Cell mode)
grid.SelectionMode = 0' back to Row mode
Gotchas
An out-of-range value (anything other than 0, 1 or 2) raises a runtime error and leaves the mode unchanged.
Row-mode highlighting only draws when SelectionMode = 0: a row selected via SelectRow while SelectionMode is 1 or 2 is still selected (IsRowSelected/GetSelectedRows report it), it simply does not paint a row highlight until SelectionMode goes back to 0.
Column selection (mode 2) has no getter in this group -- there is no member here that reports which columns are currently selected under Column mode.
See alsoMultiSelect, SelectBlock, SelectRow
SelectBlockSelection and navigation
Select a rectangular range of cells, replacing the current selection.
Defines a rectangular cell range from (rowStart, colStart) to (rowEnd, colEnd) -- either corner can be the 'first' one, since the range is normalized internally. Calling this always clears the current row selection first, then also row-selects every row the block spans (so GetSelectedRows/IsRowSelected still see those rows), and marks a block as active for GetBlockSelection to report. There is no Turbo column-store implementation of this member at all.
SignatureSub SelectBlock(rowStart As Long, colStart As Long, rowEnd As Long, colEnd As Long)
Parameters
rowStart (Long) - 0-based row index of one corner of the block.
colStart (Long) - 0-based column index of one corner of the block.
rowEnd (Long) - 0-based row index of the opposite corner.
colEnd (Long) - 0-based column index of the opposite corner.
Refused outright in Turbo column-store mode with a runtime error naming block selection as not implemented there; the selection, focus and view are left unchanged. Reading the implementation, Virtual mode is not explicitly refused, so the call succeeds and GetBlockSelection will report a block afterward, but the row-selection side effect (selecting the rows the block spans) walks the same internal row store that is required to be empty in Virtual mode, so those rows likely do not actually become row-selected there. This has not been confirmed against a live Virtual-mode control.
Always clears the existing row selection first, unlike SelectRow, which is additive.
No Turbo column-store equivalent exists at all -- calling it there always raises a runtime error, regardless of arguments.
The range check has a real gap: it verifies rowStart >= 0, colStart >= 0, rowEnd < GetRowCount() and colEnd < column count, but never checks that rowStart/colStart are themselves within range, nor that rowEnd/colEnd are non-negative. A call like SelectBlock(GetRowCount(), 0, 0, 0) is accepted without error even though its first argument is clearly out of range.
Neither ClearSelection nor a full grid Clear() resets an active block -- GetBlockSelection keeps reporting hasBlock = True until SelectBlock defines a new block or a plain interactive click elsewhere resets it.
Rows/columns are normalized internally, so passing rowStart > rowEnd (or colStart > colEnd) works exactly the same as passing them the other way around.
See alsoGetBlockSelection, ClearSelection, SelectRow, SelectionBackColor
GetBlockSelectionSelection and navigation
Read the current block selection range, if one is active.
Reports the range SelectBlock last established. If no block is active, returns False and sets all four ByRef parameters to -1. If a block is active, returns True and fills in the normalized range (rowStart <= rowEnd, colStart <= colEnd, regardless of the order SelectBlock's own arguments were given in).
SignatureFunction GetBlockSelection(ByRef rowStart As Long, ByRef colStart As Long, ByRef rowEnd As Long, ByRef colEnd As Long) As Boolean
Parameters
rowStart (Long) - [out] Receives the block's lower row bound, or -1 if no block is active.
colStart (Long) - [out] Receives the block's lower column bound, or -1 if no block is active.
rowEnd (Long) - [out] Receives the block's upper row bound, or -1 if no block is active.
colEnd (Long) - [out] Receives the block's upper column bound, or -1 if no block is active.
ReturnsTrue if a block selection is currently active; the four ByRef parameters are only meaningful when it returns True.
Refused outright in Turbo column-store mode with a runtime error; all four output parameters are set to -1 before the refusal, but since the call raises an error, VBA never completes the assignment into your own variables. Supported in Virtual mode -- reading the block's own coordinates is independent of row data, so this half of the pair does not share SelectBlock's row-selection caveat there.
All four output parameters are neutralized to -1 before any check runs, including the trial and Turbo column-store checks -- but a failed call still raises a runtime error in VBA, so your own variables never actually receive that -1 unless you assign it yourself first.
Reports whatever SelectBlock's own validation gap produced: an out-of-range coordinate passed to SelectBlock (see SelectBlock's own gotchas) is reported back here exactly as stored, with no re-validation.
hasBlock can be True even after ClearSelection, since ClearSelection does not clear the block flag -- do not assume hasBlock = False just because GetSelectedRows now returns nothing.
An active block also survives a full grid Clear(), not just ClearSelection: Clear() resets the focus position but leaves the block flag set, so immediately after clearing (and reloading) the grid, GetBlockSelection can still return hasBlock = True. rowStart and rowEnd happen to read back as -1 -- the same value used for 'no block' -- but colStart/colEnd may still hold whatever a previous SelectBlock call set, and hasBlock itself does not go back to False. Only a new SelectBlock call or a plain interactive click actually resets it.
See alsoSelectBlock, ClearSelection, GetSelectedRows
HoveredRowSelection and navigation
The display row currently under the mouse pointer, or -1.
A read-only, live reflection of the mouse position: updated on every mouse-move over the grid and reset to -1 the instant the mouse leaves the control entirely. There is no way to set it -- it exists purely for building hover-driven UI (a status readout, a custom hover highlight) on top of the grid's own built-in hover rendering.
SignatureProperty Get HoveredRow As Long
ReturnsThe 0-based display row currently under the mouse, or -1 if the mouse is not over any row (including while it is outside the control, or over the header/footer).
Works the same way in every data mode -- hover tracking is pure mouse/paint state and never touches row data.
Example
VBA
' In a real running form, wire a Timer or the grid's own MouseMove event to a' short refresh that reads this:Debug.Print grid.HoveredRow ' -> -1 when the mouse is not over any row' HoveredRow has no setter -- it only reflects live mouse position, updated as' the user moves the mouse over the grid.
Gotchas
Read-only: there is no setter. Assigning to it from VBA raises a runtime error.
Only meaningful while the mouse is actually over the control right now -- read it from a mouse-move-driven event or a short timer, not once at load time (it will simply read -1 before the user has moved the mouse over the grid).
-1 covers several distinct situations identically: the mouse has never been over the grid, it has left the grid, or it is currently over the header/footer rather than a data row.
See alsoScrollToView
ScrollToViewSelection and navigation
Scroll the minimum distance needed to bring a row into view.
Scrolls the grid so rowIndex is fully visible, moving it to the top of the scrollable band if it was above the visible area or to the bottom if it was below -- never scrolls more than necessary, and does nothing at all if the row is already fully visible. rowIndex is a VIEW index, the same space GetCellValue uses, and it respects an active tree or group projection's own row count rather than the raw row count when one is active. The header, any frozen rows, and the aggregate footer are excluded from the scrollable band this measures against.
SignatureSub ScrollToView(rowIndex As Long)
Parameters
rowIndex (Long) - 0-based view row index to bring into view.
Works the same way in every data mode -- it is a pure view/scroll operation with no row-data mutation, so it carries none of the usual Virtual/Turbo column-store restrictions other members in this cookbook have.
Example
VBA
Dim data(0To99, 0To0) AsVariantDim i AsLongFor i = 0To99
data(i, 0) = "Row " & i
Next i
grid.LoadFromArray data
grid.SetColumnHeader 0, "Label"
grid.ScrollToView 80' brings row 80 into view with the minimum scroll needed
Gotchas
An out-of-range row index (negative, or >= the current effective row count) raises a runtime error.
Does nothing if the row is already fully within the visible scrollable band -- do not assume every call causes a repaint.
A frozen row is always on screen and this call returns without scrolling anything if rowIndex falls within the frozen band.
Before a window exists yet (design time, or before the control is fully created), this call succeeds without error but there is nothing to actually scroll.
See alsoHoveredRow, WheelScrollRows
WheelScrollRowsSelection and navigation
How many rows a single mouse-wheel notch scrolls.
Controls the mouse wheel's scroll distance. -1 (the default) uses the system's own wheel-scroll-lines setting, capped at a maximum notch distance; 0 disables wheel scrolling entirely; any positive value scrolls exactly that many rows per notch, with no maximum cap applied, so the number set is exactly the number of rows moved.
SignatureProperty WheelScrollRows As Long
ReturnsThe current wheel-scroll row count.
Default-1 (automatic -- follows the Windows wheel-scroll-lines setting, capped).
Value
Meaning
-1
Automatic -- follows the system's wheel-scroll-lines setting, capped at a maximum distance per notch.
Works the same way in every data mode -- a pure scroll preference, unrelated to row data.
Example
VBA
grid.WheelScrollRows = 3' each wheel notch moves exactly 3 rowsDebug.Print grid.WheelScrollRows ' -> 3
grid.WheelScrollRows = 0' disable wheel scrolling entirely
grid.WheelScrollRows = -1' back to automatic (the system setting)
Gotchas
Any value below -1 raises a runtime error and leaves the property unchanged.
A positive value is not capped the way the automatic (-1) setting is -- a large number scrolls that many rows per notch with no maximum.
Setting it does not repaint or scroll anything immediately; it only takes effect the next time the user actually turns the wheel.
See alsoScrollToView
MatchEntrySelection and navigation
Control type-ahead search behavior when the user types into the grid.
Governs what happens when a printable character reaches the grid with no cell editor open: the grid searches forward from the row after the current focus (wrapping around) for the next row whose value in the currently focused column starts with what has been typed, then moves focus and selection to the first match. Off disables this entirely -- keystrokes do nothing. FirstLetter discards any previously typed characters on every keystroke, so only the single character just typed is matched, which makes repeating the same letter cycle through every row starting with it. Complete (the default) keeps accumulating characters into one growing prefix as long as they arrive within one second of each other; a pause longer than that resets the buffer.
SignatureProperty MatchEntry As Long
ReturnsThe current type-ahead mode (0, 1 or 2).
Default2 (Complete).
Value
Meaning
0
Off -- keystrokes never move focus or selection.
1
FirstLetter -- only the character just typed is matched; repeating it cycles through every row starting with that letter.
2
Complete (default) -- consecutive keystrokes within one second of each other accumulate into a single growing prefix.
The property itself (get/set) works in every data mode. Reading the implementation, the search it drives has a bound check that was written to skip Turbo column-store mode's own (empty) internal row store but does not skip Virtual mode's identical empty store -- meaning a keystroke's search would find every candidate row's text length reported as unavailable and never match anything in Virtual mode. This has not been confirmed against a live Virtual-mode control; it is a strong candidate for a real gap given the code comment describing the same failure mode being fixed for Turbo column-store specifically.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Amber": data(2, 0) = "Bravo"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.MatchEntry = 2' Complete (the default): keystrokes accumulate into one prefix' A user clicks into the grid, then types "A" followed by "M" -- the first' keystroke jumps to the next row starting with A (Alpha or Amber, whichever' comes first after the current row), and the second narrows it to Amber' specifically, since "AM" only matches that one row.
Gotchas
An out-of-range value (anything other than 0, 1 or 2) raises a runtime error.
Setting this property, to any value, immediately discards whatever partial prefix had been typed so far -- useful for deliberately resetting the search buffer, but a trap if you set it to the same value it already held expecting no side effect.
The search always starts from the row AFTER the current focus row, wrapping around to the top -- it never matches the currently focused row itself unless nothing else matches and the wrap comes back around to it.
Searches only the currently focused column's displayed text -- click into the column you want type-ahead to search before typing.
Reading the implementation: in Virtual mode, the search appears to never find a match at all, for the same reason a similar defect was previously found and fixed for Turbo column-store mode specifically -- this needs live confirmation before relying on type-ahead search in Virtual mode.
See alsoHoveredRow
SelectionBackColorSelection and navigation
Background color used for every selection highlight in the grid.
Overrides the background color painted behind a selected row, an active SelectBlock range, and the cell/column highlight SelectionMode produces in Cell or Column mode -- all three share this one color; there is no separate color for block or cell-mode highlighting. When the host window is not the active window, selected rows paint with a separate inactive-selection color instead -- it has no property of its own and is set through the Selection_Inactive role in ApplyThemeJson -- so this property only controls the active-window appearance.
SignatureProperty SelectionBackColor As OLE_COLOR
ReturnsThe current selection background color.
DefaultDetermined by the active color theme, not a fixed constant -- read the property to capture the current value before overriding it.
Works the same way in every data mode -- a pure rendering color, unrelated to row data.
Example
VBA
grid.SelectionBackColor = RGB(0, 90, 200)
grid.SelectionForeColor = RGB(255, 255, 255)
' Every selection highlight this group can produce now uses these two colors:' row selection, SelectBlock's block, and the Cell/Column mode highlight.Debug.Print grid.SelectionBackColor ' -> RGB(0, 90, 200)
Gotchas
Shared by every kind of selection highlight this group can produce (row, block, and Cell/Column mode) -- changing it changes all of them at once.
Has no effect on selection highlighting while the host window is not active; an inactive-window selection uses a separate color this property does not control (the Selection_Inactive role, reachable through ApplyThemeJson).
The default comes from the active color theme rather than a fixed value -- capture it with a Get before overriding it if you need to restore it later.
See alsoSelectionForeColor, SelectBlock, SelectionMode
SelectionForeColorSelection and navigation
Text color used for every selection highlight in the grid.
Overrides the text color drawn over a selected row, an active SelectBlock range, and the cell/column highlight SelectionMode produces in Cell or Column mode -- the same three surfaces SelectionBackColor's background applies to, and the same rule that there is no separate color for block or cell-mode highlighting applies here too.
SignatureProperty SelectionForeColor As OLE_COLOR
ReturnsThe current selection text color.
DefaultDetermined by the active color theme, not a fixed constant -- read the property to capture the current value before overriding it.
Works the same way in every data mode -- a pure rendering color, unrelated to row data.
Example
VBA
grid.SelectionForeColor = RGB(255, 255, 0)
Debug.Print grid.SelectionForeColor ' -> RGB(255, 255, 0)' Pair it with SelectionBackColor so selected text stays readable:
grid.SelectionBackColor = RGB(40, 40, 40)
Gotchas
Shared by every kind of selection highlight this group can produce (row, block, and Cell/Column mode) -- changing it changes all of them at once.
The default comes from the active color theme rather than a fixed value -- capture it with a Get before overriding it if you need to restore it later.
Checked against the selection background actually painted -- SelectionBackColor in an active window, the inactive-selection color otherwise -- and replaced on screen with a more readable color if it would be unreadable there, while the property keeps reporting the value you set. Choose it together with SelectionBackColor if the exact color matters to you.
See alsoSelectionBackColor, SelectBlock, SelectionMode
Sorting, filtering and searching
This group layers three largely independent mechanisms on top of the grid's row data: sorting (Sort, ClearSort, SortEx, SortMulti, AutoSortOnHeaderClick), filtering (SetColumnFilter, ClearColumnFilter, ClearAllFilters, GetFilteredRowCount, SetColumnNumericFilter, SetColumnNumericFilterRange, GetDistinctValues, ShowFilterButtons, SetColumnValueListFilter, GetColumnFilterMode, GetColumnFilterPattern, GetColumnValueListFilter, GetFilterState, SetFilterState, ShowAutoFilterRow, SaveFilterPreset, LoadFilterPreset, DeleteFilterPreset, GetFilterPresetNames), and a find panel (ShowFindBar, HideFindBar). Filtering itself splits further into four independent per-column 'slots' that combine with AND logic rather than replacing one another: a text/blank slot and a separate date slot (both set through SetColumnFilter, distinguished by which filterMode value is passed), a numeric slot (SetColumnNumericFilter / SetColumnNumericFilterRange -- a range call replaces a prior single-threshold call and vice versa, since both share the one numeric slot), and a value-list slot (SetColumnValueListFilter). A column can carry a text filter, a date filter, a numeric filter and a value-list filter all at the same time, and every active slot across every column narrows the view together. ClearColumnFilter is the one call that removes every slot on a single column in one step; ClearAllFilters does the same across every column. GetFilteredRowCount and the ordinary row count member report the identical number in Embedded and Turbo column-store grids -- both simply read the current filtered view's size -- so GetFilteredRowCount mainly exists for code that also has to run against a Virtual-mode grid, where it is refused outright because the host owns filtering there and the grid cannot know the true count.
Turbo column-store coverage in this group is deliberately narrow and uneven, and the unevenness itself is a fact worth knowing rather than a bug: the mutators that change what rows are visible are routed for a specific, limited set of operations -- SetColumnFilter routes only Contains and StartsWith, and only against a column Turbo column-store has stored as String; SetColumnNumericFilter and SetColumnNumericFilterRange route their comparison operators against a column stored as Int32, Int64 or Double (a fractional threshold against an integer-stored column is refused rather than silently truncated, since truncation would move the comparison boundary without saying so); SetColumnValueListFilter routes against String columns. Every other filterMode value SetColumnFilter accepts (Equals, EndsWith, Regex, the negated and blank modes, every date mode) has no Turbo column-store equivalent and refuses by name, leaving the view exactly as it was. ClearColumnFilter, ClearAllFilters, GetFilteredRowCount, and every member in the sorting family are fully implemented in Turbo column-store mode with no such narrowing. By contrast, the whole read-back and persistence surface -- GetColumnFilterMode, GetColumnFilterPattern, GetColumnValueListFilter, GetFilterState, SetFilterState and SaveFilterPreset -- refuses outright in Turbo column-store mode, because all six are backed by the same underlying filter-tracking engine Embedded and Virtual mode share, which stays empty for as long as a grid is in Turbo column-store mode (a Turbo column-store filter lives in the Turbo column-store store's own mask, not in that engine); answering from an engine that is always empty would misreport an active Turbo column-store filter as absent, so these six refuse by name instead. DeleteFilterPreset and GetFilterPresetNames are a deliberate exception to that refusal: they only ever touch the preset name-to-JSON map itself, never the live filter state, so both work identically in every mode. LoadFilterPreset inherits SetFilterState's refusal indirectly, since its entire body is a name lookup followed by a call to SetFilterState.
GetColumnFilterMode and GetColumnFilterPattern both describe only the FIRST filter slot that was populated on a column, in insertion order -- not the most restrictive slot, and not necessarily the same slot as each other. If a column carries both a numeric filter and a text filter at once, GetColumnFilterMode reports whichever one was set first (by slot-insertion order, regardless of which one currently matters more), while GetColumnFilterPattern specifically skips numeric and value-list slots when looking for a pattern to return, so it can end up reporting the OTHER slot's pattern even when GetColumnFilterMode just reported a different slot's mode number for the same column. Saved filter presets and the sort family are also fully independent of one another: GetFilterState/SaveFilterPreset capture filters only, never the active Sort/SortEx/SortMulti state, so restoring a preset never changes what column the grid is currently sorted by. Sort state and every filter slot are both fully reset by Clear() and by LoadFromArray -- unlike some other per-grid state in this control, neither survives a reload -- but saved filter presets themselves are a separate, in-memory-only name map that Clear() and LoadFromArray both leave completely untouched, and that nothing but DeleteFilterPreset or destroying the control ever empties. AutoSortOnHeaderClick is the one property in this group with a surprising reset rule of its own: LoadFromArray always resets it back to True, regardless of what it had been set to immediately beforehand, even though Clear() alone does not touch it.
RecipeFilter, sort, and show a live matching-row count
VBA
Sub FilterSortAndShowLiveCount(grid AsObject)
Dim data(0To7, 0To2) AsVariant
data(0, 0) = "North": data(0, 1) = CDbl(120): data(0, 2) = CLng(5)
data(1, 0) = "South": data(1, 1) = CDbl(80): data(1, 2) = CLng(2)
data(2, 0) = "East": data(2, 1) = CDbl(210): data(2, 2) = CLng(8)
data(3, 0) = "West": data(3, 1) = CDbl(60): data(3, 2) = CLng(1)
data(4, 0) = "North": data(4, 1) = CDbl(300): data(4, 2) = CLng(9)
data(5, 0) = "South": data(5, 1) = CDbl(45): data(5, 2) = CLng(3)
data(6, 0) = "East": data(6, 1) = CDbl(150): data(6, 2) = CLng(6)
data(7, 0) = "West": data(7, 1) = CDbl(400): data(7, 2) = CLng(4)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetColumnHeader 1, "Revenue"
grid.SetColumnHeader 2, "Units"' A text filter and a numeric filter on two different columns combine' with AND -- both narrow the view together
grid.SetColumnFilter 0, 2, "N"' StartsWith "N" -- Region
grid.SetColumnNumericFilter 1, 7, CDbl(100) ' Revenue >= 100Debug.Print grid.GetFilteredRowCount ' -> 1 (only the North/300 row matches both)
grid.Sort 1, 1' Revenue descending, within whatever the filter left visible' Clearing just the text filter leaves the numeric one active
grid.ClearColumnFilter 0Debug.Print grid.GetFilteredRowCount ' -> 4 (every row with Revenue >= 100)
grid.ClearAllFilters
grid.ClearSort
EndSub
SetColumnFilter and SetColumnNumericFilter write to two independent slots on two different columns, so both stay active until each is cleared individually -- ClearColumnFilter 0 only removes column 0's slots, leaving column 1's numeric filter untouched. GetFilteredRowCount always reflects the combined result of every active slot on every column, and reads identically to the grid's own row count member in both Embedded and Turbo column-store grids.
RecipeSave the user's current filter setup as a named preset and restore it later
VBA
Sub SaveAndRestoreAFilterPreset(grid AsObject)
Dim data(0To5, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = CDbl(10)
data(1, 0) = "Bravo": data(1, 1) = CDbl(55)
data(2, 0) = "Alpha": data(2, 1) = CDbl(90)
data(3, 0) = "Charlie": data(3, 1) = CDbl(30)
data(4, 0) = "Bravo": data(4, 1) = CDbl(70)
data(5, 0) = "Alpha": data(5, 1) = CDbl(20)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Category"
grid.SetColumnHeader 1, "Amount"
grid.SetColumnFilter 0, 0, "Alpha"' Contains "Alpha"Debug.Print grid.GetFilteredRowCount ' -> 3' Presets live in memory only (not written to disk or the registry) and' capture filters ONLY -- not sort state -- so save both separately if' the user's whole view needs to come back exactly as it was
grid.SaveFilterPreset "AlphaOnly"
grid.ClearAllFilters
Debug.Print grid.GetFilteredRowCount ' -> 6Dim names AsString
names = grid.GetFilterPresetNames
Debug.Print names ' -> ["AlphaOnly"]
grid.LoadFilterPreset "AlphaOnly"Debug.Print grid.GetFilteredRowCount ' -> 3 again
grid.DeleteFilterPreset "AlphaOnly"' SaveFilterPreset (and therefore LoadFilterPreset, which is built on top' of it) both refuse in a Turbo column-store grid; DeleteFilterPreset and' GetFilterPresetNames work there regardless, since they never touch the' live filter state at all.EndSub
SaveFilterPreset serializes whatever GetFilterState would return under a name kept in an in-memory map on the control instance; LoadFilterPreset is a lookup into that same map followed by an ordinary SetFilterState call, which is why it inherits SetFilterState's own Turbo column-store refusal. Because presets are not part of the row data or the filter/sort state Clear() and LoadFromArray reset, a preset saved earlier survives both -- only DeleteFilterPreset or destroying the control removes one.
RecipeWire a Ctrl+F find panel alongside a per-column value picker
VBA
Sub FindPanelAndValuePicker(grid AsObject)
Dim data(0To5, 0To0) AsVariant
data(0, 0) = "Red"
data(1, 0) = "Green"
data(2, 0) = "Blue"
data(3, 0) = "Red"
data(4, 0) = "Green"
data(5, 0) = "Red"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Color"' Open the built-in Ctrl+F equivalent programmatically -- e.g. from a' toolbar button, instead of requiring the user to know the shortcut
grid.ShowFindBar
' ... user searches, then closes it (or your own "Close" button calls this) ...
grid.HideFindBar
' Build a value-picker list for a column from what is actually present,' then let the user narrow the view to a subset of those valuesDim distinctColors AsVariant
distinctColors = grid.GetDistinctValues(0) ' -> Blue, Green, Red (sorted)Dim picked(0To0) AsVariant
picked(0) = "Red"
grid.SetColumnValueListFilter 0, picked, TrueDebug.Print grid.GetFilteredRowCount ' -> 3
grid.ClearAllFilters
' GetDistinctValues itself only works in an Embedded grid -- it always' scans every underlying row directly and has no Turbo column-store or Virtual-mode' implementation, so a Turbo column-store value picker has to get its candidate list' some other way (e.g. from the data the host loaded it from).EndSub
ShowFindBar/HideFindBar open and close the same interactive panel a user's own Ctrl+F would, so calling HideFindBar is important once your own UI is done with it -- nothing else closes it automatically. GetDistinctValues always reports every distinct value across the whole grid regardless of any filter already active, which is exactly what a value-picker list needs: the full universe of choices, not just what a previous filter left visible.
SortSorting, filtering and searching
Sort every row by one column.
Reorders the view by columnIndex. In an Embedded grid, calling Sort again on the SAME column with the OPPOSITE order just reverses the current view instead of re-sorting from scratch, and calling it again with the SAME column and SAME order is a no-op; a Turbo column-store grid and a Virtual grid re-run the full ordering (or the full host round trip) every time instead, though the visible result is identical either way. A text column sorts case-insensitively.
SignatureSub Sort(columnIndex As Long, sortOrder As Long)
Parameters
columnIndex (Long) - 0-based column to sort by.
sortOrder (Long) - 0 = ascending, any other value = descending.
ReturnsNone.
Value
Meaning
0
Ascending.
any other value
Descending -- not just 1; sortOrder itself is never range-checked.
Embedded sorts the row store directly. In a Turbo column-store grid, sorting moves only an internal row order, never the cell data itself, and dispatches to numeric or text ordering based on the column's own stored type; a column type the Turbo column-store store cannot order refuses by name rather than returning an arbitrary order, though the ordinary numeric, string, date and boolean types this cookbook's fixtures use all sort successfully. In a Virtual grid, sorting is delegated to the host: the host must handle a request for the sort and acknowledge it, or the call reports that the host declined and nothing changes. The same-column reversal shortcut and same-column/same-order no-op described above belong to the Embedded row store specifically; Turbo column-store and Virtual grids do not implement either shortcut.
sortOrder is not validated at all -- only 0 counts as ascending; every other value (2, -1, 99, ...) is treated as descending, not refused.
An out-of-range columnIndex (negative, or >= the column count) raises a runtime error and leaves the view unchanged.
Re-sorting the SAME column with the OPPOSITE order is a fast reversal of the current view rather than a full re-sort ONLY in an Embedded grid -- a Turbo column-store grid (the mode where this matters most for very large data) and a Virtual grid always redo the full sort or full host round trip instead; the visible result is the same either way, just not the shortcut.
Re-sorting the SAME column with the SAME order is a safe no-op in an Embedded grid; a Turbo column-store grid and a Virtual grid still run the full sort/host round trip again even though nothing visibly changes.
See alsoClearSort, SortEx, SortMulti, AutoSortOnHeaderClick
ClearSortSorting, filtering and searching
Remove the active sort and restore the original row order.
Restores the exact order rows were inserted in (or, for a Turbo column-store grid, the identity order the loaded table started in), undoing whatever Sort, SortEx or SortMulti last established. Safe to call when nothing is sorted.
In a Turbo column-store grid this drops the internal row-order permutation entirely rather than rebuilding it, returning to the loaded table's own original order at no ongoing memory cost. In a Virtual grid, clearing is delegated to the host the same way Sort's own request is -- the host must acknowledge it or nothing changes.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Bob": data(1, 0) = "Alice": data(2, 0) = "Cara"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.Sort 0, 0Debug.Print grid.GetCellValue(0, 0) ' -> Alice
grid.ClearSort
Debug.Print grid.GetCellValue(0, 0) ' -> Bob (original insertion order)
Gotchas
Restores INSERTION order, not the original unsorted state if rows were also reordered by some other means since loading.
Both Clear() and LoadFromArray also reset the active sort back to none -- ClearSort is only needed to drop a sort while the same data stays loaded.
See alsoSort, SortEx, SortMulti
SortExSorting, filtering and searching
Sort one column with an explicit mode, including natural (alphanumeric) order.
Modes 0 and 1 behave exactly like Sort's own ascending/descending. Modes 2 and 3 (AscNoCase/DescNoCase) are not a separate case-sensitive-vs-insensitive choice -- every text sort in this control is already case-insensitive, so they sort identically to modes 0 and 1. Mode 4 (Natural) compares embedded numbers as numbers rather than as text, so "Item 2" sorts before "Item 10"; it is always ascending -- there is no natural-descending option through SortEx itself.
SignatureSub SortEx(columnIndex As Long, sortMode As Long)
Modes 0 and 1 always call Sort itself. In an Embedded or Turbo column-store grid, modes 2 and 3 also route through Sort (Turbo column-store dispatches through the identical column-type logic Sort itself uses), so all four modes share Sort's own behavior there. In a Virtual grid, modes 2-4 do NOT go through Sort: SortEx instead fires its own SortRequested request straight to the host with the raw sortMode value (2, 3 or 4) passed as the direction argument, not a normalized 0/1 -- a host written only to Sort's own 0=ascending/anything-else=descending convention will read AscNoCase, DescNoCase and Natural alike as a non-zero, descending-like request. Mode 4 (Natural) is implemented for a Turbo column-store grid only against a column Turbo column-store has stored as String; a natural sort requested on any other stored type in Turbo column-store mode refuses by name rather than falling back to an ordinary sort.
Modes 2 and 3 exist for API completeness but never produce a different order than modes 0 and 1 -- there is no case-sensitive sort anywhere in this control.
Natural sort (mode 4) is always ascending; use SortMulti with a 'col:natdesc' spec token for a descending natural sort.
A sortMode outside 0-4 is refused with a runtime error.
In a Virtual grid specifically, modes 2, 3 and 4 hand the host the raw sortMode value as the SortRequested direction rather than a plain 0/1 -- a host built only for Sort's own convention will not recognize AscNoCase/Natural as an ascending request.
See alsoSort, SortMulti, ClearSort
SortMultiSorting, filtering and searching
Sort by up to three columns at once, using a compact spec string.
Parses spec as a comma-separated list of 'column:direction' tokens and sorts by the first three that parse successfully together (once three valid tokens are collected, anything left in the string is never even inspected), using each earlier column to break ties in the one before it. direction is 'asc' (the default, used for anything unrecognized), 'desc', 'nat' (natural, ascending) or 'natdesc' (natural, descending).
SignatureSub SortMulti(spec As String)
Parameters
spec (String) - Comma-separated 'col:dir' tokens, e.g. "1:asc,3:desc".
ReturnsNone.
Value
Meaning
asc
Ascending (the default -- also used for any direction text this control does not recognize).
desc
Descending.
nat
Natural, ascending.
natdesc
Natural, descending -- the only way to get a descending natural sort in this group.
In a Turbo column-store grid, every level is routed through the same multi-level ordering the single-column Sort/SortEx use; a natural-sort level still requires that particular column to be stored as String, exactly as SortEx's own Natural mode does, and any column Turbo column-store cannot order at all refuses the whole call by name.
Only the first THREE tokens that parse successfully are used -- an invalid token earlier in the string does not count against that limit (it's simply skipped), and once three valid specs are collected the rest of the string is silently dropped, not refused.
A token naming an out-of-range column, or a token with no ':' separator, is silently SKIPPED rather than refused, as long as at least one other token in the string is valid.
If EVERY token in the string is invalid (all skipped), the call raises a runtime error ('no valid sort specs parsed') instead of silently doing nothing.
An unrecognized direction word (anything other than asc/desc/nat/natdesc) is treated as plain ascending, not refused.
See alsoSort, SortEx, ClearSort
AutoSortOnHeaderClickSorting, filtering and searching
Let an interactive header click sort the grid without any VBA event code.
When True, clicking a column header sorts by that column (ascending, or toggling to descending if the same header is clicked again) with no event handler required -- useful for a reg-free host with no connection-point event support. The header still fires its own click notification regardless of this property's value; this property only decides whether the control ALSO calls Sort on top of that.
SignatureProperty AutoSortOnHeaderClick As Boolean
ReturnsTrue if a header click sorts the grid automatically.
The property itself is a stored preference with no mode restriction. Because a header click internally just calls Sort, its actual effect follows Sort's own mode behavior -- in a Virtual grid a click will only resort visibly if the host handles the sort request; if the host does not, the click's internal Sort call fails silently and nothing visibly happens.
Example
VBA
grid.AutoSortOnHeaderClick = False' disable the built-in click-to-sort behaviorDebug.Print grid.AutoSortOnHeaderClick ' -> False' Handle ColumnClick yourself instead (Registered activation, registered mode):' Private Sub LiteGrid1_ColumnClick(ByVal columnIndex As Long)' LiteGrid1.Sort columnIndex, 0' End Sub
grid.AutoSortOnHeaderClick = True' back to the built-in behavior
Gotchas
LoadFromArray resets this back to True in Embedded mode, regardless of what it had been set to immediately beforehand -- Clear() alone does NOT reset it. In Turbo column-store mode, LoadFromArray takes a completely separate internal path that never touches this flag at all, so a value you set before loading Turbo column-store data survives the load unchanged -- the "always resets to True" behavior is Embedded-mode-only.
Toggling behavior (ascending, then descending on a second click of the same header) is the SAME same-column optimization Sort itself uses -- clicking a different header always starts that header at ascending.
Turning this off does not disable the header's own click notification -- only the automatic Sort call that would otherwise follow it.
See alsoSort, ClearSort
SetColumnFilterSorting, filtering and searching
Filter one column by text, blank state, or date.
Sets (or replaces) the text/blank filter or the date filter on colIndex, depending on which filterMode is passed -- text/blank modes and date modes are two separate slots, so a column can carry one of each simultaneously, alongside its own independent numeric filter (SetColumnNumericFilter/Range) and value-list filter (SetColumnValueListFilter); all active slots across all columns combine with AND. Passing an empty pattern with a plain text mode clears that text slot rather than matching zero rows.
SignatureSub SetColumnFilter(colIndex As Long, filterMode As Long, pattern As String)
Parameters
colIndex (Long) - 0-based column to filter.
filterMode (Long) - See the value table -- text (0-4), negated/blank (11-14) or date (20-26).
pattern (String) - The match text (case-insensitive); ignored for IsBlank/IsNotBlank; a date string, 'lower|upper' range, or day count for date modes.
ReturnsNone.
Value
Meaning
0
Contains.
1
Equals.
2
StartsWith.
3
EndsWith.
4
Regex.
11
DoesNotContain.
12
DoesNotStartWith.
13
IsBlank (pattern is ignored).
14
IsNotBlank (pattern is ignored).
20
DateBefore.
21
DateAfter.
22
DateBetween (pattern is 'lowerDate
upperDate').
23
Today (pattern is ignored).
24
ThisWeek (pattern is ignored).
25
ThisMonth (pattern is ignored).
26
LastNDays (pattern is the day count; empty defaults to 7).
Embedded grids apply every mode against a column's own displayed text, regardless of the column's underlying type. In a Virtual grid, applying the filter is delegated to the host the same way Sort's own request is: the control fires a FilterRequested for the same colIndex/filterMode/pattern, and the host must acknowledge it or the call reports that the host declined and nothing changes. A Turbo column-store grid implements only Contains (0) and StartsWith (2), and only against a column Turbo column-store has stored as String -- every other mode, and Contains/StartsWith against any non-String column, refuses by name and leaves the view unchanged.
In Turbo column-store mode, only Contains and StartsWith work, and only on a column Turbo column-store has stored as String -- Equals, EndsWith, Regex, every negated/blank mode and every date mode all refuse by name there.
An empty pattern with a plain text mode CLEARS that column's text slot instead of matching nothing -- it is not the same as an impossible-to-match filter.
Text and date filters are two SEPARATE slots on the same column (categorized by filterMode) -- setting a date filter does not disturb an existing text filter on the same column, and vice versa.
An out-of-range filterMode (not 0-4, 11-14, or 20-26) is refused with a runtime error.
See alsoClearColumnFilter, ClearAllFilters, GetFilteredRowCount, SetColumnNumericFilter, SetColumnValueListFilter, GetColumnFilterMode, GetColumnFilterPattern
ClearColumnFilterSorting, filtering and searching
Remove every active filter on one column.
Drops all of colIndex's filter slots at once -- text, date, numeric and value-list together -- regardless of which SetColumnFilter/SetColumnNumericFilter(Range)/SetColumnValueListFilter calls created them. Other columns' filters are untouched. A column with no active filter is a safe no-op.
SignatureSub ClearColumnFilter(colIndex As Long)
Parameters
colIndex (Long) - 0-based column to clear filters from.
Clears every filter family on the column the same way in every mode -- it is the one filter mutator implemented fully for a Turbo column-store grid, clearing every family the Turbo column-store store tracks for that column in one step, the same as it does in Embedded and Virtual grids. The out-of-range colIndex check is NOT the same in every mode, though: a Turbo column-store grid explicitly range-checks colIndex and raises an error, but in an Embedded or Virtual grid the column's filters live in an ordinary map keyed by colIndex, so an out-of-range (including negative) colIndex is a silent no-op there, not an error.
Clears EVERY filter slot on the column, not just the most recently set one -- a text filter and a numeric filter on the same column both disappear together.
A column that was never filtered is a successful no-op, not an error.
An out-of-range colIndex (including negative) is refused with a runtime error only in a Turbo column-store grid; in an Embedded or Virtual grid the filter store is a plain map keyed by colIndex, so an out-of-range colIndex is a silent no-op instead.
See alsoClearAllFilters, SetColumnFilter, GetFilteredRowCount
ClearAllFiltersSorting, filtering and searching
Remove every active filter on every column.
Drops every filter slot on every column at once and restores the full row count. Safe to call when nothing is filtered.
Works the same way in every mode. This is the one filter-clearing member fully implemented for a Turbo column-store grid across every filter family (text, numeric, and value-list) at once.
Calling it when nothing is filtered is a safe no-op, not an error.
Clears every column's filters, not just the one most recently touched -- use ClearColumnFilter for a single column instead.
See alsoClearColumnFilter, GetFilteredRowCount, SetFilterState
GetFilteredRowCountSorting, filtering and searching
Read how many rows are currently visible after filtering.
Returns the number of rows the current filter set (across every column and every filter family) leaves visible. With no filter active, this equals the grid's own total row count.
SignatureFunction GetFilteredRowCount() As Long
ReturnsThe number of rows currently passing every active filter.
In Embedded and Turbo column-store grids this is an exact, cheap read of the current view size. It is refused in a Virtual grid because filtering there is delegated to the host's own data source, and the grid has no way to know how many rows genuinely match without asking the host directly.
In Embedded and Turbo column-store mode, this always returns exactly the same number the grid's own row-count member already returns -- both read the current filtered view's size, so this member is mainly useful in code that also has to run against a Virtual grid, where the two diverge.
Refused outright in Virtual mode, unlike most of this group's filter mutators (which are delegated to the host instead of refused).
See alsoSetColumnFilter, ClearAllFilters, SetColumnNumericFilter
SetColumnNumericFilterSorting, filtering and searching
Filter one column by a numeric comparison against a single threshold.
Sets (or replaces) colIndex's numeric filter slot -- independent of any text, date or value-list slot already active on the same column. Between (a range) is deliberately not one of this member's modes; call SetColumnNumericFilterRange for that, since it shares the same numeric slot and replaces whatever this member set, and vice versa.
SignatureSub SetColumnNumericFilter(colIndex As Long, mode As Long, threshold As Double)
Embedded and Virtual grids compare the column's own numeric value regardless of how it is stored. A Turbo column-store grid routes every one of these five operators, but only against a column Turbo column-store has stored as Int32, Int64 or Double -- a Currency, Date, Boolean or String column refuses by name. A fractional threshold against an Int32/Int64-stored column is also refused, rather than being silently truncated (truncation would move the comparison boundary without telling the caller).
Mode 9 (Between) is explicitly rejected here -- it is the one numeric mode that requires SetColumnNumericFilterRange instead.
Shares one numeric slot with SetColumnNumericFilterRange: calling either one replaces whatever the other most recently set on that column.
In Turbo column-store mode, only Int32/Int64/Double-stored columns are supported -- a Currency column (like a fixture's Price column) or a Date column refuses by name even though both are numeric-like.
In Turbo column-store mode, a fractional threshold against an integer-stored column is refused rather than truncated.
An out-of-range colIndex or mode is refused with a runtime error.
See alsoSetColumnNumericFilterRange, ClearColumnFilter, GetFilteredRowCount
SetColumnNumericFilterRangeSorting, filtering and searching
Filter one column to values within an inclusive numeric range.
Sets (or replaces) colIndex's numeric filter slot with an inclusive Between range -- rows where minVal <= value <= maxVal. Shares its slot with SetColumnNumericFilter: whichever of the two is called most recently on a column wins.
SignatureSub SetColumnNumericFilterRange(colIndex As Long, minVal As Double, maxVal As Double)
Same restriction as SetColumnNumericFilter in a Turbo column-store grid: only Int32/Int64/Double-stored columns are supported, and a fractional bound against an integer-stored column is refused rather than truncated.
Shares its slot with SetColumnNumericFilter -- calling this replaces a single-threshold filter previously set on the same column, and vice versa.
Same Turbo column-store type restriction as SetColumnNumericFilter: Int32/Int64/Double-stored columns only; a fractional bound against an integer column is refused there.
See alsoSetColumnNumericFilter, ClearColumnFilter, GetFilteredRowCount
GetDistinctValuesSorting, filtering and searching
Read every distinct displayed value in a column, sorted.
Scans the WHOLE column -- every row, ignoring any filter currently active -- and returns a sorted array of the distinct non-blank displayed values found. Useful for building a value-picker list (see SetColumnValueListFilter) that reflects the real data, not just what a previous filter left visible.
SignatureFunction GetDistinctValues(colIndex As Long) As Variant
Parameters
colIndex (Long) - 0-based column to scan.
ReturnsA 1-D array of distinct String values, sorted ordinally (case-sensitive: uppercase letters sort before lowercase).
Implemented for Embedded grids only. It has no Turbo column-store implementation at all (unlike most other members in this group, which route at least a narrow subset of their behavior there) and, like most data-scanning reads, is also refused in a Virtual grid, since the host -- not the grid -- owns the full set of values there.
Always scans every row directly, ignoring any filter currently active -- it does not report only the values a filtered view currently shows.
Blank/Null cells contribute no entry to the result at all -- they are not represented as an empty-string entry.
Sorted ordinally (case-sensitive), not with a locale-aware collation -- 'apple' would sort after 'Zebra'.
Refused outright in both Virtual and Turbo column-store grids -- there is no partial support here.
See alsoSetColumnValueListFilter, GetColumnValueListFilter
ShowFilterButtonsSorting, filtering and searching
Show or hide a small filter icon in each column header.
When True, each column header shows a filter icon; clicking it opens a checkbox dropdown of that column's distinct values, which is really a convenience UI over the same value-list filter SetColumnValueListFilter sets programmatically. Reading this property works in every mode; only Embedded grids can turn it on.
The getter has no mode restriction at all and can be read in every mode. The setter is Embedded-only -- both a Virtual and a Turbo column-store grid refuse an attempt to turn it on.
Example
VBA
grid.ShowFilterButtons = TrueDebug.Print grid.ShowFilterButtons ' -> True' Each column header now shows a small filter icon the user can click.
Gotchas
The getter works everywhere; only the setter is restricted to Embedded grids.
The dropdown the icon opens is built from the same distinct-value/value-list-filter machinery as GetDistinctValues/SetColumnValueListFilter -- it is UI convenience, not a separate filtering mechanism.
See alsoShowAutoFilterRow, SetColumnValueListFilter, GetDistinctValues
SetColumnValueListFilterSorting, filtering and searching
Filter a column to only the rows whose value is in an explicit list.
Sets (or replaces) colIndex's value-list filter slot -- independent of any text, date or numeric slot already active on the same column. acceptedValues is a 1-D array of Strings (or Variants containing strings); comparison is case-insensitive. Passing Empty, Null, or a zero-length array clears the value-list slot instead of matching nothing.
SignatureSub SetColumnValueListFilter(colIndex As Long, acceptedValues As Variant, includeBlanks As Boolean)
Parameters
colIndex (Long) - 0-based column to filter.
acceptedValues (Variant) - A 1-D array of accepted String values; Empty/Null/zero-length clears the filter.
includeBlanks (Boolean) - Whether a blank cell should still be shown regardless of the accepted list.
Embedded and Virtual grids accept a value-list filter on any column. A Turbo column-store grid routes this only against a column Turbo column-store has stored as String -- any other stored type refuses by name.
Comparison is case-insensitive on both sides (values are normalized before comparing).
Pass Empty (not a zero-length ReDim'd array) to clear the filter. The source does have a count<=0 check that treats a genuinely zero-element array as "clear" too, but a VBA array built with ReDim arr(0 To -1) marshals across late binding with a null underlying SafeArray pointer, which is rejected outright ("acceptedValues array is null") before that check is ever reached - Empty (or Null) hits the control's other, more direct clear path and is the reliable way to do this from VBA.
Shares its own slot: a second SetColumnValueListFilter call on the same column replaces the first, it does not add to it.
In Turbo column-store mode, only String-stored columns are supported.
See alsoGetColumnValueListFilter, GetDistinctValues, ClearColumnFilter
GetColumnFilterModeSorting, filtering and searching
Read which filter mode is active on one column.
Reports the filterMode of whichever filter slot was populated FIRST on colIndex (by insertion order across the column's up-to-four slots), not necessarily the most recently set one and not necessarily the same slot GetColumnFilterPattern describes for the same column. Returns -1 if the column has no active filter at all.
SignatureFunction GetColumnFilterMode(colIndex As Long) As Long
Parameters
colIndex (Long) - 0-based column to query.
ReturnsThe filterMode of the first-set active slot on colIndex, or -1 if none.
Refused in a Turbo column-store grid because it reads the same filter-tracking engine Embedded and Virtual mode share, which stays empty while a grid is in Turbo column-store mode even though a Turbo column-store filter may genuinely be active there -- answering from it would misreport an active Turbo column-store filter as absent, so this refuses by name instead.
With BOTH a numeric and a text filter active on the same column, this reports whichever one was set FIRST -- not the text one, not the numeric one specifically, and not necessarily the same slot GetColumnFilterPattern describes for that column.
Refused outright in Turbo column-store mode, even though a Turbo column-store filter can genuinely be active on the column at the time -- GetFilteredRowCount is the reliable Turbo column-store-safe read instead.
-1 means no filter at all, not a valid mode value.
See alsoGetColumnFilterPattern, SetColumnFilter, GetFilterState
GetColumnFilterPatternSorting, filtering and searching
Read the text/date filter pattern active on one column.
Reports the pattern of whichever TEXT or DATE filter slot is active on colIndex -- it specifically skips a numeric or value-list slot when looking for a pattern to return, even if one of those is the only filter active on the column (or is the slot GetColumnFilterMode just reported for the same column).
SignatureFunction GetColumnFilterPattern(colIndex As Long) As String
Parameters
colIndex (Long) - 0-based column to query.
ReturnsThe active text/date filter's pattern, or an empty string if none (including when the column's only active filter is numeric or value-list).
Refused in Turbo column-store mode for the same reason as GetColumnFilterMode -- it reads the shared filter-tracking engine, which is always empty in Turbo column-store mode.
Returns an empty string if the column's ONLY active filter is numeric or value-list -- it does not fall back to describing those.
Can report a DIFFERENT filter slot's pattern than GetColumnFilterMode's mode number for the very same column, when both a numeric and a text/date filter are active at once: this member always looks for the text/date slot specifically, while GetColumnFilterMode reports whichever slot was set first.
Refused outright in Turbo column-store mode.
See alsoGetColumnFilterMode, SetColumnFilter
GetColumnValueListFilterSorting, filtering and searching
Read the accepted-values list of a column's value-list filter.
Returns the accepted-values array active on colIndex, folded to UPPERCASE regardless of the case originally passed to SetColumnValueListFilter, or VT_EMPTY specifically if no value-list filter is active there.
SignatureFunction GetColumnValueListFilter(colIndex As Long) As Variant
Parameters
colIndex (Long) - 0-based column to query.
ReturnsA 1-D array of accepted String values, each folded to uppercase, or VT_EMPTY if no value-list filter is active.
Refused in Turbo column-store mode for the same reason as GetColumnFilterMode/GetColumnFilterPattern -- it reads the shared filter-tracking engine, which is always empty in Turbo column-store mode.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "North": data(1, 0) = "South"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"Dim accepted(0To0) AsVariant
accepted(0) = "North"
grid.SetColumnValueListFilter 0, accepted, TrueDim vals AsVariant
vals = grid.GetColumnValueListFilter(0)
Debug.Print vals(LBound(vals)) ' -> NORTH (accepted values are folded to uppercase)
Gotchas
Values come back UPPERCASE even if you passed mixed- or lower-case strings to SetColumnValueListFilter -- accepted values are folded to uppercase before they are stored, so "North" is read back here as "NORTH".
No active filter reports VT_EMPTY, not a zero-length array -- check with IsEmpty(), not array bounds.
The order values come back in is unspecified (they come from a set, not the original array order) -- do not depend on it.
Refused outright in Turbo column-store mode.
See alsoSetColumnValueListFilter, GetColumnFilterMode
GetFilterStateSorting, filtering and searching
Serialize every active filter, on every column, to a JSON string.
Captures every filter slot on every column (text, date, numeric and value-list) as an opaque JSON string suitable for SetFilterState or SaveFilterPreset. It captures filters ONLY -- the active sort (Sort/SortEx/SortMulti) is never part of this string.
SignatureFunction GetFilterState() As String
ReturnsA JSON string describing every active filter; "[]" when nothing is filtered.
Refused in Turbo column-store mode for the same reason as the other filter-query members: it serializes the shared filter-tracking engine, which stays empty in Turbo column-store mode even when a Turbo column-store filter is genuinely active, so the result would be misleading rather than merely incomplete.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "North": data(1, 0) = "South"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"Debug.Print grid.GetFilterState ' -> "[]" (nothing filtered yet)
grid.SetColumnFilter 0, 0, "North"Dim state AsString
state = grid.GetFilterState ' now a non-empty JSON description
Gotchas
Does NOT include sort state -- save sort settings (e.g. the column and direction passed to Sort) separately if a saved view needs to restore both.
Treat the returned string as opaque; it is meant to be handed back to SetFilterState or stored inside a preset, not parsed directly.
Refused outright in Turbo column-store mode.
See alsoSetFilterState, SaveFilterPreset
SetFilterStateSorting, filtering and searching
Replace every active filter with one previously captured by GetFilterState.
Parses jsonState and, if valid, REPLACES the entire filter set (every column, every family) with what it describes -- any filter active beforehand that is not named in jsonState is gone afterward. An invalid JSON string is refused and leaves every existing filter completely unchanged; the string is validated before anything is touched.
SignatureSub SetFilterState(jsonState As String)
Parameters
jsonState (String) - A JSON string previously returned by GetFilterState.
Refused in Turbo column-store mode -- a serialized state can carry text, date, numeric and value-list rules across any number of columns at once, more than the narrow subset of filtering Turbo column-store mode routes, so this refuses wholesale rather than attempting a partial restore. LoadFilterPreset is built entirely on top of this member and inherits the same refusal.
REPLACES every filter on every column -- a filter that was active before the call but is not described in jsonState does not survive.
An invalid JSON string is refused and changes nothing -- the existing filters are left exactly as they were, not partially applied.
Refused outright in Turbo column-store mode, which also means LoadFilterPreset (built on top of this member) refuses there too.
See alsoGetFilterState, LoadFilterPreset, ClearAllFilters
ShowFindBarSorting, filtering and searching
Open the built-in find panel, the same one Ctrl+F opens interactively.
Opens the same interactive find/search panel a user's own Ctrl+F keystroke would, so a host can wire a toolbar button or menu item to it without requiring the user to know the shortcut. Nothing about row data changes -- this only opens a UI panel.
Opens the identical Win32 panel in Embedded and Turbo column-store grids -- what differs between the two is only what the panel's own search can find, not whether the panel itself opens. Refused in a Virtual grid, since the panel's search would have nothing of the host's own data to search through.
Example
VBA
grid.ShowFindBar ' opens the same panel Ctrl+F would' ... user searches interactively ...
grid.HideFindBar ' close it again, e.g. from your own toolbar button
Gotchas
There is no member in this API that reports whether the panel is currently visible -- track that yourself if your UI needs to know.
Always call HideFindBar once your own UI is finished with it; nothing else closes the panel automatically.
See alsoHideFindBar
HideFindBarSorting, filtering and searching
Close the find panel opened by ShowFindBar (or by the user's own Ctrl+F).
Closes the find panel. Safe to call even if the panel was never shown.
Same mode support as ShowFindBar: implemented in Embedded and Turbo column-store, refused in Virtual.
Example
VBA
grid.ShowFindBar
grid.HideFindBar ' closes it again
grid.HideFindBar ' safe to call again -- no error even though it is already closed
Gotchas
Idempotent: calling it when the panel is already closed (or was never opened) raises no error.
See alsoShowFindBar
ShowAutoFilterRowSorting, filtering and searching
Show or hide a row of per-column text boxes for quick filtering.
When True, a real text box appears below the header for every column; typing into one (after a short pause) automatically applies a Contains text filter on that column, using the exact same mechanism SetColumnFilter(col, 0, text) would. Reading this property works in every mode; only Embedded grids can turn it on.
The getter has no mode restriction and can be read in every mode. The setter is Embedded-only -- both a Virtual and a Turbo column-store grid refuse an attempt to turn it on.
Example
VBA
grid.ShowAutoFilterRow = TrueDebug.Print grid.ShowAutoFilterRow ' -> True' A text box now appears under each header; typing into one filters that column.
grid.ShowAutoFilterRow = False' The text boxes are gone, but any filter one of them created is still active --' call ClearAllFilters explicitly if hiding the row should also clear it.
Gotchas
The getter works everywhere; only the setter is restricted to Embedded grids.
Each text box drives an ordinary Contains filter on that column's text slot -- it shares that slot with any Contains/StartsWith filter set programmatically via SetColumnFilter.
Setting this property back to False does not clear the Contains filters the row's text boxes may have created -- those filters remain active (GetFilteredRowCount stays reduced) until cleared explicitly with ClearColumnFilter or ClearAllFilters.
See alsoShowFilterButtons, SetColumnFilter
SaveFilterPresetSorting, filtering and searching
Save the current filter state under a name for later recall.
Captures the same information GetFilterState would return and stores it in an in-memory name-to-state map on the control instance, under presetName. Not persisted to a file or the registry -- it is lost when the control is destroyed. Saving under a name that already exists silently overwrites the previous preset with no error or confirmation.
SignatureSub SaveFilterPreset(presetName As String)
Parameters
presetName (String) - The name to save the current filter state under; cannot be empty.
Refused in Turbo column-store mode for the same reason GetFilterState is: it would capture the shared filter-tracking engine's state, which is always empty in Turbo column-store mode, silently recording 'no filter' even if a real Turbo column-store filter is active -- loading that preset back later in an Embedded grid would then wrongly clear the user's real filters.
Presets are in-memory only on the control instance -- not written to disk or the registry, and gone once the control is destroyed.
Neither Clear() nor LoadFromArray removes a saved preset -- only DeleteFilterPreset does.
Saving under a name that already exists overwrites it silently, with no error or warning.
An empty presetName is refused.
Refused outright in Turbo column-store mode.
See alsoLoadFilterPreset, DeleteFilterPreset, GetFilterPresetNames, GetFilterState
LoadFilterPresetSorting, filtering and searching
Restore a filter state previously saved with SaveFilterPreset.
Looks up presetName in the in-memory preset map and, if found, restores it via the same replace-everything logic SetFilterState uses. A name that was never saved is refused regardless of data mode.
SignatureSub LoadFilterPreset(presetName As String)
Parameters
presetName (String) - A name previously passed to SaveFilterPreset.
The name lookup itself is mode-independent -- an unknown name is refused in every mode. Once a name IS found, this member forwards to SetFilterState, which refuses in Turbo column-store mode; since SaveFilterPreset itself already refuses in Turbo column-store mode, no preset can ever exist to load while a grid is in Turbo column-store mode anyway.
An unknown preset name is refused with its own error, independent of data mode -- checked before the mode-dependent restore step runs.
Because SaveFilterPreset refuses in Turbo column-store mode, this member has no real preset to load while a grid is running in Turbo column-store mode.
Like SetFilterState, restoring a preset REPLACES the entire filter set, not just the columns the preset happens to mention.
See alsoSaveFilterPreset, SetFilterState, DeleteFilterPreset
DeleteFilterPresetSorting, filtering and searching
Remove a named filter preset.
Removes presetName from the in-memory preset map. Deleting a name that was never saved is a silent no-op, not an error. Unlike SaveFilterPreset and LoadFilterPreset, this member never touches the live filter state, so it works in every data mode including Turbo column-store.
SignatureSub DeleteFilterPreset(presetName As String)
Deliberately not refused in Turbo column-store mode, unlike SaveFilterPreset/LoadFilterPreset: it only ever edits the preset name map, never the live filter state that differs by mode.
Example
VBA
If grid.DataMode <> 2Then' SaveFilterPreset is refused in Turbo column-store mode
grid.SaveFilterPreset "Temp"EndIf
grid.DeleteFilterPreset "Temp"' safe even if never saved (e.g. in Turbo column-store mode) or already deleted
grid.DeleteFilterPreset "Temp"' safe to call again
Gotchas
Deleting an unknown name raises no error -- it is a safe no-op.
Works in every data mode, including Turbo column-store, even though SaveFilterPreset (which is what would have created the preset) refuses there.
See alsoSaveFilterPreset, GetFilterPresetNames
GetFilterPresetNamesSorting, filtering and searching
Read every saved filter preset's name as a JSON array.
Returns every name currently in the in-memory preset map as a JSON array of strings. Works in every data mode, including Turbo column-store, since it never touches the live filter state.
SignatureFunction GetFilterPresetNames() As String
ReturnsA JSON array of preset name strings; "[]" if none exist.
Returns "[]" (a valid empty JSON array), never an error, when no presets exist.
Preset names are JSON-escaped, so a name containing a quote or backslash round-trips safely.
The order names appear in is unspecified -- not necessarily save order or alphabetical.
See alsoSaveFilterPreset, DeleteFilterPreset
Editing, validation, undo and paste
This group covers what happens between a user starting to type into a cell and that value landing in the grid's data, plus the two independent safety nets sitting on either side of that moment: validation, which can refuse a bad value before it is ever written, and undo, which can reverse a value that already was written. AllowEditing is the single on/off switch for every INTERACTIVE way to start an edit -- double-click, F2, and Enter-on-a-focused-cell all call the same internal edit-start routine, and all three are blocked together when it is False. PasteFromClipboard and PasteSpecial are, perhaps surprisingly, controlled by the very same flag, even though pasting is not usually thought of as the same gesture as typing -- with AllowEditing set to False, a paste silently does nothing at all. None of this ever touches SetCell or SetCellValue: a programmatic write always succeeds regardless of AllowEditing, so a grid that looks read-only to the user can still be updated freely from code. EditAdvanceOnEnter changes what happens the instant an interactive edit commits with Enter -- normally nothing beyond the commit itself, but with this property set to True, focus and a brand-new editor jump straight to the same column on the next row, so a user can type down a column without ever reaching for the mouse. This behavior is real and current for an Embedded grid, but a Turbo column-store grid deliberately excludes it: Turbo column-store now supports genuine interactive cell editing (typing into a cell, pressing Enter, having it stick), yet Enter there always just commits once and stops, regardless of what this property is set to -- a scope decision recorded directly in the source, not an oversight.
AddValidationRule, RemoveValidationRule and ClearValidationRules manage a set of per-column rules (Required, MinLength, MaxLength, Regex, NumericRange, CustomList) that are genuinely evaluated -- unlike some other rule-shaped members elsewhere in this control, nothing here is inert. The catch is exactly when they run: a rule is only ever consulted at the moment an INTERACTIVE edit commits (the plain in-cell editor, a dropdown selection, or a date-picker commit) -- never from a programmatic SetCell or SetCellValue call, which always succeeds no matter what rules are attached to that column. Rule management itself (adding, removing, clearing) works in every data mode, including Virtual, but a rule set on a Virtual grid can never actually reject anything, because interactive editing itself is refused outright there regardless of validation. In both Embedded and Turbo column-store grids, where interactive editing is genuinely possible, a rejected edit fires ValidationFailed, leaves the cell exactly as it was, and pushes nothing to undo history.
Undo, Redo, CanUndo, CanRedo and ClearUndoHistory hold this group's least intuitive fact: the two data modes do not track the same things. An Embedded grid's undo history is narrow by design (documented directly in the source as a deliberate scope decision) -- it captures ONLY a programmatic SetCell or SetCellValue call. An edit a user types directly into a cell through the in-cell editor and commits with Enter is, despite looking identical to a scripted edit, never pushed to that history at all; Ctrl+Z has nothing to revert after it. Row operations (AddRow, InsertRow, DeleteRow) are not tracked either. A Turbo column-store grid's own, completely separate undo history is actually the more complete of the two: it covers a programmatic SetCell/SetCellValue write, an interactively typed and committed edit, AND a row insert or delete, all through the same underlying mechanism. Each history keeps at most the 50 most recent steps, oldest dropped first, with no way to change that limit from this surface. BeginUndoGroup and EndUndoGroup fold a run of Embedded edits between them into one undo step reversed by a single Undo call; they only exist for Embedded's own history and are refused outright in both Virtual and Turbo column-store grids -- there is no grouped-undo equivalent for Turbo column-store's own history at all. Undo and Redo themselves are refused only in a Virtual grid (the host owns that data, so there is nothing local to reverse); calling either from inside an event handler is also refused, since a handler is already mid-mutation.
PasteFromClipboard and PasteSpecial both parse the clipboard's own tab-and-newline-delimited text (the same layout Excel produces on a copy) and write it starting at the currently focused cell, one clipboard line per row and one tab-delimited field per column. Every pasted cell is written as a plain STRING regardless of what the target column previously held or how it is typed -- there is no numeric or date parsing on the way in. PasteSpecial adds one capability PasteFromClipboard has no way to reach: pasteMode 1 (SkipBlanks) leaves a target cell untouched wherever the corresponding clipboard field is empty, instead of overwriting it with blank text. Neither member is tracked by undo history at all, even though both plainly change cell values -- a paste can never be reversed with Undo. Both refuse outright in a Virtual grid (the host owns the data) and in a Turbo column-store grid (pasting has no Turbo column-store implementation), leaving the view completely unchanged in either case.
RecipeReject bad input outright, but let the user undo input that was merely a mistake
VBA
Sub ValidateThenAllowUndo(grid AsObject)
Dim data(0To2, 0To1) AsVariant
data(0, 0) = "Widget A": data(0, 1) = CDbl(10)
data(1, 0) = "Widget B": data(1, 1) = CDbl(25)
data(2, 0) = "Widget C": data(2, 1) = CDbl(40)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Product"
grid.SetColumnHeader 1, "Price"' A Required rule on Product stops an interactive edit from ever' clearing it out -- but this has NO effect on code-driven writesDim requiredRuleId AsLong
requiredRuleId = grid.AddValidationRule(0, 0, Empty, 0, 0) ' 0 = Required' A programmatic write still succeeds even though it violates the rule --' validation only ever runs on an interactive edit commit
grid.SetCell 0, 0, ""Debug.Print grid.GetCellValue(0, 0) ' -> "" (the Required rule was never consulted)' Undo can put that back, because SetCell IS tracked by undo history
grid.Undo
Debug.Print grid.GetCellValue(0, 0) ' -> Widget A' A genuine interactive edit that fails the Required rule (a user' selects the cell, deletes the text, and presses Enter) is rejected at' the moment it is typed -- the cell never changes, so there is nothing' for Undo to do about it; validation and undo solve two different' problems and never overlap.
grid.RemoveValidationRule requiredRuleId
EndSub
Validation and undo sit on opposite sides of the same moment: validation can only stop an interactive edit before it is written, and only ever runs on that one commit path, so it is blind to anything written from code. Undo can only reverse a write that already happened, and in an Embedded grid that means only a SetCell/SetCellValue call -- a user's own typed-and-committed edit is invisible to undo history the same way it would have been visible to validation. Relying on validation alone to guarantee clean data is a mistake if the same grid is ever updated from VBA.
RecipeLet the user undo a batch of related edits as a single step
VBA
Sub GroupedEditsAsOneUndo(grid AsObject)
Dim data(0To0, 0To2) AsVariant
data(0, 0) = "Alice": data(0, 1) = "Sales": data(0, 2) = CDbl(50000)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Department"
grid.SetColumnHeader 2, "Salary"If grid.DataMode = 2Then' BeginUndoGroup/EndUndoGroup are Embedded-only -- refused outright in' a Turbo column-store grid. Each SetCell there is still individually' undoable on Turbo column-store's own history; it is only the GROUPING that is' unavailable, so a promotion still takes two separate Undo calls' to fully reverse in Turbo column-store mode.
grid.SetCell 0, 1, "Marketing"
grid.SetCell 0, 2, CDbl(60000)
grid.Undo ' reverts only the Salary change
grid.Undo ' reverts the Department change tooElse' A "promote this employee" action changes two related columns at' once -- group them so the user's single Ctrl+Z undoes the whole' promotion, not just half of it
grid.BeginUndoGroup
grid.SetCell 0, 1, "Marketing"
grid.SetCell 0, 2, CDbl(60000)
grid.EndUndoGroup
Debug.Print grid.GetCellValue(0, 1) & " / " & grid.GetCellValue(0, 2) ' -> Marketing / 60000
grid.Undo ' one call, both columns revert togetherDebug.Print grid.GetCellValue(0, 1) & " / " & grid.GetCellValue(0, 2) ' -> Sales / 50000EndIfEndSub
Without a group, two SetCell calls are two separate undo steps, so a single Undo would only revert the Salary change and leave the Department change in place -- confusing for an action the user thinks of as one thing. BeginUndoGroup/EndUndoGroup exist to fix exactly that, but only for Embedded's own undo history; Turbo column-store has no grouping concept at all, so a caller who needs one compound step in Turbo column-store mode has to call Undo as many times as there were SetCell calls, or track the count itself.
RecipeCopy a row to the clipboard and paste it elsewhere, without losing existing values that were never copied
VBA
Sub CopyAndPasteSkippingBlanks(grid AsObject)
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "": data(0, 1) = "REV-2"' source row -- Name left blank on purpose
data(1, 0) = "Keep Me": data(1, 1) = "REV-1"' target row -- already has a name
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Revision"If grid.DataMode <> 2Then
grid.SelectRow 0
grid.CopyWithHeaders False' clipboard: "<TAB>REV-2" -- Name field is empty
grid.SelectBlock 1, 0, 1, 0' move the paste target to row 1, column 0
grid.PasteSpecial 1' 1 = SkipBlanksDebug.Print grid.GetCellValue(1, 0) ' -> Keep Me (the blank clipboard field did not overwrite it)Debug.Print grid.GetCellValue(1, 1) ' -> REV-2 (a non-blank field still overwrites normally)' Neither PasteFromClipboard nor PasteSpecial is undoable -- if this' paste needs to be reversible, capture the old values before pastingDebug.Print grid.CanUndo ' -> FalseEndIf' PasteFromClipboard/PasteSpecial have no Turbo column-store or Virtual implementation' at all -- both refuse outright in either mode, leaving the view exactly' as it was, so this recipe only runs its paste in an Embedded grid.EndSub
PasteSpecial's SkipBlanks mode is the only way to merge clipboard data into existing rows without clobbering cells the clipboard has nothing for -- PasteFromClipboard (equivalent to PasteSpecial's default ValuesOnly mode) always overwrites every cell in the pasted range, blank or not. Because neither paste member is tracked by undo history, a paste that needs to be undoable has to be handled by the caller's own code -- reading the target cells before pasting and writing them back if the user asks to undo.
AllowEditingEditing, validation, undo and paste
Turn every interactive way of starting a cell edit on or off.
Governs whether a double-click, F2, or Enter-on-a-focused-cell is allowed to open the in-cell editor -- all three call the same internal edit-start routine, and this property is the one switch that all three share. PasteFromClipboard and PasteSpecial also check this flag and do nothing at all when it is False, and so does the same paste routine when it is triggered by the Ctrl+V keyboard shortcut or by the grid's own right-click Paste command, even though pasting is not usually thought of as the same gesture as typing into a cell. The built-in Find and Replace panel's Replace and Replace All buttons are gated by this flag too, independently of the edit-start and paste checks. Programmatic writes (SetCell, SetCellValue) are a completely different path and are never affected by this property either way -- a grid with AllowEditing = False can look entirely read-only to the user while still being updated freely from VBA code.
SignatureProperty AllowEditing As Boolean
ReturnsTrue if interactive editing (and pasting) is currently allowed.
The property itself has no mode restriction and can be set and read in every mode. Its real effect follows wherever interactive editing is itself possible: in an Embedded or Turbo column-store grid (Turbo column-store now supports genuine interactive cell editing), setting this to False blocks the double-click/F2/Enter edit-start path, blocks PasteFromClipboard/PasteSpecial/Ctrl+V/the right-click Paste command the same way, and also blocks the built-in Find and Replace panel's Replace buttons. In a Virtual grid, interactive editing is already refused unconditionally regardless of this property, so setting it there has no additional observable effect.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.AllowEditing = False' A user's double-click or F2 no longer opens the in-cell editor, but code' can still update the grid directly
grid.SetCell 0, 0, "Updated By Code"Debug.Print grid.GetCellValue(0, 0) ' -> Updated By Code
grid.AllowEditing = True' interactive editing (and pasting) is back on
Gotchas
Blocks only the paths that call the internal edit-start routine (double-click, F2, Enter-to-edit), the shared paste routine (PasteFromClipboard, PasteSpecial, Ctrl+V, and the grid's own right-click Paste command), and the built-in Find and Replace panel's Replace/Replace All buttons -- SetCell and SetCellValue always succeed regardless of this flag, so a grid that looks locked down to the user can still be updated from code.
Double-clicking a cell with AllowEditing = False still fires ItemDoubleClick -- only the editor itself (and, for a double-click specifically, the edit it would have started) is suppressed, not the click notification.
PasteFromClipboard and PasteSpecial silently do nothing when this is False -- no error is raised, the clipboard is not even read. The grid's own right-click context menu also grays out its Paste entry in that state.
See alsoEditAdvanceOnEnter, PasteFromClipboard, PasteSpecial
EditAdvanceOnEnterEditing, validation, undo and paste
Make Enter commit a cell edit and immediately open the editor on the row below.
When True, pressing Enter while a cell editor is open commits that edit and then opens a brand-new editor on the SAME column, one row down, letting a user type down a column without touching the mouse. When False (the default), Enter only commits -- focus and the editor both stay put. This only ever fires from a REAL Enter keypress while an editor is genuinely open (via double-click, F2, or Enter itself); there is no way to trigger the advance-and-reopen behavior from VBA code alone.
SignatureProperty EditAdvanceOnEnter As Boolean
ReturnsTrue if Enter advances to the next row's editor after committing.
The property itself has no mode restriction anywhere. Its effect is fully implemented for an Embedded grid. In a Turbo column-store grid, Enter still commits a real, live interactive edit -- Turbo column-store now supports genuine interactive cell editing -- but the advance-to-next-row behavior is explicitly excluded there in the source, regardless of what this property is set to: Enter in Turbo column-store mode commits once and stops. In a Virtual grid, interactive editing is refused outright, so this property can be set and read but never has anything observable to affect.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Row 1": data(1, 0) = "Row 2": data(2, 0) = "Row 3"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.EditAdvanceOnEnter = True' In a running form, a user who double-clicks row 0, types a new value, and' presses Enter now commits that edit AND immediately opens row 1's editor' in the same column -- useful for fast, keyboard-only data entry down a' column. This has no effect in a Turbo column-store grid: there, Enter' always just commits and stops, regardless of this property.
Gotchas
In a Turbo column-store grid, this property can be set and read normally, but Enter never advances to the next row's editor there -- it commits the current edit and stops, no matter what this property is set to. This is a deliberate, narrower scope decision recorded in the source, not an unimplemented corner.
Advancing stops at the LAST row -- Enter on the final row's editor commits normally; it does not wrap around to row 0 or create a new row.
Only ever triggered by a genuine Enter keypress while an editor is already open (started by double-click, F2, or a prior Enter) -- there is no COM member that opens a cell editor programmatically, so this behavior cannot be exercised from code alone.
See alsoAllowEditing, Undo
AddValidationRuleEditing, validation, undo and paste
Attach a validation rule to a column that is enforced on every interactive edit commit.
Adds one rule to colIndex and returns a ruleId identifying it (for later use with RemoveValidationRule). A column may carry any number of rules, evaluated in the order they were added; the first one a value fails is the one reported. The rule is genuinely evaluated -- not just recorded -- but ONLY at the moment an interactive edit commits (the plain in-cell editor, a dropdown selection, or a date-picker commit): a failing value is rejected, the cell keeps its old value, and ValidationFailed fires with a message. A programmatic SetCell or SetCellValue call is never checked against any rule, no matter what is attached to that column.
SignatureFunction AddValidationRule(colIndex As Long, ruleType As Long, param1 As Variant, param2 As Variant) As Long
Parameters
colIndex (Long) - 0-based column the rule applies to.
param1 (Variant) - Meaning depends on ruleType: ignored for Required; the minimum/maximum length (numeric) for MinLength/MaxLength; the pattern (string) for Regex; the minimum value (numeric) for NumericRange; the semicolon-separated allowed list (string) for CustomList.
param2 (Variant) - Only used by NumericRange, as the maximum value (numeric); ignored by every other ruleType.
ReturnsA positive rule id identifying the new rule, for use with RemoveValidationRule.
Value
Meaning
0
Required -- the committed text must not be empty.
1
MinLength -- the committed text's length must be >= param1.
2
MaxLength -- the committed text's length must be <= param1.
3
Regex -- the committed text must fully match the param1 pattern.
4
NumericRange -- the committed text, parsed as a number, must fall between param1 and param2 inclusive; an empty commit is allowed through (pair with a Required rule to also forbid that).
5
CustomList -- the committed text must exactly match one entry in param1's semicolon-separated list.
Rule management itself has no mode restriction and works identically everywhere. Whether a rule can ever actually reject anything follows wherever interactive editing itself is possible: in Embedded and Turbo column-store grids (Turbo column-store's own CommitCellEditTurbo runs the identical, mode-agnostic validation check before it even parses the typed text into a value), a rule genuinely blocks a bad interactive commit. In a Virtual grid, a rule can be added successfully but can never fire, because interactive editing is refused outright there regardless of validation.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alice": data(1, 0) = "Bob"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"Dim ruleId AsLong
ruleId = grid.AddValidationRule(0, 0, Empty, 0) ' 0 = Required' A rule only takes effect while a user is typing into the cell and commits' with Enter or by tabbing away -- it is never checked against a' programmatic SetCell/SetCellValue call
grid.SetCell 0, 0, ""Debug.Print grid.GetCellValue(0, 0) ' -> "" (the Required rule was never consulted)
grid.RemoveValidationRule ruleId
Gotchas
Never consulted by a programmatic SetCell or SetCellValue call -- only an interactive edit commit (typing into the plain editor, choosing a dropdown item, or picking a date) is checked against any rule.
colIndex is bounds-checked (a negative value or one >= the column count is refused); ruleType outside 0-5 is refused.
A rule added on a Virtual grid is accepted and stored, but interactive editing is refused there unconditionally, so the rule can never actually reject a value.
Multiple rules on the same column are evaluated in the order they were added, and only the FIRST failing rule's message is reported -- later rules on that column are never reached for that commit.
See alsoRemoveValidationRule, ClearValidationRules, AllowEditing
RemoveValidationRuleEditing, validation, undo and paste
Remove one validation rule by the id AddValidationRule returned for it.
Removes exactly the rule identified by ruleId, leaving every other rule on that column (and every rule on other columns) untouched. Once removed, a value that previously failed that specific rule is accepted on the next interactive commit, unless another rule on the same column still rejects it.
SignatureSub RemoveValidationRule(ruleId As Long)
Parameters
ruleId (Long) - The id AddValidationRule returned when the rule was created.
Has no mode restriction at all -- it only ever touches the rule list itself, which is mode-agnostic. Its downstream effect (a previously-rejected value now being accepted) is only observable wherever interactive editing is itself possible: Embedded and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Alice"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"Dim ruleId AsLong
ruleId = grid.AddValidationRule(0, 1, Empty, 0) ' 1 = MinLength, param1 = Empty (0)' ... a user's typed edits are now checked against this rule ...
grid.RemoveValidationRule ruleId
' ... that rule no longer applies' Removing an id that was never issued (or already removed) is silently' ignored -- it never raises an error
grid.RemoveValidationRule 999999
Gotchas
Removing a ruleId that was never issued (or was already removed) is a silent no-op -- no error is raised.
There is no way to look up which rules exist or what ruleId a given rule has other than the value AddValidationRule returned when it was created -- hold onto that value if the rule may need to be removed individually later.
See alsoAddValidationRule, ClearValidationRules
ClearValidationRulesEditing, validation, undo and paste
Remove every validation rule on one column, or on every column at once.
Removes every rule attached to colIndex. Passing -1 removes every rule on every column instead, regardless of how many columns exist or how many rules each carries.
SignatureSub ClearValidationRules(colIndex As Long)
Parameters
colIndex (Long) - 0-based column to clear rules from, or -1 for every column.
Has no mode restriction -- it only ever touches the rule list, which is mode-agnostic. Its downstream effect follows wherever interactive editing is itself possible: Embedded and Turbo column-store grids.
Example
VBA
grid.AddValidationRule 0, 0, Empty, 0' Required, column 0
grid.AddValidationRule 1, 0, Empty, 0' Required, column 1
grid.ClearValidationRules 0' only column 0's rules are removed' column 1's Required rule is still active
grid.ClearValidationRules -1' -1 clears every column's rules at once
Gotchas
colIndex is NEVER bounds-checked against the column count -- a positive value with no matching column is a silent no-op, and ANY negative value (not just exactly -1) is treated the same as -1 and clears every column's rules.
A column with no rules at all is a safe no-op.
Clear() and LoadFromArray both reset validation rules back to none -- a fresh T.LoadFixture-style reload does not need an explicit ClearValidationRules -1 first.
See alsoAddValidationRule, RemoveValidationRule
UndoEditing, validation, undo and paste
Reverse the most recent undoable change.
Reverts the top entry of the current undo history and returns True if something was actually undone, False if the history was already empty (this is a normal, non-error outcome, not a refusal). What counts as "undoable" is NOT the same in every mode. In an Embedded grid, only a programmatic SetCell or SetCellValue call is ever pushed to undo history -- an edit a user types directly into a cell through the in-cell editor and commits with Enter, F2, or by tabbing away is never tracked, no matter how it looks to the user; row operations (AddRow, InsertRow, DeleteRow) are not tracked either. A Turbo column-store grid keeps a completely separate history that is, perhaps counter-intuitively, the more complete of the two: it tracks a programmatic SetCell/SetCellValue write, an interactively typed and committed edit, AND a row insert or delete, all through the same mechanism.
SignatureFunction Undo() As Boolean
ReturnsTrue if a change was undone; False if there was nothing to undo.
Refused only in a Virtual grid, where the host owns the data and there is nothing local for the grid itself to reverse. Fully implemented in both Embedded and Turbo column-store grids, but the two keep entirely separate histories with different scopes -- see this member's own description for exactly what each one tracks. Also refused if called from inside an event handler, since a handler already represents an in-progress mutation.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetCell 0, 0, "Changed"Debug.Print grid.GetCellValue(0, 0) ' -> ChangedDim undone AsBoolean
undone = grid.Undo()
Debug.Print undone ' -> TrueDebug.Print grid.GetCellValue(0, 0) ' -> Original' An edit typed directly into the cell by a user (rather than made through' code) is NOT tracked by undo history in an Embedded grid -- only SetCell/' SetCellValue calls are. A Turbo column-store grid tracks both, and also' tracks AddRow/InsertRow/DeleteRow, which an Embedded grid never does.
Gotchas
In Embedded mode, an interactively typed cell edit (through the in-cell editor) is NEVER pushed to undo history -- only SetCell/SetCellValue calls are. Pressing Ctrl+Z (or calling Undo from code) right after a user finishes typing into a cell has nothing to revert.
In Turbo column-store mode, the SAME interactively typed edit IS undoable, and so is a row deleted or inserted with DeleteRow/AddRow/InsertRow -- Turbo column-store's own history is broader than Embedded's, not narrower.
Each history (Embedded's own, and Turbo column-store's separate one) keeps at most the 50 most recent steps -- older entries are silently dropped, oldest first, with no way to change that limit from this surface.
Calling Undo from inside an event handler raises an error rather than doing anything -- finish the handler first.
Returns False, not an error, when there is nothing left to undo.
Calling Clear() in Embedded mode does not reset the undo/redo history the way LoadFromArray does (see ClearUndoHistory's own gotchas) -- if an edit was pending, CanUndo can still read True afterward even though the grid is now empty, and calling Undo() in that state returns True (reporting a change) while silently doing nothing, because the row it would restore no longer exists.
See alsoRedo, CanUndo, ClearUndoHistory, BeginUndoGroup
RedoEditing, validation, undo and paste
Re-apply the most recently undone change.
Re-applies the top entry of the current redo history (built up only by prior calls to Undo) and returns True if something was actually redone, False if the redo history was already empty. Making any NEW change after an Undo discards the redo history that Undo had just created -- redo is only possible up until the next fresh edit.
SignatureFunction Redo() As Boolean
ReturnsTrue if a change was redone; False if there was nothing to redo.
Refused only in a Virtual grid, for the same reason Undo is. Fully implemented in both Embedded and Turbo column-store grids, and each mode's redo history covers exactly what that mode's own undo history covers -- see Undo's own description for the difference between the two.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetCell 0, 0, "Changed"
grid.Undo
Debug.Print grid.CanRedo ' -> TrueDim redone AsBoolean
redone = grid.Redo()
Debug.Print redone ' -> TrueDebug.Print grid.GetCellValue(0, 0) ' -> Changed
grid.SetCell 0, 0, "Different"' a new edit discards the redo history aboveDebug.Print grid.CanRedo ' -> False
Gotchas
A brand-new edit (SetCell, SetCellValue, or -- in Turbo column-store mode only -- an interactively typed edit, AddRow, InsertRow or DeleteRow) clears the redo history immediately, even if it happens right after an Undo -- redo one step, then make a different change, and the original redo is gone.
Returns False, not an error, when there is nothing left to redo.
Also refused if called from inside an event handler, the same as Undo.
The same gap that affects Undo applies here: Clear() in Embedded mode does not reset the redo history either (only LoadFromArray does -- see ClearUndoHistory's own gotchas), so a redo entry created by an Undo just before Clear() can survive it; calling Redo() afterward returns True but changes nothing, since the row it would restore no longer exists.
See alsoUndo, CanRedo, ClearUndoHistory
CanUndoEditing, validation, undo and paste
Check whether Undo currently has anything to revert.
True whenever the current mode's undo history (Embedded's own, or Turbo column-store's separate one) has at least one entry; False when it is empty, either because nothing undoable has happened yet or because everything has already been undone. Reads whichever history the grid's current data mode actually uses -- see Undo's own description for exactly what each mode tracks.
Has no mode restriction at all -- it is a plain read of whichever history the current data mode uses. In a Virtual grid it reads the Embedded history (since Virtual has no history of its own); nothing in Virtual mode itself ever pushes to that history, since Virtual's own interactive editing is refused outright. But the value it reports right after entering Virtual mode depends on Embedded's history at the moment of the switch: entering Virtual mode always requires an empty Embedded store first (typically via Clear()), and Clear() does NOT reset Embedded's undo history (see this member's own gotchas) -- so CanUndo can read True in a freshly-entered Virtual grid if an edit was left pending just before that Clear(), even though calling Undo() there is always refused.
Reflects Embedded's narrower history or Turbo column-store's broader one depending on the CURRENT data mode -- see Undo's own gotchas for exactly what counts in each.
Stays True across MULTIPLE pending undo steps, not just one -- it only reports whether the history is non-empty, not how many steps it holds.
Clear() in Embedded mode does not reset the undo history (only LoadFromArray does -- see ClearUndoHistory's own gotchas). Since entering Virtual mode always requires an empty Embedded store first, typically via Clear(), an edit made just before that Clear() can leave CanUndo reporting True even once the grid is in Virtual mode, where Undo() itself is always refused.
See alsoUndo, CanRedo, ClearUndoHistory
CanRedoEditing, validation, undo and paste
Check whether Redo currently has anything to re-apply.
True whenever the current mode's redo history has at least one entry (built up only by a prior Undo); False otherwise, including right after any new edit, which always discards the redo history immediately.
Has no mode restriction -- a plain read of whichever redo history the current data mode uses. In a Virtual grid it usually reads False, since nothing in Virtual mode ever pushes to Embedded's history and Virtual has none of its own -- but see this member's own gotchas for the one path (a stale entry surviving the Clear() that entering Virtual mode requires) where it can read True instead.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"Debug.Print grid.CanRedo ' -> False
grid.SetCell 0, 0, "Changed"
grid.Undo
Debug.Print grid.CanRedo ' -> True (the undone edit can be redone)
Gotchas
Goes back to False the moment any new edit happens, even one made right after the Undo that had just set it to True -- redo is a use-it-or-lose-it window.
ClearUndoHistory drops pending redo entries too, not just pending undo entries, despite its name only mentioning undo.
The same Clear()-does-not-reset-history gap that affects CanUndo (see its own gotchas) applies to CanRedo too -- a stale redo entry from before a Clear() in Embedded mode can leave CanRedo reporting True even once the grid is in Virtual mode, where Redo() itself is always refused.
See alsoRedo, CanUndo, ClearUndoHistory
ClearUndoHistoryEditing, validation, undo and paste
Discard the entire undo and redo history without touching current data.
Empties both the undo stack and the redo stack for the current mode's history in one call. The data itself is completely unaffected -- whatever the grid currently shows stays exactly as it is; only the ABILITY to undo or redo past changes is removed.
Has no mode restriction at all -- it clears whichever history the current data mode uses (Embedded's own m_undoManager, or Turbo column-store's separate stack), and is a safe no-op if that history is already empty.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetCell 0, 0, "Changed"
grid.ClearUndoHistory
Debug.Print grid.CanUndo ' -> FalseDebug.Print grid.GetCellValue(0, 0) ' -> Changed (clearing the HISTORY does not revert the data)
Gotchas
Clears the DATA'S ability to be undone, not the data itself -- an edit already applied stays applied; only the history entry describing it is gone.
Clears pending REDO entries too, not just pending undo entries.
LoadFromArray always resets the current mode's undo/redo history as part of loading new data, and in Turbo column-store mode Clear() does too, through the same table-reattach sequence a fresh load uses. In EMBEDDED mode, however, Clear() does NOT reset the undo/redo history at all -- an edit made just before Clear() can leave CanUndo reporting True even though the grid is now empty, and Undo() will then report True (claiming a change was made) while silently doing nothing, because the row it targets no longer exists. Call ClearUndoHistory explicitly after Clear() in Embedded mode if a clean history matters.
See alsoCanUndo, CanRedo, Undo
BeginUndoGroupEditing, validation, undo and paste
Start folding the following Embedded-mode edits into one undo step.
Every SetCell/SetCellValue call made after this, up to the matching EndUndoGroup, is collected into a single compound entry on Embedded's own undo history, reversed as one unit by a single Undo call rather than one call per edit. Calling this a second time while a group is already open does nothing -- there is no nesting, and the group started by the FIRST call is still the one that gets closed by the next EndUndoGroup.
Grouped undo only exists for Embedded's own undo history -- it is refused outright in both Virtual and Turbo column-store grids. There is no grouped-undo equivalent for Turbo column-store's own (otherwise more complete) history: each SetCell/interactive edit/row operation there is always its own separate undo step.
Example
VBA
Dim data(0To0, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = "30"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Age"
grid.BeginUndoGroup
grid.SetCell 0, 0, "Bob"
grid.SetCell 0, 1, "40"
grid.EndUndoGroup
' Both changes above count as ONE undo step
grid.Undo
Debug.Print grid.GetCellValue(0, 0) & " / " & grid.GetCellValue(0, 1) ' -> Alice / 30' Not available in a Turbo column-store grid
Gotchas
No nesting: calling this again while a group is already open is a no-op -- it does not need a matching second EndUndoGroup, and only counts as one group overall.
Refused in a Turbo column-store grid even though ordinary (ungrouped) Undo/Redo work there -- a caller needing one compound undo step in Turbo column-store mode has to call Undo repeatedly instead.
An edit made OUTSIDE any Begin/End pair is tracked as its own ordinary, separate undo step, exactly as if grouping had never been used.
See alsoEndUndoGroup, Undo, CanUndo
EndUndoGroupEditing, validation, undo and paste
Close a compound undo group started by BeginUndoGroup.
Closes the group opened by the most recent BeginUndoGroup and pushes it to Embedded's undo history as ONE entry -- unless no edits happened inside the group, in which case nothing is pushed at all. Calling this with no group currently open is a safe no-op.
Refused in both Virtual and Turbo column-store grids, the same as BeginUndoGroup -- grouped undo exists only for Embedded's own history.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Original"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.BeginUndoGroup
grid.EndUndoGroup ' no edits happened inside -- nothing is pushedDebug.Print grid.CanUndo ' -> False
grid.BeginUndoGroup
grid.SetCell 0, 0, "Changed"
grid.EndUndoGroup
Debug.Print grid.CanUndo ' -> True
Gotchas
An empty group (no SetCell/SetCellValue calls happened between Begin and End) is discarded entirely -- it is never pushed as a zero-edit step.
Calling this with no matching BeginUndoGroup open is a safe no-op, not an error.
An edit made AFTER a group has been closed is tracked as its own separate, ordinary undo step -- it is never folded back into the group that just ended.
See alsoBeginUndoGroup, Undo, CanUndo
PasteFromClipboardEditing, validation, undo and paste
Paste tab-delimited clipboard text into the grid starting at the current focus cell.
Reads the clipboard's Unicode text, splits it on newlines into rows and on tabs into columns (the same layout Excel produces when copying cells), and writes it starting at the current focus cell, one clipboard row per grid row and one clipboard field per column, stopping wherever the paste would run past the last row or column rather than growing the grid. Every pasted cell is written as a plain STRING, regardless of the target column's own type or prior contents. Equivalent to PasteSpecial(0) (ValuesOnly) -- overwrites every cell in the pasted range, blank fields included.
Refused outright in both a Virtual grid (the host owns the data) and a Turbo column-store grid (pasting has no Turbo column-store implementation at all) -- the view is left completely unchanged in either case, with no partial paste.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = "One"
data(1, 0) = "Beta": data(1, 1) = "Two"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Value"
grid.SelectRow 0
grid.CopyWithHeaders False' clipboard now holds "Alpha<TAB>One", no header line
grid.SelectBlock 1, 0, 1, 0' move the paste target to row 1 without a real click
grid.PasteFromClipboard
Debug.Print grid.GetCellValue(1, 0) & " / " & grid.GetCellValue(1, 1) ' -> Alpha / One
Gotchas
Does nothing at all -- silently, with no error -- when AllowEditing is False. The clipboard is not even read.
Every pasted cell becomes a plain STRING, even into a column that previously held numbers, dates or currency -- there is no type parsing on the way in.
Has no target cell at all until FOCUS has been set somewhere (by a real click, or programmatically via SelectBlock in Embedded mode) -- pasting into a grid where nothing has ever been focused does nothing.
NOT tracked by undo history at all, even though it plainly changes cell values -- a paste can never be reversed with Undo.
Pastes starting at the focus cell regardless of what is currently SELECTED -- select and focus are two different things in this control.
See alsoPasteSpecial, AllowEditing, Undo
PasteSpecialEditing, validation, undo and paste
Paste clipboard text with the option to skip cells the clipboard has nothing for.
Behaves exactly like PasteFromClipboard, with one extra capability: pasteMode 1 (SkipBlanks) leaves a target cell completely untouched wherever the corresponding clipboard field is empty, instead of overwriting it with blank text. pasteMode 0 (ValuesOnly) is identical to plain PasteFromClipboard -- every cell in the pasted range is overwritten, blank fields included.
SignatureSub PasteSpecial(pasteMode As Long)
Parameters
pasteMode (Long) - 0 = ValuesOnly (overwrite every cell, the default paste behavior), 1 = SkipBlanks (leave a target cell alone wherever the clipboard field for it is empty).
ReturnsNone.
Value
Meaning
0
ValuesOnly -- overwrites every pasted cell, blank or not; identical to PasteFromClipboard.
1
SkipBlanks -- does not overwrite a target cell where the clipboard field is empty, leaving that cell's existing value in place.
Refused outright in both Virtual and Turbo column-store grids, the same as PasteFromClipboard, and for the same reasons.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "": data(0, 1) = "New Code"' source row -- Name is blank
data(1, 0) = "Keep Me": data(1, 1) = "Old Code"' target row -- has existing values
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Code"
grid.SelectRow 0
grid.CopyWithHeaders False' clipboard: "<TAB>New Code" -- the Name field is empty
grid.SelectBlock 1, 0, 1, 0' paste target: row 1, column 0
grid.PasteSpecial 1' 1 = SkipBlanksDebug.Print grid.GetCellValue(1, 0) ' -> Keep Me (blank clipboard field did not overwrite it)Debug.Print grid.GetCellValue(1, 1) ' -> New Code (non-blank field still overwrote the old one)
Gotchas
pasteMode must be exactly 0 or 1 -- any other value is refused with a runtime error, unlike some other mode-style parameters in this control that silently fall back to a default.
SkipBlanks only looks at whether the CLIPBOARD field is empty -- it has nothing to do with whether the TARGET cell already had a value.
Also honors AllowEditing (silently does nothing when it is False) and is also not tracked by undo history at all, exactly like PasteFromClipboard.
See alsoPasteFromClipboard, AllowEditing, Undo
Appearance, fonts and themes
This group covers every property that changes how the grid looks rather than what it holds: grid lines, the nine discrete colors (background, text, alternating rows, hover, and the header's own set of each), header and row fonts, header height, cell padding, text alignment, tooltips, the two built-in right-click menus, an icon-size setting, a per-cell paint hook, and the theme system that ties the colors together. Every single member here is presentation state -- none of it touches row data -- which is why, uniquely among this control's member groups, all 28 members here behave identically in Embedded, Virtual, and Turbo column-store grids. There is no mode to branch on anywhere in this group's examples or tests.
The theme system deserves special attention because its pieces interact in a way that is easy to get backwards. Theme selects one of four built-in presets (Light, Dark, an Excel-style preset, and a High-Contrast preset) and, every time it is set, rebuilds ALL 36 of the control's internal color roles from that preset's own palette -- including the nine roles exposed as BackColor, ForeColor, AlternateRowColor, and the rest. This is a full rebuild, not a merge: setting Theme after individually overriding BackColor discards that override completely, replacing it with the new theme's own background color. Individual color properties and ApplyThemeJson, by contrast, each touch only the one or few roles they name, leaving every other role (including whatever the last Theme call put there) untouched. The practical rule is to set Theme first, if at all, and layer individual overrides on top of it afterward -- which is also the exact order the control itself uses when restoring properties from a saved form. GetCurrentThemeJson always reports the current, fully resolved state of all 36 roles -- the active theme's own starting color for anything left alone, and the override's own value for anything since changed by a property or by ApplyThemeJson -- never just "the last theme applied." ApplyThemeJson is strict and all-or-nothing: it parses its argument as JSON, requires every key to be one of the 36 role names with a "#RRGGBB" value, and on any fault raises an error and applies nothing. Of the 36 roles, eleven have a color property of their own -- the nine in this group plus SelectionBackColor and SelectionForeColor in the Selection group -- and the other 25 (grid line colors, focus indicators, footer colors, heatmap tint stops, the three status colors, and more) are reachable only through ApplyThemeJson. A color property is simply another name for its role, so whichever of the property and an ApplyThemeJson entry for that role ran last decides the color. Per-column colors (SetColumnBackColor and its siblings, in the Columns group) are not roles, and neither Theme nor ApplyThemeJson changes them.
Most of the color properties store and return the exact OLE_COLOR value they are given, with no conversion at all -- passing a genuine system color reference (the kind produced by a VBA system color constant, with the high bit set) round-trips as that same raw reference and paints incorrectly, since the rendering code treats it as a literal RGB value rather than resolving it. HeaderBackColor is the one exception in this group: it runs its input through OleTranslateColor before storing it, so a system color reference there IS resolved to a real, paintable RGB value. Always pass an explicit RGB() value (or a hex string through ApplyThemeJson) to every color property except HeaderBackColor.
Two members in this group are worth flagging plainly: CellPadding and TextVAlign both store and return whatever value is set, and both are validated on the way in (CellPadding is clamped to 2-20, TextVAlign is restricted to 0, 1, or 2), but neither one is read anywhere by the current cell-painting code. Every cell's text is always drawn vertically centered and with a fixed internal inset, regardless of what these two properties are set to. They are documented here exactly as they behave today, with the gap noted on each one, rather than as if they worked.
RecipeApply a built-in theme, then override just the header colors
VBA
Sub ThemeThenHeaderOverride(grid AsObject)
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Widget A": data(0, 1) = CDbl(120)
data(1, 0) = "Widget B": data(1, 1) = CDbl(75)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Product"
grid.SetColumnHeader 1, "Qty"' Pick a built-in theme first -- this rebuilds ALL color roles, including' the header's, from that theme's own palette
grid.Theme = 0' Light' NOW layer a brand color onto just the header -- this only touches the' header roles, leaving every other Light-theme color (row background,' row text, hover, etc.) exactly as Theme set it
grid.HeaderBackColor = RGB(22, 54, 92) ' a dark corporate blue
grid.HeaderForeColor = RGB(255, 255, 255)
Debug.Print grid.BackColor ' -> 16777215 (still Light theme's plain white row background)Debug.Print grid.HeaderBackColor ' -> 6033430 (the override)' If Theme is set again AFTER the override, the override is gone --' Theme always rebuilds the header roles too, it does not skip ones that' have been individually overridden
grid.Theme = 0Debug.Print grid.HeaderBackColor <> RGB(22, 54, 92) ' -> True (the override did not survive)' The correct order is: Theme once, then the override, and never call' Theme again afterward unless a fresh reset is actually wanted
grid.HeaderBackColor = RGB(22, 54, 92)
grid.HeaderForeColor = RGB(255, 255, 255)
EndSub
Theme and the individual color properties both write into the same 36-role palette, but Theme replaces the whole palette while a property like HeaderBackColor replaces only its own one or two roles. Calling Theme after an override silently erases that override, which is easy to trip over in a form's Load event if the two calls end up in the wrong order. The safe pattern is Theme first, overrides second, and never a second Theme call once branding overrides are in place.
RecipeMatch alternating rows, hover highlighting, and header hover to a corporate color scheme
VBA
Sub CorporateColorScheme(grid AsObject)
Dim data(0To3, 0To1) AsVariant
data(0, 0) = "North": data(0, 1) = CDbl(1000)
data(1, 0) = "South": data(1, 1) = CDbl(2000)
data(2, 0) = "East": data(2, 1) = CDbl(1500)
data(3, 0) = "West": data(3, 1) = CDbl(1800)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetColumnHeader 1, "Revenue"' A consistent set of related colors, all as explicit RGB() values -- a' system color constant would round-trip as a raw, unresolved reference' on every one of these except HeaderBackColor
grid.AlternateRowColor = RGB(240, 244, 250) ' faint blue-gray for every other row
grid.HoverBackColor = RGB(214, 228, 245) ' a stronger tint while the mouse is over a row
grid.HoverForeColor = RGB(10, 30, 60)
grid.HeaderBackColor = RGB(22, 54, 92) ' the same family, darker, for the header
grid.HeaderForeColor = RGB(255, 255, 255)
grid.HeaderHoverColor = RGB(35, 75, 120) ' header hover reads as a lighter step up from HeaderBackColor
grid.HeaderHoverForeColor = RGB(255, 255, 255)
Debug.Print grid.AlternateRowColor ' -> 16447984Debug.Print grid.HeaderBackColor ' -> 6033430' All seven of these apply immediately and persist across Theme -- as' long as Theme itself is never called again afterwardEndSub
None of these seven colors depend on each other technically -- each is an independent override on its own palette role -- but visually they only read as one coherent scheme if they are chosen from a consistent family, since nothing in the control derives hover or header-hover shades from the base colors automatically the way Theme's own built-in presets do. Setting them as a deliberate group, all in explicit RGB(), is the reliable way to get a scheme that survives exactly as designed.
RecipeSave the current color palette to a string, and restore it later
VBA
Sub SaveAndRestorePalette(grid AsObject)
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.Theme = 0
grid.HeaderBackColor = RGB(22, 54, 92)
grid.HeaderForeColor = RGB(255, 255, 255)
' Capture EVERY color role currently in effect -- the theme's own' starting colors plus both header overrides -- as one JSON stringDim saved AsString
saved = grid.GetCurrentThemeJson()
' ... later, after something else has changed the palette ...
grid.Theme = 1' switch to Dark; this discards the header overrides tooDebug.Print grid.HeaderBackColor <> RGB(22, 54, 92) ' -> True' Restore everything in one call -- GetCurrentThemeJson's own output is' valid input for ApplyThemeJson, because it names every one of the 36' roles ApplyThemeJson knows how to read
grid.ApplyThemeJson saved
Debug.Print grid.HeaderBackColor ' -> 6033430 (back to the saved override)' NOTE: the saved string has no idea Theme was ever 0 -- it is a snapshot' of resolved COLORS only, not of the Theme property's own numeric value.' Restoring it re-creates the same colors without touching grid.Theme,' which will still read 1 (Dark) afterward even though every color now' matches what Theme 0 plus the overrides had produced.Debug.Print grid.Theme ' -> 1 (unchanged by ApplyThemeJson)EndSub
GetCurrentThemeJson and ApplyThemeJson form a matched pair for snapshotting and restoring the resolved color state -- not the Theme setting itself. Restoring a snapshot never changes what Theme reports, since ApplyThemeJson never touches that property; it only overrides the color roles named in the JSON, exactly the way an individual color property would. A caller that cares about both needs to save Theme's own value separately if it matters for anything beyond the colors themselves.
GridLinesAppearance, fonts and themes
Choose which grid line separators are painted between rows and columns.
Controls whether horizontal separators between rows, vertical separators between columns, both, or neither are painted. The value is read as two independent bit flags rather than a true enumeration: bit 0 (value 1) draws horizontal lines, bit 1 (value 2) draws vertical lines, and both bits together (value 3) draws both -- which is also why any value outside 0-3 is still accepted and simply behaves according to whichever of those two bits happens to be set in it.
No mode restriction at all -- this is pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids, since it never touches row data.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alice": data(0, 1) = CDbl(100)
data(1, 0) = "Bob": data(1, 1) = CDbl(200)
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Amount"
grid.GridLines = 1' Horizontal only -- no separators between columnsDebug.Print grid.GridLines ' -> 1
grid.GridLines = 3' restore the default (Both)
Gotchas
There is no range validation whatsoever -- only bits 0 and 1 are ever consulted when painting, so any value is accepted without error and simply behaves according to whichever of those two bits it happens to have set: 99 and -1 both have both bits set and so behave as 3 (Both), but a value like 5 has only bit 0 set and so behaves as 1 (Horizontal only) even though it looks like it should mean more.
This only controls the thin separator lines -- it has no effect on cell backgrounds, header appearance, or selection highlighting.
See alsoRenderStyle
AlternateRowColorAppearance, fonts and themes
Set the background color used for every other (odd-indexed) row.
Overrides the background painted behind alternating rows, independent of BackColor, which controls the rest. It is another name for the theme's Surface_Alt role rather than a setting of its own: an ApplyThemeJson call that names Surface_Alt replaces it, and reading this property afterward returns that new color. The value is stored and returned exactly as given, with no conversion of any kind -- unlike HeaderBackColor, this property never runs its input through OleTranslateColor, so a genuine system color reference round-trips as that same raw reference rather than being resolved to the actual color it names, and paints incorrectly as a result.
SignatureProperty AlternateRowColor As OLE_COLOR
ReturnsThe current alternate row background color.
DefaultDetermined by the active color theme, not a fixed constant -- under the default Light theme it is a very pale gray (RGB 242,242,242), a small step darker than BackColor's plain white. Read the property to capture the current value before overriding it.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Row 1": data(1, 0) = "Row 2": data(2, 0) = "Row 3"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.AlternateRowColor = RGB(240, 248, 255) ' a pale corporate blue for every other rowDebug.Print grid.AlternateRowColor ' -> 16775408' A system color reference is stored and returned exactly as given, never' translated -- always pass an explicit RGB() value here
grid.AlternateRowColor = RGB(242, 242, 242) ' restore something close to the default
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- pass an explicit RGB() value, never a system color constant, which would round-trip unchanged and paint incorrectly.
Setting Theme afterward discards this override completely, replacing it with that theme's own alternate-row color.
Shares one slot with the Surface_Alt theme role: whichever of this property and an ApplyThemeJson entry for Surface_Alt was set last is the color you get. If a theme document you apply names Surface_Alt, set this property after applying it, or leave Surface_Alt out of the document.
Independent of BackColor -- changing one never affects the other.
A column background from SetColumnBackColor paints over the alternate band in that column; a column text color alone does not.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoBackColor, ForeColor, ApplyThemeJson
ShowRowNumbersAppearance, fonts and themes
Show or hide a row-number column at the left edge of the grid.
When True, a narrow column showing each visible row's sequential position appears at the left edge, before column 0, and every other column shifts right to make room for it. When False (the default), no such column is drawn and column 0 starts at the grid's own left edge.
SignatureProperty ShowRowNumbers As Boolean
ReturnsTrue if the row-number column is currently shown.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "First": data(1, 0) = "Second": data(2, 0) = "Third"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.ShowRowNumbers = True' A "#" column now appears at the left edge, numbering the visible rowsDebug.Print grid.ShowRowNumbers ' -> True
grid.ShowRowNumbers = False' restore the default
Gotchas
The row-number column's own width is computed internally and is not exposed as a separate property -- there is no way to read or set it directly.
BackColorAppearance, fonts and themes
Set the default cell background color for non-alternate rows.
Overrides the plain (non-alternate, non-hovered, non-selected) cell background color. The value is stored and returned exactly as given, with no conversion -- passing a system color reference here stores and returns that raw reference unchanged, and it paints incorrectly, since the rendering path treats it as a literal RGB value rather than resolving it. The background itself always paints exactly the value most recently set (or the active theme's own base color) -- unlike this background, the text painted over it can be silently substituted for a more readable color when it does not contrast well against this value; see ForeColor for that behavior.
SignatureProperty BackColor As OLE_COLOR
ReturnsThe current default cell background color.
DefaultWhite (RGB 255,255,255) under the default Light theme; determined by the active theme like every color in this group, so it changes when Theme changes.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.BackColor = RGB(255, 255, 255) ' an explicit whiteDebug.Print grid.BackColor ' -> 16777215' A system color constant is stored as the raw reference, not translated
grid.BackColor = &H8000000F
Debug.Print grid.BackColor ' -> -2147483633 (the raw reference, round-tripped exactly)
grid.BackColor = RGB(255, 255, 255) ' restore a safe, real RGB value
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- pass an explicit RGB() value, never a system color constant, which would round-trip unchanged and paint incorrectly.
Setting Theme afterward discards this override completely.
Has no effect on AlternateRowColor, HoverBackColor, or HeaderBackColor -- each of the nine color properties in this group is independent.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoForeColor, AlternateRowColor, Theme
ForeColorAppearance, fonts and themes
Set the default cell text color for non-alternate, non-hovered, non-selected cells.
Overrides the default text color painted in plain cells. The value is stored and returned exactly as given, with no conversion -- a system color reference round-trips as that same raw reference rather than being resolved to a real color. This property always reports back exactly the value most recently set (or the active theme's own text color); the color actually painted can differ from it, however: at paint time, for every plain row (not selected, not disabled), the control checks this value against the background that row is actually about to be painted with -- BackColor, or AlternateRowColor on alternate rows -- and if it does not give at least 4.5:1 contrast there, a substituted, more readable color is painted on that row instead, while this property keeps reporting the original raw value you set. Selected rows get the same check against the selection background (see SelectionForeColor), and a cell with a background of its own -- from SetCellColor, a conditional-format rule, a heatmap tint, or SetColumnBackColor -- is checked against that cell's own background rather than the row's. Two cases are painted without the check: disabled rows keep their deliberately subdued text, and plain cells on the hovered row use HoverForeColor as set.
SignatureProperty ForeColor As OLE_COLOR
ReturnsThe current default cell text color.
DefaultBlack (RGB 0,0,0) under the default Light theme; determined by the active theme like every color in this group.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.ForeColor = RGB(20, 20, 20) ' a near-black text colorDebug.Print grid.ForeColor ' -> 1315860
grid.ForeColor = RGB(0, 0, 0) ' restore the default
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- pass an explicit RGB() value, never a system color constant.
Setting Theme afterward discards this override completely.
Setting BackColor or AlternateRowColor to something that clashes with the current ForeColor never changes what this property reports back -- the raw value you set is always returned exactly, even though plain rows then silently paint a different, more readable text color on screen instead (a dark AlternateRowColor with a dark ForeColor, for example, still gives readable alternate rows). The same protection covers selected rows and cells with a background of their own (SetCellColor, conditional formatting, heatmaps, column backgrounds); it does not cover disabled rows, which keep their subdued color, or HoverForeColor on the hovered row.
A column text color from SetColumnForeColor replaces this color in that column.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoBackColor, Theme, SetColumnForeColor
HoverBackColorAppearance, fonts and themes
Set the row background color shown while the mouse is over it.
Overrides the background painted behind a row while the mouse pointer is over it, as long as that row is not also selected (selection always takes priority over hover). The value is stored and returned exactly as given, with no conversion.
SignatureProperty HoverBackColor As OLE_COLOR
ReturnsThe current hover row background color.
DefaultDetermined by the active color theme, not a fixed constant.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Row A": data(1, 0) = "Row B": data(2, 0) = "Row C"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.HoverBackColor = RGB(230, 240, 255) ' a light blue highlight as the mouse moves over rowsDebug.Print grid.HoverBackColor ' -> 16773350' Only visible on screen while the mouse genuinely hovers -- there is no way' to preview it from code alone
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- a system color reference round-trips unchanged rather than being translated.
Only visible while the mouse is genuinely hovering over the grid on screen -- there is no way to preview the effect from code alone.
A selected row always paints with the selection color instead, even while the mouse is over it -- this color never shows through a selection.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoHoverForeColor, HeaderHoverColor
HeaderHoverColorAppearance, fonts and themes
Set the header background color shown while the mouse is over a column header.
Overrides the background painted behind a column header while the mouse pointer is over it. The value is stored and returned exactly as given, with no conversion.
SignatureProperty HeaderHoverColor As OLE_COLOR
ReturnsThe current header hover background color.
DefaultDetermined by the active color theme, not a fixed constant.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.HeaderHoverColor = RGB(60, 100, 150) ' a lighter step up while hovering a headerDebug.Print grid.HeaderHoverColor ' -> 9856060
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- a system color reference round-trips unchanged rather than being translated.
Only visible while the mouse is genuinely hovering over a header on screen.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoHoverBackColor, HeaderHoverForeColor
HoverForeColorAppearance, fonts and themes
Set the row text color shown while the mouse is over it.
Overrides the text color painted in a row while the mouse pointer is over it and it is not also selected. The value is stored and returned exactly as given, with no conversion.
SignatureProperty HoverForeColor As OLE_COLOR
ReturnsThe current hover row text color.
DefaultDetermined by the active color theme, not a fixed constant.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Row A": data(1, 0) = "Row B": data(2, 0) = "Row C"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.HoverBackColor = RGB(230, 240, 255)
grid.HoverForeColor = RGB(10, 30, 60) ' a dark text color that reads well on the hover backgroundDebug.Print grid.HoverForeColor ' -> 3939850
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- a system color reference round-trips unchanged rather than being translated.
Only visible while the mouse is genuinely hovering over the grid on screen.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoHoverBackColor
HeaderBackColorAppearance, fonts and themes
Set the column header's background color.
Overrides the header's background color. Setting it also derives the header's own gradient shading from the same color (a bottom edge shaded to 94% (constant) of the given color's brightness), which is only visible when RenderStyle is 0 (Modern) -- at any other RenderStyle the header paints as a single flat fill using exactly the color given here, and the derived gradient bottom is never seen. This is the only color property in this group whose input is run through OleTranslateColor before being stored, so a genuine system color reference IS resolved here to a real, paintable RGB value -- unlike every other color property in this group, which stores such a reference completely unchanged.
SignatureProperty HeaderBackColor As OLE_COLOR
ReturnsThe current header background color.
DefaultDetermined by the active color theme -- under the default Light theme, a light gray a shade darker than BackColor's plain white.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.HeaderBackColor = RGB(30, 60, 90) ' a dark corporate blue headerDebug.Print grid.HeaderBackColor ' -> 5913630' This is the only color property in this group that resolves a system' color reference to a real RGB value rather than storing it unchanged
Gotchas
Unlike every other color property in this group, this one runs its input through OleTranslateColor before storing it -- a system color reference is resolved to a real RGB value here, rather than round-tripping unchanged.
Also derives the header's gradient bottom-edge shade from the same color -- setting this always changes both the flat header color and its Modern-style gradient together; there is no way to set them independently through this property.
Setting Theme afterward discards this override completely.
See alsoHeaderForeColor, RenderStyle
HeaderForeColorAppearance, fonts and themes
Set the column header's text color.
Overrides the header text color. The value is stored and returned exactly as given, with no conversion -- a system color reference round-trips as that same raw reference rather than being resolved.
SignatureProperty HeaderForeColor As OLE_COLOR
ReturnsThe current header text color.
DefaultBlack (RGB 0,0,0) under the default Light theme.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.HeaderBackColor = RGB(30, 60, 90)
grid.HeaderForeColor = RGB(255, 255, 255) ' white text on the dark headerDebug.Print grid.HeaderForeColor ' -> 16777215
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- unlike HeaderBackColor, a system color reference here round-trips unchanged rather than being translated.
Setting Theme afterward discards this override completely.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoHeaderBackColor
HeaderFontNameAppearance, fonts and themes
Set the typeface used for header text.
Sets the font family used to draw column header text, independent of the row font. There is no validation against installed fonts -- any string is accepted and stored exactly as given; Windows' own font mapper silently substitutes the nearest available font when the header is actually painted if the name does not match anything installed, rather than the call failing.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.HeaderFontName = "Calibri"Debug.Print grid.HeaderFontName ' -> Calibri' An unrecognized name is accepted without error; Windows substitutes the' nearest available font when the header is actually painted
grid.HeaderFontName = "Segoe UI"' restore the default
Gotchas
No validation against installed fonts -- an unrecognized name is accepted without error and stored exactly as given; only Windows' font mapper, at paint time, decides what actually gets drawn.
Passing vbNullString (a null BSTR) resets it to the built-in default ("Segoe UI"). An ordinary empty-string literal ("") is a different, non-null BSTR in COM, and this property stores and returns it back as an empty string with no fallback -- only a genuinely null BSTR triggers the default.
Independent of RowFontName -- header and row text can use completely different typefaces.
See alsoHeaderFontSize, HeaderFontBold, RowFontName
HeaderFontSizeAppearance, fonts and themes
Set the point size of the header font.
Sets the header font's size in points, independent of the row font's size. A value of zero or less is not rejected -- it silently falls back to the built-in default of 9 instead. There is no upper bound at all.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.HeaderFontSize = 12Debug.Print grid.HeaderFontSize ' -> 12
grid.HeaderFontSize = 0' not rejected...Debug.Print grid.HeaderFontSize ' -> 9 (...silently falls back to the built-in default)
grid.HeaderFontSize = 9' restore the default
Gotchas
A non-positive size (zero or negative) is never rejected -- it silently falls back to 9 instead of raising an error.
No upper limit is enforced -- an unusually large size is accepted as given and will simply overflow the available header height visually.
See alsoHeaderFontName, HeaderFontBold
HeaderFontBoldAppearance, fonts and themes
Turn bold weight on or off for header text.
Toggles bold weight for the header font only; row text is never bold through this property (per-cell bold/italic styling for row text is a separate, per-cell mechanism outside this group).
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.HeaderFontBold = False' a lighter-weight headerDebug.Print grid.HeaderFontBold ' -> False
grid.HeaderFontBold = True' restore the default
Gotchas
Affects only the header font -- there is no equivalent property to bold the row font as a whole.
See alsoHeaderFontName, HeaderFontSize
HeaderHeightAppearance, fonts and themes
Set the column header row's height, in pixels.
Sets the height of the column header row. A value of zero or less is not rejected -- it silently falls back to the built-in default of 24 instead. There is no upper bound at all. The value stored and returned here is a plain, logical (unscaled) pixel count -- it is not automatically adjusted for the current display's DPI the way RowDensity-derived row heights are, so code that computes on-screen pixel positions from this value on a high-DPI display needs its own DPI scaling.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.HeaderHeight = 32' a taller header, e.g. to fit a larger header fontDebug.Print grid.HeaderHeight ' -> 32
grid.HeaderHeight = 0' not rejected...Debug.Print grid.HeaderHeight ' -> 24 (...silently falls back to the built-in default)
grid.HeaderHeight = 24' restore the default
Gotchas
A non-positive value (zero or negative) is never rejected -- it silently falls back to 24 instead of raising an error.
No upper limit is enforced.
Reports a logical, unscaled pixel value -- any code translating this into a real screen-coordinate rectangle on a high-DPI display needs to apply its own DPI scaling; this property does not do it automatically.
If the host's display DPI changes while the control is running (for example, the containing window is dragged to a monitor with a different scaling setting), the control recalculates the header height from its built-in 24-pixel base scaled to the new DPI, and this recalculated value overwrites whatever was set through this property -- any customization made before the DPI change is silently discarded.
The actual header area painted on screen can be taller than this value alone suggests when column grouping or the auto-filter row is also in use, since both add their own extra height on top of this property's value.
See alsoHeaderFontSize
HeaderHoverForeColorAppearance, fonts and themes
Set the header text color shown while the mouse is over a column header.
Overrides the header text color while the mouse pointer is over that header. The value is stored and returned exactly as given, with no conversion.
SignatureProperty HeaderHoverForeColor As OLE_COLOR
ReturnsThe current header hover text color.
DefaultDetermined by the active color theme, not a fixed constant.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.HeaderHoverColor = RGB(60, 100, 150)
grid.HeaderHoverForeColor = RGB(255, 255, 255) ' white text that reads well on the hover backgroundDebug.Print grid.HeaderHoverForeColor ' -> 16777215
Gotchas
Stores and returns the exact OLE_COLOR value it is given -- a system color reference round-trips unchanged rather than being translated.
Only visible while the mouse is genuinely hovering over a header on screen.
Reading this property back through a late-bound Object variable returns a VT_UI4 (unsigned 32-bit) Variant, not a signed Long. A bare comparison operator (=, <>) against it can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"); convert with CDbl() first if you need to compare it directly, rather than comparing it as-is.
See alsoHeaderHoverColor
RenderStyleAppearance, fonts and themes
Choose whether the header background paints as a gradient or a flat fill.
Controls exactly one thing in the current build: whether the column header's background (and, if column grouping is in use, the group header area above it) paints as a top-to-bottom color gradient or as a single flat fill. Nothing else in the grid -- cell backgrounds, grid lines, borders, selection, or any other part of the header besides its background -- changes with this property.
SignatureProperty RenderStyle As Long
ReturnsThe current render style.
Default0 (a gradient header).
Value
Meaning
0
A gradient header background, shaded from HeaderBackColor's own derived top and bottom tones.
1
A flat, single-color header background, painted in HeaderBackColor exactly as set.
2
Accepted, but currently paints identically to value 1 -- there is no third distinct header appearance in this build; nothing in the rendering code branches on this value separately from the flat fill.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.RenderStyle = 0' the header paints as a top-to-bottom gradientDebug.Print grid.RenderStyle ' -> 0
grid.RenderStyle = 1' a single solid header color instead' A value of 2 is also accepted, but currently paints identically to 1
grid.RenderStyle = 0' restore the default
Gotchas
Only the header background is affected -- there is no other visual difference anywhere else in the grid between the accepted values.
Values 1 and 2 currently produce pixel-identical header output; only value 0 (the gradient) is visually distinct from the other two.
Values outside 0-2 are refused with an error, unlike several other numeric properties in this group that silently clamp or fall back to a default instead.
See alsoHeaderBackColor, Theme
ThemeAppearance, fonts and themes
Select one of four built-in color presets, replacing every color role in the grid at once.
Rebuilds all 36 of the grid's internal color roles -- including the eleven exposed as color properties (the nine in this group plus SelectionBackColor and SelectionForeColor), plus 25 more that have no dedicated property and are reachable only through ApplyThemeJson/GetCurrentThemeJson -- from the chosen preset's own base colors. This is a full rebuild, not a merge: any individual color override made before this call (through BackColor, HeaderBackColor, ApplyThemeJson, or any other color-touching member) is discarded the moment Theme is set, because every role is rebuilt from scratch, including the ones that had been individually overridden. Individual color overrides made AFTER a Theme call are unaffected by it and remain until the next Theme call.
SignatureProperty Theme As Long
ReturnsThe current theme id.
Default0 (Light).
Value
Meaning
0
Light -- a white background with dark text (the default).
1
Dark -- a dark background with light text.
2
An Excel-style theme -- light background, stronger gridlines, a muted blue accent.
3
High-Contrast -- black background, white text, a high-visibility accent, and stronger borders throughout.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.BackColor = RGB(255, 240, 220) ' a custom background
grid.Theme = 1' switch to Dark' Switching Theme rebuilds EVERY color role from Dark's own palette,' discarding the BackColor override made a moment agoDebug.Print grid.BackColor ' -> Dark's own background color, not RGB(255, 240, 220)
grid.Theme = 0' restore the default (Light)
Gotchas
Rebuilds EVERY color role from the chosen theme's own starting colors -- it does not merge with, or skip, roles that were individually overridden beforehand. Set Theme first, then apply individual color overrides afterward, never the other way around.
Values outside 0-3 are refused with an error.
Does not touch anything set through ApplyThemeJson either -- a JSON override applied before Theme is discarded the same way a property override would be.
Per-column colors from SetColumnBackColor, SetColumnForeColor or ApplyColumnStylesJson are not color roles, so a Theme change leaves them in place. Heatmap and data-bar colors you never set yourself follow the new theme; ones set with SetHeatmapColors or SetDataBarColor do not.
When a host form saves and reloads this control's properties, Theme is always restored first, precisely so that individually-saved color overrides applied afterward are not immediately wiped out by the theme rebuild -- the same ordering this property's own gotcha describes for code.
Color properties read back through a late-bound Object variable come back as a VT_UI4 (unsigned 32-bit) Variant, not a signed Long -- a bare comparison operator against one can raise a VBA runtime error ("Variable uses an Automation type not supported in Visual Basic"). Wrap the read in CDbl() before comparing it directly, as this member's own test does.
See alsoApplyThemeJson, GetCurrentThemeJson, BackColor, ForeColor
RowFontNameAppearance, fonts and themes
Set the typeface used for row (cell) text.
Sets the font family used to draw ordinary row text, independent of the header font. As with HeaderFontName, there is no validation against installed fonts -- any string is accepted and stored exactly as given, and Windows' own font mapper substitutes the nearest available font at paint time if the name is not installed.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.RowFontName = "Calibri"Debug.Print grid.RowFontName ' -> Calibri
grid.RowFontName = "Segoe UI"' restore the default
Gotchas
No validation against installed fonts -- an unrecognized name is accepted without error; only Windows' font mapper decides what actually gets drawn.
Passing an empty BSTR resets it to the built-in default ("Segoe UI") rather than being stored as an empty string.
Independent of HeaderFontName.
See alsoRowFontSize, HeaderFontName
RowFontSizeAppearance, fonts and themes
Set the point size of the row font.
Sets the row font's size in points, independent of the header font's size. A value of zero or less is not rejected -- it silently falls back to the built-in default of 9 instead. There is no upper bound at all.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.RowFontSize = 11Debug.Print grid.RowFontSize ' -> 11
grid.RowFontSize = 0' not rejected...Debug.Print grid.RowFontSize ' -> 9 (...silently falls back to the built-in default)
grid.RowFontSize = 9' restore the default
Gotchas
A non-positive size (zero or negative) is never rejected -- it silently falls back to 9 instead of raising an error.
No upper limit is enforced -- an unusually large row font size will visually overflow the default row height, which does not resize itself to match.
See alsoRowFontName
CellPaddingAppearance, fonts and themes
A stored horizontal cell padding value, clamped to 2-20 pixels, that the current cell-painting code does not read back.
Accepts and returns a horizontal padding value in pixels, silently clamped to the range 2-20 on the way in. This value is genuinely stored and faithfully returned by the getter, and it is included in the set of properties an ActiveX host persists when saving the control -- but nothing in the current cell-painting code reads it back to compute a cell's text rectangle. Changing it has no visible effect on how cell text is laid out; every cell currently uses a fixed internal inset regardless of what this is set to.
SignatureProperty CellPadding As Long
ReturnsThe current stored padding value, in pixels (2-20).
No mode restriction at all -- a plain stored value, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.CellPadding = 12Debug.Print grid.CellPadding ' -> 12 (stored and returned faithfully)' NOTE: nothing in the current build's cell-painting code reads this value' back -- changing it does not visibly change how cell text is laid out
grid.CellPadding = 4' restore the default
Gotchas
Has no effect on rendering in the current build -- the value is stored, clamped, and returned faithfully, but the cell-painting code never reads it back. Do not rely on this property to change cell text layout.
Silently clamped to 2-20 rather than raising an error for an out-of-range value -- unlike TextVAlign, which refuses an out-of-range value outright.
See alsoTextVAlign
TextVAlignAppearance, fonts and themes
A stored vertical text alignment value that the current cell-painting code does not read back.
Accepts one of three named alignment values and returns it faithfully. This value is genuinely stored, validated, and returned by the getter, and it is included in the set of properties an ActiveX host persists when saving the control -- but nothing in the current cell-painting code reads it back. Every cell's text position is computed from a single vertical-centering offset applied uniformly across the grid, regardless of what this is set to, so Top and Bottom currently look identical to Center in every cell.
SignatureProperty TextVAlign As Long
ReturnsThe current stored vertical alignment value.
Default1 (Center).
Value
Meaning
0
Top -- accepted and stored, but currently renders the same as Center.
1
Center -- accepted and stored; this is what every cell actually renders as today, regardless of which of the three values is set.
2
Bottom -- accepted and stored, but currently renders the same as Center.
No mode restriction at all -- a plain stored value, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.TextVAlign = 0' TopDebug.Print grid.TextVAlign ' -> 0' NOTE: like CellPadding, this value is stored and returned faithfully, but' every cell's text is always painted vertically centered regardless of' what this is set to -- there is currently no visible difference between' Top, Center, and Bottom
grid.TextVAlign = 1' restore the default (Center)
Gotchas
Has no effect on rendering in the current build -- every cell's text is always painted vertically centered, regardless of which of the three values is set here.
Unlike CellPadding, an out-of-range value here is refused outright with an error rather than being silently clamped.
See alsoCellPadding
ShowTooltipsAppearance, fonts and themes
Show or hide tooltips over cell text that has been truncated with an ellipsis.
When True (the default), hovering the mouse over a cell whose text was truncated to fit its column shows the full text in a tooltip. Setting this to False while a tooltip is currently visible hides it immediately, in addition to preventing any new one from appearing.
No mode restriction at all -- pure rendering/interaction state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.ShowTooltips = False' suppress tooltips even over truncated cell textDebug.Print grid.ShowTooltips ' -> False
grid.ShowTooltips = True' restore the default
Gotchas
Only ever relevant for a cell whose displayed text was actually truncated -- a cell whose full text already fits shows no tooltip regardless of this setting.
Setting this to False while a tooltip is already showing hides it immediately, rather than waiting for the mouse to move away.
ShowHeaderMenuAppearance, fonts and themes
Enable or disable the built-in right-click menu on column headers.
When True (the default), right-clicking a column header opens a built-in menu (sort ascending/descending, clear sort, auto-fit, filter, clear filter, hide column). When False, right-clicking a header does nothing built-in at all -- the menu is never opened.
SignatureProperty ShowHeaderMenu As Boolean
ReturnsTrue if the built-in header menu is currently enabled.
No mode restriction at all -- pure interaction-gating state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.ShowHeaderMenu = False' right-clicking a column header no longer opens the built-in Sort/Filter/Hide menuDebug.Print grid.ShowHeaderMenu ' -> False
grid.ShowHeaderMenu = True' restore the default
Gotchas
The menu this gates is a real, modal, native popup menu (opened with a blocking Win32 API call) -- while it is open, it does not return control to the caller until it is dismissed. This is why this Cookbook's own automated test only exercises the property's get/set round-trip rather than triggering the menu itself with a simulated right-click, which would leave the automated test run blocked on a menu no automated process can dismiss.
Independent of ShowBuiltinContextMenu, which gates the separate menu shown when right-clicking the cell area instead of a header.
See alsoShowBuiltinContextMenu
ShowBuiltinContextMenuAppearance, fonts and themes
Enable or disable the built-in right-click menu over the cell area.
When True (the default), right-clicking within the cell area (not a header) opens a built-in menu (Copy, Paste, Select All, Delete). When False, right-clicking the cell area does nothing built-in at all.
SignatureProperty ShowBuiltinContextMenu As Boolean
ReturnsTrue if the built-in cell-area context menu is currently enabled.
No mode restriction at all -- pure interaction-gating state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.ShowBuiltinContextMenu = False' right-clicking a cell no longer opens the built-in Copy/Paste/Select All/Delete menuDebug.Print grid.ShowBuiltinContextMenu ' -> False
grid.ShowBuiltinContextMenu = True' restore the default
Gotchas
The menu this gates is a real, modal, native popup menu, opened with a blocking Win32 API call -- the same reason this Cookbook's own test only exercises the property's get/set round-trip rather than a simulated right-click, which would block an automated test run.
Independent of ShowHeaderMenu, which gates the separate menu shown when right-clicking a column header instead of the cell area.
See alsoShowHeaderMenu
ApplyThemeJsonAppearance, fonts and themes
Override any number of the grid's 36 color roles at once from a JSON object -- all of them or none.
Parses json as a JSON object whose keys are color role names and whose values are "#RRGGBB" strings, for example {"Surface_Base":"#FFFFFF","Text_Primary":"#1A1A1A"}. The whole string is checked before anything is applied. If every entry is valid, every named role is overridden at once; if anything is wrong, the call raises a runtime error whose description names the problem and ends "The active theme was NOT changed." -- and no role is touched, including the valid entries in the same string. Roles the string does not name keep their current colors, so a partial document naming two or three roles is ordinary input. Eleven of the 36 roles also have a color property of their own (BackColor for Surface_Base, ForeColor for Text_Primary, AlternateRowColor for Surface_Alt, and so on); the other 25 -- every grid line, focus, footer, heatmap and status color among them -- can only be set through this method. Call GetCurrentThemeJson to see every role name.
SignatureSub ApplyThemeJson(json As String)
Parameters
json (String) - A JSON object of "RoleName":"#RRGGBB" entries. Whitespace anywhere and either hex case are accepted, as is a leading byte-order mark such as a Windows text editor writes. An empty object {} is valid and changes nothing.
All or nothing: an unknown role name, a role named twice, a value that is not a "#RRGGBB" string (such as "#12345" or a bare number), a document that is not a JSON object, or text that is not well-formed JSON raises a runtime error and applies none of the entries.
Every key must be a role name. A descriptive field such as "name" or "description" in a saved theme file is rejected as an unknown role, so keep that kind of information outside the string you pass.
An empty string is refused.
Each role is the same slot its color property writes: after this method sets Surface_Alt, AlternateRowColor reads the new color, and a later AlternateRowColor assignment replaces it again. Whichever ran last wins.
Setting Surface_Header through this method (instead of through the HeaderBackColor property) does NOT also update Header_GradientTop/Header_GradientBottom the way the HeaderBackColor property does -- the header can end up with a flat color that visibly does not match its own gradient edges under the default RenderStyle unless those two roles are set alongside it.
Reaches 25 color roles with no dedicated property anywhere in the object model -- the only way to read their current values back is GetCurrentThemeJson.
Setting Theme afterward discards everything this method just applied, the same as it discards any other individual color override.
Per-column colors from SetColumnBackColor, SetColumnForeColor or ApplyColumnStylesJson are not color roles and are not changed by this method.
Heatmap and data-bar colors that were never set explicitly follow the new Heat_Low, Heat_Mid, Heat_High and Accent_Primary values immediately; status badges follow Status_Good, Status_Warn and Status_Bad.
See alsoGetCurrentThemeJson, Theme, ApplyColumnStylesJson
GetCurrentThemeJsonAppearance, fonts and themes
Return every one of the grid's 36 current color roles, resolved, as one JSON object.
Returns a flat JSON object mapping each of the 36 internal color role names to its current hex color, as a live snapshot of the resolved state: the active theme's own starting color for any role that has not been individually overridden since, and the override's own value (set through a discrete color property or through ApplyThemeJson) for any role that has. It is not merely "the last theme applied" -- calling this again after any single color property changes reflects that change immediately. The output uses exactly the role names ApplyThemeJson understands, so the whole string can be fed back into ApplyThemeJson later to restore the same resolved colors.
SignatureFunction GetCurrentThemeJson() As String
ReturnsA JSON object string with all 36 color role names as keys and uppercase "#RRGGBB" hex strings as values.
No mode restriction at all -- pure rendering state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim data(0To0, 0To0) AsVariant
data(0, 0) = "Sample"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Column A"
grid.BackColor = RGB(10, 20, 30)
Dim json AsString
json = grid.GetCurrentThemeJson()
Debug.Print InStr(json, "#0A141E") > 0' -> True -- the live BackColor override is in the snapshot' The snapshot includes all 36 color roles, including ones with no color' property of their own (GridLine_H, Focus_Border, Heat_Low, Status_Good, and' others) -- this is the only way to discover their current values
Gotchas
Includes only the 36 color roles -- GridLines, RenderStyle, Theme's own numeric id, fonts, per-column colors (see GetColumnStylesJson), and every other non-color member are not part of this snapshot.
Feeding this output back into ApplyThemeJson restores the same resolved colors, but it does not restore Theme's own numeric value -- Theme will keep reading whatever it read before, even though every color role now matches what the snapshot described.
The 25 roles with no dedicated property (grid lines, focus indicators, footer colors, heatmap tint stops, the three status colors, and more) can only be discovered through this method's output.
See alsoApplyThemeJson, Theme, GetColumnStylesJson
IconSizeAppearance, fonts and themes
Set the pixel size (both width and height) used for every icon in the grid's icon store.
Sets the size, in pixels, of the internal icon store used by every icon-related member elsewhere in the control (loading external icons, setting them on cells or columns, and the rest of that not-yet-documented member group). Changing this rebuilds the icon store at the new size, which drops every externally loaded icon and every cell/column icon assignment or rule that referenced one -- there is no way to resize in place while keeping loaded icons.
No mode restriction at all -- pure rendering/resource state, identical in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.IconSize = 24Debug.Print grid.IconSize ' -> 24' Changing this drops every icon already loaded with LoadIconFromFile, so' set it once at startup, before loading any icons
grid.IconSize = 16' restore the default
Gotchas
Changing this destroys and rebuilds the entire icon store -- every icon previously loaded with LoadIconFromFile is discarded, along with every cell and column icon assignment or rule that referenced one. Set this once, before loading any icons, rather than changing it later.
Values outside 4-128 are refused with an error.
Applies uniformly to every icon in the store -- there is no way to give individual icons different sizes.
EnableBeforeCellPaintAppearance, fonts and themes
Enable the per-cell BeforeCellPaint event during painting.
When True, the BeforeCellPaint event fires once for every cell as it is about to be drawn, giving a handler the chance to custom-draw that cell and cancel the default rendering. When False (the default), the event is never fired at all -- the check is skipped entirely during painting, which avoids the per-cell COM call overhead for grids that have no handler for it. This property only controls whether the event fires; the event itself, and what a handler can do with it, belongs to a different member group not yet covered by this Cookbook.
SignatureProperty EnableBeforeCellPaint As Boolean
ReturnsTrue if the BeforeCellPaint event is currently enabled.
No mode restriction at all -- a plain stored flag, identical in Embedded, Virtual, and Turbo column-store grids; painting itself happens the same way in every mode.
Example
VBA
grid.EnableBeforeCellPaint = TrueDebug.Print grid.EnableBeforeCellPaint ' -> True' On its own this only controls whether the BeforeCellPaint event fires' during painting -- it has no visible effect until a handler for that event' (lgGrid_BeforeCellPaint, registered mode) is also written
grid.EnableBeforeCellPaint = False' restore the default
Gotchas
Setting this to True has no visible effect on its own -- without a handler written for the BeforeCellPaint event, enabling it only adds the per-cell overhead of firing an event nobody observes.
This event is documented as registered-activation-only elsewhere in this control's event surface -- a Reg-Free grid using the callback bridge should not expect this event to reach it the same way a Registered grid's own form handler would.
Icons, sparklines, heatmaps, data bars and conditional formatting
This group covers every member that paints something onto a cell beyond plain text: two completely independent icon systems, per-cell sparkline charts, column-wide heatmap tinting, proportional data bars, and rule-based conditional formatting. All five are configuration set ahead of time -- none of them is evaluated until the next paint pass, and more than one can be layered onto the very same column at once (a column can carry a heatmap, a data bar, and a conditional-format rule simultaneously; each is configured, and evaluated, independently of the others).
The two icon systems are worth understanding as a pair, because the API makes them look like one system grew out of the other and it did not. The older, external system (LoadIconFromFile, SetCellIcon, SetColumnIconMap, SetColumnDefaultIcon, AddIconThreshold) loads real image files -- .ico, .bmp, .png, with PNG alpha preserved -- into a shared pool and assigns them by integer ID. The newer built-in system (SetCellBuiltinIcon, SetColumnBuiltinIcon, SetColumnBuiltinIconMap, AddBuiltinIconThreshold, SetColumnBuiltinIconDefault) draws one of eight small vector glyphs directly with GDI -- no file, no loading, nothing to fail to find on disk. They keep entirely separate storage -- nothing in one references the other, and RemoveIcon has no effect on anything built-in -- yet both can be attached to the same cell or column at once, so the control needs a resolution order for when both would apply. GetResolvedIcon and the paint routine it mirrors both check the built-in system FIRST; the external system is only consulted as a fallback when the built-in system has nothing to say about that cell. Within the built-in system there is a second, internal priority tier that has no external-system equivalent at all: SetColumnBuiltinIcon assigns one icon to literally every row in a column, unconditionally, and that column-wide icon is checked BEFORE the value-map/threshold/default rule chain built with SetColumnBuiltinIconMap, AddBuiltinIconThreshold and SetColumnBuiltinIconDefault -- so setting both on the same column makes the entire rule chain invisible until ClearColumnBuiltinIcon removes the column-wide icon. The external system's closest equivalent, SetColumnDefaultIcon, is a fallback checked LAST, not first.
Most of this group has one dominant, and initially counter-intuitive, mode story. A shallow read of this control's internal Virtual-mode capability register would suggest that heatmaps, data bars, and every conditional-formatting member are simply unavailable outside Embedded mode. They are not: every one of them is refused ONLY in Virtual mode (where the grid never holds a full dataset to scan), and each has a genuine, separate implementation for Turbo column-store mode that draws on Turbo column-store's own precomputed per-column statistics instead of scanning anything. The real restriction in Turbo column-store mode is narrower and more specific: a heatmap, a data bar, or a Top10Pct/Bottom10Pct formatting rule can only be placed on a column Turbo column-store stores as a number (its Int32, Int64 or Double logical type) -- because those are the only types Turbo column-store maintains exact statistics for -- while GreaterThan, LessThan, Equal, Contains, Between, Duplicate and Unique rules work on any column type in Turbo column-store, since they read each cell's own value or rendered text directly rather than needing a whole-column scan. Embedded mode, by contrast, never refuses a heatmap or data bar on a non-numeric column at all -- it simply finds no numbers to range over and paints no tint anywhere, which is a quieter, easier failure to miss than Turbo column-store's explicit error. The one part of this group that genuinely IS refused in both Virtual and Turbo column-store alike is sparkline DATA -- SetSparklineData and SetSparklineDataCSV -- even though a sparkline column's TYPE and its colors (SetColumnSparkline, SetSparklineColors) can be configured in every mode; a Turbo column-store sparkline column can be fully styled and never draw anything, because there is no way to attach data points to any of its cells. The built-in icon system has the same split for its own per-cell override: SetColumnBuiltinIcon and its rule chain work in Turbo column-store, but SetCellBuiltinIcon -- the one-cell override -- is refused outright in both Virtual and Turbo column-store, unlike its external-system counterpart SetCellIcon, which is fully Turbo column-store-safe (it resolves the cell's own durable row identity before storing the assignment, so it survives a later sort, filter, insert or delete).
A handful of real, source-confirmed gaps are worth knowing about up front rather than discovering by surprise. GetResolvedIcon's own row-range check reads the Embedded row store specifically, which is always empty while Virtual or Turbo column-store mode is active, so it silently reports "no icon" for every row in either mode rather than the Virtual-mode residency error its own IDL text describes -- and even in Embedded mode it never reports an icon that came from a conditional-formatting rule's SetFormatRuleIcon, a third icon source it does not look at. RemoveIcon correctly forgets an icon everywhere it was referenced in Embedded and Virtual mode, but not in Turbo column-store mode's own per-cell icon map, so a Turbo column-store cell can end up silently displaying whatever unrelated artwork later reuses that same numeric ID. AddFormatRule accepts a column index of -1 without error, and it is genuinely treated as "every column" when clearing rules -- but the cell-by-cell evaluator that actually paints a rule checks for an exact column match, so a rule added with -1 can never match any cell, in either mode; a Between rule is equally inert until SetFormatRuleThreshold2 gives it an upper bound. And two small, unrelated duplicates: ImageListIconCount and GetIconCount read the exact same internal counter and always agree, and SetColumnIconLayout's own internal enum has a fourth value the paint code never acts on and the property itself refuses to accept.
RecipeBuild a status column with the external icon system: a value map, a numeric threshold set, and a one-off cell override
VBA
Sub ExternalIconStatusColumn(grid AsObject)
Dim data(0To4, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = "Shipped"
data(1, 0) = "Bravo": data(1, 1) = "Pending"
data(2, 0) = "Charlie": data(2, 1) = "Shipped"
data(3, 0) = "Delta": data(3, 1) = "Cancelled"
data(4, 0) = "Echo": data(4, 1) = "Pending"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Order"
grid.SetColumnHeader 1, "Status"Dim okIcon AsLong, pendingIcon AsLong, cancelIcon AsLong
okIcon = grid.LoadIconFromFile("C:\Icons\check_green.ico")
pendingIcon = grid.LoadIconFromFile("C:\Icons\clock_amber.ico")
cancelIcon = grid.LoadIconFromFile("C:\Icons\x_red.ico")
' Exact-text value map -- one icon per distinct Status string
grid.SetColumnIconMap 1, "Shipped", okIcon
grid.SetColumnIconMap 1, "Pending", pendingIcon
grid.SetColumnIconMap 1, "Cancelled", cancelIcon
grid.SetColumnIconLayout 1, 2' IconLeft' A single cell can still override the column's own rule -- Bravo's' order turned out to be a rush order, flag it distinctly
grid.SetCellIcon 1, 1, cancelIcon
Dim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 1, 1, kind, extId, builtinId
Debug.Print kind & " " & extId ' -> 1 12 (External; the cell override wins over the Pending column rule)
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
Debug.Print kind & " " & extId ' -> 1 10 (External; resolved from the column's own "Shipped" -> okIcon map)' Removing an icon clears every assignment and rule entry that used it --' Charlie's row loses its icon outright rather than showing a stale one
grid.RemoveIcon okIcon
grid.GetResolvedIcon 2, 1, kind, extId, builtinId
Debug.Print kind ' -> 0 (None -- the Shipped mapping that pointed at okIcon is gone)EndSub
SetColumnIconMap gives every row a default icon based on its own text; SetCellIcon can still override any single cell on top of that, and the override always wins because cell-level lookups are checked before column rules. RemoveIcon is deliberately thorough: it does not just stop drawing the removed icon, it erases every value-map entry, threshold band and column default that named it, so a later LoadIconFromFile that reuses the same numeric ID can never silently repaint an assignment nobody meant to give it that artwork. Note this cleanup does not reach a Turbo column-store-mode cell-level assignment made with SetCellIcon while running in Turbo column-store mode -- that map is not part of what RemoveIcon purges, so in Turbo column-store mode a removed-and-reissued icon ID can resurface on a cell that never asked for it.
RecipeUse the built-in vector icon system without loading any files, and see it take priority over the external system
VBA
Sub BuiltinIconsWinOverExternal(grid AsObject)
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Low": data(1, 0) = "Medium": data(2, 0) = "High"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Priority"' An external rule on this column, for comparison ...Dim flagIcon AsLong
flagIcon = grid.LoadIconFromFile("C:\Icons\flag.ico")
grid.SetColumnIconMap 0, "High", flagIcon
' ... and a built-in rule on the SAME column, same trigger value. No file,' no LoadIconFromFile call -- builtinIcon 0 is one of eight built-in glyphs
grid.SetColumnBuiltinIconMap 0, "High", 0' e.g. BI_CircleDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 2, 0, kind, extId, builtinId
Debug.Print kind ' -> 2 (Builtin) -- the built-in system is always checked first' A column-wide built-in icon sits ABOVE the rule chain above it, with no' equivalent in the external system -- it applies to every row until' cleared, even rows the rule chain would resolve differently
grid.SetColumnBuiltinIcon 0, 3' every row in the column now shows builtinIcon 3
grid.GetResolvedIcon 0, 0, kind, extId, builtinId
Debug.Print builtinId ' -> 3 (Low's own rule-chain result, if any, is now unreachable)
grid.ClearColumnBuiltinIcon 0' the rule chain (the value map above) is reachable again
grid.GetResolvedIcon 2, 0, kind, extId, builtinId
Debug.Print builtinId ' -> 0 (back to the "High" -> 0 mapping)EndSub
The two icon systems are independent stores that never see each other, so the SAME cell can genuinely qualify under both a built-in rule and an external one at once; GetResolvedIcon (and the paint code it mirrors) always resolves the built-in system first, so the external rule is invisible whenever the built-in system has an answer. SetColumnBuiltinIcon adds a second layer of priority that exists only in the built-in system: an unconditional column-wide icon that outranks even the built-in value map and threshold rules, so it is easy to set one forgetting that it silences everything below it until it is cleared again.
RecipeHighlight outliers with a heatmap and a data bar, flag duplicates with conditional formatting, and branch for Turbo column-store's numeric-only rule
VBA
Sub OutlierDashboardColumn(grid AsObject)
grid.SetColumnHeader 3, "Amount"
grid.SetColumnHeader 2, "Region"' Heatmap and data bar both need real column statistics -- refused only' in Virtual mode; in Turbo column-store mode both also require a numeric column
grid.SetColumnHeatmap 3, True
grid.SetHeatmapColors 3, RGB(255, 230, 230), RGB(255, 255, 230), RGB(230, 255, 230)
grid.SetColumnDataBar 3, True
grid.SetDataBarColor 3, RGB(99, 142, 198)
' A numeric rule with an icon action -- also Turbo column-store-safe on a numeric columnDim warnIcon AsLong, ruleId AsLong
warnIcon = grid.LoadIconFromFile("C:\Icons\warning.ico")
ruleId = grid.AddFormatRule(3, 0, CDbl(500), RGB(255, 240, 200), RGB(120, 60, 0)) ' 0 = GreaterThan
grid.SetFormatRuleIcon ruleId, warnIcon
' A Duplicate rule reads rendered TEXT, not a numeric range, so it works' on any column type in Turbo column-store, unlike Top10Pct/Bottom10Pct or a heatmap
grid.AddFormatRule 2, 7, Empty, RGB(255, 255, 220), RGB(0, 0, 0) ' 7 = Duplicate' Top10Pct is exact-numeric-only in Turbo column-store, so a text column needs a branchIf grid.DataMode <> 2Then
grid.AddFormatRule 1, 5, CDbl(10), RGB(220, 255, 220), RGB(0, 80, 0) ' 5 = Top10Pct on a text columnElse' In Turbo column-store mode the same call would refuse: Top10Pct needs exact' numeric facts, which Turbo column-store builds only for Int32/Int64/Double columnsOnErrorResumeNext
grid.AddFormatRule 1, 5, CDbl(10), RGB(220, 255, 220), RGB(0, 80, 0)
Debug.PrintErr.Number <> 0' -> TrueOnErrorGoTo0EndIfEndSub
Heatmaps, data bars and conditional formatting all need to know something about the column as a whole -- a range to tint against, or which values are duplicated -- which is exactly what a Virtual grid cannot provide, so all three refuse there regardless of column type. Turbo column-store mode CAN provide it, from its own precomputed statistics, but only for the numeric column types (Int32, Int64, Double) it keeps exact statistics for; Top10Pct, Bottom10Pct, a heatmap and a data bar all share that same restriction, while GreaterThan, Duplicate, Unique and the other text- or single-value-based rules do not, because they never need a whole-column scan to evaluate one cell. Checking grid.DataMode directly, as this recipe does for the Top10Pct call, is the reliable way to give a demonstration that runs for real in every mode the underlying feature actually supports.
LoadIconFromFileIcons, sparklines, heatmaps, data bars and conditional formatting
Load an icon image from disk into the grid's shared icon pool and get back an ID to assign it with.
Loads a .ico, .bmp or .png file into an internal icon pool shared by the whole control and returns an integer ID identifying it, for use with SetCellIcon, SetColumnIconMap, SetColumnDefaultIcon and AddIconThreshold. PNG files are decoded through Windows Imaging Component and keep their real per-pixel alpha transparency; .ico and .bmp go through a simpler, non-WIC decode path. Loading the exact same file path a second time does not load a second copy -- it returns the same ID the first call returned, so calling this repeatedly with the same path (for example, once per grid instance a form creates) is always safe and cheap.
SignatureFunction LoadIconFromFile(filePath As String) As Long
Parameters
filePath (String) - Full path to a .ico, .bmp or .png file. PNG is detected by its file signature, not by the file's extension.
ReturnsThe new icon's ID (0 or greater). Raises an error if the file could not be loaded as an icon.
Loading an icon only touches the shared icon pool -- it never reads or writes any row or column data -- so this works identically in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
Dim iconId AsLong
iconId = grid.LoadIconFromFile("C:\Icons\flag_green.ico")
Debug.Print iconId >= 0' -> True' Loading the SAME path again returns the SAME id -- no duplicate is createdDim again AsLong
again = grid.LoadIconFromFile("C:\Icons\flag_green.ico")
Debug.Print again = iconId ' -> True
Gotchas
Loading the same path twice returns the SAME icon ID rather than creating a duplicate; the pool deduplicates by the literal path string.
Every load failure raises the same error regardless of cause (a missing file, an unsupported format, an oversized image, too many icons already loaded, or a failure to allocate storage) -- read the error's description text to tell them apart.
IconSize (in the Appearance group) sets the pixel size every icon is rasterized to. Changing IconSize destroys and rebuilds the whole icon pool, dropping every icon already loaded -- and it goes further than just the pool: it also clears every SetCellIcon assignment and every column icon rule (SetColumnIconMap, AddIconThreshold, SetColumnDefaultIcon, SetColumnIconLayout) on the entire grid, not only on columns that used the dropped icons. Set IconSize once, before the first LoadIconFromFile call, not afterward.
PNG transparency is preserved; loading a .bmp or .ico with a transparent-looking background does not give the same result -- those formats go through a different, non-alpha-aware path.
See alsoRemoveIcon, GetIconCount, SetCellIcon, IconSize
RemoveIconIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a loaded icon and clear every assignment or rule that referenced it.
Removes iconId from the icon pool and frees its slot for a future LoadIconFromFile call to reuse. Before freeing the slot, it also erases every place that referenced this exact icon: any cell it was assigned to with SetCellIcon, any value-map entry or threshold band that named it, any column default that pointed at it, and any conditional-formatting rule's icon action set with SetFormatRuleIcon -- so nothing is left holding a numeric ID that might later be reused by a completely different piece of artwork. Removing an ID that was never issued, or was already removed, is a silent no-op.
SignatureSub RemoveIcon(iconId As Long)
Parameters
iconId (Long) - The id LoadIconFromFile returned for the icon to remove.
No mode restriction at all -- removal only touches the shared icon pool and the assignment/rule structures, which are reachable in every mode.
Example
VBA
Dim iconId AsLong
iconId = grid.LoadIconFromFile("C:\Icons\flag.ico")
grid.SetCellIcon 0, 0, iconId
Debug.Print grid.GetCellIcon(0, 0) ' -> the value of iconId
grid.RemoveIcon iconId
Debug.Print grid.GetCellIcon(0, 0) ' -> -1 (the assignment was cleared along with the icon)' Removing an id that was never issued (or already removed) is silently ignored
grid.RemoveIcon 999999
Gotchas
Every icon ID is eventually recycled: once removed, the same numeric ID can be handed out again by a later LoadIconFromFile call for completely different artwork. The cleanup this method performs (clearing SetCellIcon assignments, column value maps, thresholds, defaults, and format-rule icon actions) exists specifically so a stale reference can never resurface and silently start showing that new artwork.
That cleanup does not reach a Turbo column-store grid's own cell-level icon assignments (the ones made with SetCellIcon while DataMode is 2). A cell assigned an icon with SetCellIcon in Turbo column-store mode keeps that exact numeric id in its own separate map even after RemoveIcon removes the icon -- so if a later LoadIconFromFile happens to reuse the same id, that Turbo column-store cell will silently start displaying the new artwork. Removing an icon that Turbo column-store-mode cells may reference is safest done through ClearCellIcon on each affected cell first.
Removing an ID that was never issued, or one already removed, raises no error.
See alsoLoadIconFromFile, SetCellIcon, GetCellIcon, SetFormatRuleIcon
GetIconCountIcons, sparklines, heatmaps, data bars and conditional formatting
Get the number of external icons currently loaded in the icon pool.
Returns how many icons loaded through LoadIconFromFile are currently live -- that is, loaded and not yet removed. This counts only the external (file-based) icon pool; it has nothing to do with the built-in vector icon system, which has no equivalent "how many are configured" count because built-in icons are never loaded or limited in number.
SignatureFunction GetIconCount() As Long
ReturnsThe number of currently loaded external icons.
No mode restriction -- a plain read of the shared icon pool's own counter, identical in every mode.
Example
VBA
Debug.Print grid.GetIconCount() ' -> 0 on a grid that has never loaded an icon
grid.LoadIconFromFile "C:\Icons\flag.ico"Debug.Print grid.GetIconCount() ' -> 1
Gotchas
Reads the exact same internal counter as ImageListIconCount (dispid 477) -- the two always report the same number; ImageListIconCount is simply a second name for this same value.
See alsoLoadIconFromFile, RemoveIcon, ImageListIconCount
SetCellIconIcons, sparklines, heatmaps, data bars and conditional formatting
Assign a loaded external icon to one specific cell, overriding this column's own external rules.
Attaches iconId to the single cell at (row, col). A cell-level assignment always takes priority over this column's own external rules -- its value map (SetColumnIconMap), threshold bands (AddIconThreshold) and default icon (SetColumnDefaultIcon) -- but it does NOT take priority over the built-in icon system: the built-in system is resolved first, and whenever it has anything to say about that cell -- a per-cell builtin override, a column-wide builtin icon (SetColumnBuiltinIcon), or a match from the builtin value map, threshold or default chain -- that wins instead, and this assignment is never even consulted. This call's icon is what actually paints only once the built-in system has nothing configured for that cell or column at all. iconId is not required to name an icon that is actually loaded -- an unrecognized id is accepted and stored, and simply never draws anything.
SignatureSub SetCellIcon(row As Long, col As Long, iconId As Long)
Parameters
row (Long) - 0-based row index.
col (Long) - 0-based column index.
iconId (Long) - An id returned by LoadIconFromFile. Not validated against the icon pool.
Fully supported in every mode, including Turbo column-store grids: in Turbo column-store mode the assignment is resolved to the row's own durable identity at the moment this is called, rather than its current on-screen position, so the icon stays attached to the correct row through a later sort, filter, insert or delete rather than drifting to whatever row occupies that same view position afterward.
Example
VBA
Dim iconId AsLong
iconId = grid.LoadIconFromFile("C:\Icons\star.ico")
grid.SetCellIcon 0, 1, iconId
Debug.Print grid.GetCellIcon(0, 1) ' -> the value of iconId' With no built-in icon configured anywhere on this column, the built-in' system has nothing to say about this cell, so resolution falls through' to this external assignmentDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
Debug.Print kind ' -> 1 (External)' A built-in icon anywhere on the same column is resolved FIRST and wins' outright, even over this cell's own external assignment
grid.SetColumnBuiltinIcon 1, 0
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
Debug.Print kind ' -> 2 (Builtin)
grid.ClearColumnBuiltinIcon 1
Gotchas
In Embedded and Virtual mode, row and col are NOT bounds-checked at all -- an out-of-range value is silently accepted and stored; it simply never corresponds to a paintable cell. Turbo column-store mode is stricter and refuses an out-of-range row or column outright.
iconId is never validated against the icon pool -- assigning an id that was never loaded (or was already removed) is accepted without error and paints nothing.
Removing the referenced icon later with RemoveIcon clears this assignment automatically in Embedded and Virtual mode, but NOT in Turbo column-store mode -- see RemoveIcon's own gotchas.
A cell-level assignment here only wins once the ENTIRE built-in icon system has nothing to say about that same cell -- GetResolvedIcon (and the real paint loop, which carries its own comment that built-in icons "take priority over external ImageList icons") always resolves the built-in system completely first: a per-cell builtin override, then a column-wide builtin icon (SetColumnBuiltinIcon), then the builtin value map/threshold/default chain -- and only falls through to this call's own assignment once every one of those comes up empty. A column-wide builtin icon alone is enough to make a cell paint that instead of this per-cell external assignment, even though the built-in one is only column-wide and this one is cell-specific.
Raising or lowering the IconSize property clears every SetCellIcon assignment on the whole grid (along with every column icon rule), not just the loaded icons themselves -- see LoadIconFromFile's gotchas.
See alsoClearCellIcon, GetCellIcon, GetResolvedIcon, RemoveIcon, SetColumnBuiltinIcon, IconSize
ClearCellIconIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a cell-level icon override, letting the column's own rules resolve that cell again.
Removes any icon previously assigned to (row, col) with SetCellIcon. Once cleared, that cell falls back through the normal resolution order -- the built-in system first, then any external column-level value map, threshold or default -- exactly as if SetCellIcon had never been called on it. Clearing a cell that has no assignment is a safe no-op.
SignatureSub ClearCellIcon(row As Long, col As Long)
Fully supported in every mode. In Turbo column-store mode this resolves the cell's current row identity the same way SetCellIcon does, so it clears the correct row even after a sort or filter has moved it to a different on-screen position.
Example
VBA
Dim someIconId AsLong
someIconId = grid.LoadIconFromFile("C:\Icons\flag_green.ico")
grid.SetCellIcon 0, 0, someIconId
grid.ClearCellIcon 0, 0Debug.Print grid.GetCellIcon(0, 0) ' -> -1' Clearing an already-clear cell is a safe no-op
grid.ClearCellIcon 0, 0
Gotchas
Clearing a cell with no assignment at all raises no error -- it is a safe no-op, for any row or column value including negative or wildly out-of-range ones.
Unlike SetCellIcon, ClearCellIcon never raises an error for an out-of-range row or column, not even in Turbo column-store mode. Turbo column-store mode does check the row and column internally before erasing, but only to decide whether there is anything to erase there -- not to refuse the call; either way it returns successfully, exactly as Embedded and Virtual mode do.
See alsoSetCellIcon, GetCellIcon
GetCellIconIcons, sparklines, heatmaps, data bars and conditional formatting
Get the external icon ID directly assigned to one cell, ignoring column rules.
Returns exactly what was last passed to SetCellIcon for (row, col), or -1 if no cell-level assignment exists there. This is a narrow, cell-level-only read -- it does not consult column value maps, thresholds or defaults, and it does not consult the built-in icon system at all. To find out what icon a cell will actually paint once every rule and both systems are taken into account, use GetResolvedIcon instead.
SignatureFunction GetCellIcon(row As Long, col As Long) As Long
Parameters
row (Long) - 0-based row index.
col (Long) - 0-based column index.
ReturnsThe cell's own assigned icon id, or -1 if none is assigned directly to this cell.
Fully supported in every mode, reading the same durable, row-identity-based assignment SetCellIcon writes in Turbo column-store mode.
Example
VBA
Debug.Print grid.GetCellIcon(0, 0) ' -> -1 on a cell with no direct assignment,' even if a column-level rule would resolve an icon for itDim someIconId AsLong
someIconId = grid.LoadIconFromFile("C:\Icons\flag_green.ico")
grid.SetCellIcon 0, 0, someIconId
Debug.Print grid.GetCellIcon(0, 0) ' -> someIconId
Gotchas
Returns -1 both when nothing was ever assigned AND when the assigned icon has since been removed in Embedded/Virtual mode -- there is no way to tell the two situations apart from this call alone.
Ignores column-level rules entirely, even if one would resolve an icon for this cell -- use GetResolvedIcon to see the actual, fully-resolved result in Embedded mode. In Turbo column-store or Virtual mode, GetResolvedIcon cannot help either: its own row-range check reads the Embedded row store, which is empty in those modes, so it always reports no icon there regardless of any real assignment (a confirmed limitation of GetResolvedIcon itself).
Never bounds-checks row or col and never raises an error for any value, in any mode -- a negative row/col, or one far beyond the grid's current size, simply returns -1, the same as an in-range cell with no assignment. This is unlike SetCellIcon, which explicitly refuses an out-of-range row or column in Turbo column-store mode.
See alsoSetCellIcon, ClearCellIcon, GetResolvedIcon
SetColumnIconMapIcons, sparklines, heatmaps, data bars and conditional formatting
Map an exact display-text value in a column to an external icon.
Whenever a cell in colIndex renders exactly value as its display text, and that cell has no cell-level override from SetCellIcon and no matching entry from the built-in icon system, it shows iconId. Call this once per distinct value the column can hold -- each call adds or replaces one entry in that column's own map; it does not clear entries for other values.
SignatureSub SetColumnIconMap(colIndex As Long, value As String, iconId As Long)
Parameters
colIndex (Long) - 0-based column index.
value (String) - The exact display text to match. Matching is exact and case-sensitive; there is no partial or case-insensitive mode.
iconId (Long) - An id returned by LoadIconFromFile. Not validated against the icon pool.
No mode restriction -- the value map is a single, mode-agnostic structure per column; both the Embedded/Virtual and Turbo column-store paint paths consult the identical map, only differing in how they look up which row's text to compare it against.
iconId is never checked against the icon pool -- mapping a value to an id that was never loaded (or has since been removed) is accepted without error and simply never paints an icon for that value.
A cell whose OWN display text is an empty string can never match any entry here, even one deliberately added for an empty-string value -- the resolver skips the exact-text-match step entirely whenever the cell's own text is empty.
colIndex is bounds-checked and refused if out of range, but the iconId is not -- unlike AddIconThreshold, which does validate that iconId names a currently loaded icon.
A cell-level assignment from SetCellIcon, or any match found by the built-in icon system, both take priority over this column's own value map.
GetResolvedIcon cannot verify a value-map resolution in Turbo column-store mode: its own row-range check reads the Embedded row store, which Turbo column-store never populates, so it always reports no icon there regardless of this map -- a confirmed limitation of that diagnostic call, not of SetColumnIconMap or of Turbo column-store's real paint path, which does consult this same map correctly.
Raising or lowering the IconSize property clears this column's value map -- and every other column's icon rules -- grid-wide, not just the loaded icons themselves -- see LoadIconFromFile's gotchas.
The same value-to-icon mapping also feeds the dropdown cell editor once SetColumnDropdownIcons is turned on for the column: each dropdown item is drawn with the icon its value resolves to here, so the list and the cells can never disagree.
See alsoClearColumnIconMap, SetColumnDefaultIcon, AddIconThreshold, SetColumnIconLayout, SetColumnBuiltinIconMap, SetColumnDropdownIcons, IconSize
ClearColumnIconMapIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a column's entire external icon rule -- its value map, threshold bands, default icon and layout all at once.
Despite its name, this does not clear only the value-to-icon map SetColumnIconMap builds: it erases the column's whole icon rule record for colIndex in a single call. The exact-text value map, every threshold band added with AddIconThreshold, the column default set with SetColumnDefaultIcon, and the layout set with SetColumnIconLayout are all removed together, because all four live in one shared per-column record. A column that had a customized layout goes back to having no rule record at all, which then behaves exactly like the default (IconLeft) rather than keeping the customized layout with an otherwise-empty rule.
No mode restriction -- it only touches the mode-agnostic column rule structure that both the Embedded/Virtual and Turbo column-store paint paths read from.
Example
VBA
Dim onIcon AsLong, offIcon AsLong
onIcon = grid.LoadIconFromFile("C:\Icons\flag_green.ico")
offIcon = grid.LoadIconFromFile("C:\Icons\flag_red.ico")
grid.SetColumnIconMap 0, "Active", onIcon
grid.SetColumnDefaultIcon 0, offIcon
grid.SetColumnIconLayout 0, 1' IconOnly
grid.ClearColumnIconMap 0' the value map, the default icon AND the layout are' ALL gone -- not just the value map the method's name suggests' An out-of-range column index is a silent no-op, not an error
grid.ClearColumnIconMap 999
Gotchas
colIndex is NOT bounds-checked here -- an out-of-range value is a silent no-op, unlike SetColumnIconMap, SetColumnDefaultIcon and ClearIconThresholds, all of which do refuse an out-of-range column index.
Clears the column's ENTIRE icon rule, not just the exact-text value map the name suggests -- the threshold bands (AddIconThreshold), the default icon (SetColumnDefaultIcon) and the layout (SetColumnIconLayout) are all erased in the same call, since all four are stored together as one record per column. Because the layout setting is shared with the built-in icon system (SetColumnIconLayout positions a built-in icon the same way it positions an external one), clearing it here also resets how any built-in icon in that column is positioned, even though this call never touches the built-in icon system's own rules otherwise.
GetResolvedIcon cannot be used to confirm this call's effect while running in Turbo column-store mode: it always reports no icon for every row there regardless of what rules exist, so a before/after comparison through it is meaningless in that mode even though the underlying rule record is genuinely cleared the same way in every mode.
See alsoSetColumnIconMap, AddIconThreshold, ClearIconThresholds, SetColumnDefaultIcon, SetColumnIconLayout, GetResolvedIcon
SetColumnDefaultIconIcons, sparklines, heatmaps, data bars and conditional formatting
Set the icon shown for a column's cells when no value map or threshold matches.
Sets the fallback icon for colIndex, shown for any cell that has no cell-level override, no built-in-system match, no exact value-map entry (SetColumnIconMap) and no matching threshold band (AddIconThreshold). This is checked LAST in the external system's own resolution order -- it is the external system's least specific rule.
SignatureSub SetColumnDefaultIcon(colIndex As Long, iconId As Long)
Parameters
colIndex (Long) - 0-based column index.
iconId (Long) - An id returned by LoadIconFromFile. Not validated against the icon pool.
No mode restriction -- a plain per-column setting, consulted identically regardless of data mode.
Example
VBA
Dim fallbackIcon AsLong
fallbackIcon = grid.LoadIconFromFile("C:\Icons\question_mark.ico")
grid.SetColumnDefaultIcon 2, fallbackIcon
' Any Region value with no more specific rule now shows the fallback iconDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 2, kind, extId, builtinId
Debug.Print kind & " " & extId ' -> 1 <fallbackIcon's value>
Gotchas
iconId is never checked against the icon pool, the same as SetColumnIconMap.
There is no way to clear the default back to -1 once set through this member alone -- ClearColumnIconMap removes the whole column rule entry, including the default, layout and thresholds together with the value map.
GetResolvedIcon cannot confirm this default is applied while running in Turbo column-store mode -- it always reports no icon for every row there, because it only ever inspects Embedded mode's own data. The default set here is still genuinely applied when Turbo column-store paints the grid; only this diagnostic call cannot see it in that mode.
See alsoSetColumnIconMap, AddIconThreshold, SetColumnBuiltinIconDefault, GetResolvedIcon
SetColumnIconLayoutIcons, sparklines, heatmaps, data bars and conditional formatting
Choose how a resolved icon -- external or built-in -- is positioned relative to a cell's text.
Sets whether a resolved icon in colIndex is drawn to the left of the text, to the right of it, in place of the text entirely, or not drawn at all even if a rule would otherwise resolve one. There is only one layout setting per column: the paint code applies it to a built-in vector icon exactly the same way it applies it to an external icon (both icon systems are checked against the same stored value whenever either one resolves an icon for a cell) -- there is no separate layout control for built-in icons.
SignatureSub SetColumnIconLayout(colIndex As Long, layout As Long)
No mode restriction -- a plain per-column rendering setting.
Example
VBA
Dim kind AsLong, extId AsLong, builtinId AsLong
grid.SetColumnIconLayout 2, 1' IconOnly -- text is replaced by the icon.' This is the column's only layout setting -- it repositions a built-in' vector icon here exactly the same way, if one resolves for this column
grid.GetResolvedIcon 0, 2, kind, extId, builtinId ' resolution is unaffected; only drawing changes
grid.SetColumnIconLayout 2, 2' restore the default (IconLeft)
Gotchas
The internal storage for this setting has a fifth value (an IconCenter layout) that the cell-painting code never branches on and that this method's own validation refuses outright -- only 0 through 3 are ever accepted, so IconCenter can never actually be reached through this API.
This setting is stored per column alongside the value map, threshold bands and default icon, so ClearColumnIconMap resets it back to the column having no rule entry at all (which then behaves as the default, IconLeft) rather than leaving a customized layout in place.
This is NOT external-icon-only despite being documented among the external icon system's members: it is the column's only icon layout setting. Whenever a built-in vector icon resolves for a cell in this column, the paint code positions it using this exact same value, so changing it also repositions any built-in icons here -- there is no separate SetColumnBuiltinIconLayout.
See alsoSetColumnIconMap, SetColumnDefaultIcon, SetColumnBuiltinIconMap, GetResolvedIcon
AddIconThresholdIcons, sparklines, heatmaps, data bars and conditional formatting
Add a numeric range rule -- an icon set -- to a column, checked after the exact-text value map.
Adds one band to colIndex: any cell whose value falls in [minVal, maxVal) shows iconId, checked after SetColumnIconMap's exact-text entries and before the column's default icon. A column can carry any number of bands, evaluated in the order they were added; the first band a value falls into wins, so overlapping bands are resolved by their add order, not by which is narrowest or most specific.
SignatureSub AddIconThreshold(colIndex As Long, minVal As Double, maxVal As Double, iconId As Long)
Parameters
colIndex (Long) - 0-based column index.
minVal (Double) - Inclusive lower bound.
maxVal (Double) - Exclusive upper bound. Must be greater than minVal.
iconId (Long) - An id returned by LoadIconFromFile. Unlike SetColumnIconMap and SetColumnDefaultIcon, this IS validated -- it must currently be loaded.
Unlike SetColumnIconMap and SetColumnDefaultIcon, iconId here IS validated: it must currently name a loaded icon, or the call is refused. Icon ids are slot indices rather than a dense 0..count-1 range, so this check asks the icon pool directly rather than comparing against GetIconCount().
minVal must be strictly less than maxVal, or the call is refused.
The upper bound is EXCLUSIVE -- a value exactly equal to maxVal falls into the next band (or no band) rather than this one.
Bands are matched in the order they were added, first match wins -- if two bands overlap, whichever was added first always wins for values in the overlap, regardless of which band is narrower.
GetResolvedIcon cannot confirm a threshold match while running in Turbo column-store mode -- it always reports no icon for every row there, because it only ever inspects Embedded mode's own data. The threshold genuinely is applied when Turbo column-store paints the grid; only this diagnostic call cannot see it in that mode.
See alsoClearIconThresholds, SetColumnIconMap, SetColumnDefaultIcon, AddBuiltinIconThreshold, GetResolvedIcon
ClearIconThresholdsIcons, sparklines, heatmaps, data bars and conditional formatting
Remove every numeric threshold band from a column, keeping its value map, default and layout.
Erases every band added with AddIconThreshold for colIndex. The column's exact-text value map (SetColumnIconMap), its default icon and its layout are all left untouched.
SignatureSub ClearIconThresholds(colIndex As Long)
No mode restriction -- a plain per-column structure, consulted identically regardless of data mode.
Example
VBA
Dim redIcon AsLong
redIcon = grid.LoadIconFromFile("C:\Icons\flag_red.ico")
grid.AddIconThreshold 3, 0#, 100#, redIcon
grid.ClearIconThresholds 3' the band is gone; SetColumnIconMap/SetColumnDefaultIcon entries on column 3 are untouched
Gotchas
Unlike ClearColumnIconMap, this DOES bounds-check colIndex and refuses an out-of-range value with an error, rather than treating it as a silent no-op.
A column with no threshold bands at all is a safe no-op.
GetResolvedIcon cannot confirm the surviving value map/default/layout while running in Turbo column-store mode -- it always reports no icon for every row there, because it only ever inspects Embedded mode's own data. Those surviving rules are still genuinely applied when Turbo column-store paints the grid; only this diagnostic call cannot see it in that mode.
See alsoAddIconThreshold, ClearColumnIconMap, GetResolvedIcon
SetColumnSparklineIcons, sparklines, heatmaps, data bars and conditional formatting
Choose the sparkline chart style for a column, or turn sparklines off for it.
Sets which small in-cell chart style colIndex draws for any cell that has data attached with SetSparklineData or SetSparklineDataCSV. Setting sparkType back to None (0) does not just hide the sparkline -- it deletes the column's entire sparkline configuration, including any custom colors set with SetSparklineColors; re-enabling it later starts over with the default colors.
SignatureSub SetColumnSparkline(colIndex As Long, sparkType As Long)
The TYPE setting itself has no mode restriction and can be set and read-through-effect in every mode. Its real visual effect depends entirely on SetSparklineData/SetSparklineDataCSV, which are refused in both Virtual and Turbo column-store mode -- so a Turbo column-store or Virtual column can be given a sparkline type freely, but since no cell in it can ever receive data, nothing is ever actually drawn there.
Example
VBA
grid.SetColumnSparkline 3, 1' Line sparkline on the Amount column' In Embedded mode, attach data next with SetSparklineData or SetSparklineDataCSV
grid.SetColumnSparkline 3, 0' turns it off AND discards any colors set for this column
Gotchas
Setting this back to 0 (None) deletes the column's whole sparkline configuration, not just its visibility -- any colors set with SetSparklineColors are lost too, and re-enabling the column later starts from the default colors again.
In Turbo column-store and Virtual mode this can be set without error, but it can never have a visible effect, because the one way to attach chart data to a cell (SetSparklineData / SetSparklineDataCSV) is refused in both of those modes.
See alsoSetSparklineData, SetSparklineDataCSV, SetSparklineColors
SetSparklineDataIcons, sparklines, heatmaps, data bars and conditional formatting
Attach a series of numbers to one cell for it to draw as a sparkline.
Sets the chart data points for the single cell at (row, col), replacing whatever was there before. dataPoints must be a genuine one-dimensional array (VBA passes an array by reference, which this handles automatically); numeric element types are read directly, and any other element type is converted to a number, defaulting to 0 for anything that cannot convert. This is the one member in the icon/sparkline/heatmap/data-bar/conditional-formatting group that is refused in both Virtual AND Turbo column-store mode -- not Virtual alone.
SignatureSub SetSparklineData(row As Long, col As Long, dataPoints As Variant)
Parameters
row (Long) - 0-based row index.
col (Long) - 0-based column index.
dataPoints (Variant) - A one-dimensional array of numbers (typically 5-30 points). Must not be empty.
Refused in BOTH Virtual and Turbo column-store mode -- unlike almost everything else in this group, which is refused only in Virtual mode and genuinely implemented for Turbo column-store. There is no per-cell data-point storage in Turbo column-store's column store, so a Turbo column-store sparkline column can be fully configured with SetColumnSparkline and SetSparklineColors and still never draw anything, because no cell in it can ever receive data.
Example
VBA
grid.SetColumnSparkline 3, 1' LineDim points(0To4) AsDouble
points(0) = 10.5: points(1) = 20.3: points(2) = 15.0: points(3) = 25.7: points(4) = 18.2
grid.SetSparklineData 0, 3, points
' Row 0, column 3 now draws a 5-point line sparkline
Gotchas
Refused in Turbo column-store mode as well as Virtual mode -- this is the one member in the whole group restricted by the same rule in both non-Embedded modes, rather than the narrower Virtual-only rule most of this group uses.
A non-array Variant, or an array with more than one dimension, is refused.
An array with zero elements is refused.
row is resolved to the underlying record's own durable identity at the moment this is called, not to whatever row happens to occupy that screen position afterward -- data attached to view row 0 stays with that same record through a later sort or filter rather than silently reattaching to whatever new row scrolls into position 0.
There is no getter to read a cell's sparkline data back -- SetSparklineDataCSV is the only alternative way to set it, not a way to read it.
The min/max used to scale the drawn chart are derived purely from the data points themselves each time they are set -- there is no way to force a fixed scale across multiple cells' sparklines.
See alsoSetSparklineDataCSV, SetColumnSparkline, SetSparklineColors
SetSparklineColorsIcons, sparklines, heatmaps, data bars and conditional formatting
Set the four colors used to draw a column's sparklines.
Sets the line color (used by the Line style), the fill color (used by the Bar style), and two highlight colors used to mark each sparkline's own highest and lowest points. All four are stored per column, independent of which sparkline type is currently selected -- setting them before ever calling SetColumnSparkline is harmless, they simply have no visible effect until a sparkline type and data both exist.
SignatureSub SetSparklineColors(colIndex As Long, lineColor As OLE_COLOR, fillColor As OLE_COLOR, highColor As OLE_COLOR, lowColor As OLE_COLOR)
Parameters
colIndex (Long) - 0-based column index.
lineColor (OLE_COLOR) - Line color for the Line sparkline style.
fillColor (OLE_COLOR) - Fill color for the Bar sparkline style.
highColor (OLE_COLOR) - Highlight color for each sparkline's own highest data point.
lowColor (OLE_COLOR) - Highlight color for each sparkline's own lowest data point.
No mode restriction -- a plain per-column color setting. In Virtual and Turbo column-store mode it can be set freely but has no visible effect, since no cell in those modes can ever hold sparkline data to draw with these colors.
Example
VBA
grid.SetColumnSparkline 3, 2' Bar
grid.SetSparklineColors 3, RGB(0, 100, 200), RGB(160, 200, 230), RGB(0, 150, 0), RGB(200, 0, 0)
' Bars now fill with a light blue; the tallest and shortest bar in each' cell's own data are additionally marked in green and red
Gotchas
Calling SetColumnSparkline with sparkType 0 (None) afterward deletes this configuration too, along with the sparkline type -- these colors do not survive being turned off and back on.
There is no getter to read these colors back -- verify them visually or by keeping track of what was set.
Stores and returns colors exactly as given, like every color-typed member in this control -- pass an explicit RGB() value rather than a system color constant.
See alsoSetColumnSparkline, SetSparklineData
SetSparklineDataCSVIcons, sparklines, heatmaps, data bars and conditional formatting
Attach sparkline data to one cell from a comma-separated string instead of a VBA array.
Equivalent to SetSparklineData, but parses csvData as comma-separated numbers instead of taking a native array -- useful when the data is already a delimited string (for example, pulled straight from a database field or a text file) rather than built up in a VBA array. Each non-empty token is parsed independently with the C run-time's own numeric parser; a token that does not parse as a number contributes a 0 rather than raising an error. A genuinely empty token -- from a leading comma, a trailing comma, or two commas in a row -- is treated differently again: it is dropped rather than turned into a point at all, so it can silently shrink the resulting series.
SignatureSub SetSparklineDataCSV(row As Long, col As Long, csvData As String)
Parameters
row (Long) - 0-based row index.
col (Long) - 0-based column index.
csvData (String) - Comma-separated numbers, e.g. "10.5,20.3,15.0". Must not be empty.
Refused in BOTH Virtual and Turbo column-store mode, exactly like SetSparklineData -- the two share the same underlying per-cell data store and the same blanket refusal outside Embedded mode.
Example
VBA
grid.SetColumnSparkline 3, 1' Line
grid.SetSparklineDataCSV 0, 3, "10.5,20.3,15.0,25.7,18.2"' Equivalent to passing the same five numbers as a VBA array to SetSparklineData
Gotchas
A token that fails to parse as a number becomes 0 rather than raising an error -- "10,abc,20" stores three points (10, 0, 20), not two.
An empty string is refused outright, but a string of only commas (",,") is not refused -- each empty token between, before, or after a comma is silently skipped rather than stored as a zero-value point, so ",," stores ZERO points, not a series of zeros. A non-empty-but-unparseable token like the middle one in "10,abc,20" is different and still contributes a 0-value point; it is only a truly empty token that gets dropped.
row is resolved to the underlying record's own durable identity at the moment this is called, exactly like SetSparklineData -- the two share the same per-cell data store, so this data survives a later sort or filter on the same record.
Refused in Turbo column-store mode as well as Virtual mode, the same as SetSparklineData.
See alsoSetSparklineData, SetColumnSparkline
SetColumnHeatmapIcons, sparklines, heatmaps, data bars and conditional formatting
Turn heatmap background tinting on or off for a column, auto-ranged from its own data.
Enables or disables heatmap coloring for colIndex. When first enabled with auto-ranging still in effect (the default, until SetHeatmapRange sets an explicit range), the column's own minimum and maximum values are found and used as the tint's endpoints. Disabling is never refused, in any mode or column type -- only enabling can be refused, and only in specific circumstances described below.
SignatureSub SetColumnHeatmap(colIndex As Long, enabled As Boolean)
Parameters
colIndex (Long) - 0-based column index.
enabled (Boolean) - True to enable heatmap tinting for this column, False to disable it.
Refused only in Virtual mode, where there is no full dataset resident to find a range in. In Turbo column-store mode this is genuinely implemented: enabling (with or without auto-range) requires the column to be one Turbo column-store maintains exact numeric statistics for (its stored type must be Int32, Int64, or Double) -- any other column type is refused outright, even a Currency or Date column. When admitted, the auto-range in Turbo column-store mode is read directly from statistics Turbo column-store already maintains for that column, so enabling a heatmap never triggers the row-by-row scan Embedded mode performs -- and Turbo column-store's range keeps following those statistics afterward too, on every redraw, not just at the moment the heatmap was enabled (see gotchas).
Example
VBA
grid.SetColumnHeatmap 3, True' Amount column, auto-ranged from its own min/max' Every cell's background is now tinted along the theme's low -> mid -> high scale
grid.SetColumnHeatmap 3, False' always safe, in every mode
Gotchas
Disabling (enabled = False) is never refused, in any mode or on any column -- there is always a way back to a clean state.
In Turbo column-store mode, enabling requires the column's stored type to be Int32, Int64 or Double -- a Currency, Date, Boolean or String column is refused outright, even though none of those types would necessarily be a problem in Embedded mode.
In Turbo column-store mode, an auto-ranged heatmap's min/max is not fixed at the moment it is enabled -- it is re-read from Turbo column-store's own maintained per-column statistics on every redraw, so the tint can shift on its own as the underlying data changes, with no need to call SetColumnHeatmap again. Embedded mode works the opposite way: the auto-range is computed once, by scanning the column, at the moment SetColumnHeatmap enables it, and stays fixed at that scan's result even if the data changes afterward -- the only way to force a rescan there is to disable and re-enable the heatmap.
In Embedded mode, enabling a heatmap on a non-numeric column is never refused -- cells whose value cannot convert to a number simply never receive a heatmap tint, and if NO cell in the column converts, the range silently falls back to 0..1 and nothing is tinted anywhere. Nothing raises an error.
There is no getter for whether a column's heatmap is enabled, or what range it resolved to -- GetCurrentThemeJson (Appearance group) does not cover this either.
See alsoSetHeatmapColors, SetHeatmapRange, SetColumnDataBar
SetHeatmapColorsIcons, sparklines, heatmaps, data bars and conditional formatting
Set the three-stop color gradient a column's heatmap interpolates across.
Sets the colors a heatmap-tinted cell is drawn with: lowColor at the bottom of the range, midColor at its midpoint, and highColor at its top, with every value in between smoothly blended across the nearer half of that three-stop gradient (values below the midpoint blend between lowColor and midColor; values above it blend between midColor and highColor). Can be called before SetColumnHeatmap ever enables the column -- the colors are simply stored, unused, until then. Calling it takes the column off the theme's colors: from then on, Theme and ApplyThemeJson changes no longer recolor that column's heatmap.
SignatureSub SetHeatmapColors(colIndex As Long, lowColor As OLE_COLOR, midColor As OLE_COLOR, highColor As OLE_COLOR)
Parameters
colIndex (Long) - 0-based column index.
lowColor (OLE_COLOR) - Color at the low end of the range.
midColor (OLE_COLOR) - Color at the midpoint of the range.
highColor (OLE_COLOR) - Color at the high end of the range.
ReturnsNone.
DefaultThe theme's Heat_Low, Heat_Mid and Heat_High roles, kept in step with them: until this method is called for a column, a Theme or ApplyThemeJson change recolors that column's heatmap too.
Refused only in Virtual mode. In Turbo column-store mode, this is admitted under the exact same numeric-type rule SetColumnHeatmap uses (the column must be Int32, Int64 or Double) even though setting colors alone does not itself need any data -- a column that could never carry a working heatmap in Turbo column-store mode is not allowed to carry heatmap colors either.
Example
VBA
grid.SetHeatmapColors 3, RGB(220, 50, 50), RGB(255, 255, 200), RGB(50, 160, 50)
grid.SetColumnHeatmap 3, True' now uses this red/yellow/green scale instead of the theme's colors
Gotchas
In Turbo column-store mode, this is refused for a non-numeric column even though colors are not data-dependent -- the admission check is about whether the column could ever carry a heatmap at all, not about this specific call's own needs.
A value exactly at or below the range's minimum always renders as lowColor exactly, and a value at or above the maximum always renders as highColor exactly -- neither is extrapolated past the gradient's own ends.
Once called, the column keeps these colors through every theme change. To put the column back on the theme's colors, disable and re-enable its heatmap with SetColumnHeatmap, which discards the column's heatmap configuration.
There is no getter for these colors -- verify visually or track what was set.
See alsoSetColumnHeatmap, SetHeatmapRange, ApplyThemeJson
SetHeatmapRangeIcons, sparklines, heatmaps, data bars and conditional formatting
Set an explicit heatmap value range for a column, replacing its automatic min/max.
Fixes the range a heatmap-tinted column interpolates its colors across, instead of the column's own automatically-detected minimum and maximum. Calling this switches the column to explicit-range mode -- it will not go back to auto-ranging on its own; the only way back is to disable and re-enable the heatmap with SetColumnHeatmap.
SignatureSub SetHeatmapRange(colIndex As Long, minVal As Double, maxVal As Double)
Parameters
colIndex (Long) - 0-based column index.
minVal (Double) - Value mapped to the low end of the gradient.
maxVal (Double) - Value mapped to the high end of the gradient. Must be greater than minVal.
ReturnsNone.
DefaultAutomatic range (from the column's own data) until this is called.
Refused only in Virtual mode. Admitted in Turbo column-store mode under the same numeric-type rule as SetColumnHeatmap and SetHeatmapColors.
Example
VBA
' Fix the scale at 0-500 regardless of what the data actually contains
grid.SetHeatmapRange 3, 0#, 500#
grid.SetColumnHeatmap 3, True' now uses the fixed 0-500 range, not the column's own min/max
Gotchas
minVal must be strictly less than maxVal, or the call is refused.
Switches the column to a fixed range permanently -- there is no direct call to switch back to auto-ranging; disable and re-enable the heatmap with SetColumnHeatmap instead.
In Turbo column-store mode this is refused for a non-numeric column the same way SetHeatmapColors is, even though an explicit range does not need to be computed from the data.
See alsoSetColumnHeatmap, SetHeatmapColors
SetColumnDataBarIcons, sparklines, heatmaps, data bars and conditional formatting
Turn a proportional in-cell bar on or off for a column, auto-ranged from its own data.
Enables or disables a data bar for colIndex -- a bar drawn inside each cell whose length is proportional to that cell's value within the column's range. Behaves as a close parallel to SetColumnHeatmap: auto-ranges from the column's own min/max by default, and shares the exact same Turbo column-store-mode numeric-type admission rule. Disabling is never refused.
SignatureSub SetColumnDataBar(colIndex As Long, enabled As Boolean)
Parameters
colIndex (Long) - 0-based column index.
enabled (Boolean) - True to enable a data bar for this column, False to disable it.
Refused only in Virtual mode. In Turbo column-store mode, enabling requires the column to be Int32, Int64 or Double, exactly as SetColumnHeatmap does, and uses the same precomputed statistics for auto-ranging rather than a fresh scan.
Example
VBA
grid.SetColumnDataBar 3, True' Amount column, auto-ranged from its own min/max' Each cell now draws a bar proportional to its own value within that range
Gotchas
Disabling a column's data bar is never refused for Turbo column-store admission or column-type reasons -- unlike enabling, it does not run the Turbo column-store numeric-type check at all. It is still subject to the same colIndex bounds check as enabling, though: that check runs before the code even looks at enabled, so SetColumnDataBar on a negative or out-of-range column index is refused with an error whether enabled is True or False.
In Turbo column-store mode, enabling on a non-numeric column is refused outright.
In Embedded mode, enabling on a non-numeric column is never refused; cells that cannot convert to a number simply never draw a bar.
A negative value is clamped into the bar's own 0..1 fraction of the cell width rather than drawn as a bar extending in the opposite direction -- there is no separate visual treatment for negative values reachable through this API (see SetDataBarColor's own gotchas).
Disabling does not just hide the bar -- it erases the column's entire data-bar configuration outright, including any fill color set with SetDataBarColor and any explicit range set with SetDataBarRange. Re-enabling the same column afterward starts over from the defaults (auto-range, the theme's accent color as the fill), not from whatever was configured before it was disabled.
See alsoSetDataBarColor, SetDataBarRange, SetColumnHeatmap
SetDataBarColorIcons, sparklines, heatmaps, data bars and conditional formatting
Set the fill color a column's data bars are drawn with.
Sets the single color used to fill every data bar in colIndex. Can be called before the column's data bar is ever enabled with SetColumnDataBar; the color is simply stored until then. Calling it takes the column off the theme's accent color: from then on, Theme and ApplyThemeJson changes no longer recolor that column's bars.
SignatureSub SetDataBarColor(colIndex As Long, barColor As OLE_COLOR)
Parameters
colIndex (Long) - 0-based column index.
barColor (OLE_COLOR) - Fill color for the bar.
ReturnsNone.
DefaultThe theme's Accent_Primary role, kept in step with it until this method is called for the column.
Refused only in Virtual mode. Admitted in Turbo column-store mode under the same numeric-type rule the rest of the data-bar and heatmap members use.
Example
VBA
grid.SetDataBarColor 3, RGB(80, 160, 80) ' a green fill instead of the theme's accent color
grid.SetColumnDataBar 3, True
Gotchas
A separate color for negative values, and a toggle for overlaying the numeric value as text on top of the bar, both exist internally -- but neither is read anywhere by the current bar-painting code, and neither has any property or method that can set it. There is currently no way to change how a negative value's bar is colored, or to turn the overlaid value text on or off, through this control's API.
In Turbo column-store mode this is refused for a non-numeric column, even though a fill color is not itself data-dependent -- the same admission rule as SetHeatmapColors.
There is no getter for this color -- verify visually or track what was set.
Toggling the column's data bar off and back on with SetColumnDataBar erases this color along with the rest of the column's data-bar configuration -- re-enabling returns to the theme's accent color, not the color that was set here before disabling. Call SetDataBarColor again after re-enabling if a custom color needs to persist.
See alsoSetColumnDataBar, SetDataBarRange, ApplyThemeJson
SetDataBarRangeIcons, sparklines, heatmaps, data bars and conditional formatting
Set an explicit data-bar value range for a column, replacing its automatic min/max.
Fixes the range a column's data bars are scaled against, instead of its own automatically-detected minimum and maximum. Exactly parallels SetHeatmapRange: switches the column to a fixed range permanently until the data bar is disabled and re-enabled.
SignatureSub SetDataBarRange(colIndex As Long, minVal As Double, maxVal As Double)
Parameters
colIndex (Long) - 0-based column index.
minVal (Double) - Value that draws a zero-length bar.
maxVal (Double) - Value that draws a full-width bar. Must be greater than minVal.
ReturnsNone.
DefaultAutomatic range (from the column's own data) until this is called.
Refused only in Virtual mode. Admitted in Turbo column-store mode under the same numeric-type rule as the rest of the data-bar and heatmap members.
Example
VBA
grid.SetDataBarRange 3, 0#, 1000#
grid.SetColumnDataBar 3, True' bars now scale against a fixed 0-1000 range
Gotchas
minVal must be strictly less than maxVal, or the call is refused.
Switches the column to a fixed range permanently -- disable and re-enable the data bar with SetColumnDataBar to return to auto-ranging.
In Turbo column-store mode this is refused for a non-numeric column, the same admission rule as every other heatmap/data-bar member.
See alsoSetColumnDataBar, SetDataBarColor
AddFormatRuleIcons, sparklines, heatmaps, data bars and conditional formatting
Add a conditional formatting rule to a column and get back an ID to manage it with.
Adds one rule to colIndex, evaluated against every cell in that column on each paint. A column can carry any number of rules, evaluated in the order they were added; the first rule a cell matches supplies its colors (and, if SetFormatRuleIcon has set one, its icon) -- later rules on the same column are never reached for that cell. Between rules need a second call to SetFormatRuleThreshold2 to supply their upper bound; Top10Pct/Bottom10Pct/Duplicate/Unique rules ignore threshold entirely except that Top10Pct/Bottom10Pct read it as an optional percentage.
SignatureFunction AddFormatRule(colIndex As Long, ruleType As Long, threshold As Variant, backColor As OLE_COLOR, foreColor As OLE_COLOR) As Long
Parameters
colIndex (Long) - 0-based column index. -1 is accepted without error but never actually matches any cell (see the gotchas).
threshold (Variant) - Meaning depends on ruleType: the numeric comparison value for GreaterThan/LessThan/Between's lower bound; a number or string for Equal; a string for Contains; an optional percentage (default 10) for Top10Pct/Bottom10Pct; ignored for Duplicate/Unique.
backColor (OLE_COLOR) - Background color applied when this rule matches.
foreColor (OLE_COLOR) - Text color applied when this rule matches.
ReturnsA positive rule id, for use with RemoveFormatRule, SetFormatRuleIcon and SetFormatRuleThreshold2.
Value
Meaning
0
GreaterThan -- the cell's value must convert to a number greater than threshold.
1
LessThan -- less than threshold.
2
Equal -- numerically equal, or (if threshold is a string) an exact text match.
3
Contains -- a case-insensitive substring match; threshold must be a string.
4
Between -- inclusive of both bounds; the upper bound must be set separately with SetFormatRuleThreshold2, or this rule can never match.
5
Top10Pct -- the cell is among the top threshold percent (default 10) of the column's own values. Turbo column-store mode requires an Int32/Int64/Double column.
6
Bottom10Pct -- the bottom threshold percent. Same Turbo column-store restriction as Top10Pct.
7
Duplicate -- the cell's rendered text appears more than once in the column. Works on any column type in both modes.
8
Unique -- the cell's rendered text appears exactly once in the column.
Refused only in Virtual mode, where there is no full dataset to build column-wide facts from. Genuinely implemented in Turbo column-store mode: GreaterThan, LessThan, Equal, Contains, Between, Duplicate and Unique all work on any column type there, since each is evaluated directly against a cell's own value or rendered text; only Top10Pct and Bottom10Pct require the column's stored type to be Int32, Int64 or Double, because those two need an exact whole-column numeric pass that Turbo column-store only builds for those types. Turbo column-store mode also refuses colIndex = -1 outright by name, rather than accepting it as inert the way Embedded mode does. Turbo column-store is also the only mode that checks a rule's threshold against its rule type at add time (see the gotchas below) and the only one that bounds-checks colIndex against the real column count -- Embedded mode accepts a mismatched threshold or an out-of-range colIndex without error either way and simply never matches anything with it.
Example
VBA
' Flag any Amount over 500 with a warm highlightDim ruleId AsLong
ruleId = grid.AddFormatRule(3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0)) ' 0 = GreaterThan' A Between rule needs its upper bound supplied separatelyDim rangeRuleId AsLong
rangeRuleId = grid.AddFormatRule(3, 4, CDbl(100), RGB(220, 240, 255), RGB(0, 0, 80)) ' 4 = Between, lower = 100
grid.SetFormatRuleThreshold2 rangeRuleId, CDbl(200) ' upper = 200 -- without this call the rule never matches
Gotchas
colIndex = -1 is accepted without error in Embedded and Turbo column-store... except Turbo column-store explicitly refuses it with a named error ("colIndex -1 (all columns) is not supported in Turbo column-store mode"), while Embedded silently accepts it and stores the rule -- but the per-cell evaluator that actually paints a rule compares the rule's column index for EXACT equality against the column being painted, and no real column is ever -1. A rule added with colIndex = -1 can therefore never match any cell in Embedded mode either, even though it is accepted without error there. There is currently no working way to apply one rule to every column.
A Between rule (ruleType 4) can never match anything until SetFormatRuleThreshold2 gives it an upper bound -- without that second call, the upper bound stays unset and the rule is permanently inert.
Whether a mismatched threshold is even caught depends on the mode. In Turbo column-store mode, AddFormatRule validates the threshold's type up front: Contains refuses a non-string threshold, GreaterThan/LessThan/Between refuse one that cannot convert to a number, Equal requires a number or a string, and for Top10Pct/Bottom10Pct the threshold (if supplied) must convert to a number in (0, 100] or the call is refused -- passing Empty or Null keeps the default of 10 (constant). In Embedded mode none of this is checked when the rule is added: a Contains rule created with a numeric threshold, for example, succeeds there and returns a normal positive rule id. The rule is simply inert afterward, because the comparison it needs (a string match for Contains, a numeric compare for GreaterThan/LessThan/Between/Equal) never succeeds against a wrong-typed threshold, and an unparseable Top10Pct/Bottom10Pct percentage silently falls back to the default 10% instead of causing an error.
A colIndex that is positive but names no real column (for example, 999 on a grid with 9 columns) is treated in Embedded mode exactly like colIndex = -1: it is accepted without error and returns a normal positive rule id, and the rule can never match any cell, for the same reason a -1 rule cannot. Turbo column-store mode bounds-checks this case and refuses it outright instead -- the silent-accept behavior described here is Embedded-only.
Rules on a column are evaluated in the order they were added -- only the first match on a given cell applies; reordering is not possible other than removing and re-adding.
There is no getter to read back a rule's own settings, and no way to observe through COM whether a given cell actually matched a given rule -- only the accept/refuse behavior of this call itself is directly testable.
See alsoRemoveFormatRule, ClearFormatRules, SetFormatRuleIcon, SetFormatRuleThreshold2, AddRowFormatRule
RemoveFormatRuleIcons, sparklines, heatmaps, data bars and conditional formatting
Remove one conditional formatting rule by its ID.
Removes exactly the rule identified by ruleId, leaving every other rule (on this column or any other) untouched. Unlike RemoveIcon or RemoveValidationRule, an unrecognized ruleId is NOT a silent no-op here -- it is refused with an error.
SignatureSub RemoveFormatRule(ruleId As Long)
Parameters
ruleId (Long) - The id AddFormatRule (or AddRowFormatRule) returned when the rule was created.
Refused only in Virtual mode. Genuinely implemented in Turbo column-store mode -- it rebuilds the column-wide facts (Top/Bottom thresholds, duplicate sets) for the rules that remain BEFORE actually removing the rule, so a failure while rebuilding those facts leaves the rule, and everything else, completely unchanged.
Example
VBA
Dim ruleId AsLong
ruleId = grid.AddFormatRule(3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0))
grid.RemoveFormatRule ruleId ' the rule is gone; every other rule on column 3 is unaffectedOnErrorResumeNext
grid.RemoveFormatRule 999999' refused -- unlike RemoveIcon, an unknown id is an error hereDebug.PrintErr.Number <> 0' -> TrueOnErrorGoTo0
Gotchas
Unlike RemoveIcon and RemoveValidationRule, removing a ruleId that does not currently exist is refused with an error rather than treated as a silent no-op.
Removes rules created by AddRowFormatRule the same way as ones created by AddFormatRule -- both share the same id space.
See alsoAddFormatRule, ClearFormatRules
ClearFormatRulesIcons, sparklines, heatmaps, data bars and conditional formatting
Remove every conditional formatting rule from one column, or from every column at once.
Removes every rule attached to colIndex. Passing -1 (or, in fact, any negative value) removes every rule on every column instead. Unlike AddFormatRule, -1 here IS a genuinely functional "all columns" value -- clearing does not suffer from the exact-match limitation that makes -1 permanently inert when adding a rule.
SignatureSub ClearFormatRules(colIndex As Long)
Parameters
colIndex (Long) - 0-based column to clear rules from, or -1 (or any negative value) for every column.
Refused only in Virtual mode. Genuinely implemented in Turbo column-store mode: clearing is never refused by scope there either, and the facts for whichever rules remain are rebuilt before the removal takes effect.
Example
VBA
grid.AddFormatRule 3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0)
grid.AddFormatRule 2, 7, Empty, RGB(255, 255, 220), RGB(0, 0, 0)
grid.ClearFormatRules 3' only column 3's rule is removed
grid.ClearFormatRules -1' every remaining rule, on every column, is removed
Gotchas
Any negative value, not just exactly -1, clears every column's rules -- there is no bounds check that would reject, say, -5.
A column with no rules at all is a safe no-op.
This is one of the few members in this pair where -1 genuinely means "all columns" end to end -- contrast with AddFormatRule, where -1 is accepted but the resulting rule can never match anything.
See alsoAddFormatRule, RemoveFormatRule
SetFormatRuleIconIcons, sparklines, heatmaps, data bars and conditional formatting
Attach an external icon to a formatting rule, shown on any cell the rule matches.
Associates iconId with ruleId, so that a matching cell also shows this icon in addition to the rule's colors. This is a THIRD icon source, independent of both the external per-cell/column icon system and the built-in icon system -- GetResolvedIcon does not look at it at all, so it never reports an icon that came from here.
SignatureSub SetFormatRuleIcon(ruleId As Long, iconId As Long)
Parameters
ruleId (Long) - An id returned by AddFormatRule or AddRowFormatRule.
iconId (Long) - An id returned by LoadIconFromFile, or a negative value to clear the rule's icon action.
ReturnsNone.
Default-1 (no icon action) for a newly added rule.
Refused only in Virtual mode. Genuinely implemented in Turbo column-store mode, but with STRICTER validation than Embedded mode uses for the identical call.
Example
VBA
Dim ruleId AsLong, warnIcon AsLong
warnIcon = grid.LoadIconFromFile("C:\Icons\warning.ico")
ruleId = grid.AddFormatRule(3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0))
grid.SetFormatRuleIcon ruleId, warnIcon
' A cell matching this rule now also shows the warning icon
grid.SetFormatRuleIcon ruleId, -1' clears the icon action; the color rule itself is untouched
Gotchas
In Embedded mode, iconId is never validated -- an id that names no loaded icon is accepted without error and simply never draws anything. In Turbo column-store mode the SAME call is validated: a non-negative iconId must name a currently loaded icon, or the call is refused.
GetResolvedIcon never reports an icon set this way -- it only resolves the external per-cell/column icon system and the built-in icon system, not a conditional-formatting rule's own icon action. There is no COM member that reads a rule's icon back.
A negative iconId always clears the rule's icon action and is always accepted, in every mode -- there is always a way back to "no icon".
Removing the referenced icon later with RemoveIcon clears this rule's icon action too, in every mode (ConditionalFormatEngine::ClearIconReferences), unlike the Turbo column-store-mode gap that affects SetCellIcon's own assignments.
See alsoAddFormatRule, RemoveIcon, GetResolvedIcon
SetFormatRuleThreshold2Icons, sparklines, heatmaps, data bars and conditional formatting
Set the upper bound for a Between rule.
Sets the second, upper bound for the Between rule (ruleType 4) identified by ruleId; AddFormatRule's own threshold parameter only ever supplies the lower bound. A Between rule created without ever calling this can never match any cell, since the match test needs both bounds to convert to numbers.
SignatureSub SetFormatRuleThreshold2(ruleId As Long, threshold2 As Variant)
Parameters
ruleId (Long) - An id returned by AddFormatRule for a Between (ruleType 4) rule.
threshold2 (Variant) - The upper bound. Must convert to a number for a Between rule in Turbo column-store mode; stored as given for every other rule type.
ReturnsNone.
DefaultUnset (VT_EMPTY) for a newly added rule -- a Between rule with no threshold2 can never match any cell.
Refused only in Virtual mode. Genuinely implemented in Turbo column-store mode, again with STRICTER validation than Embedded mode for the same call.
Example
VBA
Dim ruleId AsLong
ruleId = grid.AddFormatRule(3, 4, CDbl(100), RGB(220, 240, 255), RGB(0, 0, 80)) ' Between, lower = 100
grid.SetFormatRuleThreshold2 ruleId, CDbl(200) ' upper = 200' A cell with Amount between 100 and 200 (inclusive) now matches
Gotchas
In Embedded mode, threshold2 is stored exactly as given with no validation at all, even for a Between rule -- a non-numeric value is accepted, and the Between rule then simply never matches anything (it fails the same numeric-conversion test as an unset threshold2 would). In Turbo column-store mode the SAME call is validated up front: if the target rule is a Between rule, threshold2 must convert to a number, or the call is refused outright.
Calling this on a rule that is not a Between rule is accepted in every mode and simply stores the value inertly -- it has no effect on GreaterThan, LessThan, Equal, Contains, Top10Pct, Bottom10Pct, Duplicate or Unique rules.
An unknown ruleId is refused, the same as SetFormatRuleIcon.
See alsoAddFormatRule
AddRowFormatRuleIcons, sparklines, heatmaps, data bars and conditional formatting
Add a conditional formatting rule that evaluates one column but colors the entire row.
Works exactly like AddFormatRule, evaluating evalColumn with the same ruleType/threshold rules, but on a match it applies backColor/foreColor across every column in that row instead of just evalColumn's own cell. It shares AddFormatRule's own rule id space (RemoveFormatRule and SetFormatRuleIcon both work on a row-scope rule the same way), and shares its exact same Turbo column-store-mode admission logic and restrictions.
SignatureFunction AddRowFormatRule(evalColumn As Long, ruleType As Long, threshold As Variant, backColor As OLE_COLOR, foreColor As OLE_COLOR) As Long
Parameters
evalColumn (Long) - 0-based column whose value decides whether the rule matches. Not the column the color is limited to -- the whole row is colored.
ruleType (Long) - Same 0-8 values as AddFormatRule.
threshold (Variant) - Same meaning as AddFormatRule's threshold, evaluated against evalColumn.
backColor (OLE_COLOR) - Background color applied to every cell in a matching row.
foreColor (OLE_COLOR) - Text color applied to every cell in a matching row.
ReturnsA positive rule id, for use with RemoveFormatRule and SetFormatRuleIcon.
Refused only in Virtual mode. Genuinely implemented in Turbo column-store mode using the identical admission function AddFormatRule uses on evalColumn -- Top10Pct/Bottom10Pct still require an Int32/Int64/Double evalColumn there, and evalColumn = -1 is refused by name, the same as colIndex on AddFormatRule.
Example
VBA
' Highlight the ENTIRE row wherever Amount exceeds 500, not just the Amount cellDim ruleId AsLong
ruleId = grid.AddRowFormatRule(3, 0, CDbl(500), RGB(255, 235, 205), RGB(140, 70, 0)) ' 0 = GreaterThan
grid.RemoveFormatRule ruleId ' removed the same way as an ordinary AddFormatRule rule
Gotchas
The color is applied to the WHOLE row, not just evalColumn's own cell -- pick evalColumn as the column whose value should decide the row's highlighting, independent of which columns actually change color.
Shares AddFormatRule's threshold validation rules exactly (Contains needs a string, GreaterThan/LessThan/Between need a numeric threshold, and so on), evaluated against evalColumn.
A row-scope rule and a cell-scope rule can both be active on the same column at once without conflicting -- a row-scope rule is only ever evaluated once per row, never a second time as an ordinary cell-scope rule on its own evalColumn.
Removed with the same RemoveFormatRule used for ordinary rules, and given an icon with the same SetFormatRuleIcon -- there is no row-scope-specific variant of either.
See alsoAddFormatRule, RemoveFormatRule, SetFormatRuleIcon
SetCellBuiltinIconIcons, sparklines, heatmaps, data bars and conditional formatting
Set a built-in vector icon on one specific cell, overriding every column-level built-in rule.
Attaches builtinIcon to the single cell at (row, col), drawn as one of eight small vector glyphs with no file to load. A cell-level built-in assignment outranks everything else in the built-in system for that cell -- the column-wide icon (SetColumnBuiltinIcon) and the whole rule chain (SetColumnBuiltinIconMap / AddBuiltinIconThreshold / SetColumnBuiltinIconDefault) alike -- and it also outranks the entire external icon system, since GetResolvedIcon and the paint code both check the built-in system first.
SignatureSub SetCellBuiltinIcon(row As Long, col As Long, builtinIcon As Long)
Refused in BOTH Virtual and Turbo column-store mode -- unlike its external-system counterpart SetCellIcon, which is fully Turbo column-store-safe. There is currently no way to give an individual cell its own built-in icon override in a Turbo column-store grid; only the column-wide icon and rule chain (SetColumnBuiltinIcon and friends) work there.
Example
VBA
grid.SetCellBuiltinIcon 0, 1, 4' one of the eight built-in glyphs, no file involvedDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
Debug.Print kind & " " & builtinId ' -> 2 4 (Builtin)
Gotchas
This is the one true asymmetry between the two icon systems' Turbo column-store support: SetCellIcon (external, per-cell) works fully in Turbo column-store mode with durable row-identity tracking; SetCellBuiltinIcon (built-in, per-cell) is refused outright in Turbo column-store mode, even though every column-level built-in member works there.
row and col ARE bounds-checked here (unlike SetCellIcon's own Embedded/Virtual path), and refused if out of range.
builtinIcon must be 0-7; anything else is refused.
See alsoClearCellBuiltinIcon, SetColumnBuiltinIcon, SetCellIcon, GetResolvedIcon
ClearCellBuiltinIconIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a cell-level built-in icon override, letting the column's built-in rules resolve that cell again.
Removes any built-in icon previously assigned to (row, col) with SetCellBuiltinIcon. Once cleared, the cell falls back to the column-wide icon (if any) and then the rule chain, exactly as if SetCellBuiltinIcon had never been called on it.
SignatureSub ClearCellBuiltinIcon(row As Long, col As Long)
Not restricted in any mode, unlike SetCellBuiltinIcon itself. In Turbo column-store and Virtual mode this always succeeds without error -- but since SetCellBuiltinIcon is refused there, nothing could ever have been assigned in those modes in the first place, so calling this there is always a genuine no-op rather than a meaningful clear.
Example
VBA
If grid.DataMode <> 2Then' SetCellBuiltinIcon itself is Turbo column-store-refused, so this setup step only runs' outside Turbo column-store mode -- see SetCellBuiltinIcon's own entry
grid.SetCellBuiltinIcon 0, 1, 4' one of the eight built-in glyphs, no file involvedEndIf
grid.ClearCellBuiltinIcon 0, 1' supported in every mode, including Turbo column-store, and safe to call even if nothing was ever setDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
If grid.DataMode <> 2ThenDebug.Print kind ' -> 0 (None), or whatever the column's own rules resolveElse' GetResolvedIcon always reports kind = 0 in Turbo column-store mode regardless of the' real state -- see GetResolvedIcon's own entry for whyDebug.Print"GetResolvedIcon cannot verify this in Turbo column-store mode"EndIf
Gotchas
Neither row nor col is bounds-checked at all -- an out-of-range cell is silently accepted, in every mode.
Callable without error in Virtual and Turbo column-store mode, but functionally a no-op there, since the one way to assign a cell-level built-in icon (SetCellBuiltinIcon) is refused in both of those modes.
See alsoSetCellBuiltinIcon
SetColumnBuiltinIconIcons, sparklines, heatmaps, data bars and conditional formatting
Set one built-in vector icon for every row in a column, unconditionally.
Every cell in col shows builtinIcon, with no per-value logic at all -- this has no equivalent anywhere in the external icon system, where SetColumnDefaultIcon is only ever a last-resort fallback. This column-wide icon is checked BEFORE the built-in value map, threshold bands and default (SetColumnBuiltinIconMap / AddBuiltinIconThreshold / SetColumnBuiltinIconDefault), so setting this makes that whole rule chain invisible for every row in the column until ClearColumnBuiltinIcon removes it again. A cell-level override from SetCellBuiltinIcon still outranks even this.
SignatureSub SetColumnBuiltinIcon(col As Long, builtinIcon As Long)
No mode restriction at all -- this assigns one icon uniformly to every row, so there is no per-cell row-identity concern the way SetCellBuiltinIcon has, and it works identically in Embedded, Virtual, and Turbo column-store grids.
Example
VBA
grid.SetColumnBuiltinIcon 1, 5' every row in column 1 now shows builtinIcon 5Dim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 1, kind, extId, builtinId
Debug.Print builtinId ' -> 5, regardless of any value map or threshold set on this column
Gotchas
Outranks the built-in value map, threshold bands and default for this column entirely -- if this is set, none of those three are ever consulted for any row in the column until ClearColumnBuiltinIcon is called. This is easy to set and forget while also building a rule chain that then never visibly does anything.
The external icon system has no equivalent to this member -- its closest counterpart, SetColumnDefaultIcon, is checked LAST (a fallback), not first.
A cell-level override from SetCellBuiltinIcon still overrides even this column-wide icon, for that one cell.
GetResolvedIcon cannot be used to confirm this in Virtual or Turbo column-store mode: its own row-range check reads the row store Embedded mode uses, which stays empty in those two modes (LoadFromArray deliberately clears it when routing to Turbo column-store), so it reports no icon for every row there regardless of what SetColumnBuiltinIcon actually set. The column-wide icon is genuinely set and paints normally in Turbo column-store mode -- only this particular read-back call cannot see it.
See alsoClearColumnBuiltinIcon, SetColumnBuiltinIconMap, SetColumnBuiltinIconDefault, SetCellBuiltinIcon
ClearColumnBuiltinIconIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a column-wide built-in icon, letting the column's built-in rule chain and cell overrides resolve again.
Clears the unconditional column-wide icon set with SetColumnBuiltinIcon for col. Cell-level overrides (SetCellBuiltinIcon) and the value map/threshold/default rule chain are all left completely untouched -- they simply become reachable again, having been silently overridden the whole time this was set.
No mode restriction -- consistent with SetColumnBuiltinIcon itself having none.
Example
VBA
grid.SetColumnBuiltinIcon 1, 5
grid.ClearColumnBuiltinIcon 1' any value map / thresholds / default set on column 1 are reachable again
Gotchas
col is NOT bounds-checked -- an out-of-range value is a silent no-op.
Only removes the column-wide icon itself; every cell override and every rule-chain entry on the column survives and immediately becomes visible again.
GetResolvedIcon cannot be used to confirm this in Turbo column-store mode -- it always reports kind = None there regardless of what the column's rule chain would actually resolve, because of a separate defect in its own row-range check (see GetResolvedIcon's own entry). The column-wide icon and the rule chain are still cleared/resolved correctly and paint on screen in Turbo column-store mode; only this one diagnostic method cannot see the result there.
See alsoSetColumnBuiltinIcon, GetResolvedIcon
SetColumnBuiltinIconMapIcons, sparklines, heatmaps, data bars and conditional formatting
Map an exact display-text value in a column to a built-in vector icon.
The built-in-system counterpart of SetColumnIconMap: whenever a cell in col renders exactly displayValue as its text, and no cell-level built-in override and no column-wide built-in icon apply to it, it shows builtinIcon. Call this once per distinct value; each call adds or replaces one entry.
SignatureSub SetColumnBuiltinIconMap(col As Long, displayValue As String, builtinIcon As Long)
Parameters
col (Long) - 0-based column index.
displayValue (String) - The exact display text to match. Matching is exact and case-sensitive.
No mode restriction on the call itself -- accepted without error in every mode, including Virtual. The built-in rule chain is a plain per-column structure, consulted identically by the paint pass in Embedded and Turbo column-store mode. Virtual mode is the one place this differs in effect, not in acceptance: the call still succeeds, but Virtual mode's own paint pass never resolves or draws any icon for any row, so a mapping added here while DataMode is 1 has no visible effect until the grid leaves Virtual mode.
A cell whose own display text is an empty string can never match any entry here, the same limitation SetColumnIconMap has in the external system.
Checked AFTER a column-wide icon from SetColumnBuiltinIcon -- setting both on the same column makes this map unreachable until the column-wide icon is cleared.
A cell-level override from SetCellBuiltinIcon also outranks this.
Accepted without error in Virtual mode, but never visibly does anything there: Virtual mode's paint pass never resolves any icon for any row, so a mapping added while DataMode is 1 only takes visible effect once the grid switches to Embedded or Turbo column-store mode.
GetResolvedIcon cannot be used to confirm a mapping added here while running in Turbo column-store mode -- it always reports kind = None there regardless of the real, current mapping, because of a separate defect in its own row-range check (see GetResolvedIcon's own entry). The mapping itself is genuinely applied and paints correctly in Turbo column-store mode; only this one diagnostic method cannot see it.
The same value-to-icon mapping also feeds the dropdown cell editor once SetColumnDropdownIcons is turned on for the column: each dropdown item is drawn with the built-in icon its value resolves to here (built-in rules are checked before external ones there too), so the list and the cells can never disagree.
See alsoSetColumnIconMap, AddBuiltinIconThreshold, SetColumnBuiltinIconDefault, SetColumnBuiltinIcon, SetColumnDropdownIcons, GetResolvedIcon
AddBuiltinIconThresholdIcons, sparklines, heatmaps, data bars and conditional formatting
Add a numeric range rule to a column's built-in icon rule chain.
The built-in-system counterpart of AddIconThreshold: any cell in col whose value falls in [minVal, maxVal) shows builtinIcon, checked after the built-in value map (SetColumnBuiltinIconMap) and before the column's built-in default (SetColumnBuiltinIconDefault). Multiple bands are evaluated in add order, first match wins.
SignatureSub AddBuiltinIconThreshold(col As Long, minVal As Double, maxVal As Double, builtinIcon As Long)
Parameters
col (Long) - 0-based column index.
minVal (Double) - Inclusive lower bound.
maxVal (Double) - Exclusive upper bound. Must be greater than minVal.
No mode restriction on the call itself -- a plain per-column threshold list, unlike Heatmap/DataBar/Top10Pct's Turbo column-store-only numeric-type admission; there is no equivalent restriction here since this reads each cell's own value directly rather than needing whole-column statistics. Virtual mode accepts the call the same as every other mode, but as with the rest of the built-in rule chain, it never actually draws the result: Virtual mode's own paint pass never resolves any icon for any row.
Unlike AddIconThreshold in the external system, builtinIcon here is NOT validated against a pool -- there is no pool to validate against, since built-in icons are not loaded. Any value in 0-7 is always immediately usable.
The upper bound is exclusive, matching AddIconThreshold's own convention.
Checked after the built-in value map but before the column's built-in default, and outranked entirely by a column-wide icon (SetColumnBuiltinIcon) or a cell-level override (SetCellBuiltinIcon).
Accepted without error in Virtual mode, but never visibly does anything there: Virtual mode's paint pass never resolves any icon for any row, so a band added while DataMode is 1 only takes visible effect once the grid switches to Embedded or Turbo column-store mode.
GetResolvedIcon cannot be used to confirm a band added here while running in Turbo column-store mode -- it always reports kind = None there regardless of the real, current bands, because of a separate defect in its own row-range check (see GetResolvedIcon's own entry). The band itself is genuinely applied and paints correctly in Turbo column-store mode; only this one diagnostic method cannot see it.
See alsoAddIconThreshold, SetColumnBuiltinIconMap, SetColumnBuiltinIconDefault, GetResolvedIcon
SetColumnBuiltinIconDefaultIcons, sparklines, heatmaps, data bars and conditional formatting
Set the built-in icon shown when no built-in value map or threshold matches, or clear it.
The built-in-system counterpart of SetColumnDefaultIcon: the fallback builtinIcon for col, used when a cell has no cell-level override, no column-wide icon, no matching value-map entry and no matching threshold band. Passing a negative value clears the default rather than setting one, which is the one documented way to remove just the default while leaving the rest of the rule chain in place.
SignatureSub SetColumnBuiltinIconDefault(col As Long, builtinIcon As Long)
Parameters
col (Long) - 0-based column index.
builtinIcon (Long) - 0-7 (LiteGridBuiltinIcon), or a negative value to clear the default.
No mode restriction on the call itself -- accepted without error in every mode. As with the rest of the built-in rule chain, Virtual mode accepts the call but never actually draws the result: Virtual mode's own paint pass never resolves any icon for any row, so a default set here has no visible effect until the grid leaves Virtual mode.
Example
VBA
grid.SetColumnBuiltinIconDefault 2, 7' fallback for any Region value with no more specific ruleDim kind AsLong, extId AsLong, builtinId AsLong
grid.GetResolvedIcon 0, 2, kind, extId, builtinId
Debug.Print builtinId ' -> 7, if nothing more specific matched
grid.SetColumnBuiltinIconDefault 2, -1' clears just the default; the value map, if any, is untouched
Gotchas
A negative value clears the default -- this is the built-in system's documented way to remove just the default while leaving the value map and thresholds in place; the external system's SetColumnDefaultIcon has no equivalent negative-clears convention (there, only ClearColumnIconMap removes it, along with everything else on the column).
Checked last, after the value map and every threshold band -- and outranked by a column-wide icon or a cell-level override, the same as every other part of the rule chain.
Accepted without error in Virtual mode, but never visibly does anything there: Virtual mode's paint pass never resolves any icon for any row, so a default set while DataMode is 1 only takes visible effect once the grid switches to Embedded or Turbo column-store mode.
GetResolvedIcon cannot be used to confirm a default set here while running in Turbo column-store mode -- it always reports kind = None there regardless of the real, current default, because of a separate defect in its own row-range check (see GetResolvedIcon's own entry). The default itself is genuinely applied and paints correctly in Turbo column-store mode; only this one diagnostic method cannot see it.
See alsoSetColumnDefaultIcon, SetColumnBuiltinIconMap, AddBuiltinIconThreshold, GetResolvedIcon
SetColumnHeaderIconIcons, sparklines, heatmaps, data bars and conditional formatting
Show an external icon in a column's header, to the left of the header text.
Gives one column a small icon in its header cell, drawn immediately left of the header text (after the header checkbox on a checkbox column, so the two never overlap). The icon comes from the external pool -- an id returned by LoadIconFromFile or AcquireSystemIconId -- and is the header-level counterpart of SetColumnDefaultIcon's "one icon for the whole column". Passing -1 removes it. There is no getter, matching every other per-column icon setter.
SignatureSub SetColumnHeaderIcon(colIndex As Long, iconId As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
iconId (Long) - An id returned by LoadIconFromFile or AcquireSystemIconId, or -1 to clear. Unlike SetColumnDefaultIcon, the id IS validated: one that names no loaded icon raises an error and leaves the column's existing header icon unchanged.
Refused by name in Turbo column-store mode, before the column index is even looked at, so a refused call changes nothing. Virtual mode is not restricted, exactly like SetColumnHeader -- a header icon is presentation metadata the grid owns, not host data.
Example
VBA
Dim globeIcon AsLong
globeIcon = grid.LoadIconFromFile("C:\Icons\globe.png")
grid.SetColumnHeaderIcon 1, globeIcon ' "Country" header now shows the globe left of its text
grid.SetColumnHeaderIcon 1, -1' remove it again
Gotchas
Screen only: header icons are NOT included in PrintGrid output. Printing draws headers with its own font on the printer DC and does not emit header icons (neither external nor built-in). Treat the icon as a screen UI affordance, not part of the printed document.
The icon is drawn at the pool's IconSize but clipped to a slot derived from HeaderHeight (at most 14px), so a large IconSize does not enlarge the header or paint over the rows beneath it -- it is simply cropped. Set HeaderHeight, not IconSize, if you want a taller header.
A column too narrow to hold the whole icon slot alongside its padding, header checkbox, sort arrow and filter button drops the icon for that frame rather than clipping it; widening the column brings it back. Header text still ellipsizes as before.
Not saved by GetColumnLayout, GetGridState or GetViewState: an external icon id only means something inside the process that loaded it, so persisting it would restore the wrong picture. Set header icons again after restoring a layout.
RemoveIcon on the id clears every header that used it, the same as it clears cell and column rules. Changing IconSize rebuilds the whole pool and likewise forgets every header icon.
If SetColumnHeaderBuiltinIcon is also set on the same column, the built-in icon is the one shown.
AutoFitColumns reserves the icon's slot, so a fitted column stays wide enough for icon and text together.
Purely decorative: clicking the icon sorts the column exactly as clicking the header text does, and it adds nothing to what accessibility clients read for the header.
See alsoSetColumnHeaderBuiltinIcon, SetColumnHeader, LoadIconFromFile, AcquireSystemIconId, SetColumnDefaultIcon
SetColumnHeaderBuiltinIconIcons, sparklines, heatmaps, data bars and conditional formatting
Show a built-in vector icon in a column's header, to the left of the header text.
The built-in-system counterpart of SetColumnHeaderIcon: one of the eight LiteGridBuiltinIcon shapes (0-7), drawn immediately left of the header text and scaled to the header, with no icon file needed. Passing -1 removes it. There is no getter.
SignatureSub SetColumnHeaderBuiltinIcon(colIndex As Long, builtinIcon As Long)
Parameters
colIndex (Long) - 0-based index of an existing column.
builtinIcon (Long) - 0-7 (LiteGridBuiltinIcon: lgIconCircle=0 ... lgIconDiamond=7), or -1 to clear. Any other value (including -2 or 8) raises an error and leaves the column's existing header icon unchanged.
Refused by name in Turbo column-store mode, before the column index is even looked at, so a refused call changes nothing. Virtual mode is not restricted, exactly like SetColumnHeader.
Example
VBA
grid.SetColumnHeaderBuiltinIcon 0, 4' a built-in glyph on the first header, no icon file needed
grid.SetColumnHeaderBuiltinIcon 0, -1' remove it
Gotchas
Screen only: header icons are NOT included in PrintGrid output. Printing draws headers with its own font on the printer DC and does not emit header icons (neither built-in nor external).
Wins over SetColumnHeaderIcon when both are set for the same column -- the same "built-in before external" priority cell-level icon resolution already uses. Clearing the built-in reveals the external icon again.
A column too narrow to hold the whole icon slot alongside its padding, header checkbox, sort arrow and filter button drops the icon for that frame rather than clipping it.
Not saved by GetColumnLayout, GetGridState or GetViewState (kept out deliberately, alongside the external header icon, so the two never differ in what a restore brings back).
AutoFitColumns reserves the icon's slot, so a fitted column stays wide enough for icon and text together.
Purely decorative: clicking the icon sorts the column, and it adds nothing to what accessibility clients read for the header.
See alsoSetColumnHeaderIcon, SetColumnHeader, SetColumnBuiltinIconDefault
ClearColumnBuiltinIconRuleIcons, sparklines, heatmaps, data bars and conditional formatting
Remove a column's entire built-in rule chain: its value map, thresholds, and default together.
The built-in-system counterpart of ClearColumnIconMap, but broader -- it clears the value map, every threshold band, AND the default in one call, since all three live together in the same per-column rule chain. It does not touch a column-wide icon (SetColumnBuiltinIcon) or any cell-level override (SetCellBuiltinIcon); those are cleared separately.
SignatureSub ClearColumnBuiltinIconRule(col As Long)
grid.SetColumnBuiltinIconMap 2, "North", 0
grid.AddBuiltinIconThreshold 2, 0#, 1#, 1
grid.SetColumnBuiltinIconDefault 2, 2
grid.ClearColumnBuiltinIconRule 2' all three are gone in one call
Gotchas
col is NOT bounds-checked -- an out-of-range value is a silent no-op.
Unlike the external system, where the value map and thresholds/default are each cleared by different, more granular members (ClearColumnIconMap vs. nothing dedicated for just the default), this single call clears the built-in value map, every threshold band, AND the default together -- there is no way to clear just one of the three.
Does not touch SetColumnBuiltinIcon's own column-wide icon -- clear that separately with ClearColumnBuiltinIcon if it is also set.
GetResolvedIcon cannot be used to confirm this in Turbo column-store mode -- it always reports kind = None there regardless of what the column would actually resolve, before or after the clear, because of a separate defect in its own row-range check (see GetResolvedIcon's own entry). The rule chain is still cleared correctly and any surviving column-wide icon still paints on screen in Turbo column-store mode; only this one diagnostic method cannot see the result there.
See alsoSetColumnBuiltinIconMap, AddBuiltinIconThreshold, SetColumnBuiltinIconDefault, ClearColumnBuiltinIcon, GetResolvedIcon
ImageListIconCountIcons, sparklines, heatmaps, data bars and conditional formatting
Number of external icons currently held by the icon pool -- the same value GetIconCount returns.
Reads the number of currently loaded external icons. This reads the exact same internal counter GetIconCount reads -- the two always report the identical number, under two different names in two different parts of the object model (this one a property, GetIconCount a method).
SignatureProperty ImageListIconCount As Long (read-only)
ReturnsThe number of currently loaded external icons.
No mode restriction -- a plain read of the shared icon pool's own counter, identical in every mode.
Example
VBA
Debug.Print grid.ImageListIconCount ' -> 0 on a grid that has never loaded an icon
grid.LoadIconFromFile "C:\Icons\flag.ico"Debug.Print grid.ImageListIconCount ' -> 1Debug.Print grid.ImageListIconCount = grid.GetIconCount() ' -> True, always
Gotchas
Always equals GetIconCount() exactly -- there is no scenario where the two disagree, since both read the same underlying counter (m_iconManager.GetCount()). Use whichever reads more naturally in context; there is no functional reason to prefer one over the other.
Counts only the external, file-loaded icon pool (the same one LoadIconFromFile fills). The built-in vector icon system is a completely separate mechanism (drawn directly, never loaded, never limited in number) and has no equivalent count -- this property does not include it.
See alsoGetIconCount, LoadIconFromFile
GetResolvedIconIcons, sparklines, heatmaps, data bars and conditional formatting
Get the icon a cell will actually paint, once the built-in system, the external system and both of their rule chains are all taken into account.
In Embedded mode, resolves (row, col) exactly the way the paint routine does: the built-in icon system is checked first (a cell override, then a column-wide icon, then its value map, then its thresholds, then its default); the external icon system is checked only as a fallback, in the same order, if the built-in system found nothing. It does NOT consider an icon attached to a conditional-formatting rule with SetFormatRuleIcon -- that is a third, independent icon source this method does not look at. In Virtual and Turbo column-store mode it does not actually mirror the paint routine in practice -- see modeNotes.
SignatureSub GetResolvedIcon(row As Long, col As Long, kind As Long, externalId As Long, builtinId As Long)
Parameters
row (Long) - 0-based row index. Out of range resolves to kind = None rather than raising an error, so this can be used freely from a hover handler without bounds-checking first.
col (Long) - 0-based column index. Same out-of-range behavior as row.
Fully correct only in Embedded mode. The row-range check this method performs internally is tested against the Embedded row store specifically, which is always empty while Virtual or Turbo column-store mode is active (each mode keeps its own, separate store). As a direct result, every call made while running in Virtual or Turbo column-store mode falls through the same "row is out of range" path and returns kind = 0 (None), for every row, rather than the row-residency-aware Virtual-mode behavior its own parameter description suggests. This never raises an error in any mode; it simply reports nothing found. In Virtual mode this happens to match what is actually drawn, since Virtual mode's own paint pass never resolves an icon for any row either way. In Turbo column-store mode it does not: a built-in rule chain (SetColumnBuiltinIconMap, AddBuiltinIconThreshold, SetColumnBuiltinIconDefault, SetColumnBuiltinIcon) and a Turbo column-store-safe external assignment (SetCellIcon) both genuinely resolve and paint on screen there, so GetResolvedIcon's "nothing found" answer in Turbo column-store mode can be actively wrong, not merely uninformative.
In Virtual and Turbo column-store mode, this always reports kind = 0 (None), for every row, even a row that genuinely has an icon assigned (for example, through SetCellIcon, which does work correctly in Turbo column-store mode). Do not use this method to inspect icon resolution outside Embedded mode -- it cannot currently do so.
This blind spot means different things in the two modes it affects. In Turbo column-store mode it is actively misleading: the built-in rule chain and a Turbo column-store-safe external assignment genuinely resolve and paint on screen there, so "nothing found" is simply wrong. In Virtual mode the same answer happens to be accurate, because Virtual mode's own paint pass never resolves any icon for any row regardless of what is configured -- so there this method's blind spot and the grid's real behavior coincide, for unrelated reasons.
Even in Embedded mode, this never reports an icon that came from a conditional-formatting rule's SetFormatRuleIcon -- only the two per-cell/per-column icon systems are considered.
An out-of-range row or column is not an error -- it resolves to kind = 0, the same as a genuinely in-range cell with nothing resolved. This makes the method safe to call speculatively (for example, from a hover handler) without checking bounds first, but also means it cannot distinguish "nothing here" from "this isn't even a real cell".
Reflects the built-in-before-external priority order exactly -- if both systems would resolve something different for the same cell, only the built-in result is ever reported.
See alsoSetCellIcon, SetCellBuiltinIcon, SetColumnBuiltinIcon, SetFormatRuleIcon
Tree view
SetTreeParent turns an ordinary flat grid into a tree by declaring that one row is the parent of another; every row in a tree stays a completely real data row -- there are no synthetic header or group rows inserted anywhere. A row's parent can be another data row, or -1 for a top-level (root) row. Calling SetTreeParent even once -- even just to declare a row a root with parentRow:=-1 -- switches the whole grid over to tree display: TreeViewActive becomes True, and every row, including every row you never mentioned, is now laid out by the tree's own rules (an untouched row simply becomes a root of its own, in its original relative order, so the grid looks the same until you actually attach something under it). Newly attached rows start out expanded, so a tree you just built is fully visible immediately; call TreeCollapse or TreeCollapseAll if you want a starting view that only shows the top level. ClearTreeView removes every relationship and every expand/collapse state at once and returns the grid to its ordinary flat layout; to undo just one relationship, call SetTreeParent again on that one row with parentRow:=-1 instead.
This group uses two different numbering schemes for a row, and they are easy to conflate. SetTreeParent, GetTreeParent and GetTreeChildCount all address a row by its own row number -- the same number LoadFromArray, AddRow or InsertRow gave it -- regardless of where that row currently happens to be sorted on screen; unlike GetCellValue, GetCell and most of the rest of the object model, this number is never re-mapped through the grid's current sort order. Every other member here -- TreeExpand, TreeCollapse, TreeToggle, GetTreeDepth and IsTreeExpanded -- instead takes a displayRow: the row's current position within the tree's own flattened, top-to-bottom display order, which is recalculated after every expand, collapse, sort or filter change made while tree view is active. The two coincide only by coincidence -- while nothing has ever been sorted, and while the tree has no real branching yet. The moment one top-level row has children of its own alongside a second top-level row that also has children, the two numbers diverge: a tree built as Row A with children Row B and Row C flattens on screen so that all of B's own descendants appear before C ever does, so a displayRow further down the list can land on one of B's children while the SAME number, read as a plain row, refers to a completely different row. Always use GetTreeParent and GetTreeChildCount with a row's own permanent number, and TreeExpand, TreeCollapse, TreeToggle, GetTreeDepth and IsTreeExpanded with whatever displayRow you most recently read back from a tree call -- never mix the two up.
Thirteen of this group's members -- every one that touches the tree itself -- are Embedded-mode only, with no partial support anywhere. Unlike several other feature areas, where a read-only query still works in Virtual mode or Turbo column-store mode even when the matching setter does not, tree view refuses all thirteen of them outright the moment DataMode is anything other than Embedded -- including the read-only queries (GetTreeParent, GetTreeChildCount, GetTreeDepth, IsTreeExpanded, and even just reading TreeViewActive or TreeIndent). There is no way to inspect or drive a tree from Virtual or Turbo column-store mode at all.
The four expander appearance properties documented at the end of this section -- TreeExpanderStyle, TreeExpanderSize, TreeExpanderColor and TreeExpanderGlyphColor -- are the deliberate exception, and are not counted among those thirteen. They describe how a glyph is painted rather than what the grid owns, so they behave like Theme or RowHeight: accepted in every DataMode, settable before any data exists, and simply with nothing to draw wherever no tree is on screen. They are also not specific to the directory loaders -- the glyph is painted from the tree projection, which SetTreeParent and BeginTreeBatch build exactly as LoadDirectoryTree does, so one setting covers every tree the grid can show.
A few real, non-obvious interactions are worth planning around before building on this. SetTreeParent refuses to create a cycle, whether that means making a row its own parent directly or making it an indirect descendant of one of its own descendants -- the check runs against the tree's current, live shape, so it still catches a cycle created by an earlier reparent, not just the tree as it was originally built. Reparenting a row that already has its own children moves the whole branch in one call -- its children's own parent links are untouched, so they simply travel with it to wherever it now lives in the tree. Building a tree also silently clears any active row grouping, since the two layouts cannot be active at the same time. Two gaps deserve extra care: deleting a row (DeleteRow or DeleteSelectedRows) while a tree is in place does not adjust any surviving relationship for the rows that shift underneath it, so a relationship can end up describing the wrong row after a mid-tree delete; and grid.Clear(), a DataMode switch, and a Turbo column-store LoadFromArray all leave tree state completely untouched -- only an Embedded-mode LoadFromArray resets it automatically -- so relationships and expand/collapse state from before silently persist and can reattach themselves to whatever new rows happen to land on the same row numbers afterward, even across a full data reload in a different mode (reproducible: build a tree in Embedded mode, switch to Turbo column-store, load fresh unrelated data, and a row-count-sensitive member like ScrollToView can refuse a genuinely valid row because it is checking against the stale tree instead of the real data). Call ClearTreeView explicitly whenever a clean slate matters and an Embedded-mode reload is not guaranteed to happen first.
Interactively, a user expands or collapses a node by clicking its glyph (drawn in the grid's leftmost visible column, indented by TreeIndent pixels per level) or, with a row focused and outside cell- or column-selection modes, with the Right and Left arrow keys -- both gestures call TreeExpand/TreeCollapse/TreeToggle under the hood, so code that calls them directly does exactly what the user's own keyboard or mouse would do. Every read-only query in this group is deliberately forgiving rather than strict: GetTreeParent, GetTreeChildCount, GetTreeDepth and IsTreeExpanded never raise an error for an out-of-range row or displayRow, or for a row that was never part of any tree at all -- they simply report the same neutral answer (-1, 0, 0, or True/False respectively) they would give for a legitimate root, childless row, or expanded row, so none of them can tell you "this isn't a real row" apart from a genuine answer. Check TreeViewActive first if that distinction matters to your code.
RecipeBuild a small reporting hierarchy, then collapse it to an outline and expand one branch
VBA
Sub BuildAndNavigateReportingTree(grid AsObject)
Dim data(0To6, 0To1) AsVariant
data(0, 0) = "Priya Shah": data(0, 1) = "CEO"
data(1, 0) = "Tom Alvarez": data(1, 1) = "VP Sales"
data(2, 0) = "Grace Lin": data(2, 1) = "VP Engineering"
data(3, 0) = "Sam Okafor": data(3, 1) = "Sales Rep"
data(4, 0) = "Dana Petrov": data(4, 1) = "Sales Rep"
data(5, 0) = "Wen Zhao": data(5, 1) = "Engineer"
data(6, 0) = "Iris Novak": data(6, 1) = "Engineer"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Title"' childRow/parentRow address each person by their own row number -- the' same numbering LoadFromArray assigned them, NOT the row's current' on-screen position (the two happen to be identical right now, since' nothing has been sorted)
grid.SetTreeParent 1, 0' Tom reports to Priya
grid.SetTreeParent 2, 0' Grace reports to Priya
grid.SetTreeParent 3, 1' Sam reports to Tom
grid.SetTreeParent 4, 1' Dana reports to Tom
grid.SetTreeParent 5, 2' Wen reports to Grace
grid.SetTreeParent 6, 2' Iris reports to GraceDebug.Print grid.TreeViewActive ' -> TrueDebug.Print grid.GetTreeChildCount(0) ' -> 2 (Tom and Grace report directly to Priya)' row 2 (Grace's OWN row number) and displayRow 2 (the third row shown on' screen) are two different people, because the tree flattens Tom's whole' branch -- Sam and Dana -- before Grace ever appears on screen:Debug.Print grid.GetTreeParent(2) ' -> 0 (row 2 is Grace; her parent is Priya's row)Debug.Print grid.GetTreeDepth(2) ' -> 2 (displayRow 2 is Sam: Priya -> Tom -> Sam, two levels down)' New relationships start fully expanded -- collapse everything down to' an outline first
grid.TreeCollapseAll
Debug.Print grid.IsTreeExpanded(0) ' -> False (only Priya's own row is on screen now)' Expanding Priya's row reveals her direct reports, still themselves' individually collapsed (TreeExpand only ever touches the one row passed' to it)
grid.TreeExpand 0Debug.Print grid.GetTreeDepth(1) ' -> 1 (Tom, visible again)Debug.Print grid.IsTreeExpanded(1) ' -> False (Tom's own branch is still collapsed)' storage row 2 is Grace's own permanent identity -- it is unaffected by' the fact that the tree's current, collapsed shape happens to also put' her at displayRow 2 right nowDebug.Print grid.GetTreeChildCount(2) ' -> 2 (Wen and Iris, still Grace's real children)EndSub
SetTreeParent, GetTreeParent and GetTreeChildCount always take a row's own permanent number; GetTreeDepth, TreeExpand, TreeCollapse, TreeToggle and IsTreeExpanded always take a displayRow -- the row's current position in the flattened, on-screen tree order. The recipe deliberately asks for GetTreeParent(2) and GetTreeDepth(2) back to back to make the difference concrete: the same integer means Grace in one call and Sam in the other, purely because of which parameter family it is passed to. TreeCollapseAll and TreeExpand reshape that on-screen order again immediately afterward, which is why GetTreeChildCount(2) at the end still means Grace (her row number never changed) even though displayRow 2 meant someone else earlier in the same sub.
RecipeReparent a task and its whole subtree in one call, and watch the cycle check follow the new structure
VBA
Sub ReparentASubtreeInOneCall(grid AsObject)
Dim data(0To4, 0To0) AsVariant
data(0, 0) = "Website Redesign"
data(1, 0) = "Design"
data(2, 0) = "Logo"
data(3, 0) = "Banner"
data(4, 0) = "Copywriting"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Task"
grid.SetTreeParent 1, 0' Design under Website Redesign
grid.SetTreeParent 2, 1' Logo under Design
grid.SetTreeParent 3, 1' Banner under Design
grid.SetTreeParent 4, 0' Copywriting under Website Redesign, alongside DesignDebug.Print grid.GetTreeChildCount(0) ' -> 2 (Design, Copywriting)Debug.Print grid.GetTreeChildCount(1) ' -> 2 (Logo, Banner)' Move "Design" -- together with everything under it -- so it now reports' to "Copywriting" instead of directly to "Website Redesign". One call is' enough: Logo and Banner are never mentioned, because they belong to' Design, not to whatever Design's own parent happens to be.
grid.SetTreeParent 1, 4Debug.Print grid.GetTreeParent(1) ' -> 4 (Design now reports to Copywriting)Debug.Print grid.GetTreeChildCount(0) ' -> 1 (Website Redesign kept only Copywriting)Debug.Print grid.GetTreeChildCount(4) ' -> 1 (Copywriting gained Design)Debug.Print grid.GetTreeChildCount(1) ' -> 2 (Design's own children -- Logo and Banner -- moved with it, untouched)Debug.Print grid.GetTreeParent(2) ' -> 1 (Logo still reports to Design, wherever Design now lives)' The cycle check runs against the CURRENT structure, not the one at the' start of the sub: after the move above, Copywriting is now Design's' parent, and Website Redesign is Copywriting's own parent -- so making' Website Redesign a child of Logo would loop back on itselfOnErrorResumeNext
grid.SetTreeParent 0, 2Debug.PrintErr.Number <> 0' -> True (refused as a circular reference)OnErrorGoTo0EndSub
SetTreeParent only ever changes the ONE row's own parent link; a row's children are stored against the row itself, not against its position in the tree, so moving a row to a new parent automatically carries its whole subtree along -- nothing about Logo or Banner needed to change for them to now sit under Copywriting indirectly. The cycle check walks up from the proposed new parent looking for the row being moved; because that walk uses whatever the tree looks like at the moment of the call, a reparent earlier in the same sub can turn a previously-harmless request into a refused cycle, as the final SetTreeParent call here demonstrates.
RecipeDetach one row without clearing the whole tree, and what to watch for around deletes and Clear()
VBA
Sub DetachOneRowWithoutClearingEverything(grid AsObject)
Dim data(0To3, 0To0) AsVariant
data(0, 0) = "Region North"
data(1, 0) = "Store 12"
data(2, 0) = "Store 47"
data(3, 0) = "Region South"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Location"
grid.SetTreeParent 1, 0' Store 12 under Region North
grid.SetTreeParent 2, 0' Store 47 under Region North' Store 47 has been reassigned and now stands on its own -- SetTreeParent' with parentRow:=-1 detaches just this one row; every other relationship' is untouched
grid.SetTreeParent 2, -1Debug.Print grid.GetTreeParent(2) ' -> -1Debug.Print grid.GetTreeChildCount(0) ' -> 1 (Region North kept Store 12)' ClearTreeView, by contrast, is a total reset -- there is no "undo just' this one SetTreeParent call"
grid.ClearTreeView
Debug.Print grid.TreeViewActive ' -> FalseDebug.Print grid.GetTreeParent(1) ' -> -1 (relationship is gone, same as an untouched row)EndSub
SetTreeParent's parentRow:=-1 form is the scalpel: it detaches exactly the row named and leaves every other relationship exactly as it was. ClearTreeView is the opposite tool -- one call that wipes every relationship and every expand/collapse state at once, with no way to undo just part of it. Two related pitfalls are worth planning around before relying on either: deleting a row (DeleteRow or DeleteSelectedRows) while a tree is in place does not update any surviving relationship, so a parent/child link set up before a mid-tree delete can end up describing the wrong row once storage positions shift underneath it; and grid.Clear() -- unlike LoadFromArray -- does not reset the tree at all, so relationships built before a Clear() can silently reattach to whatever new rows land on the same row numbers afterward. Reload with LoadFromArray, or call ClearTreeView explicitly, whenever a guaranteed clean slate matters.
SetTreeParentTree view
Attach a row under another row as its parent, building or extending a tree over the grid's flat data.
Declares that childRow's parent is parentRow, or makes childRow a top-level root when parentRow is -1. Both parameters are the row's own permanent row number -- the same number it was given by LoadFromArray, AddRow or InsertRow -- never the row's current on-screen position (see the group introduction for what this means once a sort has run). The very first successful call switches the whole grid into tree display: every row, not just the ones attached to something, is now laid out by the tree's own top-to-bottom, indented order, and TreeViewActive becomes True. Calling this again on a row that already has a parent moves it -- and its own children, which travel with it automatically -- to the new parent in one step; there is no need to detach it first.
SignatureSub SetTreeParent(childRow As Long, parentRow As Long)
Parameters
childRow (Long) - The row to attach, by its own permanent row number.
parentRow (Long) - The row to attach it under, using the same row-number scheme as childRow; -1 makes childRow a top-level (root) row instead.
Refused in Virtual mode, where the grid does not own the row data a tree relationship would need to reference, and refused in Turbo column-store mode, which does not support tree view at all. Embedded mode only -- see the group introduction.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "North America": data(1, 0) = "USA": data(2, 0) = "Canada"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetTreeParent 1, 0' USA reports to North America
grid.SetTreeParent 2, 0' Canada reports to North AmericaDebug.Print grid.TreeViewActive ' -> TrueDebug.Print grid.GetTreeChildCount(0) ' -> 2
Gotchas
childRow and parentRow are each row's own permanent row number, never translated through the grid's current sort order the way GetCellValue, GetCell and most other row-addressed members are -- see the group introduction. The two only coincide for certain once the grid has never been sorted.
Making a row its own parent, or making it a child of one of its own descendants at any depth, is refused with the same 'circular reference or invalid indices' error either way -- the tree can never contain a cycle.
Deleting a row (DeleteRow or DeleteSelectedRows) while a tree is in place does not update any surviving relationship. Deleting shifts every later row's own permanent row number down by one, but the tree keeps whatever numbers it already had, so a relationship set up before a mid-tree delete can end up describing a completely different row afterward.
grid.Clear(), switching DataMode, and a Turbo column-store LoadFromArray all leave an existing tree completely untouched -- only Embedded-mode LoadFromArray resets it automatically. Relationships and expand/collapse state from before silently persist and reattach themselves to whatever new rows land on the same row numbers afterward, even across a full data reload in a different mode. This is real and easy to hit: build a tree, switch to Turbo column-store mode, load a fresh unrelated dataset, and a method like ScrollToView can fail with "Row index is out of range" against a row that is genuinely present in the new data, because it is being checked against the stale tree's leftover visible-row count instead of the real one. Call ClearTreeView explicitly before relying on Clear() or a mode switch to have left the grid in a plain, non-tree state.
Refused outright when called from inside any event handler.
The first successful call also silently clears any active row grouping -- tree view and row grouping cannot both be active at once.
Even a parentRow:=-1 call -- just declaring a row a root, changing nothing about how it displays -- activates tree view. TreeViewActive alone cannot tell you whether a real parent/child relationship exists yet, only that SetTreeParent has succeeded at least once.
See alsoGetTreeParent, GetTreeChildCount, ClearTreeView, TreeViewActive, DeleteRow, Clear
GetTreeParentTree view
Get a row's current parent row number, or -1 if it is a root.
Reads back the parent row number SetTreeParent most recently assigned to row, addressed by row's own permanent row number. Returns -1 for a genuine root row, for a row that has never been given a parent at all, and for a row index this method does not recognize -- there is no way to tell these three cases apart from the return value alone. Unlike some other queries in this group, this never requires tree view to already be active; it simply answers -1 until the first SetTreeParent call.
SignatureFunction GetTreeParent(row As Long) As Long
Parameters
row (Long) - The row to query, by its own permanent row number (see SetTreeParent).
ReturnsThe parent's row number, or -1 if row is a root, was never given a parent, or is out of range.
Refused in both Virtual mode and Turbo column-store mode, along with every other member in this group -- Embedded mode only. Even this simple, error-tolerant read is refused outside Embedded mode; there is no partial support.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "North America": data(1, 0) = "USA"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetTreeParent 1, 0Debug.Print grid.GetTreeParent(1) ' -> 0Debug.Print grid.GetTreeParent(0) ' -> -1 (row 0 is a root)Debug.Print grid.GetTreeParent(99) ' -> -1 (out of range never raises an error)
Gotchas
Never raises an error, for any row value -- an out-of-range row (even one far beyond the grid's actual row count) reports -1, the same as a genuine root.
row is the row's own permanent row number, not translated through the current sort order the way GetCellValue and GetCell are -- see the group introduction and SetTreeParent's own gotchas.
Unaffected by expand or collapse state: collapsing a row's ancestor hides it on screen but does not change what this reports for it.
Works even before tree view has ever been activated -- it simply reports -1 for everything until the first SetTreeParent call.
See alsoSetTreeParent, GetTreeChildCount, GetTreeDepth
GetTreeChildCountTree view
Get how many direct children a row currently has.
Counts row's direct children only -- grandchildren and deeper descendants are not included, no matter how many levels down they sit. Counts every child that has ever been attached with SetTreeParent, whether or not that child is currently visible on screen: a collapsed parent, or a child a filter is currently hiding, is still counted here. Like GetTreeParent, this never raises an error and never requires tree view to already be active; an unrecognized or childless row simply reports 0.
SignatureFunction GetTreeChildCount(row As Long) As Long
Parameters
row (Long) - The row to query, by its own permanent row number.
ReturnsThe number of direct children row has (0 if none, if row is out of range, or if tree view has never been activated).
Refused in both Virtual mode and Turbo column-store mode, the same as every other member in this group -- Embedded mode only.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "North America": data(1, 0) = "USA": data(2, 0) = "Canada"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Region"
grid.SetTreeParent 1, 0
grid.SetTreeParent 2, 0Debug.Print grid.GetTreeChildCount(0) ' -> 2Debug.Print grid.GetTreeChildCount(1) ' -> 0 (no children of its own)
Gotchas
Counts direct children only; a grandchild is not included in its grandparent's count.
Never raises an error -- an out-of-range or untouched row reports 0, the same as a genuine childless row.
Counts every attached child regardless of visibility: a collapsed parent or a filtered-out child is still counted.
row is the row's own permanent row number, not the current on-screen position -- see the group introduction.
See alsoSetTreeParent, GetTreeParent, TreeExpand
TreeExpandTree view
Show a tree node's children on screen.
Shows displayRow's direct children (and, through them, anything nested further down that is not itself individually collapsed). displayRow is the row's current on-screen position within the tree's own flattened display order while tree view is active -- not the same numbering SetTreeParent uses; see the group introduction. This has nothing to do when tree view is not active yet, or when displayRow is at or beyond the number of rows currently on screen -- and also when the row is already expanded, which is the DEFAULT state for every row (including a childless one) rather than something tied to whether it actually has children; see the gotchas for what that means in practice. This is the same action a user triggers by clicking the row's expand glyph or pressing the Right arrow key with the row focused.
SignatureSub TreeExpand(displayRow As Long)
Parameters
displayRow (Long) - The row's current on-screen position while tree view is displayed -- not its own permanent row number (see the group introduction).
The only real no-op conditions are tree view not being active and the row already being expanded -- NOT whether displayRow has any children. A childless row simply starts out expanded by default (see IsTreeExpanded), so calling this on one the first time typically has nothing to do; but the source never actually checks for children before acting, so if that same childless row had been collapsed first (TreeCollapse and TreeToggle do not check for children either), this call genuinely flips it back to expanded. Either way there is nothing to see on screen -- there was never anything to show or hide -- but IsTreeExpanded on that row reflects the real internal state afterward. Check GetTreeChildCount first if you need to know whether a call will have any effect at all.
displayRow is recalculated every time the tree is rebuilt (after any expand, collapse, sort or filter change made while tree view is active); a displayRow at or beyond the current on-screen row count is also silently a no-op rather than an error, so do not reuse a displayRow value read before such a change.
Expanding a row only affects that row's own children; a grandchild stays hidden if its own direct parent is still individually collapsed.
The same action a mouse click on the row's glyph, or the Right arrow key with the row focused (outside cell- or column-selection modes), triggers interactively.
See alsoTreeCollapse, TreeToggle, IsTreeExpanded, GetTreeDepth, TreeExpandAll
TreeCollapseTree view
Hide a tree node's children (and their own descendants) from the screen.
Hides displayRow's children from the screen -- and, with them, every one of its own further descendants, regardless of whether those deeper rows are themselves individually expanded or collapsed. Re-expanding displayRow later restores exactly what was visible before, since each descendant's own expand/collapse state is preserved underneath, untouched by the collapse. displayRow is the row's current on-screen tree position, not its own permanent row number (see the group introduction). This has nothing to do when tree view is not active, or when displayRow is at or beyond the number of rows currently on screen -- and also when the row is already collapsed, which is not the same thing as the row having no children; see the gotchas for what that means in practice. This is the same action a user triggers by clicking the row's collapse glyph or pressing the Left arrow key with the row focused.
SignatureSub TreeCollapse(displayRow As Long)
Parameters
displayRow (Long) - The row's current on-screen position while tree view is displayed -- not its own permanent row number (see the group introduction).
The only real no-op conditions are tree view not being active and the row already being collapsed -- NOT whether displayRow has any children. A childless row starts out expanded by default, so collapsing one for the first time usually has nothing visible to do (there is nothing to hide either way); but the source never actually checks for children before acting, so the call genuinely flips that row's own internal expanded flag to False, which IsTreeExpanded will then faithfully report -- even though the row never had anything to expand or collapse in the first place. Check GetTreeChildCount first if you need to know whether a call will have any real, visible effect.
displayRow is recalculated on every tree rebuild -- see TreeExpand's own gotchas and the group introduction; a displayRow at or beyond the current on-screen row count is also silently a no-op rather than an error.
Hides the entire subtree beneath displayRow, not just its direct children -- but leaves every descendant's own expand/collapse flag untouched, so re-expanding displayRow later shows exactly what was visible before.
The same action a mouse click on the row's glyph, or the Left arrow key with the row focused, triggers interactively.
See alsoTreeExpand, TreeToggle, IsTreeExpanded, TreeCollapseAll
TreeToggleTree view
Flip a tree node between expanded and collapsed.
Expands displayRow if it is currently collapsed, or collapses it if it is currently expanded -- the same underlying action TreeExpand or TreeCollapse would take, chosen automatically from the row's current state. Useful for wiring up a single click handler without having to check IsTreeExpanded first. A silent no-op only for an out-of-range displayRow or when tree view is not active; it does not check whether displayRow actually has any children, so calling it on a childless row still flips that row's own expanded state (readable back from IsTreeExpanded), even though there is no glyph and nothing visibly changes on screen.
SignatureSub TreeToggle(displayRow As Long)
Parameters
displayRow (Long) - The row's current on-screen position while tree view is displayed -- not its own permanent row number (see the group introduction).
Does not check whether displayRow actually has any children before flipping it, unlike the glyph click that drives it interactively (a glyph is only ever drawn for a row that has something to expand or collapse in the first place). Calling TreeToggle directly on a childless row still changes its own expanded state, readable back from IsTreeExpanded, even though nothing on screen changes. Only an out-of-range displayRow, or tree view not being active yet, are genuine no-ops.
displayRow numbering shifts on every tree rebuild -- see the group introduction.
This is what a mouse click on the expand/collapse glyph actually calls.
See alsoTreeExpand, TreeCollapse, IsTreeExpanded
TreeExpandAllTree view
Expand every node in the tree at once.
Expands every node in the tree in one call, regardless of how deeply nested it is or whether it currently has any visible ancestor. Since a freshly attached relationship already starts out expanded, this is most useful after some nodes have been individually or entirely collapsed -- it restores full visibility everywhere in one step rather than walking every node yourself. A silent no-op if tree view is not active.
Since a freshly built relationship already starts expanded, calling this immediately after building a tree has nothing to do -- it matters only after something has been collapsed.
Touches every node, including ones several ancestors deep behind a currently-collapsed row, not just the ones currently visible on screen.
Recalculates displayRow numbering for the whole grid immediately, the same as every other expand/collapse call -- a displayRow value read before calling this can address a different row afterward. See the group introduction.
Silent no-op if tree view has never been activated.
See alsoTreeCollapseAll, TreeExpand, IsTreeExpanded
TreeCollapseAllTree view
Collapse every node that has children, all at once.
Collapses every node that has at least one child, all at once; a row with no children of its own has no expand state that matters and is left alone. After this call, only root-level rows (and any row whose parent is currently hidden by a filter -- see the group introduction) remain visible, giving a compact, outline-style view of the tree. A silent no-op if tree view is not active.
Only touches nodes that actually have children; a childless row's own internal state is left alone, though it has no visible effect either way.
A row whose parent a filter is currently hiding still gets collapsed if it has children of its own -- it just is not visible until the filter changes or the ancestor is reached another way.
Recalculates displayRow numbering for the whole grid immediately -- often much more drastically than a single TreeCollapse call, since every branch collapses at once. A displayRow read beforehand can address a completely different row afterward, or no longer correspond to anything currently on screen at all.
Silent no-op if tree view has never been activated.
There is no dedicated call to count how many rows remain visible afterward -- GetRowCount always reports the grid's full row count, unaffected by how much of the tree is currently collapsed.
See alsoTreeExpandAll, TreeCollapse, IsTreeExpanded
TreeViewActiveTree view
Whether tree view is currently active on this grid.
True once SetTreeParent has succeeded at least once -- even a single call that only declares a row a root, changing nothing about how the grid looks, is enough to turn this True. Stays True until ClearTreeView is called or the grid is reloaded with LoadFromArray; detaching every row back to root one at a time with repeated SetTreeParent calls does not turn it back off by itself, and neither does grid.Clear() (see the gotchas below).
SignatureProperty TreeViewActive As Boolean (read-only)
ReturnsTrue once at least one SetTreeParent call has succeeded; False otherwise, or after ClearTreeView, or after a fresh LoadFromArray.
Refused in both Virtual mode and Turbo column-store mode -- even reading this property is not allowed outside Embedded mode.
Example
VBA
Debug.Print grid.TreeViewActive ' -> False, before any tree relationship
grid.SetTreeParent 0, -1' just declaring row 0 a root is enoughDebug.Print grid.TreeViewActive ' -> True
Gotchas
True means only that SetTreeParent has succeeded at least once -- not that any row currently has a real parent or child. A single parentRow:=-1 call is enough.
There is no automatic deactivation: detaching every row back to root individually still leaves this True. Only ClearTreeView, or reloading data with LoadFromArray, turns it back off.
grid.Clear() does not reset this -- it stays True, and the underlying relationships stay attached to whatever row numbers they last referenced, ready to silently apply to new rows added afterward with AddRow or InsertRow. Reload with LoadFromArray, or call ClearTreeView explicitly, for a guaranteed clean slate.
Read-only -- there is no way to set this directly; it only ever changes as a side effect of SetTreeParent, ClearTreeView, or LoadFromArray.
See alsoSetTreeParent, ClearTreeView, Clear, LoadFromArray
ClearTreeViewTree view
Remove every tree relationship and return to the grid's normal flat layout.
Removes every parent/child relationship and every node's expand/collapse state in a single call, and returns the grid to its ordinary flat row order; TreeViewActive becomes False again afterward. Safe to call even when tree view was never active. To remove just one row's own relationship without disturbing the rest of the tree, call SetTreeParent on that one row with parentRow:=-1 instead of clearing everything.
All-or-nothing: there is no way to remove just one relationship this way. Call SetTreeParent on that one row with parentRow:=-1 instead if only a single row needs to be detached.
Also resets every node's expand/collapse state, not just the relationships themselves -- a tree rebuilt afterward starts fully expanded again, like any brand-new tree.
Safe to call even if tree view was never active.
Does not reset TreeIndent -- the per-level pixel indent is a separate view setting, not tree data, and it keeps whatever value was last set (or its default of 20) across a ClearTreeView call; only setting TreeIndent directly ever changes it.
The only reliable way to guarantee a plain, non-tree state: Clear(), a DataMode switch, and a Turbo column-store-mode LoadFromArray all leave existing tree relationships and expand/collapse state in place (see SetTreeParent's own gotchas). Call this explicitly any time code cannot be sure whether a previous tree was ever built, especially before relying on row-count-sensitive members like ScrollToView after a fresh load in Turbo column-store mode.
See alsoSetTreeParent, TreeViewActive, TreeIndent
GetTreeDepthTree view
Get how many levels deep an on-screen tree row is nested.
Reports how many levels displayRow is nested below the top level: 0 for a root-level row, 1 for a row directly under a root, and so on. displayRow is the row's current on-screen tree position, not its own permanent row number (see the group introduction) -- this number changes every time the tree is rebuilt, which happens after every expand, collapse, sort or filter change made while tree view is active. Also reports 0 for an out-of-range displayRow and while tree view is not active, the same as a genuine root row -- there is no way to distinguish the three from the return value alone.
SignatureFunction GetTreeDepth(displayRow As Long) As Long
Parameters
displayRow (Long) - The row's current on-screen position while tree view is displayed -- not its own permanent row number (see the group introduction).
ReturnsThe nesting level of displayRow: 0 for a root-level row, 1 for its direct children, and so on. Also 0 if displayRow is out of range or tree view is not active.
Never raises an error: an out-of-range displayRow, or tree view not being active at all, both report 0, the same as a genuine root-level row.
displayRow is the row's current on-screen tree position, recalculated on every tree rebuild -- not the same numbering SetTreeParent, GetTreeParent or GetTreeChildCount use. See the group introduction.
Unaffected by which column is current -- depth is a property of the row, not of any particular cell.
See alsoTreeExpand, TreeCollapse, IsTreeExpanded
TreeIndentTree view
Pixels of indentation applied per nesting level.
Sets how many pixels of extra left indentation the grid draws per nesting level, applied only in the grid's leftmost visible column, where the expand/collapse glyph is also drawn. Accepts any Long value but silently clamps it to the range 8-60 rather than refusing an out-of-range value -- reading the property back immediately after setting an extreme value will not show what was actually passed in.
SignatureProperty TreeIndent As Long
ReturnsThe current indent, in pixels per level (8-60, default 20).
Refused in both Virtual mode and Turbo column-store mode, in both directions -- unlike many settable properties, even reading the current indent is not allowed outside Embedded mode.
Silently clamped to the 8-60 pixel range rather than refused -- setting 2 or 5000 both succeed without an error, and reading the property back afterward shows 8 or 60, not what was actually passed in.
Only visible in the grid's leftmost visible column, alongside the expand/collapse glyph -- it has no effect on any other column.
Setting this before any tree relationship exists is stored for later and does not force an immediate repaint, since there is nothing on screen for it to change yet.
Unlike most settable properties in this control, even reading the current value is refused outside Embedded mode -- see modeNotes.
Survives ClearTreeView, and even a fresh LoadFromArray, unchanged -- unlike the tree's own relationships and expand/collapse state, the indent is stored independently and is only ever changed by setting TreeIndent directly.
See alsoTreeExpand, GetTreeDepth, ClearTreeView, LoadFromArray
IsTreeExpandedTree view
Check whether an on-screen tree row is currently expanded.
Reports whether displayRow is currently expanded. True is the default for every row, including a childless row that has no expand/collapse glyph to control in the first place -- unlike TreeCollapseAll, which only ever touches rows that actually have children, an explicit TreeCollapse or TreeToggle call aimed at a childless row's own displayRow still flips its internal flag, even though nothing visible changes as a result. Also reports False for an out-of-range displayRow, while tree view is not active, or for a row currently hidden behind a collapsed ancestor -- such a row has no valid displayRow of its own to query until every collapsed ancestor above it is expanded again. displayRow is the row's current on-screen tree position, not its own permanent row number -- see the group introduction.
SignatureFunction IsTreeExpanded(displayRow As Long) As Boolean
Parameters
displayRow (Long) - The row's current on-screen position while tree view is displayed -- not its own permanent row number (see the group introduction).
ReturnsTrue if displayRow is currently expanded -- the default for every row, including one with no children of its own -- and False if it has been explicitly collapsed (directly, even for a childless row with no glyph), if displayRow is out of range, if it corresponds to a row currently hidden behind a collapsed ancestor, or if tree view is not active.
A childless row reports True by default, the same as a row that genuinely has children and is expanded -- there is no glyph for a childless row to control, so this default is never actually visible on screen. Check GetTreeChildCount first if only rows with something to expand matter.
That default is not fixed, though: TreeCollapse and TreeToggle do not check whether a row actually has children before flipping its internal flag, so calling either directly on a childless row's own displayRow still sets it to False -- with no glyph and nothing visibly different on screen to show for it. TreeCollapseAll, by contrast, only ever touches rows that have children, so it can never do this by itself.
Never raises an error: an out-of-range displayRow, and tree view not being active at all, both report False.
displayRow is the row's current on-screen tree position, not the row's own permanent row number -- see the group introduction.
A row hidden because an ancestor above it is collapsed has no valid displayRow at all while it stays hidden, so its own state cannot be queried through IsTreeExpanded during that time -- a stale displayRow left over from before the collapse either lands on a different row that shifted into that slot, or falls past the now-shorter display and reports the same False out-of-range default (GetTreeDepth falls back the same way for the same reason). The hidden row's own flag is preserved internally regardless, and shows through again once every collapsed ancestor above it is re-expanded.
See alsoTreeExpand, TreeCollapse, TreeToggle, TreeCollapseAll, GetTreeDepth, GetTreeChildCount
TreeExpanderStyleTree view
Which shape a tree node's expand/collapse glyph is drawn as.
Chooses between three glyph designs for the affordance the user clicks to open and close a node. An unrecognised value is refused with an error and nothing changes -- unlike TreeExpanderSize, which clamps, because a style number outside the list has no sensible nearest interpretation while a size does. This is one of the four appearance properties that are accepted in every DataMode, and it applies to every tree the grid can display, not only to one built by LoadDirectoryTree.
SignatureProperty TreeExpanderStyle As Long
ReturnsThe current style: 0, 1 or 2.
Default0 -- a plus or minus inside a filled, rounded box tinted with the theme's accent colour.
Value
Meaning
0
A plus (collapsed) or minus (expanded) inside a filled, rounded box tinted with the theme's accent colour, with the strokes drawn in whichever of light or dark reads better against that fill. The default.
1
A bare chevron with no box at all -- pointing right when collapsed, down when expanded.
2
A plus or minus inside an outlined box drawn on the row's own surface -- the familiar Windows tree-pane look.
Accepted in every DataMode, in both directions -- see the group introduction for why these four appearance properties are the deliberate exception to tree view's otherwise absolute Embedded-only rule.
Example
VBA
grid.SetTreeParent 1, 0' any tree at all, not only a directory oneDebug.Print grid.TreeExpanderStyle ' -> 0 (filled accent box, the default)
grid.TreeExpanderStyle = 1' a bare chevron
grid.TreeExpanderStyle = 2' the outlined Windows-style boxOnErrorResumeNext
grid.TreeExpanderStyle = 3' refused -- there is no style 3Debug.PrintErr.Number <> 0' -> TrueErr.ClearOnErrorGoTo0Debug.Print grid.TreeExpanderStyle ' -> 2 (still the last value that was accepted)
Gotchas
Refused, not clamped: any value outside 0-2 raises an error and leaves the current style untouched. TreeExpanderSize, right below, behaves the opposite way, so the two cannot be treated interchangeably when validating user input.
Style 0's box is FILLED with the accent colour; style 2's is an outline on the row's own background. If what you want is the plain Windows tree look, ask for 2 -- style 0 is not it, despite both being "a plus in a box".
The glyph is rendered with antialiasing rather than drawn with plain GDI shapes, which is what lets its strokes stay thin -- roughly one to one-and-a-half pixels -- at every size. Making the glyph bigger with TreeExpanderSize therefore does not make its strokes proportionally heavier; it enlarges the shape, not the weight.
Applies to every tree, however it was built -- SetTreeParent, BeginTreeBatch and LoadDirectoryTree all feed the same tree projection, and the glyph is painted from that. There is no separate setting for directory trees.
Safe to set before any tree, or any data, exists: the value is stored and takes effect as soon as something with children is on screen. It is held independently of the tree's own state, so ClearTreeView and a fresh LoadFromArray both leave it alone -- only setting the property changes it.
See alsoTreeExpanderSize, TreeExpanderColor, TreeExpanderGlyphColor, TreeIndent
TreeExpanderSizeTree view
Edge length of the expander glyph, in pixels.
Sets how large the expand/collapse glyph is drawn, as the length of one edge of its square. 0 means automatic and is the default: the glyph is sized from the grid's row height so the affordance keeps its proportions when RowHeight or the row density changes, instead of staying a fixed dot on a tall row. Any other value is silently clamped to 7-24 rather than refused, so reading the property back after setting an extreme value will not show what was passed in.
SignatureProperty TreeExpanderSize As Long
ReturnsThe requested edge length in pixels (7-24), or 0 when the size is automatic.
Default0 -- automatic, which currently resolves to 13 pixels, or 15 once RowHeight is 28 or more.
Accepted in every DataMode, in both directions -- see the group introduction.
Example
VBA
Debug.Print grid.TreeExpanderSize ' -> 0 (automatic, the default)
grid.TreeExpanderSize = 16Debug.Print grid.TreeExpanderSize ' -> 16
grid.TreeExpanderSize = 999Debug.Print grid.TreeExpanderSize ' -> 24 (silently clamped, never refused)
grid.TreeExpanderSize = 0' back to automaticDebug.Print grid.TreeExpanderSize ' -> 0 (stored as 0, NOT as the pixel size it resolves to)
Gotchas
Silently clamped to 7-24 rather than refused -- setting 2 or 5000 both succeed, and reading back shows 7 or 24. TreeExpanderStyle refuses instead of clamping, so the two properties disagree on this deliberately.
0 is the one value that escapes the clamp: it is stored as 0 and read back as 0, meaning "automatic". It never reads back as the pixel size it currently resolves to, so code cannot discover the automatic size through this property.
The automatic size comes from the grid's default row height, not from an individual row's own height set by SetRowHeight -- a single unusually tall row does not get a larger glyph.
The zone reserved for the glyph is the larger of 16 pixels and the glyph plus 3, so a glyph bigger than 13 pixels widens that zone and pushes the leftmost column's text further right. That same zone is exactly what a click on the glyph is tested against, so a large glyph stays clickable across its whole width.
A bigger glyph is not a heavier one: stroke weight is deliberately near-constant across the range, so raising this enlarges the shape rather than thickening its strokes.
See alsoTreeExpanderStyle, TreeExpanderColor, TreeExpanderGlyphColor, TreeIndent
TreeExpanderColorTree view
Colour of the expander's box fill, or of the chevron's stroke.
Overrides the colour the glyph's main shape is painted in: the box interior for styles 0 and 2, and the chevron's own stroke for style 1. The default, &HFFFFFFFF, means automatic and follows the theme -- the accent colour for style 0, the row's own surface for style 2, and the text colour for style 1. Any OLE_COLOR is accepted and none is ever refused, including the system colour constants such as vbButtonFace, which are resolved when the glyph is painted rather than when the property is set, so they keep tracking a Windows theme change afterward.
SignatureProperty TreeExpanderColor As OLE_COLOR
ReturnsThe colour last set, or &HFFFFFFFF while the colour is automatic.
Default&HFFFFFFFF -- automatic, following the theme.
Accepted in every DataMode, in both directions -- see the group introduction.
Example
VBA
Debug.Print Hex(grid.TreeExpanderColor) ' -> FFFFFFFF (automatic)
grid.TreeExpanderStyle = 0
grid.TreeExpanderColor = RGB(46, 139, 34) ' a green box instead of the theme accent
grid.TreeExpanderColor = &HFFFFFFFF ' back to the theme's own accent colourDebug.Print Hex(grid.TreeExpanderColor) ' -> FFFFFFFF
Gotchas
&HFFFFFFFF is the "automatic" sentinel, not a colour. It is the one value that cannot be used to ask for a specific colour, and it is what the property reads back until something else is written -- so writing it back is always how you restore the default.
With style 2 this sets the box INTERIOR only. That box's outline is always derived from the theme and cannot be set through any property.
While TreeExpanderGlyphColor is still automatic, changing this also changes the plus/minus colour, because the automatic glyph colour is picked for contrast against whatever fill is in force. Set both if you want an exact pair.
Never refused and never validated -- an unintended value simply paints, including one that leaves the glyph invisible against the row behind it.
See alsoTreeExpanderGlyphColor, TreeExpanderStyle, TreeExpanderSize
TreeExpanderGlyphColorTree view
Colour of the plus/minus strokes drawn inside the expander box.
Overrides the colour of the glyph itself -- the plus or minus inside the box. The default, &HFFFFFFFF, means automatic: the colour is chosen for contrast against the box fill actually in force, so a dark fill gets a white glyph and a light one gets a darkened version of the fill, and a caller who sets only TreeExpanderColor still ends up with legible strokes. With TreeExpanderStyle 1 the chevron IS the glyph, so this property wins over TreeExpanderColor whenever both are set. Never refused.
SignatureProperty TreeExpanderGlyphColor As OLE_COLOR
ReturnsThe colour last set, or &HFFFFFFFF while the colour is automatic.
Default&HFFFFFFFF -- automatic, picked for contrast against the box fill.
Accepted in every DataMode, in both directions -- see the group introduction.
Example
VBA
grid.TreeExpanderStyle = 0
grid.TreeExpanderColor = RGB(46, 139, 34)
Debug.Print Hex(grid.TreeExpanderGlyphColor) ' -> FFFFFFFF (automatic: white, since the fill is dark)
grid.TreeExpanderGlyphColor = vbYellow ' an explicit glyph colour instead
grid.TreeExpanderGlyphColor = &HFFFFFFFF ' back to the contrast-picked default
Gotchas
The automatic value is derived from TreeExpanderColor's resolved fill, so a custom fill on its own usually already produces a readable glyph. Set this only when the contrast-picked colour is not the one you want.
With TreeExpanderStyle 1 this overrides TreeExpanderColor rather than being ignored, because the chevron is the glyph and there is no box for the two colours to divide between.
Has nothing to draw on style 2's outline, which is theme-derived like style 0's box border -- this property only ever colours the plus or minus itself.
Never refused and never validated -- setting it to the same colour as the fill hides the glyph without any error.
See alsoTreeExpanderColor, TreeExpanderStyle, TreeExpanderSize
Export and clipboard
Eight members handle everything the grid can do with its own data once it is already loaded: write it to a file, hand it back as a single in-memory string, or put it on the Windows clipboard. They split cleanly into three shapes. ExportToCSV, ExportToCSVEx, ExportToPdf and ExportToXlsx write a file and return nothing but success or an error. ExportToJson and ExportToHTML write nothing to disk at all -- they build the whole result in memory and return it as one string (a BSTR), for a caller that wants to stream it, POST it, or hand it to something else without ever touching a file. CopySelectedToClipboard and CopyWithHeaders put text on the Windows clipboard for pasting into Excel, Notepad, or back into this same grid with PasteFromClipboard. All eight read the grid's CURRENT view -- whatever the active sort and filter currently show -- not the raw order data was loaded in, with one deliberate, documented exception described below.
Every one of these eight genuinely works in Turbo column-store mode, not merely as a formality -- and Turbo column-store mode is also the ONLY place any of them ever refuses purely because of size. Before writing a single byte, a Turbo column-store-mode call estimates how big the result would be from a deliberately generous, worst-plausible-case per-cell width (never a real measurement, and never an underestimate) and checks that estimate against something real: free space on the target drive for a file, or available process memory for an in-memory string or a clipboard payload. ExportToXlsx and ExportToPdf each add one more check ahead of that, and it runs unconditionally, resource estimate or not: ExportToXlsx refuses outright if the row or column count would exceed Excel's own hard format ceiling (1,048,576 rows including the header, 16,384 columns), and ExportToPdf refuses outright above a fixed page count, computed from the very same page layout the export would actually render. Embedded mode has NONE of this. Every one of these eight simply attempts the operation when DataMode is Embedded -- no size estimate, no ceiling, nothing. This is easy to miss because Embedded mode is what every quick test naturally runs against, and small data never comes close to any limit either way.
A second split, orthogonal to the first, matters just as much in practice: whether a hidden column (SetColumnVisible False) is left out. ExportToCSV, ExportToCSVEx, ExportToJson, CopySelectedToClipboard and CopyWithHeaders all include EVERY column, regardless of visibility -- there is no way to exclude a column from any of these five short of not loading it in the first place. ExportToPdf, ExportToXlsx and ExportToHTML do the opposite: a hidden column is left out of the output entirely, the same way it is left off the screen. Nothing in any method's name signals which side of this split it falls on, so it is worth remembering as a pair of short lists rather than trying to guess from the method name alone. ExportToCSVEx's own includeHidden parameter is a common point of confusion here -- it controls only which ROWS a currently active filter is hiding, never which columns SetColumnVisible is hiding; the two "hidden"s are unrelated.
File safety differs across this group as well, and only in Turbo column-store mode. ExportToXlsx and ExportToPdf, and ExportToCSVEx's own includeHidden:=True path, all write to a temporary file next to the real target and only take the real target's name once writing has finished cleanly and been verified -- an interrupted write can never leave a corrupt or partial file at the path the caller asked for. Every other write in this group -- plain ExportToCSV, ExportToCSVEx with includeHidden:=False, and every one of these methods in Embedded mode, with no exception -- writes directly to the target path with no such protection: an interruption partway through (most realistically, the disk filling up after the size check already passed) leaves whatever was written so far sitting at that path, and the call still reports success, because the individual writes that build the file are never checked for failure.
Refusal in Virtual mode is simple and absolute: all eight refuse outright, because a Virtual grid never holds a full dataset of its own for any of them to read -- there is nothing here for the host's own data source to opt back into, unlike some other feature areas where a read-only query still works while the matching setter does not.
RecipeExport the same grid to CSV, JSON and HTML, and see the column-visibility rule change what comes out
VBA
Sub CompareExportsAcrossFormats(grid AsObject)
Dim data(0To2, 0To2) AsVariant
data(0, 0) = "Alpha": data(0, 1) = "Internal": data(0, 2) = 10
data(1, 0) = "Beta": data(1, 1) = "Internal": data(1, 2) = 20
data(2, 0) = "Gamma": data(2, 1) = "Internal": data(2, 2) = 30
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Source"
grid.SetColumnHeader 2, "Score"' "Source" is internal bookkeeping nobody wants on a printed report
grid.SetColumnVisible 1, FalseDim csvPath AsString
csvPath = Environ$("TEMP") & "\export_demo.csv"
grid.ExportToCSV csvPath
Dim json AsString
json = grid.ExportToJson()
Dim html AsString
html = grid.ExportToHTML()
' CSV and JSON never look at column visibility -- "Source" is in bothDebug.Print InStr(json, "Source") > 0' -> True' HTML DOES respect it -- "Source" never appears anywhere in the tableDebug.Print InStr(html, "Source") > 0' -> False' If "Source" genuinely must never leave the building in ANY format,' hiding the column is not enough on its own for CSV/JSON/clipboard --' build a narrower array without that column instead, or strip it from' the CSV/JSON result downstream.EndSub
SetColumnVisible drives what a human sees on screen, and ExportToPdf/ExportToXlsx/ExportToHTML follow that faithfully -- but ExportToCSV, ExportToCSVEx, ExportToJson, CopySelectedToClipboard and CopyWithHeaders were built for a different job (moving every value somewhere else exactly, for a machine to read) and none of them consult column visibility at all. The same SetColumnVisible call that correctly hides a column from a printed PDF report silently fails to hide it from a CSV a scheduled task emails out an hour later -- the fix is to know which list a given export belongs to, not to add more hiding calls.
RecipeCopy a selection to the clipboard for Excel, without the empty-selection trap
VBA
Sub CopySelectionSafely(grid AsObject)
Dim data(0To3, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = 100
data(1, 0) = "Beta": data(1, 1) = 200
data(2, 0) = "Gamma": data(2, 1) = 300
data(3, 0) = "Delta": data(3, 1) = 400
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Amount"
grid.SelectRow 1
grid.SelectRow 3' Headers are always included with this call -- paste straight into' Excel and the header row lands exactly where Excel expects it
grid.CopySelectedToClipboard
' Elsewhere in the same form, a button handler clears the selection' once the user is done with it, then later code re-copies "to be' safe" before doing something else with the clipboard
grid.ClearSelection
' WRONG assumption: an empty selection means nothing happens. It does' not -- this still succeeds and still changes the clipboard, silently' overwriting the real Alpha/Delta data above with just the' "Name<TAB>Amount" header line, because headers alone are never empty
grid.CopySelectedToClipboard
' The safe pattern: know there is at least one selected row before' relying on the result, or use CopyWithHeaders(False) instead, which' genuinely leaves the clipboard alone when nothing is selected
grid.SelectRow 1
grid.SelectRow 3Dim anySelected AsBoolean, r AsLongFor r = 0To grid.GetRowCount - 1If grid.IsRowSelected(r) Then anySelected = True: ExitForNext r
If anySelected Then grid.CopySelectedToClipboard
EndSub
CopySelectedToClipboard always builds a header line first, so the text it copies is never actually empty even when no data row qualifies -- which means an empty selection does not raise an error AND does not leave the clipboard alone, the combination that is easiest to assume by mistake. CopyWithHeaders(False) is the one call in this pair whose result really can be empty, and it is the only one that leaves a prior clipboard payload genuinely untouched when nothing is selected.
RecipeExport XLSX and PDF defensively: treat a refusal as a real outcome, not an edge case to ignore
VBA
Sub ExportDefensively(grid AsObject, basePath AsString)
Dim ok AsBooleanOnErrorResumeNextErr.Clear
grid.ExportToXlsx basePath & ".xlsx"
ok = (Err.Number = 0)
OnErrorGoTo0Debug.Print"XLSX: " & IIf(ok, "written", "refused - " & Err.Description)
OnErrorResumeNextErr.Clear
grid.ExportToPdf basePath & ".pdf"
ok = (Err.Number = 0)
OnErrorGoTo0Debug.Print"PDF: " & IIf(ok, "written", "refused - " & Err.Description)
' Both calls above run a real check before writing anything, but ONLY' in Turbo column-store mode: ExportToXlsx against Excel's own' 1,048,576-row / 16,384-column format ceiling and the free space on' the target drive; ExportToPdf against a fixed page count computed' from the same page layout it would actually render. In Embedded' mode neither check exists at all -- the write is simply attempted,' whatever the row count. A production export routine (a scheduled' task, a "Export All" button over a dataset that grows every month)' should treat a refusal here as a real, expected outcome once the' data is large enough, not a bug to chase down.EndSub
The size and page-count checks in this group exist only in Turbo column-store mode, and only for exactly the reasons named in each error message -- a real ceiling (disk space, memory, Excel's own format limit, or a fixed page count), never a guess. Code that calls these exports unattended should catch the error and decide what to do next (shrink the request, tell the user, log it) rather than assume every export always succeeds, especially since the very same call can behave differently purely because of which DataMode the grid happens to be in.
ExportToCSVExport and clipboard
Write every row and column of the current view to a CSV file.
Writes the grid's current view -- whatever the active sort and filter currently show -- to a CSV file at filePath, one line per row plus a header line built from the column headers. The file is UTF-8 with a byte-order mark and CRLF line endings. A field is wrapped in double quotes only if it contains a comma, a double quote, or a line break (RFC 4180); an embedded quote is doubled. Every column is written regardless of SetColumnVisible -- there is no way to exclude a hidden column from this call. To export every row regardless of the active filter, use ExportToCSVEx instead.
SignatureSub ExportToCSV(filePath As String)
Parameters
filePath (String) - Full path to the CSV file to create or overwrite.
ReturnsNone. Raises an error if the file could not be created or written, or if the estimated output size is refused in Turbo column-store mode.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this runs a real size estimate first (rows x columns x a deliberately generous assumed field width, widened to worst-case UTF-8 bytes) against the free space on the target volume, and refuses with a message naming the estimated size and the free space if it plainly would not fit; the estimate can never be exact, only pessimistic, so it can refuse a request that would actually have fit, but never overlook one that would not. Embedded mode runs no such check at all and simply attempts the write.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = "First, item"
data(1, 0) = "Beta": data(1, 1) = "Second item"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Notes"
grid.ExportToCSV "C:\Exports\rows.csv"' rows.csv now contains a UTF-8 byte-order mark, then:' Name,Notes' Alpha,"First, item"' Beta,Second item' -- the comma inside "First, item" forced that one field into quotes;' the other three fields needed no quoting at all
Gotchas
Every column is included regardless of SetColumnVisible -- unlike ExportToPdf, ExportToXlsx and ExportToHTML, this call never looks at column visibility at all.
The file is written directly to filePath with no temporary-file safety net, in both Embedded and Turbo column-store mode: if writing is interrupted partway (most realistically, the disk filling up after the size check already passed), whatever was written so far is left in place at filePath and the call still reports success, because the individual writes that build the file are never checked for failure. Contrast ExportToCSVEx's own includeHidden:=True path in Turbo column-store mode, which is the one CSV write in this group that is fully atomic.
An empty file path is refused. A path whose containing folder does not exist fails the same way any application's file write would.
Reflects the grid's CURRENT view: an active sort determines the row order and an active filter determines which rows appear, exactly as ExportToCSVEx(filePath, False) does. To bypass both, use ExportToCSVEx with includeHidden:=True instead.
See alsoExportToCSVEx, ExportToJson, ExportToHTML, ExportToXlsx, ExportToPdf, CopySelectedToClipboard, SetColumnVisible
CopySelectedToClipboardExport and clipboard
Copy the currently selected rows to the Windows clipboard, with column headers.
Builds tab-delimited, CRLF-terminated text from every currently selected row -- in the rows' current on-screen order, not the order they were selected in -- always with a header line built from the column headers first, and puts it on the Windows clipboard as plain Unicode text. This is the exact layout Excel produces when copying its own cells, and what PasteFromClipboard expects back. Headers are always included with this call; use CopyWithHeaders(False) instead for a data-only copy.
SignatureSub CopySelectedToClipboard()
ReturnsNone. Raises an error only if the clipboard itself could not be opened, or if an estimated copy size is refused in Turbo column-store mode.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this runs a real size estimate first (the same per-cell width assumption ExportToCSV's own estimate uses, widened to the actual bytes a clipboard allocation needs) against available process memory -- 80% of what Windows currently reports free, capped further to roughly 1.2 GB (constant) on a 32-bit build -- and refuses if the estimate exceeds it. Embedded mode runs no such check and simply attempts the copy.
Example
VBA
Dim data(0To2, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Beta": data(2, 0) = "Gamma"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SelectRow 0
grid.SelectRow 2
grid.CopySelectedToClipboard
' the clipboard now holds, as tab-and-CRLF-delimited text:' Name' Alpha' Gamma' -- headers are always included, and rows appear in on-screen order
Gotchas
Column headers are always included as the first line -- there is no parameter to omit them with this call; use CopyWithHeaders(False) if a plain, data-only copy is needed.
Copies rows in their CURRENT ON-SCREEN order, never the order they were selected in: selecting row 40 and then row 10 still produces row 10's line before row 40's, because the header-always-included text is built by walking the view from top to bottom and including whichever rows happen to be selected along the way.
With nothing selected, this does not raise an error and does not copy every row either. Because the header line is always built first, the text this call produces is never actually empty, so it still overwrites the clipboard -- with just that one header line. Contrast CopyWithHeaders(False), whose result really can be empty when nothing is selected, and which leaves a prior clipboard payload completely untouched in that case.
Every column is included regardless of SetColumnVisible.
Reading the clipboard back from VBA has no direct API in this control -- PasteFromClipboard (into this same grid, or another one) is the practical way to inspect what a copy actually produced.
See alsoCopyWithHeaders, PasteFromClipboard, SelectRow, GetSelectedRows, ExportToCSV
ExportToCSVExExport and clipboard
Export to CSV like ExportToCSV, with the option to ignore the active filter (and the active sort) entirely.
With includeHidden:=False, behaves exactly like plain ExportToCSV: the current view, respecting whatever sort and filter are active. With includeHidden:=True, every row is written -- including rows an active filter is currently hiding -- but the active SORT is abandoned too: rows come out in the grid's own original, underlying creation order, not the order the sort would show them in. includeHidden refers only to rows; it has no connection to SetColumnVisible, which is a separate concept -- every column is written in both includeHidden states, exactly like plain ExportToCSV.
SignatureSub ExportToCSVEx(filePath As String, includeHidden As Boolean)
Parameters
filePath (String) - Full path to the CSV file to create or overwrite.
includeHidden (Boolean) - False: only rows the active filter currently shows, in the current sort order -- identical to ExportToCSV. True: every row, with both the filter and the sort ignored, in the grid's own original creation order.
ReturnsNone. Raises an error if the file could not be created or written, or if the estimated output size is refused in Turbo column-store mode.
Refused in Virtual mode, the same as ExportToCSV. In Turbo column-store mode, includeHidden:=True estimates its row count from the physical row count plus every appended row (deliberately counting a little high rather than low) before running the same disk-space check ExportToCSV's own preflight uses; includeHidden:=False uses the same estimate and check as plain ExportToCSV. Embedded mode runs no size check in either state.
Example
VBA
Dim data(0To3, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Beta"
data(2, 0) = "Gamma": data(3, 0) = "Delta"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnFilter 0, 0, "Beta"' 0 = Contains -- the only filter mode Turbo column-store' column-store mode implements; no other row' contains "Beta" as a substring, so this' narrows to exactly the same one row Equals would
grid.ExportToCSVEx "C:\Exports\shown_only.csv", False' shown_only.csv: header line + exactly one data line ("Beta")
grid.ExportToCSVEx "C:\Exports\every_row.csv", True' every_row.csv: header line + all four rows, Alpha/Beta/Gamma/Delta, in' their ORIGINAL order -- the filter above no longer applies here, and' neither would any sort that had been applied
Gotchas
includeHidden controls ROWS only -- rows an active filter is currently hiding. It has nothing to do with hidden COLUMNS: every column is written regardless of SetColumnVisible, in both includeHidden states, exactly like plain ExportToCSV. The name is easy to misread as covering both kinds of 'hidden'.
includeHidden:=True does not just add the filtered-out rows back -- it also abandons the current SORT ORDER entirely, writing every row in the grid's own original, underlying creation order. A grid that has been both filtered and sorted exports, with includeHidden:=True, as though neither had ever been applied.
includeHidden:=False is not a distinct code path -- it behaves exactly like plain ExportToCSV, respecting both the active filter and the active sort.
In Turbo column-store mode, includeHidden:=True is the one CSV write in this group that is written atomically (ExportToPdf and ExportToXlsx are also atomic in Turbo column-store mode, but neither of those writes CSV): it is built in a temporary file next to the real target and only takes the real target's name once writing has finished cleanly, so an interrupted write can never leave a corrupt file at filePath. Every other CSV write in this group -- plain ExportToCSV, and this same call with includeHidden:=False -- writes directly to the target with no such protection.
An empty file path is refused, the same as ExportToCSV.
The Cookbook example filters with Contains (mode 0), not Equals (mode 1) -- Equals is refused in Turbo column-store mode (only Contains and StartsWith are implemented there), so an example meant to run in every mode has to use one of those two.
See alsoExportToCSV, SetColumnFilter, GetFilteredRowCount, Sort, SetColumnVisible
CopyWithHeadersExport and clipboard
Copy the currently selected rows to the clipboard, with the choice of including a header line or not.
Behaves exactly like CopySelectedToClipboard, except the header line is optional: includeHeaders:=True reproduces CopySelectedToClipboard's own always-include-headers behavior exactly, and includeHeaders:=False copies data rows only, with no header line at all. This is the one difference between the two calls -- everything else (on-screen row order, tab/CRLF layout, every column regardless of visibility) is identical.
SignatureSub CopyWithHeaders(includeHeaders As Boolean)
Parameters
includeHeaders (Boolean) - True: prepend a header line built from the column headers, exactly like CopySelectedToClipboard always does. False: copy data rows only, with no header line.
ReturnsNone. Raises an error only if the clipboard itself could not be opened, or if an estimated copy size is refused in Turbo column-store mode.
Refused in Virtual mode, the same as CopySelectedToClipboard. In Turbo column-store mode this runs the identical memory-based size estimate CopySelectedToClipboard's own preflight uses, regardless of includeHeaders. Embedded mode runs no such check.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Beta"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SelectRow 0
grid.CopyWithHeaders False' the clipboard now holds only: Alpha (no header line at all)
grid.ClearSelection
grid.CopyWithHeaders False' nothing is selected and includeHeaders is False, so there is nothing at' all to copy -- the clipboard is left exactly as it was, still "Alpha"
Gotchas
With nothing selected, includeHeaders:=False can produce genuinely EMPTY text -- and when it does, this call returns without ever touching the clipboard, leaving whatever it held before completely untouched, from any source. This is the one case in this pair where an empty selection truly changes nothing.
By contrast, includeHeaders:=True with nothing selected still overwrites the clipboard with a header-only line, the same as CopySelectedToClipboard -- headers alone are never empty, so the text built is never empty either.
Copies rows in their current on-screen order, never the order they were selected in -- see CopySelectedToClipboard's own gotcha on this.
Every column is included regardless of SetColumnVisible, in both includeHeaders states.
See alsoCopySelectedToClipboard, PasteFromClipboard, SelectRow
ExportToJsonExport and clipboard
Return the grid's current view as a JSON array of one object per row.
Builds and returns a JSON string: an array with one object per row (in the grid's current view order), each object keyed by the column headers. Every value in every object comes out as a JSON STRING built from the cell's on-screen display text, regardless of the column's underlying type: a Long, Double or Currency column comes out as a quoted digit string (e.g. "20.5", not the JSON number 20.5), in Embedded mode a Boolean column comes out as the quoted text "True" or "False" (never the JSON literals true/false) -- but in Turbo column-store mode the same column is exported as the quoted raw VARIANT_BOOL integer instead, "-1" or "0" (see gotchas), and a Date column comes out exactly as it displays on screen. A cell with no display text -- including a genuinely Null value -- is written as the bare JSON literal null, never an empty string. String values are fully escaped per RFC 8259 (backslash, double quote, the common control-character shorthands, and a real surrogate pair kept intact as two \uXXXX escapes). Unlike the file-writing exports in this group, this returns its whole result as one in-memory string -- there is no filePath, and nothing is ever written to disk.
SignatureFunction ExportToJson() As String
ReturnsA JSON string: an array of one object per row, each keyed by the column headers.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this runs a real size estimate first -- weighted by the real, exact length of every column header, since a wide grid's header names are repeated in full inside every single row object here, unlike CSV's one-time header line -- against available process memory, the same 80%-of-available (capped near 1.2 GB (constant) on a 32-bit build) ceiling CopySelectedToClipboard's own preflight checks. Embedded mode runs no such check. A Boolean column's JSON text also differs by mode: "True"/"False" in Embedded, but the raw quoted VARIANT_BOOL integer ("-1"/"0") in Turbo column-store, because Turbo column-store's automatic column-type inference from LoadFromArray does not recognize Boolean and SetColumnType is refused there -- see gotchas.
Example
VBA
Dim data(0To1, 0To2) AsVariant
data(0, 0) = "Alpha": data(0, 1) = 20.5: data(0, 2) = True
data(1, 0) = "Beta": data(1, 1) = 9: data(1, 2) = False
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Score"
grid.SetColumnHeader 2, "Passed"Dim json AsString
json = grid.ExportToJson()
Debug.Print json
' -> [{"Name":"Alpha","Score":"20.5","Passed":"True"},{"Name":"Beta","Score":"9","Passed":"False"}]' -- Score (a Double) and Passed (a Boolean) still come out quoted, as' JSON strings -- "20.5", not 20.5; "True", not true
Gotchas
Every value comes out as a JSON string, never a native JSON number, boolean, or any other typed value -- including columns typed Long, Double, Currency, Date or Boolean. A numeric column is written as a quoted digit string, e.g. "20.5" rather than 20.5. There is no way to ask this call for typed JSON output -- code that expects a real number or boolean back needs to convert the string itself.
A Boolean column’s JSON text differs by data mode, and this is a real, permanent characteristic, not a bug: in Embedded mode, explicit SetColumnType(col, 5) tags the column Boolean and it is exported as the quoted text "True"/"False". In Turbo column-store mode, SetColumnType is refused outright (Turbo column-store infers column typing automatically from LoadFromArray's own Variant subtypes) and that automatic inference does not recognize VT_BOOL -- GetColumnType returns 0 (Auto) for a Boolean-valued Turbo column-store column, confirmed live. With no Boolean type tag, ExportToJson falls back to the column's raw underlying value and writes the quoted VARIANT_BOOL integer instead: "-1" for True, "0" for False. There is no supported way to force Boolean typing on a Turbo column-store-mode column, so code reading a Turbo column-store-exported JSON Boolean column must accept "-1"/"0" as well as "True"/"False", or convert via CBool/CLng before comparing.
One JSON object per row, with every column's own key name repeated in full inside every single object -- unlike CSV's one-time header line, a wide grid's column names are duplicated across every row. This is part of why the Turbo column-store-mode size estimate for this call weighs header length so heavily, and part of why a JSON export of the same data is noticeably larger than the equivalent CSV.
A cell with no display text -- including a genuinely Null value, like the standard fixture's own every-tenth-row gap in its Notes column -- is written as the bare JSON literal null, never an empty string "". Code reading the result back needs to treat the two differently.
Every column is included regardless of SetColumnVisible.
Returns its result as a single in-memory string (a BSTR) with no filePath -- in Turbo column-store mode its size estimate is checked against available memory, not disk space, the same constraint shape CopySelectedToClipboard's own preflight uses.
See alsoExportToHTML, ExportToCSV, SetColumnVisible
ExportToHTMLExport and clipboard
Return the grid's current view as one self-contained, styled HTML table.
Builds and returns a single complete <table>...</table> as a string, with an inline style attribute on the table itself and on every header and data cell -- header background/text color, alternating row background, row font family and size all come straight from the grid's CURRENT theme and appearance settings at the moment of export, so the same call made before and after a Theme change can look different even though the data has not changed. Cell text is HTML-escaped (&, < and > only -- the correct, complete rule for plain text content). A hidden column (SetColumnVisible False) is left out of the table entirely -- this is one of only three members in this group that behaves this way.
SignatureFunction ExportToHTML() As String
ReturnsAn HTML string containing one complete <table>...</table>, styled inline from the current theme.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this runs a real size estimate first, using a wider per-cell allowance than ExportToJson's own estimate to account for this format's own tag overhead, against the same available-process-memory ceiling ExportToJson and CopySelectedToClipboard use. Embedded mode runs no such check.
Example
VBA
Dim data(0To1, 0To1) AsVariant
data(0, 0) = "Alpha": data(0, 1) = "A & B"
data(1, 0) = "Beta": data(1, 1) = "C < D"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.SetColumnHeader 1, "Note"
grid.SetColumnVisible 1, False' hide the Note columnDim html AsString
html = grid.ExportToHTML()
Debug.Print InStr(html, ">Name<") > 0' -> TrueDebug.Print InStr(html, ">Note<") > 0' -> False -- the hidden column never appears at all
Gotchas
This is one of only three members in this group -- with ExportToPdf and ExportToXlsx -- that DOES respect column visibility: a column hidden with SetColumnVisible is left out of the exported table entirely, unlike ExportToCSV, ExportToCSVEx, ExportToJson, CopySelectedToClipboard or CopyWithHeaders.
Only &, < and > are escaped in cell text -- this is correct and sufficient, since every value lands inside ordinary HTML text content, never inside a quoted attribute.
Inline styles carry the CURRENT header colors, alternating row colors and row font straight from the active theme and appearance settings at the moment of export -- calling this again after changing Theme, AlternateRowColor, RowFontName or similar produces visibly different HTML from the same underlying data.
In Embedded mode, the current RowFontName is inserted into the table's own inline style attribute exactly as set, with no escaping at all; Turbo column-store mode escapes it first. An ordinary font name is unaffected, but a RowFontName containing a single quote or a double quote would corrupt the style attribute in Embedded mode specifically (a double quote would break the surrounding HTML tag itself). This applies only to the RowFontName property value, never to cell data -- cell values are escaped identically, and safely, in both modes.
Returns its result as a single in-memory string (a BSTR) with no filePath -- refusal in Turbo column-store mode is checked against available memory, not disk space.
See alsoExportToJson, ExportToPdf, SetColumnVisible, Theme, AlternateRowColor
ExportToPdfExport and clipboard
Write the grid's current view to a paginated PDF file, with no external library required.
Writes the grid's current view to a PDF 1.4 file at filePath using a fixed US-Letter page layout (0.5-inch margins, 9-point data text, 10-point headers) -- there is no property to change any of these. Only visible columns are included, each proportionally scaled to fill one page's width, so a wide grid never grows wider than one page, only taller. As many rows as fit are placed on each page and the header row repeats at the top of every page; a large row count spans multiple pages automatically. Text renders through a standard, non-embedded font limited to plain ASCII -- any character outside that range, including ordinary accented letters (e-acute, n-tilde, u-umlaut and similar), prints as a literal question mark rather than the real character.
SignatureSub ExportToPdf(filePath As String)
Parameters
filePath (String) - Full path to the PDF file to create or overwrite.
ReturnsNone. Raises an error if the file could not be written, if the grid has no columns at all, or if the resulting page count is refused in Turbo column-store mode.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this first computes the real page count from the same fixed page layout the export itself uses and refuses outright above a fixed 5,000-page ceiling, regardless of how much disk space is actually available -- a wide, tall dataset can be turned away on page count alone before any size estimate even runs. Only once that page-count check passes does a second, ordinary disk-space estimate run. Embedded mode has neither check and simply attempts the write, however many pages the result would need.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Beta"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.ExportToPdf "C:\Exports\names.pdf"' names.pdf now exists: one page, a "Name" header row, then "Alpha" and' "Beta" beneath it, in a standard US-Letter layout
Gotchas
Only visible columns are included, proportionally scaled to fill one page's width -- a grid never grows wider than one page, only taller. This is one of only three members in this group -- with ExportToXlsx and ExportToHTML -- that respects SetColumnVisible.
Pagination uses a fixed US-Letter layout that is not configurable through any exposed property; the full header row repeats at the top of every page.
Text renders through a standard, non-embedded font limited to plain ASCII (character codes 0-127) -- anything outside that range prints as a literal question mark, including ordinary accented Western European letters such as e-acute or u-umlaut, not only non-Latin scripts, symbols or emoji.
In Turbo column-store mode this refuses outright above a fixed 5,000-page ceiling, decided purely from the same page layout the export itself would use -- independent of disk space, and independent of row/column byte size. Embedded mode has no equivalent ceiling of any kind.
In Turbo column-store mode this is one of the members in this group that writes atomically: a temporary file next to the real target, only replacing filePath once writing has finished cleanly. Embedded mode writes directly to filePath with no such protection -- an interrupted write there can leave a truncated file in place while still reporting success.
An empty file path is refused. A grid with no columns at all is refused too, with its own distinct error.
See alsoExportToXlsx, SetColumnVisible, ExportToHTML
ExportToXlsxExport and clipboard
Write the grid's current view to a real .xlsx workbook, with no external library required.
Writes the grid's current view to a valid Open XML (.xlsx) workbook at filePath containing one worksheet. Only visible columns are included, in the order they appear in the grid. The header row is bold; every other cell is plain. Every value -- including one that happens to start with =, +, - or @ -- is written as a literal inline string, never as a formula, so nothing in an exported workbook is ever evaluated by Excel; this also means none of this control's own cell colors, data bars, heatmaps or icons carry over into the workbook. This applies to every column alike, not just text ones: a Long, Double, Currency or Date column is written as the same inline-string cell type as a text column, so Excel opens it as ordinary left-aligned text rather than a number or date it can sum, sort numerically, or apply date arithmetic to, until the column is converted. The archive itself is a valid, uncompressed ZIP -- Excel opens an uncompressed ZIP exactly as readily as a compressed one, but the resulting file is noticeably larger than the same data saved from within Excel.
SignatureSub ExportToXlsx(filePath As String)
Parameters
filePath (String) - Full path to the .xlsx file to create or overwrite.
ReturnsNone. Raises an error if the file could not be written, if the grid has no columns at all, or if the row/column count is refused in Turbo column-store mode.
Refused in Virtual mode -- the grid does not hold a full dataset of its own for this call to read. In Turbo column-store mode this first checks the row count (plus the header row this engine always writes) and the column count against Excel's own hard format ceiling -- 1,048,576 rows, 16,384 columns -- and refuses outright if either is exceeded, regardless of how much disk space is available; only once that check passes does an ordinary disk-space estimate run. Embedded mode has neither check: it will attempt to write a workbook that exceeds Excel's own limit, producing a file Excel itself then refuses to open.
Example
VBA
Dim data(0To1, 0To0) AsVariant
data(0, 0) = "Alpha": data(1, 0) = "Beta"
grid.LoadFromArray data
grid.SetColumnHeader 0, "Name"
grid.ExportToXlsx "C:\Exports\names.xlsx"' names.xlsx now exists and opens directly in Excel: row 1 is a bold' "Name" header, rows 2-3 are "Alpha" and "Beta" -- every value is a' plain inline string, never a formula
Gotchas
Only visible columns are included, in the same order they appear in the grid -- one of only three members in this group, with ExportToPdf and ExportToHTML, that respects SetColumnVisible.
Every cell is written as a literal inline string, never a formula -- a value that happens to start with =, +, - or @ is always shown exactly as typed and is never evaluated. There is no formula-injection risk from exported data by construction.
The same inline-string design applies to every column, not just text ones: a numeric or date column (Id, Amount, Price, OrderDate, ...) opens in Excel as plain text -- left-aligned, not summable with SUM/AVERAGE, and not sortable or filterable as numbers or dates -- until the user converts it (Excel's own 'Convert to Number' or Text to Columns). There is no per-column option to write a real Excel number or date cell instead.
Formatting is limited to a bold header row; none of this control's own cell colors, data bars, heatmaps, sparklines or icons (see the Icons/Visuals group) carry over into the workbook at all.
In Turbo column-store mode, this refuses outright, before writing anything, if the row or column count would exceed Excel's own hard format ceiling (1,048,576 rows including the header, 16,384 columns) -- regardless of available disk space. Embedded mode has no equivalent check and can silently produce a file that Excel itself then refuses to open.
The archive is written uncompressed by design -- a valid ZIP structure Excel accepts as-is, but noticeably larger on disk than the same data saved from Excel itself.
In Turbo column-store mode this writes atomically (a temporary file, replaced into place only once writing finishes cleanly); Embedded mode writes directly to filePath with no such protection.
An empty file path is refused. A grid with no columns at all is refused too, with its own distinct error.
See alsoExportToPdf, SetColumnVisible, ExportToHTML
Virtual mode: the deferred fetch protocol
Virtual mode (DataMode = 1) is for a grid backed by a dataset the host application owns -- a database cursor, a paged web API, an in-memory structure too large to hand over as one array -- where LoadFromArray's copy-everything-in model does not fit. In Virtual mode the grid never holds more than a sliding window of rows around the current viewport. It is told the total row count up front with SetVirtualRowCount, and it asks for whatever it actually needs to paint through the NeedRows event, answered with SupplyRows.
Setting up Virtual mode follows one fixed order, and it is not obvious from any single member's own error text: call Clear() first, while the grid is still in Embedded mode -- Clear() itself refuses once DataMode is already 1, because a grid that does not own its data has nothing left of its own to clear. Then add whatever columns the data needs with AddColumn -- AddColumn is refused once DataMode = 1 too, so every column must exist before the switch. Only then set DataMode = 1, and only after that call SetVirtualRowCount to declare how many rows exist. Every method in this group that is specific to Virtual mode refuses, identically, outside it: SetVirtualRowCount, FillVirtualSynthetic, SetVirtualViewportTop, SupplyRows and InvalidateAllRows all raise the same kind of error in both Embedded and Turbo column-store mode, naming themselves and saying Virtual mode is required.
Two members are the fastest way to see any of this working before writing a line of host code. FillVirtualSynthetic needs no callback wiring at all: it fills whatever the grid's last paint reported as missing with built-in, deterministic text ("R{row}C{col}" per cell) and repaints, so scrolling, paint and eviction can all be watched behaving correctly first. SetVirtualViewportTop is the programmatic equivalent of a scrollbar-thumb drag, useful for jumping straight to a row far from the top to exercise the same behavior somewhere other than row 0.
The real, host-driven path is the deferred fetch protocol. A paint that finds a row missing -- or an explicit call such as InvalidateAllRows -- posts one deferred message; once that message is processed (after the triggering call has already returned, on the next message pump) NeedRows fires, once per requested range, and the host answers with SupplyRows: with real data, or with an explicit decline by passing Empty or Null as the data argument. Returning from the NeedRows handler without calling SupplyRows at all is not a decline -- the request simply stays pending, and it is asked for again automatically once it times out or scrolls out of view. A second SupplyRows call answering a request that was already answered is accepted at the COM level (it does not raise a VBA error) but does nothing beyond that: the value is only visible through GetVirtualDiagnostics.
Sorting and filtering follow the same delegation shape. Because the grid does not own the data, calling any of the sort methods (Sort, SortEx, ClearSort, SortMulti) or filter methods (SetColumnFilter, ClearColumnFilter, ClearAllFilters, SetColumnNumericFilter, SetColumnNumericFilterRange, SetColumnValueListFilter, SetFilterState, LoadFilterPreset) while DataMode = 1 does not sort or filter anything by itself -- it fires SortRequested or FilterRequested and waits for the handler to set handled = True, having performed the operation itself against its own data, before treating the request as satisfied. Leaving handled at its default False value is a real, deliberate decline: the triggering call then raises an error and nothing about the grid's view changes.
InvalidateAllRows is the ONE invalidation entry point Virtual mode offers. There is no way to signal that a single row, or a handful of rows, changed -- one changed cell and a full requery are announced identically, by calling InvalidateAllRows, which discards the cache, drops every pending request and every previously-declined range, and re-fetches the current viewport from scratch.
GetVirtualDiagnostics and the SetPrefetchMargin/PrefetchMargin pair are the exceptions to the rule that this group's members require Virtual mode: none of the three is restricted to it at all, confirmed individually from source rather than assumed. SetPrefetchMargin and PrefetchMargin can be set and read in every mode -- the value only affects anything once DataMode = 1. GetVirtualDiagnostics can be called in every mode too, and reports whatever Virtual-mode bookkeeping this control instance currently holds: all zero if it has never entered Virtual mode, or left over from the last time it did.
RecipeWatch Virtual mode paint and scroll with no host code at all
VBA
Sub PreviewVirtualMode(grid AsObject)
' Columns have to exist before the switch to Virtual mode -- AddColumn' is refused once DataMode = 1.
grid.Clear
grid.AddColumn "Col A", 120
grid.AddColumn "Col B", 120
grid.AddColumn "Col C", 120
grid.DataMode = 1' 1 = Virtual' A million-row grid costs nothing to declare -- Virtual mode keeps only' a sliding window of rows around the viewport, never the whole count.
grid.SetVirtualRowCount 1000000' FillVirtualSynthetic fills whatever the grid's last paint reported as' missing, using built-in "R{row}C{col}" text -- no NeedRows handler,' no SupplyRows call, nothing wired up at all.Dim filled AsLong
filled = grid.FillVirtualSynthetic()
Debug.Print filled ' -> however many rows the current viewport showsDebug.Print grid.GetCellValue(0, 0) ' -> "R0C0"' SetVirtualViewportTop is the programmatic equivalent of dragging the' scrollbar thumb -- useful for exercising the cache far from row 0.
grid.SetVirtualViewportTop 500000
filled = grid.FillVirtualSynthetic()
Debug.Print grid.GetCellValue(500000, 0) ' -> "R500000C0"EndSub
FillVirtualSynthetic and SetVirtualViewportTop exist specifically so a developer can see Virtual mode's paint, scroll and eviction behavior working correctly before investing in the real NeedRows/SupplyRows protocol -- there is no other member in this group that demonstrates anything without a host to answer requests.
RecipeAnswer NeedRows for real, including a deliberate decline
VBA
Sub SupplyRealData(grid AsObject)
grid.Clear
grid.AddColumn "Name", 150
grid.AddColumn "Score", 80
grid.DataMode = 1
grid.SetVirtualRowCount 10000' In a real application, a class module (a form's own event handlers, or a' reg-free callback object) would answer NeedRows like this:'' Private Sub lgGrid_NeedRows(ByVal requestId As Long, ByVal generation As Long, _' ByVal firstRow As Long, ByVal lastRow As Long)' Dim n As Long, c As Long' n = lastRow - firstRow + 1' If SomeRowsAreUnavailable Then' m_grid.SupplyRows requestId, generation, firstRow, Empty ' explicit decline' Else' Dim data(0 To n - 1, 0 To 1) As Variant' For r = 0 To n - 1' data(r, 0) = "Row " & (firstRow + r)' data(r, 1) = (firstRow + r) * 10' Next r' m_grid.SupplyRows requestId, generation, firstRow, data ' real answer' End If' End Sub'' Whichever answer arrives, it may be a SHORTER page than what was asked' for (the remainder is simply requested again later), but it must start' at firstRow and never extend past lastRow, or SupplyRows raises an' error and caches nothing.EndSub
SupplyRows may be called synchronously from inside the NeedRows handler, or later, from outside any event, once the host's own data becomes available -- both are legal. Passing Empty or Null is an explicit decline; simply returning from the handler without calling SupplyRows at all is not a decline and leaves the request pending until it times out or scrolls out of view.
RecipeDelegate a sort to the host, and handle a decline correctly
VBA
Sub DelegateSort(grid AsObject)
grid.Clear
grid.AddColumn "Name", 150
grid.DataMode = 1
grid.SetVirtualRowCount 100' A real host handler looks like this:'' Private Sub lgGrid_SortRequested(ByVal colIndex As Long, ByVal direction As Long, _' ByRef handled As Boolean)' SortMyOwnDataSource colIndex, direction ' direction: 0 = Ascending, 1 = Descending' handled = True ' MUST be set, or the call below fails' End Sub'OnErrorResumeNext
grid.Sort 0, 0' ascending on column 0IfErr.Number <> 0Then' No handler set handled = True: SortRequested's default is declined,' Sort raises an error, and the grid's view does not change at all.Debug.Print"Sort was declined: " & Err.Description
EndIfOnErrorGoTo0EndSub
SortRequested and FilterRequested both carry an in/out handled argument that starts False before the handler runs. A handler that performs the operation on its own data and sets handled = True lets whichever sort or filter call triggered the event report success; leaving handled False is a real, deliberate decline that surfaces as an error to whichever call triggered the event.
SetVirtualRowCountVirtual mode: the deferred fetch protocol
Declare the total number of rows the host's dataset has, in Virtual mode.
Tells the grid how many rows exist in total. Scrollbars and row-based navigation span this full declared count, even though the grid never materializes more than a small window of it. Calling this always discards every cached row and every pending or declined request the grid currently holds, whether the new count is larger or smaller than the previous one -- growing and shrinking a declared dataset are the identical code path, so no cached row can ever outlive a count change. Declaring even a very large count (a million rows or more) costs essentially nothing, because Virtual mode tracks row order as a scalar range rather than a per-row array.
SignatureSub SetVirtualRowCount(count As Long)
Parameters
count (Long) - The total number of rows the host's dataset has. Must be >= 0.
ReturnsNone. Raises an error if count is negative, or if called outside Virtual mode.
Available only while DataMode = 1. Calling it in Embedded or Turbo column-store mode raises an error naming SetVirtualRowCount and saying Virtual mode is required -- the same wording every other Virtual-only member in this group uses. It also refuses while called from inside any event handler (including NeedRows, SortRequested and FilterRequested), and while a paint or a load is in progress.
Example
VBA
grid.Clear
grid.AddColumn "Name", 150
grid.DataMode = 1
grid.SetVirtualRowCount 1000000Debug.Print grid.GetRowCount()
' -> 1000000, even though nothing has been supplied for a single row yet' Shrinking discards everything cached so far, exactly like growing does
grid.SetVirtualRowCount 10Debug.Print grid.GetRowCount()
' -> 10
Gotchas
Always discards the cache, every pending request and every previously-declined range, even when the new count is identical in spirit to the old one (for example, calling it twice in a row with the same value still clears everything). There is no cheaper way to just change the count without a full reset.
A negative count raises an error and changes nothing; there is no upper bound check, so an unrealistically large count (more than the host could ever supply) is accepted without complaint.
Cannot be called from inside any event handler, including NeedRows itself -- unlike SupplyRows, which is specifically allowed there.
Columns must already exist before this is useful: AddColumn is refused once DataMode = 1, so the columns a virtual dataset needs have to be added in Embedded mode, before switching DataMode to 1.
Selection is unconditionally cleared on every call; the focused row is left alone unless it would fall outside the new count, in which case it is reset.
See alsoFillVirtualSynthetic, SetVirtualViewportTop, InvalidateAllRows, GetVirtualDiagnostics
GetVirtualDiagnosticsVirtual mode: the deferred fetch protocol
Return a JSON snapshot of the Virtual-mode fetch protocol's internal bookkeeping.
Returns a single flat JSON object describing the current state of the deferred fetch protocol: the generation counter, the declared row count, how many requests are outstanding, how many have been issued and answered, several counters for the different ways a SupplyRows reply can be rejected or ignored, and a cache hit rate. The key names are stable identifiers a test or a monitoring routine can rely on across calls. Unlike every Virtual-only member in this group, GetVirtualDiagnostics is NOT restricted to Virtual mode -- it can be called at any time, in any DataMode, and simply reports whatever Virtual-mode bookkeeping this control instance currently holds.
The JSON object has these keys: generation, virtualRowCount, pendingCount, requestsIssued, suppliesAccepted, declinedRangeCount, staleSupplyCount, lateSupplyCount, duplicateSupplyCount, supersededSupplyCount, unknownRequestCount, rangeViolationCount, shapeViolationCount, tombstoneCount, tombstoneEvictedCount, unhandledRequestCount, cachedRowCount, cacheHitRate, outstandingMax, noSink, exhausted.
Most of these counters reset only when DataMode is set to 1 (Virtual mode is entered) -- they are NOT reset by a generation bump (SetVirtualRowCount, InvalidateAllRows, or a handled sort/filter request), specifically so a test or a host can watch behavior across a sort, a filter, or a requery without the numbers being wiped out from under it. cacheHitRate is the one deliberate exception: it is tracked in the row cache itself and resets on every generation bump, not just on entering Virtual mode, so it always reflects paint lookups since the most recent cache-clearing event rather than since Virtual mode began.
SignatureFunction GetVirtualDiagnostics() As String
ReturnsA JSON string (a flat object) describing the current Virtual-mode fetch state.
Callable in every DataMode -- verified from source, not assumed: the method has no Virtual-mode guard at all. In Embedded and Turbo column-store mode it reports whatever Virtual-mode state this control instance currently holds. Switching DataMode = 1 -> 0 (back to Embedded) clears the cache and every pending request but leaves the counters as they were until Virtual mode is entered again; switching DataMode = 1 -> 2 (straight to Turbo column-store) does not even do that much, so the figures reported in Turbo column-store mode can be exactly what they were the moment Virtual mode was last active. A control that has never entered Virtual mode reports an all-zero, freshly-initialized snapshot in every mode.
Not restricted to Virtual mode -- unlike every other Virtual-specific member in this group, this can be called (and returns real, parseable JSON) in Embedded and Turbo column-store mode too.
Its figures in Embedded/Turbo column-store mode are not necessarily zero: they reflect leftover state from the last time this same control instance was in Virtual mode, because leaving Virtual mode for Turbo column-store mode does not clear the fetch bookkeeping at all, and leaving it for Embedded mode clears the cache and pending requests but not the request/supply counters.
cacheHitRate resets on every generation bump (SetVirtualRowCount, InvalidateAllRows, a handled Sort/SortEx/SetColumnFilter/ClearColumnFilter/ClearAllFilters call) -- every other counter in this object survives a generation bump and resets only when DataMode is set to 1.
Every value comes back as a JSON number or boolean literal (not a quoted string) except generation-adjacent identifiers, which are also numbers -- there is no string-typed field in this object at all.
noSink reflects whether ANY event sink is attached at all (a registered-mode connection or a reg-free event callback) -- with no sink, Virtual mode still paints placeholders, but never issues a single NeedRows request, and noSink stays true to say so plainly rather than looking like a hang.
See alsoSetVirtualRowCount, SupplyRows, InvalidateAllRows, NeedRows
FillVirtualSyntheticVirtual mode: the deferred fetch protocol
Fill every row the grid's last paint reported as missing with built-in placeholder text, with no host wiring at all.
A real, callable method (not an internal test stub) that fills every row the grid's most recent paint pass found missing, using a deterministic built-in generator, then repaints. Each cell's text follows the exact pattern "R{row}C{col}" -- for example row 12, column 3 always becomes "R12C3" -- so the exact text any row must show is predictable and checkable. It needs no NeedRows handler, no SupplyRows call and no host of any kind: it is a genuinely useful way to see Virtual mode's paint, scroll and eviction behavior working immediately, before writing any of the NeedRows/SupplyRows plumbing a real host integration needs. It reports how many rows it filled.
SignatureFunction FillVirtualSynthetic() As Long
ReturnsLong: the number of rows filled. 0 if nothing was missing (nothing has been painted yet, or every visible row is already cached).
Available only while DataMode = 1; refused with the same "Virtual mode required" wording in Embedded and Turbo column-store mode. It also refuses while a paint is in progress and while called from inside any event handler.
Example
VBA
grid.Clear
grid.AddColumn "A", 100
grid.AddColumn "B", 100
grid.DataMode = 1
grid.SetVirtualRowCount 100000' FillVirtualSynthetic only fills rows a real paint has already recorded' as missing -- a single attempt can legitimately return 0 if no paint has' been processed yet, so retry with DoEvents until it succeeds or gives up.Dim filled AsLong, attempts AsLongDo
DoEvents
filled = grid.FillVirtualSynthetic()
attempts = attempts + 1LoopWhile filled = 0And attempts < 20Debug.Print filled > 0' -> True (however many rows the visible viewport shows)If filled > 0ThenDebug.Print grid.GetCellValue(0, 0) ' -> "R0C0"Debug.Print grid.GetCellValue(0, 1) ' -> "R0C1"EndIf
Gotchas
Fills only rows the grid's LAST PAINT reported as missing -- not the whole declared row count. Calling it before the grid has ever painted (for example immediately after SetVirtualRowCount, with no repaint or message pump in between) can fill 0 rows, because nothing has recorded a gap yet.
The generated text is always exactly "R{row}C{col}" for every cell, regardless of column type or header -- there is no way to make it generate anything else.
Filled rows are ordinary cached rows as far as the rest of the grid is concerned: they count toward cachedRowCount in GetVirtualDiagnostics and are evicted the same way a real SupplyRows response would be.
Does not touch the generation counter -- calling it does not invalidate anything and does not cause any pending NeedRows request to be reissued.
See alsoSetVirtualViewportTop, SetVirtualRowCount, SupplyRows, InvalidateAllRows
SetVirtualViewportTopVirtual mode: the deferred fetch protocol
Jump the viewport so the given row is the first row shown, exactly as a scrollbar drag would.
Scrolls the grid so topRow becomes the first visible row -- the same result a user dragging the scrollbar thumb would produce, but callable directly. Useful for jumping to a specific row programmatically, or for deliberately exercising cache and eviction behavior at a row far from wherever the viewport currently sits. A topRow at or beyond the declared row count is clamped to the last valid row (count - 1), or to row 0 if the declared count is 0, rather than refused; the resulting scroll position is then clamped a second time by the ordinary scrollbar logic, so a jump near the very end lands on the last full page rather than scrolling past it.
SignatureSub SetVirtualViewportTop(topRow As Long)
Parameters
topRow (Long) - The row that should become the first row visible. Must be >= 0; a value at or past the declared row count is clamped to the last valid row instead of being refused.
ReturnsNone. Raises an error if topRow is negative, or if called outside Virtual mode.
Available only while DataMode = 1; refused with the same "Virtual mode required" wording in Embedded and Turbo column-store mode.
Example
VBA
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 1000000' FillVirtualSynthetic only fills rows a real paint has already recorded' as missing -- retry with DoEvents until it succeeds or gives up.Dim filled AsLong, attempts AsLong
grid.SetVirtualViewportTop 500000
attempts = 0Do
DoEvents
filled = grid.FillVirtualSynthetic()
attempts = attempts + 1LoopWhile filled = 0And attempts < 20If filled > 0ThenDebug.Print grid.GetCellValue(500000, 0) ' -> "R500000C0"' A jump past the end clamps to the last valid row instead of erroring
grid.SetVirtualViewportTop 999999999
attempts = 0Do
DoEvents
filled = grid.FillVirtualSynthetic()
attempts = attempts + 1LoopWhile filled = 0And attempts < 20If filled > 0ThenDebug.Print grid.GetCellValue(999999, 0) ' -> "R999999C0" (the last row)
Gotchas
A negative topRow is refused with an error; a topRow at or past the declared row count is silently clamped to the last valid row instead -- these two out-of-range cases behave differently.
Unlike most mutating members in this group, this one is NOT rejected when called from inside an event handler -- only a paint in progress blocks it.
Moving the viewport does not by itself fill any rows -- the newly exposed rows still need either a real paint-and-fetch cycle (NeedRows/SupplyRows) or a follow-up FillVirtualSynthetic call before GetCellValue on them returns anything but placeholder text.
There is no companion getter for the current viewport position in this group; ScrollChanged (a general-purpose event, not part of this group) reports the pixel scroll position whenever it changes, including as a result of this call.
See alsoFillVirtualSynthetic, SetVirtualRowCount, NeedRows
SupplyRowsVirtual mode: the deferred fetch protocol
Answer a NeedRows request with real data, or an explicit decline.
The method a host calls, in response to a NeedRows event, to actually hand rows back to the grid. requestId and generation must be copied verbatim from the NeedRows event's own arguments -- they are not chosen by the caller. data is a 2-dimensional Variant array: rows in the first dimension, columns in the second, with exactly as many columns as the grid currently has. Either a 0-based Array() or a 1-based Range.Value array works -- the lower bound of either dimension is never checked, only the number of elements.
The response must start at the requested firstRow and must never extend past the requested range -- a SHORTER, partial page is explicitly legal (whatever rows are left over are simply requested again later); a longer one is refused outright and nothing is cached. Passing Empty or Null as data is an explicit, valid decline for that request. Simply returning from the NeedRows handler without calling SupplyRows at all is NOT the same as declining -- the request stays pending and is retried automatically (see InvalidateAllRows and the modeNotes below for how). Exactly one response is accepted per request: a second SupplyRows call naming a request that has already been answered is accepted at the COM level with no VBA-visible error, but does nothing beyond that -- it is silently counted, not applied. It is legal to call SupplyRows synchronously from inside the NeedRows handler itself, which is the common case.
SignatureSub SupplyRows(requestId As Long, generation As Long, firstRow As Long, data As Variant)
Parameters
requestId (Long) - The request being answered. Must be copied from the NeedRows event that raised it; 0 is always rejected.
generation (Long) - The dataset generation the request was issued under. Must be copied from the same NeedRows event; a stale value (after a sort, filter, requery or SetVirtualRowCount changed the generation) causes the reply to be silently discarded.
firstRow (Long) - The first row this response supplies. Must equal the request's own first row exactly, whether supplying data or declining.
data (Variant) - A 2-dimensional Variant array (rows x columns), with a column count matching the grid exactly, holding the response's row data -- or Empty/Null to explicitly decline this request.
ReturnsNone from VBA's perspective. At the COM level this returns S_OK for an accepted response and S_FALSE (not a VBA-visible error) for a stale, superseded or duplicate one that was silently ignored.
Available only while DataMode = 1; refused with the same "Virtual mode required" wording in Embedded and Turbo column-store mode. Unlike most mutating members in this group, SupplyRows is specifically permitted from inside a NeedRows handler -- but the check only confirms a NeedRows dispatch is currently in progress, not that requestId matches the specific request that triggered it. A host may legally answer a different, still-pending request from inside another request's handler. Called from inside any other kind of event it is rejected the same way any other data change during an event is rejected.
Example
VBA
grid.Clear
grid.AddColumn "Name", 150
grid.DataMode = 1
grid.SetVirtualRowCount 200' A real handler wired via registered mode (registered) or a reg-free callback' answers like this -- shown here as the shape the handler takes:'' Private Sub lgGrid_NeedRows(ByVal requestId As Long, ByVal generation As Long, _' ByVal firstRow As Long, ByVal lastRow As Long)' Dim n As Long, r As Long' n = lastRow - firstRow + 1' Dim data(0 To n - 1, 0 To 0) As Variant' For r = 0 To n - 1' data(r, 0) = "Row " & (firstRow + r)' Next r' m_grid.SupplyRows requestId, generation, firstRow, data ' called synchronously, inside the handler' End Sub'' A decline for a request the host genuinely cannot answer yet:' m_grid.SupplyRows requestId, generation, firstRow, Empty
Gotchas
A second SupplyRows call for a request that was already answered does NOT raise a VBA error -- the underlying COM return code (S_FALSE) is a success code, so On Error Resume Next sees Err.Number = 0. The only way to observe that it happened at all is GetVirtualDiagnostics' duplicateSupplyCount, which increments.
Silence -- returning from the NeedRows handler without calling SupplyRows -- is explicitly NOT a decline. The request stays pending; it ages out either when the viewport scrolls far enough that the request no longer overlaps it, or after a fixed stall of 5000ms (constant) with no reply, whichever comes first, and its rows then become eligible to be requested again on a later tick, as if never asked for.
A response may be SHORTER than the requested range (a partial page) but never longer -- supplying more rows than lastRow - firstRow + 1 asked for is refused outright and nothing is cached, not even the rows that would have fit.
The column count of the data array must match the grid's current column count EXACTLY, or the call is refused; the array's lower bound on either dimension does not matter (0-based and 1-based arrays both work).
A decline (Empty or Null) still requires firstRow to exactly match the request's own first row -- a mismatched firstRow on a decline is refused, and nothing is recorded as declined.
A reply whose generation no longer matches the grid's current generation (because a sort, filter, requery or SetVirtualRowCount happened since the request was issued) is silently discarded, without a VBA-visible error.
requestId 0 is always rejected -- it is reserved and never a real request's id.
SupplyRows may be called at most once per NeedRows dispatch -- for any pending requestId, not necessarily the one currently firing. A second call within the same dispatch, even one naming a different, legitimately unanswered request, is counted the same way as answering an already-completed request twice (duplicateSupplyCount increments; nothing is applied).
See alsoNeedRows, SupplyRequestedRows, GetVirtualDiagnostics, InvalidateAllRows
SupplyRequestedRowsVirtual mode: the deferred fetch protocol
Answer the NeedRows request currently being dispatched, without echoing its tokens back.
The synchronous form of SupplyRows. It takes the data and nothing else: requestId, generation and firstRow come from the grid's own live request context, so they cannot be mis-typed, mis-ordered, or carried across a variable that a later edit reused. Everything after that point is the same code SupplyRows runs -- the same shape checks, the same authorised-range check, the same all-or-nothing rule, the same cache commit, the same diagnostics counters. There is one ingestion path, with two ways in.
It does not change who owns anything. The host still fetches the rows, still declares the row count with SetVirtualRowCount, and still handles SortRequested and FilterRequested against its own dataset. No source object is registered and the grid holds no reference to the host.
SignatureSub SupplyRequestedRows(data As Variant)
Parameters
data (Variant) - a 2-dimensional Variant array of rows x columns, with exactly as many columns as the grid currently has; either bound base works. A shorter (partial) page is legal and the remainder is requested again later. Pass Empty or Null to DECLINE the request explicitly.
Returnsnothing. Inside the handler the response is only staged -- the cache commit happens after the handler returns -- so there is no honest "rows accepted" count to give you at that moment. Use GetVirtualDiagnostics to see what the protocol actually did.
Legal ONLY while a NeedRows handler is on the stack. Called anywhere else -- including from inside a different event -- it is refused with a message naming SupplyRows as the form to use instead. It is not a general-purpose supply method.
It cannot answer a delayed response. If your data arrives after the handler returns, the dispatch it would have answered has already closed; keep requestId/generation/firstRow yourself and call SupplyRows. Asynchronous fulfilment is fully supported -- just not through this member.
Returning from the handler without calling either supply method is NOT a decline. The request stays pending and is retried; only Empty/Null declines.
One request accepts exactly one response. A second call in the same dispatch changes nothing and raises no error; it is visible only as duplicateSupplyCount in GetVirtualDiagnostics.
Every shape, column-count and range rule SupplyRows documents applies here identically, because it is the same code.
Must be called on the thread that created the control, like every other Virtual-mode member.
See alsoSupplyRows, NeedRows, SetVirtualRowCount, GetVirtualDiagnostics, SortRequested, FilterRequested
InvalidateAllRowsVirtual mode: the deferred fetch protocol
Tell the grid the host's entire dataset changed, and re-fetch the current viewport.
The one invalidation entry point Virtual mode offers. There is no way to invalidate a single row or a range of rows -- a change to one cell in the host's data still requires a full InvalidateAllRows() call, exactly the same call a full requery would use. It bumps the internal generation counter and discards the cache, every pending request and every previously-declined range together, as one atomic step, then re-fetches whatever the current viewport needs. Any reply that arrives afterward for a request issued before the call is recognized as stale by its generation and discarded rather than applied.
SignatureSub InvalidateAllRows()
ReturnsNone. Raises an error if called outside Virtual mode.
Available only while DataMode = 1; refused with the same "Virtual mode required" wording in Embedded and Turbo column-store mode. It is also rejected from inside ANY event handler, including NeedRows -- unlike SupplyRows, there is no exception carved out for it.
Example
VBA
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 1000
grid.FillVirtualSynthetic
' The host's data changed underneath the grid -- there is no way to say' "just row 42 changed"; the whole visible viewport is invalidated and' re-fetched instead
grid.InvalidateAllRows
OnErrorResumeNextDebug.Print grid.GetCellValue(0, 0)
Debug.PrintErr.Description
OnErrorGoTo0' -> raises an error: row 0 is no longer resident. The earlier synthetic' fill was discarded by the invalidation, and nothing has supplied it' again yet -- Virtual mode never returns an empty string for a row it' does not actually hold, by design.
Gotchas
This is the ONLY invalidation entry point in Virtual mode -- there is no row- or range-scoped equivalent. A host that knows exactly which one row changed still has to call InvalidateAllRows() and let the whole visible viewport be re-fetched.
Cannot be called from inside any event handler at all, including NeedRows -- calling it from a NeedRows handler while answering a different request is rejected, unlike SupplyRows.
Posts a fetch tick unconditionally, regardless of whether any paint has happened yet -- this makes it a reliable way to trigger a fresh NeedRows request even before the grid has ever been painted.
A request that was in flight when this is called is not cancelled at the host (the grid has no way to cancel a call it does not own) -- if the host answers it later anyway, that answer is recognized as stale by its generation and discarded, not applied.
See alsoSupplyRows, NeedRows, SetVirtualRowCount, GetVirtualDiagnostics
SetPrefetchMarginVirtual mode: the deferred fetch protocol
Widen the row range Virtual mode requests from the host on each side of the visible viewport.
Sets how many extra rows, beyond the visible viewport, Virtual mode actually asks the host for on each side, for smoother scrolling. It widens only what gets REQUESTED -- it never changes what paint itself reads, and it never causes a request for a row outside the declared row count (the request is clamped to [0, count) the same way any other request is). This is the documented way to make the grid request larger pages at once; there is no separate page-size setting. Unlike every Virtual-only member in this group, this is NOT restricted to Virtual mode: it can be set in any DataMode, though it has no visible effect until DataMode = 1, because prefetching itself only happens there.
SignatureSub SetPrefetchMargin(rows As Long)
Parameters
rows (Long) - How many extra rows to request on each side of the viewport. Must be >= 0.
ReturnsNone. Raises an error only if rows is negative.
Not gated to any mode -- verified from source, not assumed: it can be called successfully in Embedded and Turbo column-store mode as well as Virtual mode, and simply stores the value every time. The only mode-dependent part of its behavior is that setting it in Virtual mode also nudges a fresh fetch tick, so a larger margin can take effect on the next request cycle immediately; setting it in Embedded or Turbo column-store mode does that nudge too, but since neither of those modes ever issues a fetch request, nothing observable happens beyond the value being stored.
Example
VBA
grid.SetPrefetchMargin 50Debug.Print grid.PrefetchMargin
' -> 50, even called here in Embedded mode, before any Virtual-mode setup
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 100000' From here on, every NeedRows request the grid issues covers roughly 50' extra rows on each side of whatever is actually visible on screen
Gotchas
A negative value is refused with an error; there is no upper bound at all -- an unrealistically large margin is accepted without complaint and is simply clamped down to the declared row count whenever an actual request is computed.
Settable in every DataMode, but only has any observable effect in Virtual mode -- setting it in Embedded or Turbo column-store mode succeeds and changes what PrefetchMargin reads back, but nothing else in the grid reacts to it.
Widens only the REQUEST range -- it never changes which rows paint itself reads or displays, and a host may never supply rows outside what its own request actually asked for, so raising this value cannot be used to receive "extra" rows a request did not cover.
Cannot be called from inside any event handler, including NeedRows -- it raises the same 'cannot modify the grid from inside an event handler' error most other mutators in this group raise. Having no DataMode gate only means it works in every mode; it is not unrestricted everywhere.
See alsoPrefetchMargin, NeedRows, SupplyRows
PrefetchMarginVirtual mode: the deferred fetch protocol
The current prefetch margin set by SetPrefetchMargin.
Returns the value most recently set by SetPrefetchMargin. There is no separate PrefetchMargin setter property -- SetPrefetchMargin is the only way to change it. Like SetPrefetchMargin, this getter is not restricted to any DataMode.
Signaturegrid.PrefetchMargin As Long (get)
ReturnsLong: the current prefetch margin. 0 by default.
Not gated to any mode -- callable and accurate in Embedded, Virtual and Turbo column-store mode alike, confirmed from source (no mode check of any kind in the getter).
Example
VBA
Debug.Print grid.PrefetchMargin
' -> 0 (the default, before SetPrefetchMargin has ever been called)
grid.SetPrefetchMargin 100Debug.Print grid.PrefetchMargin
' -> 100
Gotchas
Get-only: there is no PrefetchMargin = value property-set form; SetPrefetchMargin is the only setter.
Reads back whatever was last stored, even in a mode where the value has no observable effect (Embedded or Turbo column-store) -- it is not somehow reset or hidden outside Virtual mode.
See alsoSetPrefetchMargin
NeedRowsVirtual mode: the deferred fetch protocol
Fires when Virtual mode needs rows it does not currently have cached.
Raised when the deferred fetch protocol determines a range of rows is needed but not resident, not already pending, and not previously declined. It is never raised synchronously from inside paint -- a paint that finds a row missing only records the gap and posts one deferred message; NeedRows fires later, when that message is processed on the next message pump, after the call that triggered the paint has already returned. An explicit call to InvalidateAllRows also posts that same deferred message, unconditionally, which is a reliable way to trigger a NeedRows cycle even before any paint has happened. The event carries the request's identity (requestId, generation) and the row range being asked for (firstRow, lastRow inclusive); the handler answers by calling SupplyRows with that same requestId and generation, either synchronously inside the handler or later, from outside any event. With no event sink attached at all -- neither a registered-mode connection nor a reg-free event callback -- Virtual mode still paints placeholders correctly, but this event never fires, because there would be nobody to answer it; that state is reported by GetVirtualDiagnostics' noSink field rather than treated as an error.
SignatureEvent NeedRows(requestId As Long, generation As Long, firstRow As Long, lastRow As Long)
Parameters
requestId (Long) - This request's identity. Never 0 (0 is reserved). Must be copied verbatim into the matching SupplyRows call.
generation (Long) - The dataset generation this request was issued under. Must be copied verbatim into the matching SupplyRows call.
firstRow (Long) - The first row being requested.
lastRow (Long) - The last row being requested, inclusive. A single request never exceeds 200 rows -- a wider gap is split into multiple requests.
Only ever raised while DataMode = 1 -- the deferred fetch tick that raises it returns immediately without doing anything if the grid is not in Virtual mode. There is no equivalent event or notification for missing data in Embedded or Turbo column-store mode.
Example
VBA
' A real handler is wired at the module level, not inline in a Sub -- shown' here as the shape the form's own handler takes (control named lgGrid):'' Private Sub lgGrid_NeedRows(ByVal requestId As Long, ByVal generation As Long, _' ByVal firstRow As Long, ByVal lastRow As Long)' Dim n As Long, r As Long' n = lastRow - firstRow + 1' Dim data(0 To n - 1, 0 To 0) As Variant' For r = 0 To n - 1' data(r, 0) = "Row " & (firstRow + r)' Next r' m_grid.SupplyRows requestId, generation, firstRow, data' End Sub'' Triggering it only needs the ordinary Virtual-mode setup, followed by' letting a message pump run so the deferred tick gets processed:
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 1000
grid.InvalidateAllRows ' reliably queues a fetch tick
DoEvents ' NeedRows fires during this pump, not before it
Gotchas
Never fires synchronously from the call that made rows visible (SetVirtualRowCount, SetVirtualViewportTop, a real scroll, InvalidateAllRows) -- it fires later, once the deferred message it posted is processed on the next message pump. Code that expects it immediately after one of those calls, with no DoEvents/message pump in between, will not see it yet.
At most 3 requests are outstanding at once, and each one covers at most 200 rows -- a large gap is split into multiple NeedRows firings rather than one giant range.
With no event sink attached (no registered-mode connection, no reg-free event callback), this simply never fires -- Virtual mode still paints placeholders and does not treat this as an error or a hang; GetVirtualDiagnostics' noSink field is the only way to confirm this state from code.
A request that is not answered at all (no SupplyRows call, not even a decline) is neither an error nor treated as declined -- it stays pending and is retried automatically once it ages out (by scrolling out of view or by a fixed stall), which can produce a second NeedRows firing for overlapping rows later.
See alsoSupplyRows, InvalidateAllRows, GetVirtualDiagnostics, SetPrefetchMargin
SortRequestedVirtual mode: the deferred fetch protocol
Fires when a sort is requested in Virtual mode, since the grid has no data of its own to sort.
Raised whenever Sort, SortEx, ClearSort or SortMulti is called while DataMode = 1, because Virtual mode has no dataset of its own to reorder -- sorting has to be delegated to whatever owns the real data. The handled argument is passed by reference and starts False before the handler runs; the handler must perform the sort itself, against the host's own data source, and set handled = True for the triggering Sort/SortEx call to report success. Leaving handled False is a real, deliberate decline: the triggering call then raises an error, and nothing about the grid's own state (sort column, generation, view) changes. direction's value is passed through unvalidated from whichever entry point triggered the request: calling Sort(colIndex, sortOrder) forwards sortOrder directly (0 = Ascending, 1 = Descending, by convention, but not checked); calling SortEx(colIndex, sortMode) with sortMode 0 or 1 behaves the same way, while sortMode 2, 3 or 4 (case-insensitive ascending/descending, or a natural sort) is forwarded to direction as-is, so direction is not always the simple binary value Sort's own parameter implies.
SignatureEvent SortRequested(colIndex As Long, direction As Long, handled As Boolean)
Parameters
colIndex (Long) - The column being sorted. -1 signals a clear-sort request (from ClearSort in Virtual mode).
direction (Long) - The direction/mode requested, forwarded unvalidated from whichever call triggered the event: 0/1 (Ascending/Descending) from Sort or from SortEx modes 0-1, or 2-4 (case-insensitive ascending/descending, or natural) forwarded as-is from SortEx.
handled (Boolean (ByRef)) - Starts False. The handler must set it True after performing the sort itself, or the triggering call reports a decline.
Only ever raised while DataMode = 1. In Embedded and Turbo column-store mode, Sort and SortEx perform the sort directly against the grid's own data and fire BeforeSort/AfterSort instead -- SortRequested is never involved outside Virtual mode.
Example
VBA
' A real handler is wired at module scope, for example:'' Private Sub lgGrid_SortRequested(ByVal colIndex As Long, ByVal direction As Long, _' ByRef handled As Boolean)' If colIndex = -1 Then' ClearMyOwnSort' Else' SortMyOwnDataSource colIndex, direction' End If' handled = True' End Sub'' Triggering it in Virtual mode:
grid.Clear
grid.AddColumn "A", 100
grid.DataMode = 1
grid.SetVirtualRowCount 100
grid.Sort 0, 0' fires SortRequested(0, 0, handled) and waits for handled = True
Gotchas
handled starts False before every firing -- a handler that does not explicitly set it True is a real decline, not a no-op default-accept.
direction is NOT always the simple 0/1 value Sort's own parameter suggests: SortEx's modes 2-4 (case-insensitive, natural) are forwarded through unchanged, so a handler written only for 0/1 will silently mis-handle a SortEx call using those modes.
colIndex can be -1, meaning "clear the current sort" (from ClearSort), not an actual column -- a handler that indexes into a column array without checking for -1 first will fail on that call.
Declining (leaving handled False) raises an error on the CALLER's side (Sort/SortEx), not just a silent no-op -- code that calls Sort in Virtual mode must be prepared to catch that error.
SortMulti (up to three sort levels in one call) only reports its FIRST level through this event -- the host learns nothing about the second or third sort level from SortRequested's own arguments.
See alsoFilterRequested, SupplyRows, GetVirtualDiagnostics
FilterRequestedVirtual mode: the deferred fetch protocol
Fires when a filter is requested in Virtual mode, since the grid has no data of its own to filter.
Raised whenever SetColumnFilter, ClearColumnFilter, ClearAllFilters, SetColumnNumericFilter, SetColumnNumericFilterRange, SetColumnValueListFilter, SetFilterState or LoadFilterPreset is called while DataMode = 1, because Virtual mode has no dataset of its own to filter -- filtering has to be delegated to whatever owns the real data. The handled argument is passed by reference and starts False before the handler runs; the handler must apply (or clear) the filter itself, against the host's own data source, and set handled = True for the triggering call to report success. Leaving handled False is a real, deliberate decline: the triggering call raises an error, and nothing about the grid's own filter state changes. mode's value depends entirely on which entry point triggered the event, and is not always a SetColumnFilter mode number: a direct SetColumnFilter call forwards its own text/negation/blank/date mode number (0-4, 11-14, 20-26) unchanged; SetColumnNumericFilter forwards a numeric comparison mode (5, 6, 7, 8 or 10) that SetColumnFilter itself would actually refuse if passed to it directly, since numeric comparison filtering is a separate entry point with its own validation; SetColumnNumericFilterRange always forwards mode 9 (between); SetColumnValueListFilter forwards a dedicated value-list mode, reachable directly through that method call and not only through the built-in filter dropdown UI; SetFilterState and LoadFilterPreset (which delegates its whole body to SetFilterState) forward a dedicated whole-state-overwrite mode, with colIndex = -1 meaning "the whole filter state is being replaced," not "every column"; and ClearColumnFilter/ClearAllFilters forward a dedicated clear mode, with colIndex = -1 from ClearAllFilters specifically meaning "every column." colIndex = -1 therefore has two genuinely different meanings depending on which mode value accompanies it -- a handler must not assume -1 always means "clear every column."
SignatureEvent FilterRequested(colIndex As Long, mode As Long, pattern As String, handled As Boolean)
Parameters
colIndex (Long) - The column being filtered. -1 means either every column (from ClearAllFilters) or, when mode is -2, that the whole filter state is being replaced (from SetFilterState/LoadFilterPreset) rather than any specific column -- check mode to tell these apart.
mode (Long) - The filter mode, mirroring SetColumnFilter's own numbering when triggered from SetColumnFilter directly; -1 signals a clear request (from ClearColumnFilter or ClearAllFilters).
pattern (String) - The filter text, exactly as passed to SetColumnFilter. Empty for a clear request.
handled (Boolean (ByRef)) - Starts False. The handler must set it True after applying or clearing the filter itself, or the triggering call reports a decline.
ReturnsNone (event).
Value
Meaning
-2
The whole filter state is being replaced (from SetFilterState or LoadFilterPreset). colIndex is -1 here, but it means "whole-state overwrite," not "every column."
-1
Clear this filter (from ClearColumnFilter) or every filter (from ClearAllFilters, with colIndex also -1, meaning every column here)
0-4, 11-14, 20-26
SetColumnFilter's own text/negation-blank/date mode values, forwarded unchanged when triggered directly from SetColumnFilter
5, 6, 7, 8, 10
Numeric comparison modes (greater-than, less-than, greater-or-equal, less-or-equal, not-equal) from SetColumnNumericFilter -- SetColumnFilter itself refuses these same values if passed to it directly
9
Numeric between, from SetColumnNumericFilterRange
-10
Value-list/checkbox filter, from SetColumnValueListFilter -- also reachable through the built-in filter dropdown UI, but not only from there
Only ever raised while DataMode = 1. In Embedded and Turbo column-store mode, SetColumnFilter/ClearColumnFilter/ClearAllFilters apply directly against the grid's own data -- FilterRequested is never involved outside Virtual mode.
Example
VBA
' A real handler is wired at module scope, for example:'' Private Sub lgGrid_FilterRequested(ByVal colIndex As Long, ByVal mode As Long, _' ByVal pattern As String, ByRef handled As Boolean)' If mode = -1 Then' ClearMyOwnFilter colIndex ' colIndex = -1 here means "clear everything"' Else' ApplyMyOwnFilter colIndex, mode, pattern' End If' handled = True' End Sub'' Triggering it in Virtual mode:
grid.Clear
grid.AddColumn "Name", 150
grid.DataMode = 1
grid.SetVirtualRowCount 100
grid.SetColumnFilter 0, 0, "Smith"' 0 = Contains; fires FilterRequested(0, 0, "Smith", handled)
Gotchas
handled starts False before every firing -- a handler that does not explicitly set it True is a real decline, not a no-op default-accept.
mode is not always a SetColumnFilter mode number: -1 means clear, and appears both from ClearColumnFilter (a single column) and ClearAllFilters (colIndex also -1, meaning every column) -- a handler needs to check colIndex to tell those two apart, since mode alone does not.
Declining (leaving handled False) raises an error on the CALLER's side (SetColumnFilter/ClearColumnFilter/ClearAllFilters), not just a silent no-op.
mode is not always one of SetColumnFilter's own values -- SetColumnNumericFilter, SetColumnNumericFilterRange and SetColumnValueListFilter are all separate, real, directly-callable COM methods that route through this same event with their own mode numbers (some of which SetColumnFilter itself would refuse if called directly), and the built-in filter dropdown UI reaches the value-list mode the same way SetColumnValueListFilter does, not through a UI-only special case.
colIndex = -1 has two different meanings depending on mode: with mode -1 it means "clear every column" (from ClearAllFilters); with mode -2 it means "the whole filter state is being replaced" (from SetFilterState/LoadFilterPreset), not any statement about which columns are affected. Check mode before interpreting colIndex = -1.
See alsoSortRequested, SupplyRows, GetVirtualDiagnostics
Events and the event callback bridge
LiteGrid Turbo raises 36 events in total. The 30 general-purpose ones covering clicks, editing, sorting, filtering, paste, paint and more are documented here, together with RowsInserted and RowsDeleted; 3 further Virtual-mode-specific ones (NeedRows, SortRequested, FilterRequested) are documented in a separate group, and TreeExpanding is documented with the tree view. Every event reaches a host that placed the control on a form the ordinary way, as a registered-mode handler (Private Sub lgGrid_<EventName>(...) in the form module) -- this is registered-mode, connection-point delivery, and it always works, for every event, with no setup at all.
A host that created the control reg-free (no type library registered, so connection-point events are not available) uses the event callback bridge instead: SetEventCallback attaches a plain object exposing one method, OnGridEvent(gridId, eventId, a1..a6, ByRef Cancel), and every bridged event arrives there instead, identified by its numeric eventId (the same ids used throughout this group and in the IDL). SetEventCallbackMacro offers a second bridge route for hosts that would rather point at a macro name than an object. Both routes can be attached at the same time, and each fires independently.
The bridge is opt-in per event beyond a tier default: most events are delivered by default, a handful of high-frequency ones (SelectionChanged, ScrollChanged, CellHover, CellMouseEnter, CellMouseLeave, LoadProgress) are off by default because a callback boundary is expensive to cross on every mouse move or scroll tick, and SetEventCallbackEnabled/GetEventCallbackEnabled control that per event id. This on/off switch governs the callback bridge ONLY -- a registered-mode subscriber always receives every event regardless of what the bridge is enabled for. One event, BeforeCellPaint, can never be delivered to the bridge at all: it hands the handler a live GDI device context that is only valid for the duration of one paint call, which cannot survive a late-bound cross-process-shaped callback boundary, so custom cell painting requires registered mode.
Most of this group's members work the same way in Embedded mode and Turbo column-store mode -- exceptions are named in their own entries (CheckboxToggle, BeforePaste and AfterPaste are Embedded-only; BeforeSort/AfterSort do not fire in Turbo column-store mode at all, unlike the rest of this group). A handful of the editing- and paste-related events also do not fire in Virtual mode because the operations that raise them (interactive cell editing, clipboard paste) are themselves unavailable there -- Virtual mode has its own, separately documented events (NeedRows, SortRequested, FilterRequested) for the operations it delegates to the host instead.
RecipeWire a reg-free host to the same events a registered form gets
VBA
Sub WireRegFreeEvents(grid AsObject, callbackObject AsObject)
' callbackObject is any object (a class module instance is typical) exposing:'' Public Sub OnGridEvent(ByVal gridId As Long, ByVal eventId As Long, _' a1 As Variant, a2 As Variant, a3 As Variant, _' a4 As Variant, a5 As Variant, a6 As Variant, _' ByRef Cancel As Boolean)' Select Case eventId' Case 2 ' ItemClick' Debug.Print "Clicked row " & a1 & ", col " & a2' Case 7 ' BeforeSort' If SomeCondition Then Cancel = True' End Select' End Sub
grid.SetEventCallback callbackObject, 1' gridId = 1, echoed back on every callback' Tier-B events are off by default for the bridge -- opt in explicitly
grid.SetEventCallbackEnabled 4, True' SelectionChanged
grid.SetEventCallbackEnabled 13, True' ScrollChangedEndSub
One callback object can serve any number of grids: gridId is whatever the host passed to SetEventCallback for that grid, echoed back verbatim on every OnGridEvent call, so a single Select Case on eventId (and, if needed, gridId) is enough to route every event from every grid the host owns.
RecipeDiagnose a callback bridge that seems to have gone quiet
VBA
Sub DiagnoseBridge(grid AsObject)
Dim json AsString
json = grid.GetEventCallbackInfo()
Debug.Print json
' {"direct":{"attached":true,"gridId":1,"method":"OnGridEvent",' "invokeCount":42,"failureCount":1},' "macro":{"attached":false,"gridId":0,"macroName":"",' "invokeCount":0,"failureCount":0},' "last":{"eventId":7,"route":"direct","hresult":"0x80020009",' "error":"..."},"ownerThreadId":1234,"disabledEvents":[4,13,22,23,24,26,28]}' A callback object that throws, or a macro that does not exist, never' raises a VBA error back on the call that triggered the event (a click,' a Sort call, ...) -- GetEventCallbackInfo's own "last" block and the' per-route failureCount are the only way to see that a handler is broken.EndSub
A broken callback handler (wrong signature, an unhandled error inside it, a macro name that does not exist) never surfaces as an error on the code that triggered the underlying event -- the bridge dispatch catches every failure itself so one bad handler can never break the grid. GetEventCallbackInfo is the only way to see that something failed.
SetEventCallbackEvents and the event callback bridge
Attach a reg-free callback object that receives every enabled event through one method.
The reg-free equivalent of a registered form's own event handlers: attaches callbackObject as the direct callback route, and every event enabled for the callback bridge is delivered from then on as a call to callbackObject.OnGridEvent(gridId, eventId, a1, a2, a3, a4, a5, a6, ByRef Cancel). gridId is not interpreted or validated in any way -- it is stored verbatim and echoed back on every callback exactly as passed, purely so one callback object can tell several grids apart. The call is transactional: callbackObject's IDispatch and its OnGridEvent method are both verified before anything is committed, so a call that fails (Nothing, no IDispatch, no OnGridEvent) leaves a previously working callback attached and untouched. A successful attach resets this route's own invoke/failure counters (visible through GetEventCallbackInfo) but does not touch the macro route (SetEventCallbackMacro) or any per-event enablement set with SetEventCallbackEnabled -- both routes can be attached at the same time, and each is dispatched independently for every enabled event. It also resets GetEventCallbackInfo's shared last-failure block (last.eventId/route/hresult/error), even if that block was currently reporting a failure from the OTHER (macro) route -- attaching a direct callback silently clears evidence of a prior macro-route failure, without the macro route itself being affected at all.
SignatureSub SetEventCallback(callbackObject As Object, gridId As Long)
Parameters
callbackObject (Object) - An object exposing Public Sub OnGridEvent(gridId As Long, eventId As Long, a1 As Variant, a2 As Variant, a3 As Variant, a4 As Variant, a5 As Variant, a6 As Variant, ByRef Cancel As Boolean). Cannot be Nothing.
gridId (Long) - Any value the host chooses. Echoed back verbatim as OnGridEvent's first argument on every callback; not otherwise interpreted.
ReturnsNone. Raises an error if callbackObject is Nothing, does not support IDispatch, or does not expose a method named OnGridEvent.
Not mode-dependent -- attaching a callback is control-level configuration, unrelated to DataMode. Must be called on the same thread that created the control; calling it from any other thread raises an error naming the requirement.
Example
VBA
' callbackHandler is a class module instance exposing:' Public Sub OnGridEvent(ByVal gridId As Long, ByVal eventId As Long, _' a1 As Variant, a2 As Variant, a3 As Variant, _' a4 As Variant, a5 As Variant, a6 As Variant, _' ByRef Cancel As Boolean)' If eventId = 2 Then Debug.Print "ItemClick row=" & a1 & " col=" & a2' End SubDim callbackHandler AsNew clsEventCallbackDemo ' a minimal class implementing OnGridEvent -- in real code, your own class module implementing the same shape shown above
grid.SetEventCallback callbackHandler, 7' every subsequent enabled event now calls callbackHandler.OnGridEvent(7, eventId, ...)
Gotchas
OnGridEvent's existence is checked immediately, at Set time (via IDispatch::GetIDsOfNames) -- a callback object missing the method is refused right away, unlike SetEventCallbackMacro, whose macro name is not checked until the first event actually tries to use it.
A callback object that throws, has the wrong parameter count, or otherwise fails on a real event never raises a VBA error back on the call that triggered the event (a click, a Sort call, ...) -- the failure is caught internally and only visible through GetEventCallbackInfo's per-route failureCount and its 'last' block.
Attaching a new callback object replaces the previous direct-route object outright; it does not touch the macro route or any SetEventCallbackEnabled overrides, which both survive unchanged.
BeforeCellPaint (event id 26) can never reach this callback no matter what SetEventCallbackEnabled is asked to do -- it is registered-mode only, because it carries a device context that is only valid inside one paint call.
The Tier-B events (SelectionChanged, ScrollChanged, CellHover, CellMouseEnter, CellMouseLeave, LoadProgress) are OFF by default for this callback even after a successful attach -- call SetEventCallbackEnabled for each one the host actually wants.
See alsoClearEventCallback, ClearEventCallbackObject, SetEventCallbackMacro, SetEventCallbackEnabled, GetEventCallbackInfo
ClearEventCallbackEvents and the event callback bridge
Detach both the direct-object and macro callback routes at once.
Detaches everything the callback bridge currently has attached -- the direct object route (SetEventCallback) and the macro route (SetEventCallbackMacro) together -- and resets both routes' invoke/failure counters. It never touches per-event enablement set with SetEventCallbackEnabled: those overrides are control configuration, not callback state, and survive a clear the same way they survive a brand-new attach.
SignatureSub ClearEventCallback()
ReturnsNone. Never raises an error, even with nothing attached.
Dim callbackHandler AsNew clsEventCallbackDemo ' a minimal class implementing OnGridEvent -- in real code, your own class module implementing the same shape shown above
grid.SetEventCallback callbackHandler, 1' ... later, tearing down ...
grid.ClearEventCallback
Debug.Print InStr(grid.GetEventCallbackInfo(), """attached"":false") > 0' -> True for both the direct and macro blocks
Gotchas
Clears BOTH routes -- use ClearEventCallbackObject or ClearEventCallbackMacro instead to detach only one of them.
Idempotent: calling it with nothing attached, or calling it twice in a row, raises no error either time.
Does not reset SetEventCallbackEnabled overrides -- an event a host previously enabled or disabled for the bridge keeps that setting for the next callback attached, if any.
See alsoClearEventCallbackObject, ClearEventCallbackMacro, SetEventCallback, SetEventCallbackMacro
ClearEventCallbackObjectEvents and the event callback bridge
Detach only the direct-object callback route.
Detaches the direct callback object attached with SetEventCallback, and resets that route's own invoke/failure counters. Unlike ClearEventCallback, this leaves the macro route (SetEventCallbackMacro) exactly as it was -- if a macro callback is also attached, it keeps receiving every enabled event after this call. Per-event enablement (SetEventCallbackEnabled) is untouched either way.
SignatureSub ClearEventCallbackObject()
ReturnsNone. Never raises an error, even with no direct callback attached.
Dim callbackHandler AsNew clsEventCallbackDemo ' a minimal class implementing OnGridEvent -- in real code, your own class module implementing the same shape shown above
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackMacro Application, "Module1.OnGridEvent", 1
grid.ClearEventCallbackObject ' the macro route above keeps workingDebug.Print InStr(grid.GetEventCallbackInfo(), """direct"":{""attached"":false") > 0' -> TrueDebug.Print InStr(grid.GetEventCallbackInfo(), """macro"":{""attached"":true") > 0' -> True, unaffected
Gotchas
Detaches ONLY the direct-object route -- a macro callback attached with SetEventCallbackMacro is left running. Use ClearEventCallback to detach both at once.
Idempotent: safe to call with nothing attached, or more than once in a row.
Does not touch SetEventCallbackEnabled overrides.
See alsoClearEventCallback, ClearEventCallbackMacro, SetEventCallback
GetEventCallbackInfoEvents and the event callback bridge
Return a JSON snapshot of the callback bridge's current state and counters.
Returns a single JSON object describing both callback routes: whether each is attached, its gridId, its own invoke and failure counts, plus a shared 'last' block (the most recent dispatch attempt on EITHER route -- eventId, which route it was, its HRESULT as a hex string, and its error text if it failed), the owning thread id, and the list of event ids currently not delivered to the bridge. The exact keys are: direct.attached, direct.gridId, direct.method (always the literal string "OnGridEvent"), direct.invokeCount, direct.failureCount, macro.attached, macro.gridId, macro.macroName, macro.invokeCount, macro.failureCount, last.eventId, last.route, last.hresult, last.error, ownerThreadId, disabledEvents (an array of event ids). This never fails because of a broken handler -- a callback object that throws, or a macro that does not exist, shows up here (failureCount incremented, last.error populated) rather than as a VBA error anywhere else.
SignatureFunction GetEventCallbackInfo() As String
ReturnsA JSON string (a single flat-ish object, described above).
Not mode-dependent -- purely reports callback-bridge bookkeeping, unrelated to DataMode.
Example
VBA
Dim json AsString
json = grid.GetEventCallbackInfo()
Debug.Print InStr(json, """direct"":{""attached"":false") > 0' -> True before anything is attachedDebug.Print InStr(json, """ownerThreadId"":") > 0' -> True, always presentDebug.Print InStr(json, "26") > 0' -> True: BeforeCellPaint (26) is always in disabledEvents
Gotchas
last.error is NOT cleared by a subsequent successful dispatch -- once a failure has been recorded, last.error keeps showing that failure's text until another dispatch attempt (success or failure) updates last.eventId/last.route/last.hresult again; a successful call does not blank it out on its own.
The 'last' block is shared between the two routes -- it reflects whichever route (direct or macro) most recently attempted a dispatch, not one 'last' per route. Only invokeCount/failureCount are tracked separately per route.
disabledEvents always includes 26 (BeforeCellPaint), because GetEventCallbackEnabled(26) reports False by design -- this keeps the two surfaces consistent rather than needing a special case.
macroName and the error text are each truncated (200 and 400 characters respectively) before JSON-escaping, so an unusually long macro name or error message is shortened, not corrupted, in the returned JSON.
A callback object or macro that has never been invoked (or was invoked but never failed) leaves last.error as an empty string, not absent -- the key is always present.
See alsoSetEventCallback, SetEventCallbackMacro, SetEventCallbackEnabled, GetEventCallbackEnabled
SetEventCallbackEnabledEvents and the event callback bridge
Turn delivery of one event to the callback bridge on or off.
Controls whether a given event id is delivered to whichever callback route (direct object, macro, or both) is currently attached. This is purely a callback-bridge setting: it has no effect at all on registered-mode (connection-point) delivery, which always receives every event regardless of what is enabled or disabled here. Most events are delivered by default; SelectionChanged, ScrollChanged, CellHover, CellMouseEnter, CellMouseLeave and LoadProgress are off by default because they fire at high frequency (every mouse move, every scroll tick) -- delivering every one of those to a callback would be needlessly noisy for a host that does not need per-move granularity; call this to opt in. The setting is control-level configuration, not callback-instance state: it survives ClearEventCallback, attaching a different callback object, and even the control's window being destroyed and recreated by its container.
SignatureSub SetEventCallbackEnabled(eventId As Long, enabled As Boolean)
Parameters
eventId (Long) - The numeric event id (the same ids used throughout this group, and passed as OnGridEvent's own eventId argument). Valid ids are the bridgeable event ids — every id in this group except 26 (BeforeCellPaint), which is deliberately not deliverable through the callback bridge. An unrecognized id raises an error that lists the exact ids this build accepts, so the message stays correct as events are added; do not hardcode the range in your own code.
enabled (Boolean) - True to deliver this event to the callback bridge, False to stop delivering it.
ReturnsNone. Raises an error for an unrecognized eventId, and for enabled:=True on event id 26 (BeforeCellPaint).
OFF by default for the callback bridge -- high-frequency events; call this to opt in.
BeforeCellPaint (26)
Can never be enabled -- setting False succeeds as a no-op (it is already the effective state), setting True always raises an error. Registered mode only.
every other event id (1-36 except 4, 13, 22, 23, 24, 26, 28)
Dim callbackHandler AsNew clsEventCallbackDemo ' a minimal class implementing OnGridEvent -- in real code, your own class module implementing the same shape shown above
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 4, True' opt in to SelectionChanged for the bridgeDebug.Print grid.GetEventCallbackEnabled(4) ' -> TrueOnErrorResumeNext
grid.SetEventCallbackEnabled 26, True' BeforeCellPaintDebug.PrintErr.Description
' -> "Event 26 (BeforeCellPaint) cannot be delivered to an event callback..."OnErrorGoTo0
Gotchas
Never affects registered-mode delivery -- a registered-mode host that hosts the control on a form receives every event regardless of what this method is asked to do; it exists purely for the callback bridge.
Event id 26 (BeforeCellPaint) can never be enabled: it carries a live GDI device context valid only inside one paint call, which cannot cross a late-bound callback boundary. Passing False for it succeeds (it is already effectively disabled); passing True raises an error explaining why.
Valid event ids are 1 through 33, including NeedRows (31), SortRequested (32) and FilterRequested (33), which belong to a different, Virtual-mode-specific documented group but share the same id space and the same enable/disable mechanism.
The override this stores is per control INSTANCE, not per callback object -- detaching and reattaching a callback (or switching from the direct route to the macro route) does not reset it back to the tier default.
See alsoGetEventCallbackEnabled, SetEventCallback, SetEventCallbackMacro
GetEventCallbackEnabledEvents and the event callback bridge
Query whether an event is currently delivered to the callback bridge.
Returns the effective callback-bridge delivery state for one event id: the value most recently set with SetEventCallbackEnabled if any, otherwise the tier default (most events default on; SelectionChanged, ScrollChanged, CellHover, CellMouseEnter, CellMouseLeave and LoadProgress default off). This reflects the bridge only -- registered-mode delivery is unconditional and this method says nothing about it. Querying event id 26 (BeforeCellPaint) always returns False; unlike SetEventCallbackEnabled(26, True), which raises an error, a query never fails just for asking about it.
SignatureFunction GetEventCallbackEnabled(eventId As Long) As Boolean
Parameters
eventId (Long) - The numeric event id to query. Valid ids are the bridgeable event ids — every id in this group except 26 (BeforeCellPaint), which is deliberately not deliverable through the callback bridge. An unrecognized id raises an error that lists the exact ids this build accepts, so the message stays correct as events are added; do not hardcode the range in your own code.
ReturnsBoolean: True if this event is currently delivered to the callback bridge, False otherwise.
Debug.Print grid.GetEventCallbackEnabled(1) ' -> True (ColumnClick, on by default)Debug.Print grid.GetEventCallbackEnabled(4) ' -> False (SelectionChanged, off by default)Debug.Print grid.GetEventCallbackEnabled(26) ' -> False (BeforeCellPaint, can never be enabled)
Gotchas
Always returns False for event id 26 (BeforeCellPaint), and never raises an error for asking about it -- only trying to turn it ON with SetEventCallbackEnabled raises an error.
Reports the bridge's state, not registered mode's -- a False here does not mean a registered-mode handler stops receiving the event; registered mode always receives everything.
An unrecognized event id raises an error rather than returning False -- do not treat a caught error the same as a real disabled reading.
See alsoSetEventCallbackEnabled, GetEventCallbackInfo
SetEventCallbackMacroEvents and the event callback bridge
Attach a macro, invoked through Application.Run, as the callback bridge's second route.
An alternative to SetEventCallback for a host that would rather name a macro than build a callback object: every enabled event is delivered by calling hostApplication.Run(macroName, gridId, eventId, a1, a2, a3, a4, a5, a6, cancelSoFar), the same way a host would call Application.Run directly. hostApplication must support IDispatch and expose a Run method (checked immediately, the same way SetEventCallback checks for OnGridEvent) -- but macroName's own existence is NOT checked at this point. A macro name that does not resolve to a real public Sub or Function only fails the first time an event actually tries to invoke it, and even then the failure is caught internally and recorded in GetEventCallbackInfo's macro block (macroName, failureCount, and the shared 'last' block) rather than raised back on whatever triggered that event -- a typo in macroName produces no visible error anywhere except that diagnostic call. Both the direct route and the macro route can be attached at the same time; each is dispatched independently for every enabled event.
SignatureSub SetEventCallbackMacro(hostApplication As Object, macroName As String, gridId As Long)
Parameters
hostApplication (Object) - The host's own Application object (Access: Application; Excel: Application). Must support IDispatch and expose Run. Cannot be Nothing.
macroName (String) - The name of a public Sub or Function Application.Run can invoke, e.g. "Module1.OnGridEvent". Cannot be empty or all-whitespace; leading/trailing whitespace is trimmed before it is stored.
gridId (Long) - Echoed back as the second argument to the macro on every invocation, exactly like SetEventCallback's own gridId.
ReturnsNone. Raises an error if hostApplication is Nothing, does not support IDispatch, or does not expose Run; or if macroName is empty or only whitespace.
Not mode-dependent. Must be called on the control's owner thread, exactly like SetEventCallback.
Example
VBA
' Module1 exposes:' Public Sub OnGridEvent(ByVal gridId As Long, ByVal eventId As Long, _' a1 As Variant, a2 As Variant, a3 As Variant, _' a4 As Variant, a5 As Variant, a6 As Variant, _' ByVal cancelSoFar As Boolean)' If eventId = 2 Then Debug.Print "ItemClick row=" & a1' End Sub
grid.SetEventCallbackMacro Application, "Module1.OnGridEvent", 3' every subsequent enabled event now calls Application.Run("Module1.OnGridEvent", 3, eventId, ...)
Gotchas
macroName's existence is NEVER verified at Set time -- only that hostApplication itself exposes Run. A typo in macroName is only discovered on the first event that tries to invoke it, and even then produces no visible VBA error: the failure is caught and only shows up in GetEventCallbackInfo's macro.failureCount and the shared 'last' block.
The macro is invoked with exactly 9 parameters -- gridId, eventId, a1..a6, and cancelSoFar (passed BY VALUE, not ByRef like the direct callback's Cancel) -- the same count as OnGridEvent's own 9-parameter list, just with the last one passed differently. A macro that copies OnGridEvent's parameter list one-for-one, changing only ByRef Cancel to ByVal cancelSoFar, matches exactly and will not raise DISP_E_BADPARAMCOUNT.
macroName is trimmed of leading/trailing whitespace before it is stored -- a name that was accidentally padded with spaces is not treated as a distinct (and therefore always-failing) name.
Attaching a macro does not touch the direct object route (SetEventCallback) -- both can run side by side, each with its own invoke/failure counters.
Like the direct route, SetEventCallbackMacro is transactional: a call that fails validation (Nothing, no Run, empty name) leaves a previously working macro attached and untouched.
See alsoClearEventCallbackMacro, SetEventCallback, SetEventCallbackEnabled, GetEventCallbackInfo
ClearEventCallbackMacroEvents and the event callback bridge
Detach only the macro callback route.
Detaches the macro attached with SetEventCallbackMacro, and resets that route's own invoke/failure counters. Unlike ClearEventCallback, this leaves the direct object route (SetEventCallback) exactly as it was -- if a direct callback object is also attached, it keeps receiving every enabled event after this call. Per-event enablement (SetEventCallbackEnabled) is untouched either way.
SignatureSub ClearEventCallbackMacro()
ReturnsNone. Never raises an error, even with no macro attached.
Dim callbackHandler AsNew clsEventCallbackDemo ' a minimal class implementing OnGridEvent -- in real code, your own class module implementing the same shape shown above
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackMacro Application, "Module1.OnGridEvent", 1
grid.ClearEventCallbackMacro ' the direct-object route above keeps workingDebug.Print InStr(grid.GetEventCallbackInfo(), """macro"":{""attached"":false") > 0' -> TrueDebug.Print InStr(grid.GetEventCallbackInfo(), """direct"":{""attached"":true") > 0' -> True, unaffected
Gotchas
Detaches ONLY the macro route -- a direct callback object attached with SetEventCallback is left running. Use ClearEventCallback to detach both at once.
Idempotent: safe to call with nothing attached, or more than once in a row.
Does not touch SetEventCallbackEnabled overrides.
See alsoClearEventCallback, ClearEventCallbackObject, SetEventCallbackMacro
ColumnClickEvents and the event callback bridge
Fires when a column header is clicked.
Raised on a plain left-click anywhere in a column's header, after the click has been checked against the resize-edge zone and the filter-icon zone (which handle themselves instead) but is otherwise a simple notification -- there is no way to cancel the click itself from this event. When AutoSortOnHeaderClick is enabled, the built-in sort toggle runs immediately after this event fires, not before.
SignatureEvent ColumnClick(columnIndex As Long)
Parameters
columnIndex (Long) - The 0-based index of the column whose header was clicked.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Header interaction is independent of DataMode -- fires the same way in all three modes as long as at least one column exists.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ColumnClick(ByVal columnIndex As Long)' Debug.Print "Header clicked: " & columnIndex' End Sub'' Reg-free callback bridge (eventId = 1):' Public Sub OnGridEvent(ByVal gridId As Long, ByVal eventId As Long, _' a1 As Variant, a2 As Variant, a3 As Variant, _' a4 As Variant, a5 As Variant, a6 As Variant, ByRef Cancel As Boolean)' If eventId = 1 Then Debug.Print "Header clicked: " & a1 ' columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data ' any array with at least one column
grid.SetColumnHeader 0, "Name"' a real click on the header now fires ColumnClick(0)
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 1, True' ColumnClick is Tier A (on by default) -- shown for completeness
Gotchas
Enabling AllowColumnReorder (off by default) silently STOPS this event from firing on an ordinary header click: with reordering allowed, every header mousedown starts a drag-and-drop gesture instead (see ColumnReorder), and a plain click without a real drag never reaches the code that fires ColumnClick. Leave AllowColumnReorder False (its default) if header clicks need to keep working as clicks.
A click on the resize-edge zone (when AllowColumnResize is on) or the filter-icon zone (when ShowFilterButtons is on) is intercepted before this event and never fires it.
Carries no Cancel argument -- there is no way to suppress the built-in AutoSortOnHeaderClick behavior from this handler; it always runs afterward if enabled.
See alsoItemClick, ColumnReorder, AllowColumnReorder
ItemClickEvents and the event callback bridge
Fires when a data cell is clicked.
Raised on a plain left-click on an ordinary data cell -- one that is not a checkbox-typed column (which fires CheckboxToggle instead), not on a cell icon's own hit rectangle (IconClick instead), and not on a column whose renderer (SetColumnRenderer) claims the click itself (ButtonClick, or a rating write plus CellValueChanged for the star renderer). For a plain text/number/date cell, this is the only event a click produces.
SignatureEvent ItemClick(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row that was clicked.
columnIndex (Long) - The 0-based column that was clicked.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- fires the same way regardless of DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ItemClick(ByVal rowIndex As Long, ByVal columnIndex As Long)' Debug.Print "Clicked row " & rowIndex & ", col " & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' clicking any ordinary data cell now fires ItemClick(row, col)
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1' ItemClick (id 2) is Tier A -- delivered by default, no SetEventCallbackEnabled call needed
Gotchas
Never fires for a click on a checkbox-typed column's cell (CheckboxToggle fires there instead), a cell icon's hit rectangle (IconClick), or a cell whose column has a Button/Star/Checkbox/Progress renderer assigned with SetColumnRenderer -- those consume the click and fire their own, more specific event instead.
Also fires as the first half of a double-click (a real double-click delivers ItemClick once, then ItemDoubleClick) -- a handler that reacts to both may see both events for the same user gesture.
See alsoItemDoubleClick, ColumnClick, CheckboxToggle, IconClick, ButtonClick
ItemDoubleClickEvents and the event callback bridge
Fires when a data cell is double-clicked.
Raised on a double-click on a data cell, and fires BEFORE the built-in behavior a double-click normally triggers there: if AllowEditing is True, the interactive cell editor opens immediately afterward (which itself then fires BeforeCellEdit). A double-click on a column header's resize edge is a separate gesture (auto-fit that column's width) and does not fire this event at all.
SignatureEvent ItemDoubleClick(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row that was double-clicked.
columnIndex (Long) - The 0-based column that was double-clicked.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Fires in every DataMode -- the event itself does not check DataMode. In Virtual mode, AllowEditing being True does not actually open an editor afterward (StartCellEdit refuses in Virtual mode before doing anything), but ItemDoubleClick itself still fires normally.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ItemDoubleClick(ByVal rowIndex As Long, ByVal columnIndex As Long)' Debug.Print "Double-clicked row " & rowIndex & ", col " & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' a real double-click on any cell now fires ItemDoubleClick(row, col)
Gotchas
Carries no Cancel argument -- there is no way to stop the interactive editor from opening afterward (when AllowEditing is True) from this handler; use BeforeCellEdit for that instead.
A double-click on a header resize edge (auto-fit) is a different gesture entirely and does not fire this event.
A real double-click also fires ItemClick once, immediately before ItemDoubleClick, for the same user gesture.
See alsoItemClick, BeforeCellEdit
SelectionChangedEvents and the event callback bridge
Fires on every selection-affecting call or gesture, whether or not the resulting selection actually differs from before.
Raised whenever a selection-affecting action happens, from any source: a mouse click, Ctrl+A / Space / Shift+Arrow keyboard selection, incremental search navigation, or any of the selection API methods (SelectRow, SelectAll, ClearSelection). It fires unconditionally on every such action -- there is no comparison against the prior selection, so clicking an already-selected row, or calling SelectRow on a row already selected, still fires this event. Some paths fire it immediately and synchronously (SelectRow, SelectAll, a plain click); others -- rapid keyboard range extension in particular -- coalesce multiple firings into one via a short internal throttle so a held-down arrow key does not flood a handler with one event per row. There is no cancel.
SignatureEvent SelectionChanged()
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- selection is tracked the same way in every DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_SelectionChanged()' Debug.Print "Selected rows: " & UBound(m_grid.GetSelectedRows) + 1' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SelectRow 2' fires SelectionChanged immediately
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 4, True' required -- SelectionChanged is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B, high frequency) -- a registered-mode handler always receives it, but a reg-free host must call SetEventCallbackEnabled 4, True to receive it through OnGridEvent.
Some call paths fire it immediately (SelectRow, SelectAll, ClearSelection, a plain click); a rapid keyboard range extension can instead coalesce several changes into one throttled firing a short time later -- do not assume exactly one firing per keystroke.
Carries no arguments describing what changed -- a handler that needs to know the new selection reads it back with GetSelectedRows/IsRowSelected after the fact.
See alsoSelectRow, SelectAll, ClearSelection, GetSelectedRows
BeforeCellEditEvents and the event callback bridge
Fires just before the interactive cell editor opens, and can cancel it.
Raised right before StartCellEdit actually opens the interactive EDIT control for a cell -- triggered by a double-click on the cell (with AllowEditing True) or by pressing F2 while the cell has focus. Setting Cancel = True stops the editor from opening at all; nothing about the cell changes. This is the ONLY point at which starting an edit can be vetoed -- AfterCellEdit and CellValueChanged fire only once an edit has already been committed.
SignatureEvent BeforeCellEdit(rowIndex As Long, columnIndex As Long, Cancel As Boolean)
Parameters
rowIndex (Long) - The 0-based view row about to be edited.
columnIndex (Long) - The 0-based column about to be edited.
Cancel (Boolean (ByRef)) - Starts False. Set True to prevent the editor from opening.
Never fires in Virtual mode: StartCellEdit refuses outright as its very first action there (before this event, before anything else), because Virtual mode editing is read-only in this version -- there is no BeforeCellEdit to handle in Virtual mode at all. Fires normally in both Embedded and Turbo column-store mode, including the real interactive Turbo column-store edit path.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforeCellEdit(ByVal rowIndex As Long, ByVal columnIndex As Long, Cancel As Boolean)' If columnIndex = 0 Then Cancel = True ' column 0 (Id) is never editable' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.AllowEditing = True' pressing F2 on a focused cell, or double-clicking it, now fires BeforeCellEdit first
Gotchas
Only reachable through the interactive editor (a double-click or F2) -- SetCellValue and SetCell, the programmatic cell-write methods, never fire this event, and never open an editor to fire it from.
Cancelling here leaves the cell completely untouched -- no AfterCellEdit, no CellValueChanged, no editor window at all.
AllowEditing = False prevents the editor from ever being requested in the first place, so this event never fires either -- it only fires once editing has already been permitted at the AllowEditing level.
See alsoAfterCellEdit, CellValueChanged, ValidationFailed, ItemDoubleClick
AfterCellEditEvents and the event callback bridge
Fires once an interactive cell edit has been committed, with the raw typed text.
Raised after the interactive editor commits a real change -- pressing Enter, Tab, or clicking away from an EDIT control (or a dropdown/combo editor) whose text actually differs from what the cell held before. oldValue and newValue are the literal text involved, exactly as typed and as it was before, never a reformatted or type-coerced rendering. This fires ONLY from the interactive commit path: SetCellValue and SetCell, the programmatic write methods, never fire it, no matter what they change.
SignatureEvent AfterCellEdit(rowIndex As Long, columnIndex As Long, oldValue As String, newValue As String)
Parameters
rowIndex (Long) - The 0-based view row that was edited.
columnIndex (Long) - The 0-based column that was edited.
oldValue (String (BSTR)) - The cell's text before the edit.
newValue (String (BSTR)) - The text just committed.
Never fires in Virtual mode, for the same reason BeforeCellEdit never does: the interactive editor refuses to open there at all. Fires in both Embedded and Turbo column-store mode, through their own separate commit code paths, with the identical raw-old/raw-new-text contract in both.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_AfterCellEdit(ByVal rowIndex As Long, ByVal columnIndex As Long, _' ByVal oldValue As String, ByVal newValue As String)' Debug.Print rowIndex & "," & columnIndex & ": '" & oldValue & "' -> '" & newValue & "'"' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.AllowEditing = True' typing over a cell's text and pressing Enter now fires AfterCellEdit' By contrast, this NEVER fires AfterCellEdit, no matter what changes:
grid.SetCellValue 0, 0, "programmatic write"
Gotchas
NEVER fires from SetCellValue or SetCell -- only the interactive editor's own commit does. A host that writes cells programmatically and expects a change notification should not rely on this event; CellValueChanged has the same limitation.
Does not fire if the typed text is unchanged from the original (committing with no real edit is a silent no-op -- no event, no undo entry).
Does not fire if validation rejects the new text (ValidationFailed fires instead, and the cell reverts to its old value) or if the value could not be parsed into the column's declared type in Turbo column-store mode.
oldValue/newValue are always the raw typed strings -- for a numeric or date column, compare CellValueChanged's own typed VARIANT arguments instead if a parsed value is needed.
See alsoBeforeCellEdit, CellValueChanged, ValidationFailed
BeforeSortEvents and the event callback bridge
Fires before Sort, SortEx or SortMulti actually reorders the view, and can cancel it.
Raised at the start of Sort, SortEx and SortMulti in Embedded and Turbo column-store mode, before any reordering happens -- including on the fast paths (reversing an already-sorted column, or a no-op repeat of the current sort). Setting Cancel = True stops the sort: the view stays exactly as it was, and AfterSort does not fire. In Virtual mode, sorting is delegated to the host through the separate SortRequested event instead -- BeforeSort is not involved there at all.
SignatureEvent BeforeSort(columnIndex As Long, Cancel As Boolean)
Parameters
columnIndex (Long) - The 0-based column about to be sorted. For SortMulti, this is only the FIRST sort level's column.
Cancel (Boolean (ByRef)) - Starts False. Set True to prevent the sort from happening.
Fires from Sort/SortEx/SortMulti in Embedded mode only. In Virtual mode, Sort/SortEx/SortMulti raise SortRequested instead and never raise BeforeSort/AfterSort. In Turbo column-store mode, none of Sort/SortEx/SortMulti fire BeforeSort or AfterSort at all -- the sort simply happens, unstoppably, with no notification of any kind; a host relying on BeforeSort's Cancel to veto a sort column is silently ignored in Turbo column-store mode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforeSort(ByVal columnIndex As Long, Cancel As Boolean)' If columnIndex = 8 Then Cancel = True ' Notes column is never sortable' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.Sort 0, 0' fires BeforeSort(0, Cancel) first
Gotchas
Fires even when the requested sort turns out to be a genuine no-op (the same column, the same order, already active) -- but AfterSort does NOT fire for that specific no-op case, so BeforeSort and AfterSort are not guaranteed to pair up 1:1.
For SortMulti, columnIndex reports only the first of up to three sort levels -- the second and third levels are invisible to this event.
Cancelling leaves the view, the sort column and the sort order completely unchanged, and AfterSort never fires for that call.
See alsoAfterSort, SortRequested
AfterSortEvents and the event callback bridge
Fires once a sort has actually been applied.
Raised immediately after Sort, SortEx or SortMulti finishes reordering the view, including on the fast 'same column, reversed order' path. sortOrder mirrors whichever call triggered it: 0/1 (Ascending/Descending) from Sort or from SortEx modes 0-1, or SortEx's own extended modes 2-4 (case-insensitive ascending/descending, or a natural sort) forwarded unchanged. Does NOT fire for a genuine no-op repeat of the currently active sort, even though BeforeSort did fire for that same call.
SignatureEvent AfterSort(columnIndex As Long, sortOrder As Long)
Parameters
columnIndex (Long) - The 0-based column that was sorted. For SortMulti, only the FIRST sort level's column.
sortOrder (Long) - 0=Ascending, 1=Descending (from Sort or SortEx modes 0-1), or SortEx's own 2=Ascending case-insensitive, 3=Descending case-insensitive, 4=Natural, forwarded unchanged.
Fires from Sort/SortEx/SortMulti in Embedded mode only. In Virtual mode, a successful sort is confirmed through SortRequested's own handled=True path instead -- AfterSort itself is never raised there. In Turbo column-store mode, none of Sort/SortEx/SortMulti fire AfterSort at all -- a sort completes with no notification of any kind, the same silent gap BeforeSort has in Turbo column-store mode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_AfterSort(ByVal columnIndex As Long, ByVal sortOrder As Long)' Debug.Print "Sorted column " & columnIndex & ", order " & sortOrder' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.Sort 0, 1' fires AfterSort(0, 1) once the descending sort is applied
Gotchas
Does not fire for a genuine no-op repeat of the sort already active (same column, same order) -- BeforeSort fires for that call, AfterSort does not.
For SortMulti, columnIndex/sortOrder report only the first of up to three sort levels.
sortOrder can be 2, 3 or 4 (from SortEx) -- a handler written only for the simple 0/1 Ascending/Descending values will misinterpret those.
See alsoBeforeSort, SortRequested
ContextMenuEvents and the event callback bridge
Fires on every cell-area right-click, and on a header-area right-click only when ShowHeaderMenu is False.
Raised after a right-click, but its relationship to the other two right-click events is NOT symmetric between the cell and header areas. In the cell area, ContextMenu always fires, unconditionally, after BeforeContextMenu -- regardless of Cancel and regardless of ShowBuiltinContextMenu's value. In the header area, ContextMenu fires ONLY when ShowHeaderMenu is False; when ShowHeaderMenu is True (the default), BeforeHeaderMenu fires INSTEAD and ContextMenu does not fire at all for that click -- the two are mutually exclusive there, not sequential. It carries no cancel of its own; it exists purely as a general-purpose 'a right-click happened here' notification, most useful for a host that wants to show its own menu instead of (or alongside) the built-in one.
SignatureEvent ContextMenu(rowIndex As Long, columnIndex As Long, x As Long, y As Long)
Parameters
rowIndex (Long) - The 0-based view row under the pointer, or -1 if the right-click was not over a data row.
columnIndex (Long) - The 0-based column under the pointer, or -1 if not over a column.
x (Long) - Screen-coordinate x, suitable for positioning a custom menu (e.g. with a Shell/API PopupMenu call).
y (Long) - Screen-coordinate y.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- fires the same way regardless of DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ContextMenu(ByVal rowIndex As Long, ByVal columnIndex As Long, ByVal x As Long, ByVal y As Long)' Debug.Print "Right-click at row " & rowIndex & ", col " & columnIndex & " (screen " & x & "," & y & ")"' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' any real right-click, anywhere on the grid, fires ContextMenu
Gotchas
Cell-area right-clicks: ALWAYS fires, even when BeforeContextMenu was cancelled, and even when ShowBuiltinContextMenu is False -- it is not restricted by either.
Header-area right-clicks: fires ONLY when ShowHeaderMenu is False. When ShowHeaderMenu is True (the default), BeforeHeaderMenu fires instead and ContextMenu is skipped entirely for that click -- not merely suppressed by a Cancel, genuinely never reached.
x/y are SCREEN coordinates (already converted from the click's client-area position), not view-relative coordinates like most other event arguments in this group.
See alsoBeforeContextMenu, BeforeHeaderMenu
KeyDownEvents and the event callback bridge
Fires on every keystroke the grid receives, before any of its own built-in key handling.
Raised for every WM_KEYDOWN the grid's window receives -- arrow keys, F2, Delete, Ctrl+F, Ctrl+A, Escape, Space, and everything else -- fired first, before the grid's own built-in handling for that key runs. Despite that ordering suggesting a veto, this event carries no Cancel argument at all: a handler cannot suppress the grid's own subsequent handling of the same key from here. It is a pure notification, useful for logging or for adding NEW keyboard shortcuts alongside the built-in ones, not for overriding them.
SignatureEvent KeyDown(keyCode As Long, shift As Long)
Parameters
keyCode (Long) - The virtual-key code (a VK_* constant, e.g. 38 for the up arrow).
shift (Long) - Modifier bitmask: 1 = Shift held, 2 = Ctrl held, 4 = Alt held. Combine bits for more than one modifier.
ReturnsNone (event).
Value
Meaning
1
Shift
2
Ctrl
4
Alt
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- fires for every keystroke regardless of DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_KeyDown(ByVal keyCode As Long, ByVal shift As Long)' Debug.Print "Key " & keyCode & ", shift bits " & shift' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' pressing any key while the grid has focus now fires KeyDown first
Gotchas
Carries no Cancel argument, despite firing before the grid's own built-in handling for the same key -- there is no way to suppress arrow-key navigation, F2-to-edit, Delete, or any other built-in shortcut from this handler.
Fires for every key, including ones the grid does not otherwise react to at all -- do not assume a firing means the grid is about to do something with that key.
See alsoBeforeCellEdit
DataLoadedEvents and the event callback bridge
Fires once, after a bulk load finishes.
Raised once a data-loading call has finished: LoadFromArray, LoadFromRecordset, or LoadFromJson (a different, already-documented member group). rowCount/columnCount report what was actually loaded -- for LoadFromArray, this reflects the truncated total if CancelLoad stopped the load partway through, not the originally requested size. LoadFromArray also fires the more general-purpose DataChanged(0) event immediately afterward, for the same load; LoadFromRecordset and LoadFromJson do not -- DataLoaded fires alone for those two loaders.
SignatureEvent DataLoaded(rowCount As Long, columnCount As Long)
Parameters
rowCount (Long) - The number of rows actually loaded.
columnCount (Long) - The number of columns the grid now has.
None of the three triggering loaders (LoadFromArray, LoadFromRecordset, LoadFromJson) work in Virtual mode, so DataLoaded never fires there. In Turbo column-store mode the picture is loader-specific: LoadFromRecordset fires it normally, but LoadFromArray does not -- Turbo column-store-mode LoadFromArray routes to a separate internal load path that skips this event entirely (the same gap LoadProgress has). LoadFromJson is Embedded-only regardless of mode, so it never contributes a Turbo column-store-mode firing either.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_DataLoaded(ByVal rowCount As Long, ByVal columnCount As Long)' Debug.Print "Loaded " & rowCount & " rows, " & columnCount & " columns"' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data ' fires DataLoaded once the load completes
Gotchas
If CancelLoad interrupted a LoadFromArray call partway through, rowCount reflects the actually-loaded (truncated) count, not the size of the array that was passed in.
Only LoadFromArray fires DataChanged(0) immediately afterward for the same load -- LoadFromRecordset and LoadFromJson fire DataLoaded alone, with no companion DataChanged. A handler subscribing to both sees two events per load only for LoadFromArray.
LoadFromArray does NOT fire DataLoaded in Turbo column-store mode -- Turbo column-store-mode LoadFromArray routes to a separate internal load path that never reaches this event's firing site. LoadFromRecordset does fire it normally in Turbo column-store mode. A host that only ever loads via LoadFromArray will never see DataLoaded fire at all once it switches to Turbo column-store mode.
See alsoDataChanged, LoadProgress
BeforeRowDeleteEvents and the event callback bridge
Fires before a row is actually removed, and can cancel that one row's deletion.
Raised once per row, immediately before DeleteRow or DeleteSelectedRows removes it, in both Embedded and Turbo column-store mode. Setting Cancel = True skips just that row (leaving it in place) without aborting the rest of the operation -- DeleteSelectedRows continues on to the next selected row even if an earlier one was cancelled. Never fires in Virtual mode: DeleteRow/DeleteSelectedRows both refuse outright there (the grid does not own the data), so there is nothing to fire it for.
SignatureEvent BeforeRowDelete(rowIndex As Long, Cancel As Boolean)
Parameters
rowIndex (Long) - The 0-based view row about to be deleted.
Cancel (Boolean (ByRef)) - Starts False. Set True to keep this specific row (a DeleteSelectedRows call still continues on to any other selected rows).
Never fires in Virtual mode: DeleteRow and DeleteSelectedRows are both refused there outright, before this event could ever be reached. Fires identically for both Embedded and Turbo column-store mode's own separate delete implementations.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforeRowDelete(ByVal rowIndex As Long, Cancel As Boolean)' If grid.GetCellValue(rowIndex, 7) = "True" Then Cancel = True ' protect Active rows' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.DeleteRow 0' fires BeforeRowDelete(0, Cancel) first
Gotchas
For DeleteSelectedRows, a per-row Cancel = True only spares that ONE row -- the loop continues deleting every other selected row normally.
After this event, the row index is re-validated against the current row count before the delete actually proceeds -- a handler that itself deletes or filters rows from inside this event can cause the pending delete to be silently abandoned (no error) if the index is no longer valid.
Never fires in Virtual mode -- DeleteRow/DeleteSelectedRows raise an error there before reaching this event at all.
See alsoDeleteRow, DeleteSelectedRows
ScrollChangedEvents and the event callback bridge
Fires (throttled) whenever the scroll position changes, from any source.
Raised whenever the pixel scroll position changes -- mouse wheel, scrollbar drag, WM_VSCROLL/WM_HSCROLL, or a ScrollToView call that actually had to move the viewport. Delivery is throttled to roughly one firing per 16 milliseconds so a fast wheel spin or a scrollbar drag does not flood a handler with one event per pixel; a message pump (DoEvents, or T.Pump in this harness) is needed after the triggering action for the throttled firing to actually arrive.
SignatureEvent ScrollChanged(scrollTop As Long, scrollLeft As Long)
Parameters
scrollTop (Long) - The current vertical scroll position, in pixels.
scrollLeft (Long) - The current horizontal scroll position, in pixels.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- scrolling is pure view state, unrelated to DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ScrollChanged(ByVal scrollTop As Long, ByVal scrollLeft As Long)' Debug.Print "Scrolled to " & scrollTop & "," & scrollLeft' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.ScrollToView 15' if row 15 was not already visible, fires ScrollChanged shortly after
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 13, True' required -- ScrollChanged is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B, high frequency) -- a registered-mode handler always receives it, but a reg-free host must call SetEventCallbackEnabled 13, True to receive it through OnGridEvent.
Throttled to roughly one firing per 16ms (constant) of real time -- a handler will not see one event per pixel scrolled during a fast wheel spin or drag, only the most recent position once the throttle window elapses.
ScrollToView only fires this when it actually had to move the viewport -- calling it on a row that is already fully visible does nothing and does not fire ScrollChanged.
See alsoScrollToView
IconClickEvents and the event callback bridge
Fires when a click lands on a cell's icon, not just anywhere in the cell.
Raised when a left-click's exact pixel position falls within a resolved cell icon's own narrow hit rectangle -- computed from the icon's fixed width and its position near the cell's left or right edge (depending on the column's icon layout setting), not the cell's full bounds. A click anywhere else in the same cell fires ItemClick instead. Requires an icon to actually be resolvable for that cell first (LoadIconFromFile plus SetCellIcon, a column icon map, or a threshold rule -- members documented elsewhere).
SignatureEvent IconClick(rowIndex As Long, columnIndex As Long, iconId As Long)
Parameters
rowIndex (Long) - The 0-based view row whose icon was clicked.
columnIndex (Long) - The 0-based column whose icon was clicked.
iconId (Long) - The id (from LoadIconFromFile) of the icon that was clicked.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent, but only reachable once a cell has a resolvable icon, which itself may be restricted by mode elsewhere in the icon members (documented in a different group).
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_IconClick(ByVal rowIndex As Long, ByVal columnIndex As Long, ByVal iconId As Long)' Debug.Print "Icon " & iconId & " clicked at row " & rowIndex' End SubDim iconId AsLong
grid.LoadIconFromFile "C:\icons\flag.ico", iconId
grid.SetCellIcon 0, 0, iconId
' a real click precisely on that icon's small hit rectangle now fires IconClick
Gotchas
Only fires for a click precisely within the icon's own small hit rectangle (its fixed pixel width, positioned near one edge of the cell) -- a click on the rest of the same cell fires ItemClick instead, never this event. This harness's own T.ClickCell targets the cell's CENTRE, which for an ordinary column width does not land inside that narrow rectangle, so this event cannot be triggered deterministically through the simulated-click helpers available here.
Never fires for a cell with no resolvable icon (no SetCellIcon, no matching column icon rule) -- the click falls through to ItemClick in that case.
See alsoItemClick, CheckboxToggle
FilterChangedEvents and the event callback bridge
Fires whenever a filter is applied or cleared, in Embedded and Turbo column-store mode.
Raised after SetColumnFilter, ClearColumnFilter, ClearAllFilters, SetColumnNumericFilter, SetColumnNumericFilterRange, SetColumnValueListFilter, SetFilterState (and LoadFilterPreset, which applies through SetFilterState) actually change the active filter, in Embedded and Turbo column-store mode -- and also from the built-in filter-dropdown UI's own OK/Clear buttons. filteredRowCount is the number of rows the view now shows after the change. In Virtual mode, these same calls raise the separate FilterRequested event instead and delegate the actual filtering to the host; FilterChanged never fires there.
SignatureEvent FilterChanged(filteredRowCount As Long)
Parameters
filteredRowCount (Long) - The number of rows visible in the view after the filter change.
In Virtual mode, filtering is delegated to the host through FilterRequested instead -- FilterChanged is never raised there. Fires from every filter-setting/clearing method in both Embedded and Turbo column-store mode, with one difference: in Turbo column-store mode, ClearColumnFilter/ClearAllFilters are a true no-op (including no event at all) when nothing was already filtered on that column/any column; in Embedded mode the same calls always fire this event, even when clearing a filter that wasn't set.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_FilterChanged(ByVal filteredRowCount As Long)' Debug.Print "Now showing " & filteredRowCount & " rows"' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SetColumnFilter 2, 0, "North"' 0 = Contains; fires FilterChanged once applied
Gotchas
Fires from SEVEN distinct filter-setting/clearing methods, not just SetColumnFilter/ClearColumnFilter -- SetColumnNumericFilter, SetColumnNumericFilterRange, SetColumnValueListFilter and SetFilterState (and LoadFilterPreset, through it) all raise it too.
Also fires from the built-in filter-dropdown UI's OK and Clear buttons, not only from a programmatic call.
Carries no information about WHICH column or what kind of filter changed -- only the resulting row count. A handler that needs to know what changed reads it back with GetColumnFilterMode/GetColumnFilterPattern.
See alsoSetColumnFilter, ClearColumnFilter, ClearAllFilters, FilterRequested
CheckboxToggleEvents and the event callback bridge
Fires when a checkbox-typed cell's state is toggled.
Raised whenever a checkbox-typed column's cell is toggled -- a click anywhere in the cell (not just a narrow checkbox glyph), pressing Space while a checkbox cell has focus, or a Shift-click range toggle (which fires once per row the range covers, in a loop, each with its own checked value). Requires the column to actually be a checkbox-typed column: set with SetColumnType(col, 6), a different member. checked reports the NEW state, after the toggle.
SignatureEvent CheckboxToggle(rowIndex As Long, columnIndex As Long, checked As Boolean)
Parameters
rowIndex (Long) - The 0-based view row toggled.
columnIndex (Long) - The 0-based checkbox column toggled.
checked (Boolean) - The new checked state after the toggle.
Only reachable through a column typed as Checkbox with SetColumnType(col, 6); SetColumnType itself is refused in Turbo column-store mode (it is a read-only seam there), so a Turbo column-store-mode column cannot be made a checkbox column through this member family, and this event has no way to fire there as a result.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_CheckboxToggle(ByVal rowIndex As Long, ByVal columnIndex As Long, ByVal checked As Boolean)' Debug.Print "Row " & rowIndex & " is now " & checked' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SetColumnType 7, 6' 6 = Checkbox' clicking anywhere in that column's cells now fires CheckboxToggle
Gotchas
Requires the target column to be typed as Checkbox (SetColumnType(col, 6)) -- an ordinary Boolean-typed column (type 5) does not trigger this event on a click; it is treated as a plain data cell.
A click toggles ANYWHERE in the cell, not just a small glyph -- unlike IconClick's narrow hit rectangle.
A Shift-click range toggle fires this event ONCE PER ROW in the range, each with the range's own single target checked value, rather than once for the whole range.
checked reports the state AFTER the toggle, not before.
See alsoItemClick, SetColumnType
BeforePasteEvents and the event callback bridge
Fires before clipboard text is written into cells, and can cancel the whole paste.
Raised by PasteFromClipboard and PasteSpecial, right after the clipboard's tab/CRLF-delimited text has been parsed into a rectangular block but before a single cell is written. rowIndex/columnIndex are the paste's anchor cell (the current focus cell); rowCount/colCount describe the parsed block's shape. Setting Cancel = True stops the entire paste -- no cell is written, and AfterPaste does not fire.
SignatureEvent BeforePaste(rowIndex As Long, columnIndex As Long, rowCount As Long, colCount As Long, Cancel As Boolean)
Parameters
rowIndex (Long) - The 0-based view row the paste will start at (the current focus row).
columnIndex (Long) - The 0-based column the paste will start at (the current focus column).
rowCount (Long) - The number of rows the parsed clipboard block has.
colCount (Long) - The number of columns the parsed clipboard block has.
Cancel (Boolean (ByRef)) - Starts False. Set True to abandon the paste entirely.
PasteFromClipboard and PasteSpecial are both refused in Virtual mode AND in Turbo column-store mode (not just Virtual) -- paste only ever works, and this event only ever fires, in Embedded mode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforePaste(ByVal rowIndex As Long, ByVal columnIndex As Long, _' ByVal rowCount As Long, ByVal colCount As Long, Cancel As Boolean)' If rowCount * colCount > 10000 Then Cancel = True ' refuse very large pastes' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SelectBlock 0, 0, 0, 0' anchor the paste at row 0, col 0
grid.PasteFromClipboard ' fires BeforePaste first
Gotchas
Refused in BOTH Virtual mode and Turbo column-store mode -- unlike most of this group's members, which only lose Virtual mode, paste is Embedded-only.
The anchor cell comes from the current FOCUS, not from any argument the caller passes to PasteFromClipboard -- set focus first (a click, or SelectBlock's own paste-anchor behavior) before pasting.
Cancelling here stops the WHOLE paste, not row-by-row -- there is no way to accept part of a pasted block and reject the rest from this event.
See alsoAfterPaste, PasteFromClipboard, PasteSpecial
AfterPasteEvents and the event callback bridge
Fires once a paste has actually been written into cells.
Raised immediately after PasteFromClipboard or PasteSpecial finishes writing the parsed clipboard block into cells (unless BeforePaste cancelled it). rowIndex/columnIndex/rowCount/colCount describe the same anchor and shape BeforePaste reported for the same paste.
SignatureEvent AfterPaste(rowIndex As Long, columnIndex As Long, rowCount As Long, colCount As Long)
Parameters
rowIndex (Long) - The 0-based view row the paste started at.
columnIndex (Long) - The 0-based column the paste started at.
Same restriction as BeforePaste: refused in both Virtual and Turbo column-store mode, so this only ever fires in Embedded mode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_AfterPaste(ByVal rowIndex As Long, ByVal columnIndex As Long, _' ByVal rowCount As Long, ByVal colCount As Long)' Debug.Print "Pasted " & rowCount & "x" & colCount & " starting at " & rowIndex & "," & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SelectBlock 0, 0, 0, 0
grid.PasteFromClipboard ' fires AfterPaste once the write completes
Gotchas
Does not fire at all if BeforePaste cancelled the paste.
rowCount/colCount describe the block that was WRITTEN, which can be clipped by the grid's own bounds (a paste near the bottom-right edge writes fewer rows/columns than the clipboard actually held, but this event still reports the full parsed block's shape, the same as BeforePaste did -- not the clipped write count).
See alsoBeforePaste, PasteFromClipboard, CellValueChanged
ValidationFailedEvents and the event callback bridge
Fires when a committed edit is rejected by a validation rule.
Raised from the interactive commit path when the typed text fails a rule added with AddValidationRule (a different member), or -- in Turbo column-store mode only -- when the typed text cannot be parsed into the column's own declared type at all. In every case the cell reverts to its previous value: no write happens, no undo entry is pushed, and AfterCellEdit/CellValueChanged do not fire for that commit. message is the human-readable reason, suitable for showing directly to the user.
SignatureEvent ValidationFailed(rowIndex As Long, columnIndex As Long, message As String)
Parameters
rowIndex (Long) - The 0-based view row whose edit was rejected.
columnIndex (Long) - The 0-based column whose edit was rejected.
message (String (BSTR)) - The validation failure's message text.
Never fires in Virtual mode, for the same reason AfterCellEdit never does -- the interactive editor refuses to open there. Fires in both Embedded and Turbo column-store mode; Turbo column-store mode has an EXTRA source for it beyond AddValidationRule (a type-parse failure), that Embedded mode does not have.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ValidationFailed(ByVal rowIndex As Long, ByVal columnIndex As Long, ByVal message As String)' Debug.Print "Row " & rowIndex & ": " & message' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.AddValidationRule 3, 0, Empty, Empty' rule type 0 = Required, on the Amount column
grid.AllowEditing = True' committing an empty value into that column now fires ValidationFailed
Gotchas
Only reachable through the interactive editor's commit -- SetCellValue and SetCell never trigger validation and never fire this event, no matter what they write.
In Turbo column-store mode, this can fire for a reason AddValidationRule never covers: typed text that simply cannot be converted into the column's own declared type (e.g. non-numeric text committed into a numeric column) is rejected here too, with its own message, even with no validation rule configured at all.
The cell always reverts on this event -- there is no ByRef argument to accept the value anyway; a genuine override requires the host to call SetCellValue/SetCell itself afterward.
See alsoAfterCellEdit, BeforeCellEdit, AddValidationRule
ColumnReorderEvents and the event callback bridge
Fires once a column header has actually been dragged to a new position.
Raised at the end of a completed column-header drag-and-drop, once the move has actually been applied -- never during the drag, and never for a drag that was released back at its own original slot or the immediately adjacent no-op slot. Requires AllowColumnReorder to be True first (it is False by default); with it False, a header mousedown never starts a drag at all, and this event can never fire. There is no programmatic column-move API in this group to trigger it any other way -- unlike MoveRow for rows, there is no MoveColumn COM method.
SignatureEvent ColumnReorder(fromIndex As Long, toIndex As Long)
Parameters
fromIndex (Long) - The column's 0-based index before the move.
toIndex (Long) - The column's new 0-based index after the move.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not restricted by DataMode itself, but only reachable through a real header drag gesture -- AllowColumnReorder must be set True first in every mode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ColumnReorder(ByVal fromIndex As Long, ByVal toIndex As Long)' Debug.Print "Column moved from " & fromIndex & " to " & toIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.AllowColumnReorder = True' a real drag of any header to a different position now fires ColumnReorder
Gotchas
AllowColumnReorder defaults to False -- a header drag does nothing at all (and this event never fires) until it is explicitly set True.
Setting AllowColumnReorder = True also changes what a PLAIN header click does: every header mousedown now starts a drag attempt instead of firing ColumnClick (see ColumnClick's own gotcha on this).
A drop back at the column's own original slot, or at the slot immediately after it (which is the same visual position once the source column is removed), is treated as no real move -- nothing is permuted and this event does not fire for it.
There is no programmatic way to trigger a column move from code -- unlike MoveRow, there is no MoveColumn COM method. This event can only be produced by a real, completed mouse drag-and-drop of a column header; there is no way to script that gesture from VBA at all, so it can only be observed by a real user actually dragging a header.
See alsoColumnClick, AllowColumnReorder, GetColumnLayout
ButtonClickEvents and the event callback bridge
Fires when a cell in a Button-rendered column is clicked.
Raised when a click lands on a cell whose column has been given the Button renderer with SetColumnRenderer(colIndex, 4). Unlike IconClick's narrow hit rectangle, the button renderer claims the WHOLE cell for hit-testing -- the visible padding around the button is purely cosmetic, not a dead zone, so an ordinary click anywhere in the cell fires this event. It does not itself change any cell value; it exists purely as a click notification for a host that wants to react to a per-row action button.
SignatureEvent ButtonClick(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row whose button was clicked.
columnIndex (Long) - The 0-based Button-rendered column that was clicked.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent, but only reachable once SetColumnRenderer(colIndex, 4) has been applied to the column.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_ButtonClick(ByVal rowIndex As Long, ByVal columnIndex As Long)' Debug.Print "Button clicked at row " & rowIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.SetColumnRenderer 1, 4' 4 = Button' clicking that column's cells now fires ButtonClick instead of ItemClick
Gotchas
Requires SetColumnRenderer(colIndex, 4) on the column first -- an ordinary column never fires this event no matter where it is clicked.
Claims the whole cell for hit-testing, unlike IconClick -- a plain T.ClickCell at the cell's centre reliably lands on it.
Never fires ItemClick for the same click -- the renderer dispatch runs before, and instead of, the plain-cell ItemClick check.
See alsoItemClick, SetColumnRenderer, CellValueChanged
CellHoverEvents and the event callback bridge
Fires (throttled) with the cell currently under the mouse.
Raised a short time (roughly 100ms (constant)) after the mouse settles over a new cell, tracking the same underlying hover state HoveredRow (a different member) reflects live. It fires once per cell the pointer moves onto, not continuously while it sits still, and not for every intermediate cell passed over during a fast mouse movement -- only the cell the pointer is over when the throttle window elapses.
SignatureEvent CellHover(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row under the mouse.
columnIndex (Long) - The 0-based column under the mouse.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent -- pure mouse/paint state, unrelated to DataMode.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_CellHover(ByVal rowIndex As Long, ByVal columnIndex As Long)' StatusLabel.Caption = "Row " & rowIndex & ", Col " & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' moving the mouse over a cell and letting it settle now fires CellHover shortly after
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 22, True' required -- CellHover is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B, high frequency) -- a registered-mode handler always receives it, but a reg-free host must call SetEventCallbackEnabled 22, True to receive it through OnGridEvent.
Throttled to roughly 100ms (constant) after the hovered cell changes -- a fast mouse sweep across many cells does not fire this once per cell, only for wherever the pointer is once the throttle elapses.
Depends on the OS genuinely recognizing this window as the one under the cursor at that instant (the same TrackMouseEvent-based tracking HoveredRow uses) -- a minimized window, a window that isn't actually focused/foreground, or the pointer genuinely being elsewhere on screen, all mean this simply does not fire; that is expected OS behavior, not a control defect.
See alsoCellMouseEnter, CellMouseLeave, HoveredRow
CellMouseEnterEvents and the event callback bridge
Fires immediately, the moment the mouse moves onto a new cell.
Raised the instant the tracked hover cell changes -- not throttled, unlike CellHover -- immediately after CellMouseLeave fires for whichever cell the pointer just left (if any). Tracks the same underlying mouse-move machinery as CellHover and HoveredRow.
SignatureEvent CellMouseEnter(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row the mouse just moved onto.
columnIndex (Long) - The 0-based column the mouse just moved onto.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_CellMouseEnter(ByVal rowIndex As Long, ByVal columnIndex As Long)' Debug.Print "Entered row " & rowIndex & ", col " & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' moving the mouse onto any cell fires CellMouseEnter immediately
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 23, True' required -- CellMouseEnter is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B) -- call SetEventCallbackEnabled 23, True to receive it through OnGridEvent.
Fires once PER CELL TRANSITION, immediately (not throttled like CellHover) -- a fast mouse sweep across several cells fires this once per cell crossed, not just for the final resting cell.
Always paired with CellMouseLeave for the previously hovered cell, fired in the SAME WM_MOUSEMOVE handling, Leave before Enter.
Same real-input/OS-hover-tracking reliability caveat as CellHover/HoveredRow applies here too.
See alsoCellMouseLeave, CellHover, HoveredRow
CellMouseLeaveEvents and the event callback bridge
Fires immediately for the cell the mouse just moved away from.
Raised the instant the tracked hover cell changes, for whichever cell the pointer WAS over, immediately before CellMouseEnter fires for the new one, in the same WM_MOUSEMOVE handling. Only fires when there was a previously tracked cell to leave -- the very first mouse move over the grid (with no prior hover cell) fires CellMouseEnter alone.
SignatureEvent CellMouseLeave(rowIndex As Long, columnIndex As Long)
Parameters
rowIndex (Long) - The 0-based view row the mouse just left.
columnIndex (Long) - The 0-based column the mouse just left.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_CellMouseLeave(ByVal rowIndex As Long, ByVal columnIndex As Long)' Debug.Print "Left row " & rowIndex & ", col " & columnIndex' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' moving the mouse from one cell to another fires CellMouseLeave for the old cell first
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 24, True' required -- CellMouseLeave is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B) -- call SetEventCallbackEnabled 24, True to receive it through OnGridEvent.
Only fires when there was a genuinely tracked previous cell -- the very first hover after the mouse enters the grid (or after leaving it entirely) has nothing to fire this for, and only CellMouseEnter fires.
Fires BEFORE CellMouseEnter for the new cell, in the same handling pass, not after.
Same real-input/OS-hover-tracking reliability caveat as CellHover/CellMouseEnter/HoveredRow applies here too.
See alsoCellMouseEnter, CellHover, HoveredRow
CellValueChangedEvents and the event callback bridge
Fires alongside AfterCellEdit, with typed VARIANT values instead of raw text.
Raised from the same interactive-commit path as AfterCellEdit (immediately after it, for the same edit), but carries TYPED VARIANT old/new values -- the same representation GetCell returns, not a re-parsed rendering of the raw text -- rather than AfterCellEdit's raw BSTR text. Also fires from a Star-rating renderer click (SetColumnRenderer(col, 5)), which writes a new numeric rating directly with no interactive text editor involved at all -- that path fires CellValueChanged alone, with no accompanying AfterCellEdit. Like editing generally, this NEVER fires from SetCellValue/SetCell, and NEVER fires at all in Virtual mode.
SignatureEvent CellValueChanged(rowIndex As Long, columnIndex As Long, oldValue As Variant, newValue As Variant)
Parameters
rowIndex (Long) - The 0-based view row that changed.
columnIndex (Long) - The 0-based column that changed.
oldValue (Variant) - The cell's typed value before the change.
newValue (Variant) - The cell's typed value after the change.
Deliberately, explicitly never raised in Virtual mode -- editing is refused before it begins there, and there is no delegated equivalent for this specific event the way sorting and filtering have SortRequested/FilterRequested. Fires in both Embedded and Turbo column-store mode from the interactive commit path.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_CellValueChanged(ByVal rowIndex As Long, ByVal columnIndex As Long, _' oldValue As Variant, newValue As Variant)' Debug.Print rowIndex & "," & columnIndex & ": " & CStr(oldValue) & " -> " & CStr(newValue)' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.AllowEditing = True' committing a real edit through the interactive editor fires CellValueChanged alongside AfterCellEdit' By contrast, this NEVER fires CellValueChanged:
grid.SetCellValue 0, 0, "programmatic write"
Gotchas
NEVER fires from SetCellValue or SetCell -- exactly like AfterCellEdit, only the interactive editor's own commit (or a Star-rating renderer click) raises it.
The Star-rating renderer path (SetColumnRenderer(col, 5)) fires ONLY CellValueChanged, with no accompanying AfterCellEdit -- the two events are not always paired.
oldValue/newValue are typed VARIANTs (matching GetCell's own representation) -- for a Currency or Date column this is the raw scaled-integer/OLE-double representation, not a formatted display string; compare AfterCellEdit's raw text arguments instead if display text is what is needed.
The Star-rating renderer path fires unconditionally whenever a click resolves to a new rating -- unlike the interactive text-edit commit path (which skips firing when the committed value equals the old one), there is no old-vs-new comparison here, so clicking a star rating to the SAME value it already had still fires this event.
See alsoAfterCellEdit, ValidationFailed, GetCell
BeforeCellPaintEvents and the event callback bridge
Fires once per cell, per paint, for custom drawing -- registered mode only, and off by default.
Raised once for every visible cell on every paint pass, but ONLY once EnableBeforeCellPaint has been explicitly set True (its default is False, to avoid the per-cell dispatch overhead when nothing uses it). Setting Cancel = True skips the grid's own default rendering for that one cell, leaving whatever the handler already drew (using hdc, the live device context for this paint) in place. This is the one event in the whole group that can never reach the reg-free callback bridge, no matter what SetEventCallbackEnabled is asked to do -- hdc is a live GDI handle valid only for the duration of this single paint call, which cannot survive a late-bound callback boundary, so custom cell painting requires registered mode.
SignatureEvent BeforeCellPaint(rowIndex As Long, columnIndex As Long, hdc As OLE_HANDLE, Cancel As Boolean)
Parameters
rowIndex (Long) - The 0-based view row being painted.
columnIndex (Long) - The 0-based column being painted.
hdc (OLE_HANDLE) - The live device context for this paint call. Only valid for the duration of this one event -- never save it for later use.
Cancel (Boolean (ByRef)) - Starts False. Set True to skip the grid's own default rendering for this cell (after the handler has drawn its own content into hdc).
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not restricted by DataMode, but restricted by the separate EnableBeforeCellPaint property (default False, in every mode) -- set it True first, in whichever mode the grid is in, for this event to fire at all.
Example
VBA
' Registered mode ONLY (control on the form, handler on its name) -- BeforeCellPaint can never reach the reg-free' callback bridge. hdc MUST be declared As OLE_HANDLE, not As Long, or this' fails to compile ("Procedure declaration does not match..."):'' Private Sub lgGrid_BeforeCellPaint(ByVal rowIndex As Long, ByVal columnIndex As Long, _' ByVal hdc As OLE_HANDLE, Cancel As Boolean)' ' custom drawing into hdc would go here; Cancel = True skips default rendering' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
grid.EnableBeforeCellPaint = True' required -- off by default
Reg-Free difference
VBA
' Not available: SetEventCallbackEnabled 26, True always raises an error.' Custom cell painting requires registered mode .
Gotchas
Does not fire at all until EnableBeforeCellPaint has been set True -- unlike most events in this group, there is a SEPARATE property controlling it here, off by default, purely to avoid the cost of a per-cell event dispatch for hosts that never use it.
Can NEVER be delivered to the reg-free callback bridge -- SetEventCallbackEnabled(26, True) always raises an error explaining why (a live device context cannot cross that boundary); registered mode (registered mode) is the only way to receive it.
In the handler, hdc MUST be declared As OLE_HANDLE, not As Long, even though the two are binary-compatible -- VBA's event signature matcher checks the declared type NAME against the type library, and using Long instead raises a real compile-time error ('Procedure declaration does not match description of event or procedure having the same name').
hdc is only valid for the duration of this single call -- it must never be stored and used after the handler returns.
Fires once PER CELL, on every paint -- a handler that does anything expensive here can measurably slow scrolling; keep it lightweight.
See alsoEnableBeforeCellPaint, SetColumnRenderer
DataChangedEvents and the event callback bridge
A general-purpose 'the dataset changed' notification, alongside the more specific events.
Raised for four distinct triggers, collapsed into three changeType values: 0 (Loaded) from LoadFromArray and separately from EndStreaming closing out a BeginStreaming/AppendRows session; 1 (Cleared) from Clear(); and 2 (RowsChanged) from SetCellRange. It exists as a single, coarse hook a host can subscribe to once instead of several specific events (DataLoaded, AfterCellEdit, CellValueChanged, ...) when only 'something about the data changed' matters, not the specifics.
SignatureEvent DataChanged(changeType As Long)
Parameters
changeType (Long) - What kind of change happened: 0=Loaded, 1=Cleared, 2=RowsChanged.
ReturnsNone (event).
Value
Meaning
0
Loaded -- from LoadFromArray, or from EndStreaming closing a streaming session
None of the three triggering methods work in Virtual mode -- LoadFromArray and BeginStreaming/EndStreaming both refuse there, Clear() refuses there too (the host owns the data), and SetCellRange refuses there -- so DataChanged never fires in Virtual mode at all. In Turbo column-store mode the picture is narrower than in Embedded: Clear() works normally and fires changeType 1, but changeType 0 (Loaded) and changeType 2 (RowsChanged) do not fire in Turbo column-store -- Turbo column-store-mode LoadFromArray routes around the code that would fire changeType 0 (the same internal gap DataLoaded and LoadProgress have), and SetCellRange itself is refused outright in Turbo column-store mode. In practice, DataChanged in Turbo column-store mode is really only ever the Cleared (1) variant.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_DataChanged(ByVal changeType As Long)' Select Case changeType' Case 0: Debug.Print "Loaded"' Case 1: Debug.Print "Cleared"' Case 2: Debug.Print "RowsChanged"' End Select' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data ' fires DataChanged(0)
grid.Clear' fires DataChanged(1)
Gotchas
Fires ALONGSIDE the more specific events for the same change, not instead of them -- loading data fires both DataLoaded and DataChanged(0) for the same LoadFromArray call.
changeType 2 (RowsChanged) only comes from SetCellRange, a bulk-range write -- ordinary single-cell writes (SetCellValue, SetCell, an interactive edit) do not raise DataChanged at all.
During a BeginStreaming/AppendRows session, DataChanged(0) fires only once, from EndStreaming, at the end -- not once per AppendRows call.
In Turbo column-store mode, DataChanged effectively only ever reports changeType 1 (Cleared) -- changeType 0 (Loaded) never fires because Turbo column-store-mode LoadFromArray routes around the code that would fire it, and changeType 2 (RowsChanged) never fires because SetCellRange itself is refused in Turbo column-store mode. A handler that only watches for changeType 0 to know when a Turbo column-store-mode load finished will never see it -- use DataLoaded instead, keeping in mind DataLoaded has this same LoadFromArray-in-Turbo column-store gap (see its own gotchas).
See alsoDataLoaded, AfterCellEdit, SetCellRange
LoadProgressEvents and the event callback bridge
Fires repeatedly during LoadFromArray, once per internally-sized chunk.
Raised during LoadFromArray's own loop, once after each internally-sized chunk of rows has been copied in -- chunk size adapts on its own toward a target time-per-chunk, so the number of firings for a given load is not predictable from the row count alone. Even a load small enough to complete in a single chunk still fires this once, at the end of that one chunk, with rowsLoaded equal to the total. It fires DURING the load, before the load's own DataLoaded/DataChanged(0) events, which only fire once, after the whole call returns.
SignatureEvent LoadProgress(rowsLoaded As Long, totalRows As Long)
Parameters
rowsLoaded (Long) - Cumulative rows copied in so far.
totalRows (Long) - The total row count being loaded.
LoadProgress's only firing site is inside LoadFromArray's own chunked-loading loop -- it never fires anywhere else. LoadFromArray itself refuses outright in Virtual mode, so LoadProgress cannot fire there. In Turbo column-store mode, LoadFromArray immediately routes to a separate internal Turbo column-store load path before ever reaching that loop, so LoadProgress does not fire there either -- LoadProgress is effectively Embedded-only, even though LoadFromArray itself is supported in both Embedded and Turbo column-store.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_LoadProgress(ByVal rowsLoaded As Long, ByVal totalRows As Long)' StatusLabel.Caption = rowsLoaded & " / " & totalRows' DoEvents ' let the status label repaint during a long load' End SubDim bigData(0To14999, 0To2) AsVariantDim r AsLongFor r = 0To14999
bigData(r, 0) = r + 1
bigData(r, 1) = "Item " & r
bigData(r, 2) = r * 1.5Next r
grid.LoadFromArray bigData ' 15,000 rows -- exceeds the initial adaptive chunk size, so this fires LoadProgress more than once while loading
Reg-Free difference
VBA
Dim callbackHandler AsObject' in real code, a class module instance implementing OnGridEvent (shown above)
grid.SetEventCallback callbackHandler, 1
grid.SetEventCallbackEnabled 28, True' required -- LoadProgress is off by default for the bridge
Gotchas
OFF by default for the reg-free callback bridge (Tier B, high frequency) -- call SetEventCallbackEnabled 28, True to receive it through OnGridEvent.
The number of firings per load is not predictable from the row count -- chunk size self-tunes toward a target time budget per chunk, so a small load may fire this once and a large one many times, depending on how fast each chunk actually processes on this machine.
Embedded-only: LoadFromArray is this event's ONLY source, and while LoadFromArray itself works in Embedded and Turbo column-store alike, its Turbo column-store-mode path routes around the loop that fires this event entirely -- LoadProgress never fires in Turbo column-store mode, or in Virtual mode (where LoadFromArray itself is refused).
See alsoDataLoaded, DataChanged
BeforeContextMenuEvents and the event callback bridge
Fires on a cell-area right-click, before the built-in context menu opens, and can suppress it.
Raised on EVERY right-click that lands on the cell area (below the header) -- unlike BeforeHeaderMenu, this event's own firing is NOT gated by ShowBuiltinContextMenu at all; that property only controls whether the built-in menu is subsequently SHOWN after this event has already fired and been resolved. Setting Cancel = True suppresses just the built-in menu display for that click -- it does not stop the general-purpose ContextMenu event, which always fires afterward regardless of Cancel or ShowBuiltinContextMenu here.
SignatureEvent BeforeContextMenu(rowIndex As Long, columnIndex As Long, x As Long, y As Long, Cancel As Boolean)
Parameters
rowIndex (Long) - The 0-based view row under the pointer, or -1 if not over a data row.
columnIndex (Long) - The 0-based column under the pointer, or -1 if not over a column.
x (Long) - Screen-coordinate x.
y (Long) - Screen-coordinate y.
Cancel (Boolean (ByRef)) - Starts False. Set True to suppress the built-in cell context menu for this click.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforeContextMenu(ByVal rowIndex As Long, ByVal columnIndex As Long, _' ByVal x As Long, ByVal y As Long, Cancel As Boolean)' If rowIndex < 0 Then Cancel = True ' no menu when right-clicking empty space' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' a real right-click on a data cell fires BeforeContextMenu, then ContextMenu
Gotchas
Fires unconditionally for every cell-area right-click -- NOT gated by ShowBuiltinContextMenu the way BeforeHeaderMenu is gated by ShowHeaderMenu. ShowBuiltinContextMenu only controls whether the built-in menu is shown afterward, never whether this event itself fires.
Cancelling here suppresses only the built-in MENU display -- the general-purpose ContextMenu event fires afterward either way, unconditionally, regardless of Cancel or ShowBuiltinContextMenu.
Only fires for a right-click on the cell area (y at or below the header) -- a header-area right-click fires BeforeHeaderMenu instead, and only when ShowHeaderMenu is True.
See alsoContextMenu, BeforeHeaderMenu, ShowBuiltinContextMenu
BeforeHeaderMenuEvents and the event callback bridge
Fires on a header-area right-click, before the built-in header menu opens, and can suppress it.
Raised when a right-click lands on the header area, but ONLY when ShowHeaderMenu is True (its default) -- unlike BeforeContextMenu, whether this event fires AT ALL is gated by its own Show* property, not just whether the built-in menu is subsequently shown. Setting Cancel = True suppresses just the built-in header menu for that click; the general-purpose ContextMenu event still does NOT fire either way for that click -- firing BeforeHeaderMenu and firing ContextMenu are mutually exclusive for a header-area right-click, not sequential. With ShowHeaderMenu already False, this event is skipped entirely, and ContextMenu fires INSTEAD for that click.
SignatureEvent BeforeHeaderMenu(columnIndex As Long, Cancel As Boolean)
Parameters
columnIndex (Long) - The 0-based column whose header was right-clicked.
Cancel (Boolean (ByRef)) - Starts False. Set True to suppress the built-in header menu for this click.
ReturnsNone (event).
EmbeddedfiresVirtualfiresTurbo column-storefires
Not mode-dependent.
Example
VBA
' Registered (control named lgGrid on the form; handler on its name):' Private Sub lgGrid_BeforeHeaderMenu(ByVal columnIndex As Long, Cancel As Boolean)' If columnIndex = 0 Then Cancel = True ' no header menu for the Id column' End SubDim data(0To19, 0To7) AsVariantDim r AsLongFor r = 0To19
data(r, 0) = r + 1
data(r, 1) = "Item " & r
data(r, 2) = "Region " & (r Mod4)
data(r, 3) = r * 10.5
data(r, 4) = r Mod7
data(r, 5) = r + 9.99
data(r, 6) = DateSerial(2026, 1, 1) + r
data(r, 7) = (r Mod2 = 0)
Next r
grid.LoadFromArray data
' a real right-click on a column header fires BeforeHeaderMenu, then ContextMenu
Gotchas
Whether this event fires AT ALL is gated by ShowHeaderMenu (True by default) -- with it False, this event never fires for a header click.
Cancelling here (Cancel = True) suppresses only the built-in header MENU display -- it has no effect on ContextMenu, because ContextMenu was never going to fire for this click regardless of Cancel's value (the code path that would reach ContextMenu is never entered once BeforeHeaderMenu's own condition is met).
Only fires for a right-click on the HEADER area -- a cell-area right-click fires BeforeContextMenu instead.
There is no way to script a header-area right-click from VBA at all, so this event's own firing can only be observed by a real user actually right-clicking a header.
See alsoContextMenu, BeforeContextMenu, ShowHeaderMenu
RowsInsertedEvents and the event callback bridge
Fires after a row has been added to the grid, reporting where it landed.
Raised once per inserted row by AddRow and InsertRow, after the row exists and after any active tree or grouping projection has been rebuilt around it -- so the index it reports is already valid to pass straight back into GetCellValue, SelectRow or SetRowKey. It is a notification, not a veto: there is no Cancel, and by the time it fires the row is in. Bulk loads do not raise it -- LoadFromArray, LoadFromRecordset and the other whole-dataset loaders report through DataLoaded instead, because a load is one event about a dataset rather than N events about rows. Never fires in Virtual mode, where the host owns the rows and the grid has nothing to insert into.
Together with RowsDeleted and the row-key members (SetRowKey, GetChangedRows), this is the notification half of change tracking: the key members tell you what changed when you ask, this tells you when it changed without asking.
SignatureEvent RowsInserted(firstRow As Long, count As Long)
Parameters
firstRow (Long) - The 0-based view index of the first row inserted.
count (Long) - How many consecutive rows were inserted. Every current caller inserts one row at a time, so this is always 1 today; the parameter exists so a future batch insert can report a run without changing the event's shape.
count is always 1 from AddRow and InsertRow. Write the handler as a loop over firstRow to firstRow + count - 1 anyway, so a later batch insert does not silently skip rows.
firstRow is a VIEW index, and a sort or filter can move that row the moment anything else changes. If you need to remember the row, convert it to a key with GetRowKey inside the handler rather than storing the index.
Not raised by the bulk loaders. If you need one notification per row on a load, you are looking for the wrong event -- handle DataLoaded and read the rows you want.
See alsoRowsDeleted, BeforeRowDelete, SetRowKey, GetChangedRows, DataLoaded
RowsDeletedEvents and the event callback bridge
Fires after rows have been deleted, reporting them by KEY rather than by index.
Raised once per delete operation -- once for DeleteRow, and once for a whole DeleteSelectedRows call however many rows it removed -- after the rows are gone and the grid is consistent again. The payload is an array of the deleted records' keys, snapshotted while each row still existed.
It reports keys because an index would be meaningless by the time you received it: once a row is gone, its old index names whatever row moved up into the gap. That is why this event's shape differs from RowsInserted's.
The array carries one slot per deleted row. A row that was keyed -- with SetRowKey or KeyColumn -- contributes its key; a row that was never keyed contributes an empty slot, so count always equals both the number of rows removed and the array's length. That way a grid with no keys still tells you that rows went, even though it cannot say which; test a slot with IsEmpty before using it.
Firing does not require TrackChanges. Change tracking is a log you query; this is a notification you receive. Turning tracking on is not a precondition for hearing about deletions.
SignatureEvent RowsDeleted(keys As Variant, count As Long)
Parameters
keys (Variant) - A one-dimensional array with one element per deleted row, in the order they were erased. Each element is that row's key, or Empty if the row had no key.
count (Long) - The number of keys in the array. Always equal to the array's length.
Never fires in Virtual mode: DeleteRow and DeleteSelectedRows are both refused there, so there is nothing to report.
Example
VBA
' Registered (control named lgGrid on the form):' Private Sub lgGrid_RowsDeleted(ByVal keys As Variant, ByVal count As Long)' Dim i As Long' For i = LBound(keys) To UBound(keys)' If IsEmpty(keys(i)) Then' Debug.Print "a row was deleted (no key set)"' Else' Debug.Print "deleted record: " & CStr(keys(i))' End If' Next i' End Sub
grid.KeyColumn = 0' key every row off its first column
grid.SelectRow 2
grid.DeleteSelectedRows ' one RowsDeleted, carrying every key removed
Gotchas
Unkeyed rows arrive as Empty slots, not as missing entries. Check IsEmpty(keys(i)) before using a value, or set KeyColumn/SetRowKey so every row is identifiable.
DeleteSelectedRows fires ONCE, not once per row. Loop the array; do not assume a single key.
The keys arrive in erase order, which for DeleteSelectedRows in Embedded mode is bottom-up. That is the deletion order, not a sort -- do not read ordering into it.
BeforeRowDelete still fires per row beforehand and can cancel individual rows; a cancelled row never reaches this event.
See alsoRowsInserted, BeforeRowDelete, GetDeletedRows, SetRowKey, KeyColumn, TrackChanges
Row grouping
Row grouping collapses a flat result set under synthetic group header rows -- one header per distinct value in a chosen column, with that group's rows nested beneath it and collapsible. It is a display projection: the row store underneath is never touched, so GetRowCount keeps reporting the real number of data rows while grouping is active, and clearing the grouping restores the flat view exactly.
Grouping and the tree view are mutually exclusive -- activating one clears the other -- because both project the same row store into a different display order and there is only one projection.
Everything in this group is Embedded-mode only. In Turbo column-store and Virtual mode the two methods refuse by name rather than silently doing nothing; the GroupColumn property stays readable in every mode and simply reports -1 where grouping cannot be active.
GroupByColumnRow grouping
Group rows by one column, or clear grouping.
Groups the current rows by the distinct values of colIndex, inserting a synthetic header row above each group. Passing -1 clears grouping and returns the grid to its flat view. Activating grouping clears any active tree view, since the two projections cannot both be live. Because the header rows are synthetic, they occupy display positions that no data row owns -- see the gotchas below, which is where most grouping bugs come from.
SignatureSub GroupByColumn(colIndex As Long)
Parameters
colIndex (Long) - 0-based column to group by, or -1 to clear grouping and return to the flat view.
ReturnsNone. Raises an error in Turbo column-store and Virtual mode.
EmbeddedsupportedVirtualrefuses by nameTurbo column-storerefuses by name
Gotchas
Display indices stop matching data indices. Once grouping is active, the indices used by the click, selection and scroll paths count the synthetic header rows too, while GetRowCount still returns only real data rows. Convert through row keys (GetRowKey) rather than assuming an index means the same thing before and after grouping.
Group headers are not records. A header row has no cell values behind it, so treat a selection that lands on one as "no record selected" rather than reading cells from it.
Grouping is not printed as headers: PrintGrid resolves each printed row back to a storage row and skips anything that has none, so a grouped view prints its data rows without the group header lines.
Clearing with -1 discards the collapsed/expanded state. Re-grouping the same column starts fully expanded.
See alsoGroupColumn, ToggleGroupExpand, SetTreeParent, GetRowKey
ToggleGroupExpandRow grouping
Collapse or expand one group.
Flips the collapsed state of the group whose header sits at displayRowIndex. The index is a display index -- the position in the projected view including header rows -- not a data row index. Collapsing a group removes its data rows from the display projection, so every display index below it shifts; re-read positions after toggling rather than caching them.
SignatureSub ToggleGroupExpand(displayRowIndex As Long)
Parameters
displayRowIndex (Long) - Display-row position of the group header to toggle.
ReturnsNone. Raises an error in Turbo column-store and Virtual mode.
EmbeddedsupportedVirtualrefuses by nameTurbo column-storerefuses by name
Gotchas
Passing the index of an ordinary data row rather than a group header does nothing -- it is not an error, so a mistaken index fails silently. Verify you have a header position first.
Collapsing changes every display index after the collapsed group. Do not hold display indices across a toggle.
See alsoGroupByColumn, GroupColumn
GroupColumnRow grouping
Read which column grouping is currently applied to.
Returns the 0-based column index grouping is active on, or -1 when grouping is not active. Unlike the two methods above, this property is readable in every data mode -- it reports -1 in Turbo column-store and Virtual mode, where grouping cannot be activated at all -- so a host can query it without first checking DataMode.
SignatureProperty Get GroupColumn() As Long
ReturnsLong. The grouped column's 0-based index, or -1 if grouping is not active.
Licensing: trial state, activation, and license info
Every LiteGrid Turbo evaluation copy carries its own signed evaluation entitlement inside the OCX, fixing the issue date and the expiry date for that copy. That signed window is the sole authority for how long the evaluation runs: copying the file to another machine, user or folder does not start another one, and an OCX that carries no entitlement does not start a trial at all — it reports EXPIRED. A small local marker (a start date and a last-run date, nothing else) is still written to both the registry (HKCU\Software\LiteGridTurbo) and a file under %LOCALAPPDATA%\LiteGridTurbo, but only as clock-rollback evidence; it cannot create or extend an evaluation. While the evaluation is running, enforcement is entirely visual: nothing is refused, nothing is altered, and every method and property behaves exactly as it does when licensed.
Once the evaluation has expired, three things change. The grid is covered by an opaque panel carrying a purchase link. Mouse and keyboard interaction with the covered grid is swallowed silently -- no dialog, no error into the host's input path -- while focus, activation, sizing, DPI changes, accessibility and painting keep working normally, so the control stays a well-behaved child of Access or Excel. And ten operations that produce a durable artifact you could keep, ship or hand to another program behave differently, splitting by what reads the result:
Operations
On an expired evaluation
Marked
ExportToPdf, ExportToHTML, PrintGrid
The artifact is still produced, in full, and carries a short evaluation notice outside the data -- a banner above the table, a line in the page footer margin. Row count, column layout and page count are identical to licensed output, so anything that reads the table still reads the same table.
The call fails with one specific, always-identical error naming the licence state, and changes nothing: no file is created, the clipboard still holds whatever it held, and out-parameters come back empty.
The split is by data semantics, not file type. Marking something a person reads is honest; injecting a notice into machine-readable output would be indistinguishable from a bug in whatever parses it next, so those calls refuse instead of handing back data you could not trust. ExportToXlsx is refused rather than marked for exactly that reason -- there is no way to mark a workbook that leaves Worksheets(1) and every positional reference meaning what they meant.
Registering an event callback (SetEventCallback, SetEventCallbackMacro) is also refused, for the same reason as the refused exports: a callback is a data channel. Delivery of an already-registered callback is not affected, and ClearEventCallback* and the read-only callback getters are never gated.
Nothing else is affected: programmatic loading, sorting, filtering, formatting and every mode call keep working on an expired evaluation, and no property accessor is ever gated, so design-time property persistence is identical in every licence state. ActivateLicense removes the cover, the input gate and everything above, on the same control, with no recreation.
The seven read-only members in this group (LicenseStatus, TrialDaysRemaining, LicenseState, LicenseType, LicenseExpiry, LicenseVersion, and GetLicenseInfo) exist to let a host read that state and act on it deliberately -- showing its own reminder, logging it, deciding whether to prompt for a key -- not to block anything.
ActivateLicense(company, key) is the only member in this group that can genuinely fail: it accepts a signed activation string (an EVAL, PRO, or ENT key issued for product code LG) and either applies it or raises an error for a key that does not validate -- wrong signature, wrong product, or malformed text. A real key is never something to hard-code: it is customer- and company-name-specific, and it must never appear in shared source code, sample projects, or documentation. Every example and test below uses the literal string "PLACEHOLDER-KEY-NEVER-A-REAL-ONE", which is guaranteed to fail validation the same way any other malformed string does -- it exists purely to exercise ActivateLicense's real, reproducible rejection path.
Activation is session-only and process-wide: a successful call updates one shared, in-memory license state used by every LiteGrid Turbo control in the process (registered or reg-free, one grid or twenty, as long as they were created from the same loaded OCX file), and nothing about the activation itself is written to disk -- only a small local clock-evidence marker persists, and it never grants, extends or shortens an evaluation. A new process (a new Access or Excel session, a new instance of the host application) starts back in the state described by the evaluation entitlement signed into its OCX, and ActivateLicense must be called again to reapply a key. A failed ActivateLicense call changes nothing: every other member in this group keeps reporting exactly the state it reported before the call.
License state is control-instance-independent and mode-independent -- it lives entirely outside the grid's data, so Clear, LoadFromArray, and switching DataMode never touch it, and all eight members here behave identically in Embedded, Virtual, and Turbo column-store mode.
RecipeRead the current license state without assuming which one it is
VBA
Sub ReadLicenseState(grid AsObject)
' Do not assume which state you are in: this copy's signed evaluation' window may already have closed, and a build with a key already applied' earlier in this same process may already be fully licensed.Dim st AsString, days AsLong, stateCode AsLong
st = grid.LicenseStatus ' "TRIAL", "EXPIRED", "EVAL", "PRO" or "ENTERPRISE"
days = grid.TrialDaysRemaining ' 0 once expired or once any key is applied
stateCode = grid.LicenseState ' 0=Valid, 1=Trial, 2=Expired, 3=InvalidSelectCase stateCode
Case0Debug.Print"Licensed (" & grid.LicenseType & "), version " & grid.LicenseVersion
Case1Debug.Print"Trial, " & days & " day(s) remaining"Case2Debug.Print"Evaluation expired -- grid is covered and not interactive"EndSelectDebug.Print grid.GetLicenseInfo()
EndSub
LicenseStatus, LicenseState, LicenseType and TrialDaysRemaining always agree with each other -- they read from the same underlying state -- so a host can pick whichever shape (string, integer code, or the full JSON from GetLicenseInfo) is most convenient for its own logic rather than parsing LicenseStatus's string.
RecipeActivate once per process launch, and handle a rejected key
VBA
Sub ActivateOnStartup(grid AsObject, companyName AsString, signedKey AsString)
OnErrorResumeNextErr.Clear
grid.ActivateLicense companyName, signedKey
IfErr.Number <> 0ThenDebug.Print"Activation failed: " & Err.Description
' A rejected key changes nothing: the grid keeps the licence state it had' (an expired evaluation stays covered) until a valid key is supplied.ElseDebug.Print"Activated: " & grid.GetLicenseInfo()
EndIfOnErrorGoTo0EndSub
Activation is session-only and shared by every LiteGrid Turbo control in the process, so calling this once per process launch (for example from the startup form) is enough to cover every grid the host creates afterward -- but it must be called again the next time the process starts. Never store a real signed key in shared source code; read it from a per-deployment configuration source instead.
LicenseStatusLicensing: trial state, activation, and license info
The current license state as a short, human-readable string.
Reports the control's current license state as one of five strings: "TRIAL" (this copy's signed evaluation window is running), "EXPIRED" (that window closed, or no entitlement is embedded at all, or an activated EVAL key expired, and no other key has replaced it), "EVAL" (an activated, time-limited evaluation key), "PRO" or "ENTERPRISE" (an activated perpetual key). The value comes from the same underlying state as LicenseState, LicenseType and TrialDaysRemaining -- all four always agree, so a host can use whichever shape is most convenient rather than treating this string as the single source of truth. The trial/expired distinction is decided the first time anything in the process reads any license member (or the control window is created), from the signed entitlement embedded in the OCX; reading it repeatedly does not force a full re-check, but the expiry comparison is re-checked whenever licence state is read or the control checks whether interaction is allowed, so a session left running across the expiry instant does transition to EXPIRED without a restart.
SignatureProperty Get LicenseStatus As String
ReturnsString: "TRIAL", "EXPIRED", "EVAL", "PRO", or "ENTERPRISE".
Default"TRIAL" for an evaluation copy whose signed window is still open and which has had no key activated in this process; "EXPIRED" once that signed window has closed, and also for any OCX that carries no evaluation entitlement at all.
Value
Meaning
TRIAL
The evaluation is running, inside the window signed into this copy.
EXPIRED
The trial ended, or an activated EVAL key's own expiry has passed, and no other key is currently applied.
EVAL
An activated, time-limited evaluation key is applied and has not yet expired.
Identical in every DataMode -- license state is process-wide and has nothing to do with the grid's data or mode.
Example
VBA
Dim status AsString
status = grid.LicenseStatus
Debug.Print status
' -> "TRIAL" while this copy's signed evaluation window is open and no key' has been activated in this process; could also read "EXPIRED" if that' window has already closed (the grid is then covered and not interactive).
Gotchas
The IDL helpstring for this member (and the equivalent Object Browser text) lists only "TRIAL, EXPIRED, PRO, ENTERPRISE" -- it omits "EVAL", which is a real, distinct fifth value returned after activating a time-limited evaluation key. Do not hard-code a 4-way match against this string.
Reads as "EXPIRED" the moment an activated EVAL key's own expiry passes, with no restart required -- a live EVAL license genuinely turns into EXPIRED partway through a long-running process, not just for a fresh trial that ran out.
An "EXPIRED" reading never causes a property on this control to fail, and never causes a programmatic load, sort, filter or format call to fail. What it does affect is the cover the control paints over itself, interactive mouse/keyboard use of the covered grid, event-callback registration, and the ten file/clipboard operations listed above.
See alsoLicenseState, LicenseType, TrialDaysRemaining, GetLicenseInfo, ActivateLicense
TrialDaysRemainingLicensing: trial state, activation, and license info
Whole days left in this copy's signed evaluation window, or 0 once expired or licensed.
Reports how many whole days remain in this copy's signed evaluation window, computed from the entitlement's own expiry date (rounded up, so any part of the current day still counts as a full day remaining). Reads 0 once the window has closed, 0 for an OCX carrying no entitlement, and 0 for any activated key -- it does not report an EVAL key's own remaining time; GetLicenseInfo's daysRemaining field does that separately for an activated EVAL.
SignatureProperty Get TrialDaysRemaining As Long
ReturnsLong: whole days remaining in this copy's signed evaluation window, or 0 if that window has closed, no entitlement is present, or a key is activated. The usual issued length is 30 days, but the window is whatever was signed into the copy you were sent.
Defaultthe number of whole days between now and the expiry date signed into this copy -- typically 30 on a freshly issued evaluation, and correspondingly fewer on one issued earlier. 0 once the signed window has closed, and 0 for an OCX carrying no entitlement.
Dim days AsLong
days = grid.TrialDaysRemaining
Debug.Print days
' -> whole days left in this copy's signed window; 0 if that window is' expired, or if a key has been activated in this process
Gotchas
Reads 0 for every activated license (EVAL, PRO, or ENTERPRISE), not just an expired trial -- do not treat 0 as proof the trial has expired without also checking LicenseStatus or LicenseState.
The count rounds up to whole days against this copy's signed expiry, so any part of the current day counts as a day remaining.
A reading of 0 on an expired evaluation comes with the restrictions described at the top of this group (cover, input gate, refused exports and callback registration); no property accessor ever fails.
See alsoLicenseStatus, LicenseState, GetLicenseInfo
ActivateLicenseLicensing: trial state, activation, and license info
Apply a signed activation key, or raise an error if it does not validate.
Validates key as a signed activation string for product code "LG" and, if it validates, applies it to every LiteGrid Turbo control in this process from this call onward and repaints to remove the evaluation cover or notice. If key does not validate -- wrong signature, wrong product code, malformed text, or an already-expired EVAL key -- the call raises an error and changes nothing: every license property keeps reporting exactly the state it reported before the call. company has NO observable effect on activation at all -- it is not recorded anywhere, not validated against the key, and not what GetLicenseInfo's own "company" field reports (that field is populated entirely from the customer name embedded and signed inside the key itself). An empty, garbage, or entirely wrong company string activates exactly as successfully as the correct one, provided the key itself is genuine. Pass the real company name anyway for readability in your own code -- it is simply not read back by anything on this control. Activation applies for the lifetime of the current process only -- nothing about the key itself is written to disk, and the next process launch starts back from whatever the evaluation entitlement embedded in the OCX describes, requiring another call to ActivateLicense.
A real signed key is never something to place in shared source code, a checked-in sample project, or documentation -- it is customer- and company-name-specific and must be supplied from a per-deployment configuration source. Every example here uses the literal placeholder "PLACEHOLDER-KEY-NEVER-A-REAL-ONE", which is guaranteed to fail validation for the same reason any other non-signed text does.
SignatureSub ActivateLicense(company As String, key As String)
Parameters
company (String) - Not recorded or validated anywhere -- has zero observable effect on activation or on any later license property, including GetLicenseInfo's own "company" field, which comes entirely from the customer name signed inside the key itself. Pass the real company name for readability in your own code; nothing on this control reads it back.
key (String) - A signed activation string issued for product code "LG" (an EVAL, PRO, or ENT key). Never hard-code a real one.
ReturnsNone. Raises an error if key does not validate as a genuine, unexpired "LG" activation string.
Identical in every DataMode -- activation is process-wide state, unrelated to any one control's data or mode.
Example
VBA
OnErrorResumeNextErr.Clear
grid.ActivateLicense "Acme Inc", "PLACEHOLDER-KEY-NEVER-A-REAL-ONE"IfErr.Number <> 0ThenDebug.Print"Rejected: " & Err.Description
' -> "Rejected: Invalid license key. Verify company name and key are correct."EndIfOnErrorGoTo0' A real key comes from your license issuer, never hard-coded in shared source.
Gotchas
A failed call leaves every license property completely unchanged -- LicenseStatus, LicenseState, LicenseType, TrialDaysRemaining, LicenseExpiry, LicenseVersion and GetLicenseInfo all keep reporting exactly what they reported before the failed call.
Success is shared process-wide: activating on one control instance (registered or reg-free) applies to every other LiteGrid Turbo control already created or created later in the same process, not just the instance ActivateLicense was called on.
Not persisted: nothing about a successful activation survives the process exiting. The next process launch must call ActivateLicense again, or the control falls back to the state described by the evaluation entitlement signed into its OCX.
Never place a real signed key in shared source code or documentation -- read it from a per-deployment configuration source (an INI file, a registry value the installer writes, etc.) instead.
See alsoLicenseStatus, LicenseState, LicenseType, GetLicenseInfo
LicenseStateLicensing: trial state, activation, and license info
The current license state as an integer code, for simple branching.
Reports the same underlying state as LicenseStatus, as a small integer instead of a string: 0 for any activated, valid license (EVAL, PRO, or ENTERPRISE), 1 for an active trial, 2 for an expired trial or an expired EVAL key. 3 ("Invalid") is reserved for a defensive fallback and is not reached by any state this control's licensing logic actually produces in practice.
Dim stateCode AsLong
stateCode = grid.LicenseState
SelectCase stateCode
Case0: Debug.Print"Valid"Case1: Debug.Print"Trial"Case2: Debug.Print"Expired"EndSelect' -> "Trial" while the evaluation window is open; "Expired" once it has closed
Gotchas
0 (Valid) covers all three activated tiers alike -- EVAL, PRO, and ENTERPRISE are not distinguished here; read LicenseType or GetLicenseInfo's tier field to tell them apart.
Reading 2 (Expired) comes with the restrictions described at the top of this group; no property accessor ever fails.
See alsoLicenseStatus, LicenseType, GetLicenseInfo
LicenseTypeLicensing: trial state, activation, and license info
The current license tier as a lowercase string, unaffected by trial/expired.
Reports the license tier as a lowercase string: "trial", "eval", "pro", or "enterprise", with "none" as a defensive fallback that is not reached in practice. Unlike LicenseStatus, this does not distinguish an active trial from an expired one -- both read "trial" here, since the tier (what kind of license this is) and the state (whether it is currently valid) are reported separately: use LicenseStatus or LicenseState for the valid/expired distinction.
SignatureProperty Get LicenseType As String
ReturnsString: "trial", "eval", "pro", or "enterprise".
Default"trial" for any unactivated control, whether its trial is still active or has already expired.
Value
Meaning
trial
The embedded evaluation entitlement -- reported whether its signed window is still counting down or has already closed, and also when no entitlement is present.
Dim licType AsString
licType = grid.LicenseType
Debug.Print licType
' -> "trial" for any unactivated control, whether the trial is still running' or has already expired -- LicenseType alone does not tell the two apart
Gotchas
The IDL helpstring for this member lists only "trial, pro, enterprise, none" -- it omits "eval", a real, distinct value returned after activating a time-limited evaluation key.
Reads "trial" for both an active AND an already-expired trial -- do not use this property alone to decide whether the trial is still usable; pair it with LicenseStatus, LicenseState, or TrialDaysRemaining.
GetLicenseInfo's JSON has its own, differently-formatted tier field for the same information ("Trial", "EVAL", "PRO", "ENT" -- capitalized, and "ENT" rather than "enterprise"): the two are not the same string for the same state, so do not compare them for equality.
See alsoLicenseStatus, LicenseState, GetLicenseInfo
LicenseExpiryLicensing: trial state, activation, and license info
The date this license (trial or activated) stops being valid, or 0 for a perpetual license.
Reports the date this license becomes invalid. For an evaluation copy (active or already expired), this is the exact expiry date signed into that copy -- a real date, never the 0 sentinel, and identical on every copy of the same issued file. 0 is reserved for a perpetual license (an activated PRO or ENT key), and is also what an OCX carrying no entitlement reports, since there is no window to name. For an activated, time-limited EVAL key, this is that key's own signed expiry date.
SignatureProperty Get LicenseExpiry As Date
ReturnsDate: the license's expiry date, or 0 (which VBA displays as 12/30/1899) for a perpetual license.
Defaultthe exact expiry date signed into this evaluation copy -- NOT 0 -- for an unactivated evaluation copy, whether that window is still open or has already closed. An OCX carrying no evaluation entitlement reports 0.
Dim expiry AsDate
expiry = grid.LicenseExpiry
Debug.Print expiry <> 0' -> True for an evaluation copy, running or expired; a perpetual PRO/ENT' license, or an OCX with no evaluation entitlement, reports 0
Gotchas
Do not assume 0 for an ordinary, unactivated trial -- a running or already-expired trial both report a real, non-zero date: the trial's own end date, in the past once expired. The IDL helpstring's "0 = no expiry / perpetual" applies to the perpetual (PRO/ENT) case and, separately, to an OCX that carries no valid evaluation entitlement (for example a copy that was never personalized) -- see this member's own description.
For an already-expired trial, this still reports the (now past) date the trial ended -- it does not reset to 0 or to today's date.
See alsoLicenseStatus, TrialDaysRemaining, GetLicenseInfo
LicenseVersionLicensing: trial state, activation, and license info
A version string associated with the current license state.
Reports a short version string tracked alongside the license state -- "1.0" for the trial (active or expired), and "1.0+" once a key has been successfully activated in this process. It is not the product's file or build version; it exists as a simple, stable field for support and diagnostics to read alongside the rest of this group.
SignatureProperty Get LicenseVersion As String
ReturnsString, non-empty. "1.0" for the trial; "1.0+" after a successful activation in this process.
Default"1.0" for any unactivated control, whether the trial is active or expired.
Debug.Print grid.LicenseVersion
' -> "1.0" for any unactivated control
Gotchas
This is a licensing-state version tag, not the control's product or build version -- do not use it to detect the installed LiteGrid Turbo version.
See alsoLicenseStatus, GetLicenseInfo
GetLicenseInfoLicensing: trial state, activation, and license info
Return every license fact as one flat JSON object.
Returns a single flat JSON object with the complete license picture in one call, informational only -- it never includes any credential material (the activation key itself is never echoed back). The keys, in the order they appear, are: schemaVersion (always 1), tier (a differently-formatted string from LicenseType's own value -- see gotchas), product (always "LG"), company (the company name signed into the active entitlement: an activated key's customer, or "Evaluation" for an evaluation copy; empty when no entitlement is present), expiryUtc (an ISO-8601 UTC timestamp string, or JSON null for a perpetual license), isPerpetual (a JSON boolean), daysRemaining (a JSON number for the trial or an activated EVAL, or JSON null for a perpetual license), and state (a third, differently-formatted string for the same status LicenseStatus and LicenseState already report).
SignatureFunction GetLicenseInfo() As String
ReturnsA JSON string (a flat object) with the keys schemaVersion, tier, product, company, expiryUtc, isPerpetual, daysRemaining, and state.
Value
Meaning
tier
"Trial" (both active and expired trial), "EVAL", "PRO", or "ENT" -- NOT the same strings LicenseType returns.
state
"trial", "expired", "valid", or "unknown" -- lowercase, NOT the same casing LicenseStatus returns.
Dim json AsString
json = grid.GetLicenseInfo()
Debug.Print json
' -> {"schemaVersion":1,"tier":"Trial","product":"LG","company":"Evaluation",' "expiryUtc":"2026-09-26T00:00:00Z","isPerpetual":false,' "daysRemaining":30,"state":"trial"} (exact expiryUtc/daysRemaining' vary with the evaluation window signed into this copy)
Gotchas
tier and state are each their OWN, independently-formatted strings -- neither matches LicenseType's or LicenseStatus's own casing/spelling for the identical underlying state (for example, an unactivated trial reads LicenseType="trial" but this object's tier="Trial"; LicenseStatus="TRIAL" but this object's state="trial"). Parse whichever field you standardize on; do not compare two of these three surfaces for string equality.
tier uses "ENT" for an activated Enterprise key, not "enterprise" or "ENTERPRISE" as the other two surfaces (LicenseType, LicenseStatus) spell it.
expiryUtc and daysRemaining are independently null-or-not: expiryUtc is null only for a perpetual license; daysRemaining is null for a perpetual license too, but is a real number (including 0) for the trial in both its active and expired states.
Contains no credential material of any kind -- safe to log or display as-is, unlike the activation key passed to ActivateLicense.
See alsoLicenseStatus, LicenseState, LicenseType, LicenseExpiry, TrialDaysRemaining
The standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
ILiteGridTurboHelper is a separate, non-visual COM object -- not a member of the grid control. It ships in the same OCX and is created independently (CreateObject("LiteGridTurbo.LiteGridTurboHelper"), or the registration-free factory below); nothing in this group requires a grid to exist, and nothing here is affected by any grid's DataMode. Use it anywhere a VBA project needs to read, build or reshape JSON text -- an HTTP response, a config file, a payload you are about to send somewhere -- independent of whether a LiteGrid Turbo control is on the form at all.
The single most important thing to understand before using this object: it is stateless. Every one of the 18 methods takes the complete JSON text you are working with as an ordinary [in] BSTR parameter, and every method that changes something returns a brand-new JSON string as its result -- there is no internal document the Helper is building up for you call by call. Calling jh.SetValue(myJson, "total", 100) does not change myJson; it returns a new string that you must capture (myJson = jh.SetValue(myJson, "total", 100)) if you want the change to stick. Two calls in a row against the same jh instance are completely independent of each other -- nothing carries over except that both happen to run inside the same object. This is a different shape than a typical "open a document, mutate it in place, save it" JSON library, and it is worth internalizing before writing anything more than a single call.
Paths use dot notation for object keys and bracket notation for array indexes, e.g. customer.orders[0].total. A path may also start directly with a bracket for a root-level array ([0].name). Segments are separated by .; a leading dot is accepted and silently ignored (.customer.name parses identically to customer.name), but two dots in a row, or a trailing dot, is a path syntax error. Chaining brackets directly (matrix[0][1]) indexes into a nested array-of-arrays -- it does not mean "index 1 of the same key again". Bracket indexes must be plain non-negative integers (no negative indexes, and no */?/: wildcards -- all three are reserved and currently rejected) up to 999,999, and a path may hold at most 32 segments total; anything beyond either limit is a path syntax error, not a truncation. These rules are the same across every method in this group that takes a path parameter.
The 18 methods split into three genuinely different error-handling behaviors, and this split is the second most important thing to know before relying on any of them in production code. SetValue, RemoveKey, Append, Insert, Clear, Merge, PrettyPrint, Flatten and Unflatten never raise a VBA error on a bad path, malformed JSON, wrong container shape or an oversized input -- on internal failure they silently return your original input string, completely unchanged, and there is no member anywhere on this interface that reports what went wrong. The only way to notice that one of these nine calls did nothing is to compare its result to what you passed in, or to independently re-check the JSON afterward with Exists or GetValue. Count, ToArray and Serialize are the opposite: they raise an ordinary, catchable VBA runtime error (On Error/Err.Number) on failure -- though even here, Err.Description is not reliably the specific reason, since nothing on this interface attaches a description to that error; only Err.Number (the underlying HRESULT) is dependable. GetValue, Exists, IsValid, GetArrayLength, GetArrayItem and GetKeys are read-only lookups that never raise an error under any input at all -- a missing path or invalid JSON gets you a neutral, safe result (Null, False, 0, an empty string, or "[]", depending on the method) rather than a failure of any kind.
Creating and using the Helper is not affected by trial status -- there is no license-related refusal mode on any of these 18 methods, at any time. The grid control has its own, separate whole-document JSON round-trip (LoadFromJson/ExportToJson) for loading and exporting an entire dataset; this Helper is for working with arbitrary JSON text a piece at a time and does not require, and is not related to, that grid-level import/export.
RecipeRead a value out of a JSON API response without assuming it is there
VBA
PublicSub Recipe_ReadApiResponse(responseJson AsString)
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
IfNot jh.Exists(responseJson, "customer.name") ThenDebug.Print"No customer.name in this response."ExitSubEndIfDim custName AsVariant
custName = jh.GetValue(responseJson, "customer.name")
Debug.Print"Customer: " & CStr(custName)
' GetValue on a missing path returns Null rather than raising an error,' so checking Exists first (or IsNull afterward) avoids a type-mismatch' surprise when the field genuinely is not present.Dim discount AsVariant
discount = jh.GetValue(responseJson, "customer.discountPct")
If IsNull(discount) Then discount = 0EndSub
GetValue never raises an error for a missing path -- it returns Null -- so code that reads optional fields should either check Exists first or explicitly handle IsNull(result) rather than assuming a value came back.
RecipeBuild a JSON payload from VBA variables
VBA
PublicFunction Recipe_BuildPayload(orderId AsLong, customerName AsString, total AsDouble) AsStringDim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim payload AsString
payload = jh.Serialize(orderId) ' "501" -- still just a JSON number, not an object yet
payload = "{}"' start from an empty object instead
payload = jh.SetValue(payload, "orderId", orderId)
payload = jh.SetValue(payload, "customer.name", customerName)
payload = jh.SetValue(payload, "total", total)
Recipe_BuildPayload = payload
' -> {"orderId":501,"customer":{"name":"Ada"},"total":249.5}EndFunction
SetValue auto-creates any object or array a path passes through that does not exist yet -- writing to "customer.name" against "{}" creates the nested "customer" object for you. Each call returns a new string, so the pattern is always payload = jh.SetValue(payload, ...), reassigning the variable every time.
RecipeMerge a user's partial edit into a base configuration
VBA
PublicFunction Recipe_MergeUserEdit(baseConfigJson AsString, userEditJson AsString) AsStringDim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
' deep:=True so a partially-edited nested object (e.g. only "theme.color"' changed) merges key-by-key instead of replacing the whole "theme" object.
Recipe_MergeUserEdit = jh.Merge(baseConfigJson, userEditJson, True)
EndFunction
Merge never raises an error: if either argument is not a JSON object at the top level, it silently returns the first argument unchanged. Always verify the result actually changed (or check IsValid/Exists against it) before assuming the merge took effect, especially when userEditJson comes from an external source.
RecipeNotice when a fail-quiet mutator silently did nothing
VBA
PublicSub Recipe_DetectNoOp(configJson AsString)
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim updated AsString
updated = jh.SetValue(configJson, "settings.retryCount", 5)
If updated = configJson Then' SetValue never raises an error on failure -- an unchanged string is' the only signal. This particular call only fails this way for a' root that is not itself an object or array (e.g. configJson = "5").Debug.Print"SetValue made no change -- configJson was not a JSON object/array."Else
configJson = updated
EndIfEndSub
Every fail-quiet method in this group (SetValue, RemoveKey, Append, Insert, Clear, Merge, PrettyPrint, Flatten, Unflatten) can be checked the same way: compare the result to the input. There is no other way to learn that one of these calls declined to do anything.
RecipeFlatten nested JSON into a simple two-column key/value list
VBA
PublicSub Recipe_FlattenForDisplay(sourceJson AsString)
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim flatJson AsString
flatJson = jh.Flatten(sourceJson)
' {"a":{"b":1},"c":[10,20]} -> {"a.b":1,"c[0]":10,"c[1]":20}Dim keysJson AsString
keysJson = jh.GetKeys(flatJson, "")
Dim n AsLong
n = jh.Count(flatJson, "")
Dim i AsLongFor i = 0To n - 1Dim k AsString
k = jh.GetArrayItem(keysJson, "", i) ' e.g. "a.b", then "c[0]", then "c[1]"Debug.Print k & " = " & CStr(jh.GetValue(flatJson, k))
Next i
EndSub
Flatten turns any nested structure into a single-level object whose keys are the original dot/bracket paths, which pairs naturally with GetKeys and GetArrayItem to walk every value with a simple loop -- convenient for populating a two-column key/value grid or list box.
RecipeValidate JSON before doing anything else with it
VBA
PublicFunction Recipe_TryParse(candidateJson AsString) AsBooleanDim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Recipe_TryParse = jh.IsValid(candidateJson)
' IsValid never raises an error, even for an empty string or garbage text' -- it simply returns False. Call it before GetValue/SetValue/etc. when' the JSON came from outside your own code (a file, a web request).EndFunction
IsValid is a pure syntax check with no side effects and no failure mode of its own, making it a safe first call on any JSON text of unknown origin before handing it to the rest of this group.
RecipeConvert a JSON array of objects into a 2-D array for display
VBA
PublicFunction Recipe_ToDisplayArray(rowsJson AsString) AsVariantDim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
OnErrorResumeNextDim arr AsVariant
arr = jh.ToArray(rowsJson)
IfErr.Number <> 0Then' Unlike the fail-quiet mutators, ToArray genuinely raises an error --' e.g. rowsJson is not a JSON array of objects, or it is empty.Debug.Print"ToArray failed, Err.Number=" & Err.Number
Err.ClearOnErrorGoTo0
Recipe_ToDisplayArray = EmptyExitFunctionEndIfOnErrorGoTo0
Recipe_ToDisplayArray = arr ' row 1 = headers, rows 2..N+1 = dataEndFunction
ToArray is one of the three methods in this group that raises a real, catchable error rather than failing quietly, so wrapping it in On Error is required if the input's shape is not already guaranteed -- unlike the fail-quiet mutators, there is no unchanged-string signal to check instead.
RecipeGrow an array safely: append, insert, and read items back
VBA
PublicSub Recipe_WorkWithArrays()
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim tagsJson AsString
tagsJson = "{""tags"":[""a"",""b""]}"
tagsJson = jh.Append(tagsJson, "tags", """c""") ' -> tags: [a,b,c]
tagsJson = jh.Insert(tagsJson, "tags", 0, """first""") ' -> tags: [first,a,b,c]Dim n AsLong
n = jh.GetArrayLength(tagsJson, "tags")
Dim i AsLongFor i = 0To n - 1Debug.Print jh.GetArrayItem(tagsJson, "tags", i)
Next i
EndSub
Append and Insert both take the new value already encoded as JSON text (a quoted string for a string value, as shown here), not a bare VBA value -- use Serialize first if the value is not already a JSON literal. GetArrayLength and GetArrayItem then read the result back without needing to parse the whole array yourself.
GetValueThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return the typed value at a path, or Null if the path is missing.
Resolves path against json and returns the value it finds as a natively-typed Variant: a JSON string becomes a VBA String, true/false becomes Boolean, a JSON number becomes Long (VT_I4) when it parses as a plain integer or Double (VT_R8) otherwise, and null becomes VBA Null. A JSON object or array found at the path is returned as a String containing the raw JSON text for that sub-value -- it is not converted into a VBA object or array, so a further GetValue/GetArrayItem/etc. call against that returned string is how you go deeper. An empty json string, or a path that does not resolve (a missing key, an out-of-range index, or a path that walks through the wrong container type), returns Null rather than raising an error; passing an empty path returns the value at the document root.
SignatureFunction GetValue(json As String, path As String) As Variant
Parameters
json (String) - The complete JSON text to read from.
path (String) - Dot/bracket path to the value, e.g. "customer.orders[0].total". An empty string means the whole document.
ReturnsVariant: a typed value (String/Long/Double/Boolean), Null for a missing path or empty json, or a String of raw JSON text for an object/array value.
ILiteGridTurboHelper has no DataMode of its own -- it is a separate COM object and behaves identically whether or not any grid exists, and regardless of any grid's current mode.
Example
VBA
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim v AsVariant
v = jh.GetValue("{""customer"":{""name"":""Ada"",""age"":36}}", "customer.name")
Debug.Print v ' -> Ada
v = jh.GetValue("{""customer"":{""name"":""Ada""}}", "customer.missing")
Debug.Print IsNull(v) ' -> True
Reg-Free difference
VBA
' Registration-free creation of the Helper (separate from the grid's own' reg-free factories). Note the real exported signature takes the output' object BYREF and returns an HRESULT as a Long, so it is called as a' statement -- not Set jh = LiteGridTurbo_CreateHelper() -- mirroring how' LiteGridTurbo_CreateControlInParent is called elsewhere in this Cookbook.#If Win64 ThenPrivateDeclarePtrSafeFunction LiteGridTurbo_CreateHelper _
Lib"LiteGridTurbo_x64.ocx" (ByRef ppDisp AsObject) AsLong#ElsePrivateDeclarePtrSafeFunction LiteGridTurbo_CreateHelper _
Lib"LiteGridTurbo_x86.ocx" (ByRef ppDisp AsObject) AsLong#End IfDim jh AsObject
LiteGridTurbo_CreateHelper jh
Dim v AsVariant
v = jh.GetValue("{""customer"":{""name"":""Ada""}}", "customer.name")
Debug.Print v ' -> Ada
Gotchas
An object or array value comes back as a JSON-text String, not a VBA object/array -- call GetValue, GetArrayItem, ToArray, etc. again against that returned string to read further into it.
A missing path and a path whose value is genuinely JSON null are indistinguishable -- both return VBA Null. Use Exists first if you need to tell "absent" from "present but null" apart.
Internally this method locates the value using the JSON text's first embedded NUL character as an end-of-string marker rather than the BSTR's own recorded length. Ordinary JSON text from a file, a web response, or a VBA string literal never contains an embedded NUL, so this has no practical effect in normal use -- it only matters if json was built by concatenating in a Chr$(0) deliberately.
See alsoExists, SetValue, GetArrayItem, ToArray
SetValueThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string with value set at path, creating containers as needed.
Returns a new JSON string equal to json with the value at path created or overwritten. value is converted to JSON the same way Serialize converts a VARIANT. Any object or array a path passes through that does not exist yet is created automatically -- SetValue("{}", "customer.name", "Ada") creates the "customer" object for you. Auto-created arrays are padded with JSON null up to the target index, but the documented 9,999-entry padding cap is only enforced when overwriting an already-existing non-array value or extending an array that already exists at the target segment; creating a brand-new array via a path whose key does not exist yet (the common case, e.g. SetValue("{}", "items[50000]", "x")) is NOT capped and will happily create a 50,001-element array. Passing an empty path replaces the entire document with value's JSON, root and all. If the existing value at an intermediate step is not the container type the rest of the path needs (e.g. a segment expects an object but finds a string), that value is silently overwritten with a freshly created subtree rather than treated as an error -- the sole exception is a root that is a JSON array while the first path segment is a plain key, which fails outright. On any internal failure (a path syntax error, a root that is not valid JSON, or 256-level nesting exceeded), SetValue returns json completely unchanged; it never raises a VBA error.
SignatureFunction SetValue(json As String, path As String, value As Variant) As String
Parameters
json (String) - The JSON text to base the result on.
path (String) - Where to write. An empty string replaces the entire document.
value (Variant) - The VBA value to write, converted to JSON the same way Serialize does.
ReturnsString: the new JSON document, or json unchanged on failure.
SetValue never raises a VBA error on failure -- a bad path or invalid root JSON silently returns json unchanged. Compare the result to the input if you need to know whether anything actually happened. Note the 9,999-entry auto-create padding cap is inconsistently enforced (see description) -- do not rely on it as a general safety limit against a mistyped huge array index.
The result is a brand-new string; SetValue never modifies json itself. You must reassign your variable (json = jh.SetValue(json, ...)) to keep the change.
A type conflict along the path (e.g. writing "a.b" when "a" is currently the number 5) silently replaces the conflicting value with a new container rather than failing -- there is no warning that the old value at "a" was discarded.
An empty path does not merge into the existing document -- it replaces it entirely with value's JSON.
See alsoGetValue, RemoveKey, Merge, Serialize
RemoveKeyThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string with the key or array element at path removed.
Returns a new JSON string equal to json with the object key or array element named by the final segment of path removed, closing up any resulting comma. path must not be empty -- there is no way to remove the document root through this method. If path simply does not resolve (a key that is not present, an index past the end of an array, a segment that walks through the wrong container type), RemoveKey treats that as nothing to do and returns json unchanged -- this is not treated as an error internally. Genuine malformation (an empty path, a path syntax error, or 256-level nesting exceeded) is a separate internal failure case that also returns json unchanged, but for a different reason. Either way, RemoveKey never raises a VBA error.
SignatureFunction RemoveKey(json As String, path As String) As String
Parameters
json (String) - The JSON text to remove a key/element from.
path (String) - Path to the key or array element to remove. Must not be empty.
ReturnsString: the new JSON document with the target removed, or json unchanged if the path was not found, was invalid, or was empty.
Removing a path that is not present is a silent no-op, not an error -- RemoveKey does not tell you whether it actually found and removed something. Check Exists before and after if that distinction matters to your code.
path cannot be empty -- there is no way to remove the whole document this way; use SetValue(json, "", ...) to replace the root instead, or simply discard json.
The result is a new string; json itself is never modified.
See alsoExists, SetValue, Clear
ExistsThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return whether path resolves to something in json.
Walks path against json and returns True if every segment resolves (each key is present, each array index is in range and each intermediate value is the container type the next segment needs), False otherwise. An empty path returns True whenever json itself is non-empty text -- the document root always "exists" if there is a document at all. Exists never raises a VBA error: invalid json, a path syntax error, or a path that simply does not resolve are all reported as False, with no way to tell those three cases apart from the return value alone.
SignatureFunction Exists(json As String, path As String) As Boolean
Parameters
json (String) - The JSON text to check.
path (String) - Path to check. An empty string checks whether json itself is non-empty.
ReturnsBoolean: True if path resolves in json, False otherwise (including for invalid JSON, a bad path, or empty json).
False covers three different real situations (invalid JSON, a malformed path, and a genuinely missing path) with no way to distinguish them from the return value alone.
An empty path returns True for any non-empty json, even json that is not valid JSON syntax at all -- Exists(json, "") is really just a non-empty-string check, not a validity check. Use IsValid for that.
See alsoGetValue, IsValid
AppendThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string with value added to the end of the array at path.
Returns a new JSON string with value's JSON added as the last element of the array found at path. An empty path appends to the root array directly. If path names an object key that does not exist yet, Append creates it as a new one-element array holding value, rather than failing -- this is the one difference from Insert, which requires the target array to already exist. This auto-create only behaves correctly for a single-segment path, or when the missing key is the LAST segment of path: if an EARLIER segment is the one that is missing (e.g. path is "a.b" and "a" itself does not exist yet), Append does not build out the full remaining structure the way SetValue does -- it collapses every remaining segment into that first missing key as a flat one-element array, silently producing a differently-shaped document (Append("{}", "a.b", 1) returns {"a":[1]}, not {"a":{"b":[1]}}) rather than either the expected nested shape or an unchanged fail-quiet result. If path resolves to something that is not an array (a scalar, an object, or an array reached by walking through the wrong container type along the way), Append fails internally and returns json unchanged without raising a VBA error.
SignatureFunction Append(json As String, path As String, value As Variant) As String
Parameters
json (String) - The JSON text to append into.
path (String) - Path to the target array. An empty string targets the root array.
value (Variant) - The VBA value to append, converted to JSON the same way Serialize does.
ReturnsString: the new JSON document with value appended, or json unchanged on failure.
Append never raises a VBA error -- targeting something that is not an array silently returns json unchanged. Compare the result to the input to detect this.
Unlike Insert, Append auto-creates a missing object key as a fresh one-element array. Insert requires the array to already exist at path.
value must already be the VBA value to append, not pre-encoded JSON text -- Append converts it the same way Serialize does, so passing an already-quoted JSON string literal would double-quote it. Use Insert/SetValue directly with a JSON literal string when you need to append raw JSON text (see the recipe for working with arrays).
A missing INTERMEDIATE path segment is not handled the same way as a missing terminal one: Append("{}", "a.b", 1) silently returns {"a":[1]} -- it collapses the unresolved remainder of the path into a flat array under the first missing key, rather than building {"a":{"b":[1]}} or leaving the document unchanged. Because the output string genuinely changes, the usual "compare output to input" trick for detecting a fail-quiet no-op will NOT catch this -- verify the resulting shape with GetValue/Exists after any Append onto a path with more than one segment where you are not certain every earlier segment already exists.
See alsoInsert, GetArrayLength, GetArrayItem
InsertThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string with value inserted into the array at path at position index.
Returns a new JSON string with value's JSON inserted into the array at path, before whatever is currently at index. index equal to the array's current length is allowed and behaves like Append (inserts at the end); index greater than the current length fails. Unlike Append, Insert does not auto-create a missing target -- path must already resolve to an existing array, and index must not be negative. Any of these conditions failing -- a negative index, an out-of-range index, or a target that is not an array -- returns json unchanged without raising a VBA error.
SignatureFunction Insert(json As String, path As String, index As Long, value As Variant) As String
Parameters
json (String) - The JSON text to insert into.
path (String) - Path to the target array, which must already exist. An empty string targets the root array.
index (Long) - Position to insert before. 0 = beginning; the array's current length = end (same as Append).
value (Variant) - The VBA value to insert, converted to JSON the same way Serialize does.
ReturnsString: the new JSON document with value inserted, or json unchanged on failure.
Insert never raises a VBA error -- a negative index, an index past the array's length, or a non-array target all silently return json unchanged.
The target array must already exist -- Insert does not auto-create it the way Append auto-creates a missing key. Use Append or SetValue first if the array might not exist yet.
index equal to the current array length is valid and appends; anything greater fails silently.
See alsoAppend, GetArrayLength, GetArrayItem
ClearThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string with the object or array at path emptied.
Returns a new JSON string with the container at path replaced by an empty version of the same kind -- an object becomes {} and an array becomes []. An empty path clears the whole document (the root itself must be an object or array). If path does not resolve, or resolves to a scalar value rather than a container, Clear fails internally and returns json unchanged without raising a VBA error.
SignatureFunction Clear(json As String, path As String) As String
Parameters
json (String) - The JSON text to clear a container in.
path (String) - Path to the object or array to empty. An empty string clears the whole document.
ReturnsString: the new JSON document with the target container emptied, or json unchanged on failure.
Clear never raises a VBA error -- a missing path or a scalar target both silently return json unchanged.
Clearing an object always produces {} and clearing an array always produces [] -- Clear does not change which kind of container is at that path, only empties whichever kind is already there.
An empty path clears the entire document down to {} or [], keeping only the root's own container type.
See alsoRemoveKey, SetValue, Count
MergeThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a new JSON string combining two JSON objects.
Returns a new JSON string built by starting from json1 and applying every top-level key of json2 onto it. With deep:=False (shallow), each key from json2 simply overwrites the same key in the result, whatever was there before -- including replacing an entire nested object outright. With deep:=True, when a key exists as an object in both the current result and json2, the two objects are merged recursively instead of one replacing the other; any other case (a key only in json2, or a type that is not an object on either side) still behaves like a shallow overwrite for that key. Both json1 and json2 must be JSON objects at the top level -- if either is not, Merge fails internally and returns json1 unchanged, discarding json2 entirely, without raising a VBA error.
SignatureFunction Merge(json1 As String, json2 As String, deep As Boolean) As String
Parameters
json1 (String) - The base JSON object.
json2 (String) - The JSON object whose keys are applied onto json1.
deep (Boolean) - True to recursively merge nested objects that exist on both sides; False to have json2's value simply replace json1's at each top-level key.
ReturnsString: the merged JSON object, or json1 unchanged if either argument is not a JSON object.
No DataMode dependency -- this is a standalone object.
Example
VBA
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim base AsString, edit AsString
base = "{""theme"":{""color"":""blue"",""size"":10},""lang"":""en""}"
edit = "{""theme"":{""color"":""red""}}"Debug.Print jh.Merge(base, edit, False)
' -> {"theme":{"color":"red"},"lang":"en"} (theme replaced wholesale)Debug.Print jh.Merge(base, edit, True)
' -> {"theme":{"color":"red","size":10},"lang":"en"} (theme merged key-by-key)
Gotchas
Merge never raises a VBA error -- if either argument is not a JSON object at the top level, it silently returns json1 unchanged and json2 is not applied at all.
With deep:=False, a nested object present in both arguments is replaced wholesale by json2's version, not combined -- use deep:=True to merge nested objects key-by-key instead.
Arrays are never merged element-by-element even with deep:=True -- an array value at a shared key is simply replaced by json2's array, the same as any other non-object value.
See alsoSetValue, GetValue
CountThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return the number of keys or elements at path, raising a real error on failure.
Returns the number of keys (for an object) or elements (for an array) found at path. An empty path counts the document root, which must itself be an object or array. Unlike the mutators in this group, Count is one of the three members that raises a genuine, catchable VBA runtime error on failure -- a path syntax error, a path that does not resolve, or a target that is neither an object nor an array all raise an error rather than returning a sentinel value. Err.Number reflects the underlying failure, but Count never calls the mechanism this product uses to attach a description to a COM error (the same one every guarded grid/licensing member relies on for a readable Err.Description) -- it only records the reason to a private, COM-invisible field. A caller never sees a real description for a Count failure at all, not merely an unreliable one; do not read Err.Description after a failed Count call expecting any specific text.
SignatureFunction Count(json As String, path As String) As Long
Parameters
json (String) - The JSON text to count within.
path (String) - Path to the object or array to count. An empty string counts the document root.
ReturnsLong: the number of keys or elements found. Raises a VBA error instead of returning a value on failure.
This is a genuinely error-raising method, unlike most of this group -- always wrap it in On Error when the shape of json/path is not already guaranteed.
Err.Description is never populated for a Count failure -- not merely unreliable, genuinely absent, because Count's failure path never attaches a COM error description the way most of this product's guarded members do. Only Err.Number is available; if you need to know WHY a call failed (bad path vs. malformed JSON vs. wrong target type), you must re-derive it yourself (e.g. with IsValid/Exists) rather than reading Err.Description.
A target that resolves to a scalar (not an object or array) raises an error rather than returning 0 or 1 -- do not assume Count is safe to call on a value of unknown shape without error handling.
See alsoGetKeys, GetArrayLength, Exists
PrettyPrintThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return json reformatted with 2-space indentation.
Returns a new JSON string with the same content as json, reformatted with newlines and 2-space (constant) indentation per nesting level. PrettyPrint does not validate JSON syntax first -- it reformats on a best-effort basis by walking the text's structural characters, so malformed input can produce malformed-looking output rather than a clean error. There is a 5MB input size limit; PrettyPrint is intended for debugging output, not as a general-purpose formatter for very large documents. Exceeding the limit is a real, enforced failure internally, but because PrettyPrint is one of the fail-quiet members of this group, exceeding it does not raise a VBA error -- it silently returns json completely unformatted, with no indication the limit was hit.
SignatureFunction PrettyPrint(json As String) As String
Parameters
json (String) - The JSON text to reformat.
ReturnsString: json reformatted with indentation, or json unchanged if it exceeds the 5MB (constant) input limit.
There is a real 5MB input size cap (constant). Exceeding it never raises a VBA error -- it silently returns json completely unchanged (unformatted), so a very large document can appear to "do nothing" with no diagnostic.
PrettyPrint does not validate JSON syntax -- feeding it malformed text does not raise an error and does not fall back to IsValid's stricter checking; the output for malformed input is not guaranteed to be well-formed JSON.
Intended for debugging output, not for producing a canonical or minimal representation -- there is no matching "minify" method in this group.
See alsoIsValid, Flatten
FlattenThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a single-level JSON object whose keys are the original dot/bracket paths.
Returns a new, single-level JSON object built by walking every nested value in json and recording it under a flat key equal to the dot/bracket path that would reach it, e.g. {"a":{"b":1},"c":[10,20]} becomes {"c[0]":10,"c[1]":20,"a.b":1} -- the flattened object's key order reflects Flatten's internal traversal order, not the original nesting order, so do not rely on it matching the input's key order. An empty nested object or array is kept as a value ({} or []) at its own flat key rather than vanishing or being flattened further. json must be a JSON object or array at the top level; anything else fails internally and returns json unchanged without raising a VBA error.
SignatureFunction Flatten(json As String) As String
Parameters
json (String) - The JSON object or array to flatten.
ReturnsString: a single-level JSON object of flat-path keys, or json unchanged if the root is not an object or array.
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Debug.Print jh.Flatten("{""a"":{""b"":1},""c"":[10,20]}")
' -> {"c[0]":10,"c[1]":20,"a.b":1}' (key order reflects Flatten's internal traversal, not the input's key' order -- do not rely on the flattened object's key order matching the' original nesting order)
Gotchas
Never raises a VBA error -- a root that is not an object or array (a bare number, string, boolean, or null) silently returns json unchanged.
Each flat key is exactly the dot/bracket path Unflatten and GetValue would both understand for that value, so Flatten and Unflatten are meant to be used together as a round trip.
Empty nested containers are preserved as their own flat entries ({} or []) rather than being dropped -- do not assume every key in the flattened result maps to a scalar value.
Flatten's output keys are literal text containing dots/brackets (e.g. the JSON key "a.b"), not a navigable path -- you cannot read a value back out of a Flatten result with GetValue(flat, "a.b"), because GetValue's path parser treats "." and "[...]" as path separators, not literal key characters, and will look for a nested structure that does not exist. Read a flattened value back either by inspecting the raw JSON text directly, or by calling Unflatten first to restore the navigable nested structure.
See alsoUnflatten, GetKeys, Count
UnflattenThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return a nested JSON document rebuilt from a Flatten-style single-level object.
Returns a new, nested JSON document built by applying each key of the single-level json object as a SetValue path onto an initially empty document, so {"a.b":1,"c[0]":10} becomes {"a":{"b":1},"c":[10]}. Because it reuses the same path engine SetValue uses, Unflatten inherits SetValue's auto-creation and null-padding behavior -- a flat key like "a[2]" on its own produces a 3-element array with the first two entries set to JSON null. json must be a JSON object at the top level (not an array); anything else fails internally and returns json unchanged without raising a VBA error.
SignatureFunction Unflatten(json As String) As String
Parameters
json (String) - A single-level JSON object whose keys are dot/bracket paths, typically produced by Flatten.
ReturnsString: the rebuilt nested JSON document, or json unchanged if the root is not a JSON object.
Never raises a VBA error -- a root that is not a JSON object (including a JSON array) silently returns json unchanged.
A non-contiguous array key (e.g. only "a[2]" present, without "a[0]"/"a[1]") produces a null-padded array, not a 1-element array at index 0 -- the same auto-creation behavior SetValue documents.
Unflatten(Flatten(x)) is a genuine round trip for any x Flatten accepts, since both use the same path grammar.
See alsoFlatten, SetValue
IsValidThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return whether json is syntactically valid JSON text.
Returns True if json is well-formed JSON per standard JSON grammar (matching quotes and brackets, valid escapes, valid number formats, no trailing content after the top-level value), False otherwise, including for an empty string. IsValid never raises a VBA error under any input. Its own nesting-depth cap (512 levels) is deliberately higher than the 256-level cap enforced by SetValue, RemoveKey, Append, Insert, Clear, Flatten and Unflatten -- a document nested between 257 and 512 levels deep can validate as True here while still failing internally (fail-quiet, unchanged) in any of those mutators.
SignatureFunction IsValid(json As String) As Boolean
Parameters
json (String) - The text to check.
ReturnsBoolean: True if json is syntactically valid JSON, False otherwise (including an empty string).
IsValid's own nesting-depth limit (512) is higher than the 256-level limit the mutating methods enforce -- a document that passes IsValid can still be too deeply nested for SetValue/RemoveKey/Append/Insert/Clear/Flatten/Unflatten to modify, which then fail quietly rather than with an error.
An empty string is not valid JSON and returns False -- IsValid does not treat "nothing to check" as trivially valid.
IsValid checks syntax only -- it does not check that the document has any particular shape (object vs array vs scalar) your code might be expecting next.
See alsoGetValue, Exists
GetArrayLengthThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return the number of elements in the array at path, or 0 if it is missing or not an array.
Returns the element count of the array found at path. An empty path targets the root array. GetArrayLength never raises a VBA error: an empty json string, a path that does not resolve, and a path that resolves to something that is not an array all return 0 -- the same value a genuinely empty array would return. There is no separate sentinel for "not an array" here.
SignatureFunction GetArrayLength(json As String, path As String) As Long
Parameters
json (String) - The JSON text to look in.
path (String) - Path to the array. An empty string targets the root array.
ReturnsLong: the array's element count, or 0 for a missing path, invalid JSON, or a target that is not an array.
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Debug.Print jh.GetArrayLength("{""tags"":[""a"",""b"",""c""]}", "tags") ' -> 3Debug.Print jh.GetArrayLength("{""name"":""Ada""}", "name") ' -> 0 (not an array)
Gotchas
0 is genuinely ambiguous here: a real empty array, a missing path, and a target that is not an array at all are all reported as 0, with no way to tell them apart from this return value alone. Use Exists first if the distinction matters.
Same embedded-NUL caveat as GetValue: path resolution treats the JSON text's first embedded NUL as the end of the string rather than using its recorded length. This has no practical effect on ordinary JSON text.
See alsoGetArrayItem, Count, Exists
GetArrayItemThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return the array element at index as text, or an empty string if it is not found.
Returns the element at index of the array found at path as a String: a JSON string element comes back unescaped with its quotes stripped, and any other element (a number, boolean, null, object or array) comes back as its raw JSON text exactly as written. An empty path targets the root array. GetArrayItem never raises a VBA error -- an empty json string, a path that does not resolve, a target that is not an array, or an out-of-range index all return an empty string.
SignatureFunction GetArrayItem(json As String, path As String, index As Long) As String
Parameters
json (String) - The JSON text to look in.
path (String) - Path to the array. An empty string targets the root array.
index (Long) - Zero-based element index.
ReturnsString: the element's unescaped text (for a string element) or raw JSON text (for any other element type), or an empty string if not found.
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim json AsString
json = "{""tags"":[""a"",""b"",42,null]}"Debug.Print jh.GetArrayItem(json, "tags", 0) ' -> aDebug.Print jh.GetArrayItem(json, "tags", 2) ' -> 42 (raw JSON text, still a VBA String)Debug.Print jh.GetArrayItem(json, "tags", 3) ' -> null (raw JSON text, the literal word "null")Debug.Print jh.GetArrayItem(json, "tags", 99) ' -> "" (out of range)
Gotchas
An empty string return is ambiguous: it means both "nothing was found at that index" and "the element genuinely is the empty JSON string """"". Check GetArrayLength (index < length) first if you need to tell these apart.
Only a string element has its quotes stripped -- a number, boolean, null, object or array element comes back as raw JSON text (e.g. the text "null", or a full "{...}" object), which is a String either way but is not automatically converted to the matching VBA type the way GetValue converts a path result.
Same embedded-NUL caveat as GetValue applies to path resolution here.
See alsoGetArrayLength, GetValue
GetKeysThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Return the key names of the object at path as a JSON array of strings.
Returns a JSON array of the object's own key names (each as a quoted JSON string, in their original document order) for the object found at path, e.g. "[\"a\",\"b\"]". An empty path targets the root object. GetKeys never raises a VBA error: an empty json string, a path that does not resolve, and a path that resolves to something other than an object all return the text "[]" -- the same result a genuinely empty object would produce.
SignatureFunction GetKeys(json As String, path As String) As String
Parameters
json (String) - The JSON text to look in.
path (String) - Path to the object. An empty string targets the root object.
ReturnsString: a JSON array of the object's key names (e.g. "[\"a\",\"b\"]"), or "[]" if json/path is missing, invalid, or not an object.
Dim jh AsObjectSet jh = CreateObject("LiteGridTurbo.LiteGridTurboHelper")
Dim keysJson AsString
keysJson = jh.GetKeys("{""name"":""Ada"",""age"":36}", "")
Debug.Print keysJson ' -> ["name","age"]Debug.Print jh.GetArrayItem(keysJson, "", 0) ' -> name
Gotchas
"[]" is ambiguous the same way GetArrayLength's 0 is: a genuinely empty object, a missing path, and a non-object target are all reported identically.
The result is JSON text (a String holding "[...]"), not a VBA array -- use GetArrayItem/GetArrayLength/ToArray against the returned string to walk the key names, as shown in the recipe for flattening JSON for display.
Same embedded-NUL caveat as GetValue applies to path resolution here.
See alsoCount, GetArrayItem, Flatten
ToArrayThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Convert a JSON array of objects into a 2-D array, raising a real error on failure.
Converts json -- which must be a JSON array of objects -- into a 2-D VBA array: row 1 holds column headers (the union of every key seen across all the objects, in first-appearance order), and rows 2 through N+1 hold one data row per object, in the same order as the input array. A row missing a given column gets Null in that cell. Any array elements that are not themselves JSON objects are skipped when collecting rows. Unlike most of this group, ToArray genuinely raises a VBA error rather than failing quietly: it rejects a root that is not a JSON array, an empty array, an array with no object elements at all, and -- fixed limits (constant) -- input text over 50MB, more than 100,000 object rows, or more than 1,000 distinct columns. As with Count, ToArray never attaches a COM error description on failure -- Err.Description is not merely unreliable, it is never populated at all; only Err.Number is available.
SignatureFunction ToArray(json As String) As Variant
Parameters
json (String) - A JSON array of objects to convert.
ReturnsVariant: a 2-D array (1-based; row 1 = headers, rows 2..N+1 = data). Raises a VBA error instead of returning a value on failure.
This is a genuinely error-raising method -- always wrap it in On Error, especially against JSON of unverified shape or size.
Real, enforced limits (constant): input over 50MB, more than 100,000 object rows, or more than 1,000 distinct columns all raise an error rather than truncating the result.
Column order is first-appearance order across all rows, not alphabetical and not the key order of any single row -- a key that only appears in a later object still gets its own column, and earlier rows get Null there.
Non-object elements in the input array (a bare number or string mixed into the array) are silently skipped when building rows rather than causing an error, unless skipping them leaves zero object rows at all, which does raise an error.
Err.Description is never populated for a ToArray failure, the same as Count -- only Err.Number is available. Do not read Err.Description expecting to distinguish which of the real limits (constant) -- non-array root, empty array, no object rows, 50MB input, 100,000 rows, 1,000 columns -- was actually hit.
See alsoCount, Serialize, GetArrayItem
SerializeThe standalone JSON Helper: path-based read, write and reshape of arbitrary JSON text
Convert a VBA value to a JSON-encoded string.
Converts value to a JSON-encoded string: a String becomes a quoted, escaped JSON string; a numeric type becomes a JSON number; a Boolean becomes true/false; Empty, Null and a Nothing object reference all become JSON null; a Date becomes an ISO-8601 timestamp string with millisecond precision, except a Date value of 0 (December 30, 1899), which becomes JSON null; a 1-D or 2-D VBA array becomes a JSON array (nested for 2-D); and a Scripting.Dictionary becomes a JSON object built from its Keys()/Items(). Any other Dispatch-based object reference (the normal case for a VBA object variable, e.g. Set x = New Collection) becomes the literal string "[Object]"; the rarer case of a raw IUnknown-only object reference (no default Dispatch interface) becomes the literal string "[IUnknown]" instead -- ordinary late-bound VBA object variables are Dispatch-based and will hit the "[Object]" case, not this one. This is essentially a type-safe, non-failing conversion -- almost every VBA value this method could be given has a defined JSON representation, so in practice it does not fail; it is one of the three error-raising members of this group only in the sense that a catastrophic internal failure (out of memory) would surface as a real VBA error rather than being swallowed.
SignatureFunction Serialize(value As Variant) As String
Parameters
value (Variant) - The VBA value to convert. Virtually any VBA type is accepted; there is no type this method is expected to reject under normal conditions.
ReturnsString: the JSON-encoded representation of value.
A Date value of 0 (December 30, 1899, VBA's zero date) becomes JSON null, not an ISO-8601 timestamp for that date -- the same convention GetLicenseInfo and similar members use elsewhere in this product.
An object reference that is not a Scripting.Dictionary is not introspected -- it becomes the fixed literal string "[Object]" (or, for the rare raw-IUnknown-only case, "[IUnknown]"), losing whatever data it held. Convert your own object's relevant fields to a plain Variant/array/Dictionary first if you need them represented.
The result is always JSON-encoded text, even for a plain string -- Serialize("Ada") returns the 5-character text "Ada" including its surrounding quotes, not the bare 3-character word Ada.
See alsoSetValue, ToArray
Part III
Troubleshooting and Limits
Capability matrix by data mode
This is the single answer to "does this work in that mode". Every row says what each of the three data modes does, and — in the last column — the test that proves it. A row with nothing behind it is not allowed here: a guard checks that every line of this table names a scenario or a script that really exists, so the table cannot quietly drift away from the product.
How to read the states
State
Meaning
S
Supported
U
Unsupported, and it refuses by name — never a silent no-op, never a wrong answer
P
Partial, with the narrowing stated on the row
H
Host-driven: in Virtual mode your code owns the data, so the grid asks and you answer
NV
Not verified. It is not claimed to work and it is not claimed to be broken
NV is not a synonym for unsupported. Where something has not been tested, this documentation says so rather than guessing in either direction.
P — the value's type must match the column's stored type
U
Cells.CellValueAlias
AddRow / InsertRow / DeleteRow
S
P — per-column type check, all-or-nothing
U
Identity.RowKeyStability
Sort (single and multi-column)
S
S
H
SortingFiltering.CustomerOrders
Filter UI (the header filter button)
S
U — the affordance is withheld
NV
SortingFiltering.InteractiveDiscoveryUi
SetColumnFilter and the filter-state readers
S
U
H
SortingFiltering.FilterAndRowState
Row grouping (GroupByColumn)
S
U — refuses by name
U — refuses by name
RowGrouping.SalesPerformanceByRegion
Tree view (SetTreeParent)
S
U — refuses by name
U — refuses by name
TreeView.HierarchicalRollup
Undo / redo
S
S
U
Editing.WarehouseRestock
SelectBlock / GetBlockSelection
S
U
NV
Selection.CustomerAccountSelection
GetCell / GetCellValue
S
S
P — cached rows only; a row that is not resident refuses
Cells.CellValueAlias
GetDataArray
S
S
U
Limitations.X86OutputMemoryCeilingRefusal
Exports: CSV, JSON, HTML, XLSX, PDF
S
S
U
Export.CustomerReportExport
Row keys and change tracking
S
S
U — your code owns the data there, so it already owns identity
Identity.RowKeyStability, Identity.ChangeTracking
Per-row heights (SetRowHeight)
S
U
U
Appearance.PerRowHeight
Global RowHeight / RowDensity
S
S
S
Appearance.GridChromeAndLayout
Design-time placement and property persistence
S
S
S
LayoutPersistence.SavedViewsAndPresets
UI Automation (screen readers)
S
S
NV
ClaimProof.UiaGridCompatibility
Printing (PrintGrid / SetPrintMargins)
S
U — refuses by name
U
Printing.PrintGridExport
Expired-evaluation enforcement on durable output
S
S
U — a Virtual grid produces none of these
scripts/Verify-PrintGridTrialEnforcement.ps1
Merged cells
U — not a feature of this release
U
U
not implemented; no API exists to call
Two things this table is careful about.
Turbo column-store mode is not "Embedded but bigger." It trades a narrower feature set for scale. Where it refuses, it refuses by name, with a message saying what to do instead — you will never get a silently wrong answer or an empty result standing in for one.
Virtual mode inverts ownership. Your code owns the data, so the grid asks for rows rather than holding them. Anything that would require the grid to own or change the data is refused there, by name, for the same reason.
Error handling and troubleshooting
COM failures populate normal VBA Err.Number, Err.Description and Err.Source values:
Class not registered: run the per-user installer for the host's architecture, or use the registration-free factory.
Wrong architecture: a 32-bit process must load LiteGridTurbo_x86.ocx; a 64-bit process must load LiteGridTurbo_x64.ocx.
Mode change refused: call Clear before leaving a grid-owned mode, or finish/cancel an active column-array load.
Loader refused: check the mode table; Virtual owns host data and the Turbo column-store engine intentionally supports a narrower loader set.
No reg-free events: attach the callback bridge and enable the needed event. Use registered mode for BeforeCellPaint.
Access toolbox placement: programmatic CreateObject and reg-free creation are supported. Interactive Access form-designer toolbox placement has not been verified; do not depend on it without testing your deployment.
Licence notice remains: activate again after every process launch and confirm the key is an LG key whose company and expiry are valid.
Practical limitations
Both architectures can be installed side by side, but each host process can load only the matching architecture.
The three data engines do not keep synchronized copies of a dataset.
Virtual reads can fail for rows that are not currently cached.
Turbo column-store mode refuses loaders it does not implement; it never redirects them to Embedded mode.
Registration-free BeforeCellPaint delivery is unavailable.
Helper GetValue returns string content only up to the first embedded NUL.
Performance and capacity depend on data types, null density, string cardinality, host bitness and available memory. Consult the claims, benchmark and limits documents for measured boundaries; do not generalize a load result into a rendering or scrolling claim.