The lxOutline unit provides engine-neutral geometry and mutation planning for row and column outlines
It stores only materialised spans, recognises summary nodes from adjacent outline-level transitions, and creates compressed state changes for summary-after and summary-before layouts without allocating an entry for every worksheet coordinate
Core types
TXLSOutlineAxis = (xoaRows, xoaColumns);
TXLSOutlineChildMode = (xocPreserve, xocCollapse, xocExpand);
TXLSOutlineMutationPlan = class
constructor Create(AAxis: TXLSOutlineAxis);
procedure Clear;
property Axis: TXLSOutlineAxis;
property Count: Integer;
property Changes[Index: Integer]: TXLSOutlineStateChange;
end;
TXLSOutlineGeometry = class
constructor Create(AAxis: TXLSOutlineAxis;
ASummaryAfter: Boolean);
procedure Clear;
property Axis: TXLSOutlineAxis;
property SpanCount: Integer;
procedure AddSpan(AFirstIndex, ALastIndex, ALevel: Integer;
AHidden, ACollapsed: Boolean);
function TryGetNode(ANodeIndex: Integer;
out ANode: TXLSOutlineNode): Boolean;
function TryBuildNodeCollapsePlan(ANodeIndex: Integer;
ACollapsed: Boolean; AChildMode: TXLSOutlineChildMode;
APlan: TXLSOutlineMutationPlan): Boolean;
procedure BuildDisplayLevelPlan(ADisplayLevel: Integer;
AChildMode: TXLSOutlineChildMode;
APlan: TXLSOutlineMutationPlan);
end;
Row and column indices are one-based, outline levels are non-negative, and spans must be added in strictly increasing non-overlapping order
Adjacent spans with identical level, hidden, and collapsed state are coalesced automatically; omitted indices have implicit level zero, visible state, and no collapse marker
Both classes remember their Axis from construction, Clear empties a plan or geometry for reuse without reallocating the internal buffers, and SpanCount reports how many coalesced spans the geometry currently holds
Node semantics
| NodeIndex | The physical summary row or column queried by the caller |
| FirstDetailIndex / LastDetailIndex | The contiguous detail range whose level is deeper than the summary item |
| SummaryLevel | The outline level stored on the summary item, or zero when that item is not materialised |
| Level | The group level represented by the node, equal to SummaryLevel + 1 |
| SummaryHidden | The hidden state stored on the summary item |
| Collapsed | The explicit collapse marker stored on the summary item |
| SummaryAfter | True when details precede the summary item, false when details follow it |
Nested groups remain independent because each query stops at the first adjacent span whose level is not deeper than the selected summary item
A gap also terminates the detail range, so sparse metadata cannot accidentally bridge unused coordinates into one group
Mutation plans
A mutation plan contains ordered one-based ranges and a field mask indicating whether each range changes hidden state, collapsed state, or both; adjacent equivalent changes are coalesced automatically
| xocPreserve | Changes the selected node while retaining descendant collapse markers, which is the default nested-group behaviour |
| xocCollapse | Marks descendant nodes collapsed when they are inside the selected node or hidden below the requested display level |
| xocExpand | Clears descendant collapse markers when they are inside the selected node or hidden below the requested display level |
Expanding a parent with xocPreserve reveals immediate details while details belonging to collapsed children remain hidden; expanding a child inside a collapsed ancestor does not expose content through that ancestor
Display levels are one-based: level 1 collapses every top-level group, and each higher value reveals one additional outline level while setting the matching boundary-node markers
Building the same requested state from geometry that already contains it produces an empty plan, and querying a non-node returns false with an empty plan
Node operations leave the selected summary item's independent hidden state unchanged, and display-level plans do not unhide level-zero items that were hidden outside an outline group
Worksheet adapters
function TryGetOutlineNode(AAxis: TXLSOutlineAxis;
ANodeIndex: Integer; out ANode: TXLSOutlineNode): Boolean;
function SetOutlineNodeCollapsed(AAxis: TXLSOutlineAxis;
ANodeIndex: Integer; ACollapsed: Boolean;
AChildMode: TXLSOutlineChildMode = xocPreserve): Boolean;
procedure SetOutlineDisplayLevel(AAxis: TXLSOutlineAxis;
ADisplayLevel: Integer;
AChildMode: TXLSOutlineChildMode = xocPreserve);
TXLSWorksheet and TXLSXWorksheet expose the same three methods, so callers can query or change a one-based row or column summary node without building geometry manually
The classic adapter reads only allocated row blocks and assigned column metadata, while the XLSX adapter merges its sparse outline-level, hidden, and collapsed lists before applying one compressed mutation plan in a single write transaction
OpenDocument row and column groups retain their native nested table:display state; direct metadata edits and structural row or column changes safely project that state to the logical summary node, and conversion to XLSX writes the collapse marker on the XLSX summary item rather than every group member
Existing TXLSXWorksheet.ToggleRowGrouped and ToggleColGrouped methods remain available as compatibility wrappers; their argument is the first detail row or column, and the wrapper resolves the direction-aware summary node before toggling it
Delphi example
Geometry := TXLSOutlineGeometry.Create(xoaRows, True);
Plan := TXLSOutlineMutationPlan.Create(xoaRows);
try
Geometry.AddSpan(2, 4, 1, False, False);
Geometry.AddSpan(5, 5, 0, False, False);
if Geometry.TryGetNode(5, Node) then
UseOutlineNode(Node.FirstDetailIndex,
Node.LastDetailIndex, Node.Collapsed);
if Geometry.TryBuildNodeCollapsePlan(5, True,
xocPreserve, Plan) then
ApplyOutlineChanges(Plan);
finally
Plan.Free;
Geometry.Free;
end;
C++Builder example
Lxoutline::TXLSOutlineGeometry *geometry =
new Lxoutline::TXLSOutlineGeometry(Lxoutline::xoaColumns, false);
try {
geometry->AddSpan(3, 3, 0, false, true);
geometry->AddSpan(4, 7, 1, true, false);
Lxoutline::TXLSOutlineNode node;
if (geometry->TryGetNode(3, node))
UseOutlineNode(node.FirstDetailIndex, node.LastDetailIndex,
node.Collapsed);
}
__finally {
delete geometry;
}
Performance and scope
Node lookup uses a binary search followed only by the adjacent materialised spans crossed by the selected group, giving O(log n + k) work for n spans and k visited spans
Bulk mutation planning discovers every materialised node with a linear monotonic-stack pass, merges collapsed visibility intervals, and emits compressed range changes without visiting unused coordinates
The builder accepts the last XLSX worksheet row while retaining only the supplied spans, so a high coordinate does not imply a million-entry allocation or scan
The worksheet adapter regression gate runs the same 32,768 node queries and 2,048 collapse or expand mutations over six materialised row entries near row 8 and row 1,048,576, requires zero cell materialisation, and bounds the high-coordinate cost against the low-coordinate baseline
TXLSOutlineGeometry never writes workbook data directly; engine adapters validate and apply the returned plan, allowing geometry analysis and state mutation to remain separate
See also