From 3cedaca16fb1f49b00208065cc1393e89776a526 Mon Sep 17 00:00:00 2001 From: Daniel Trierweiler Date: Tue, 25 Aug 2026 08:47:36 +0200 Subject: [PATCH] fix #983: When scrolling down, consider the height of the horizontal scrollbar to calculate whether the bar should be visible or not. This effectively enlarges the view port by one (or more) rows, so that we can calculate the need for the scrollbar as if it was not visible. Downside of this approach: If the first invisible row would require the scrollbar to be visible, but no other visible row would, we would still show the scrollbar. --- Source/VirtualTrees.BaseTree.pas | 43 ++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/Source/VirtualTrees.BaseTree.pas b/Source/VirtualTrees.BaseTree.pas index d82bc984..1ad921d7 100644 --- a/Source/VirtualTrees.BaseTree.pas +++ b/Source/VirtualTrees.BaseTree.pas @@ -53,7 +53,7 @@ interface {$ELSE} TVTBaseAncestor = TVTBaseAncestorVcl; TCanvas = Vcl.Graphics.TCanvas; - TFormatEtcArray = VirtualTrees.Types.TFormatEtcArray; + TFormatEtcArray = VirtualTrees.Types.TFormatEtcArray; {$ENDIF} // Alias defintions for convenience @@ -540,7 +540,8 @@ TBaseVirtualTree = class abstract(TVTBaseAncestor) FOffsetY: TDimension; // Determines left and top scroll offset. FEffectiveOffsetX: TDimension; // Actual position of the horizontal scroll bar (varies depending on bidi mode). FRangeX, - FRangeY: TNodeHeight; // current virtual width and height of the tree + FRangeY: TNodeHeight; // current virtual width and height of the tree + FScrolling: Boolean; // True while updating scroll bars in reaction to scrolling. See issue #983. FBottomSpace: TDimension; // Extra space below the last node. FDefaultPasteMode: TVTNodeAttachMode; // Used to determine where to add pasted nodes to. @@ -1855,7 +1856,7 @@ implementation //These allow us access to protected members in the classes TVirtualTreeColumnsCracker = class(TVirtualTreeColumns); TVTHeaderCracker = class(TVTHeader); - TVirtualTreeColumnCracker = class(TVirtualTreeColumn); + TVirtualTreeColumnCracker = class(TVirtualTreeColumn); TBaseVirtualTreeCracker = class(TBaseVirtualTree); // streaming support @@ -8344,7 +8345,12 @@ procedure TBaseVirtualTree.WMVScroll(var Message: TWMVScroll); DoStateChange([], [tsThumbTracking]); // Avoiding to adjust the horizontal scroll position while tracking makes scrolling much smoother // but we need to adjust the final position here then. - UpdateScrollBars(True); + FScrolling := True; // issue #983, see UpdateHorizontalRange + try + UpdateScrollBars(True); + finally + FScrolling := False; + end; // Really weird invalidation needed here (and I do it only because it happens so rarely), because // when showing the horizontal scrollbar while scrolling down using the down arrow button, // the button will be repainted on mouse up (at the wrong place in the far right lower corner)... @@ -11216,7 +11222,14 @@ function TBaseVirtualTree.DoSetOffsetXY(Value: TPoint; Options: TScrollUpdateOpt UpdateVerticalScrollBar(suoRepaintScrollBars in Options); if not (FHeader.UseColumns or IsMouseSelecting) and (FScrollBarOptions.ScrollBars in [System.UITypes.TScrollStyle.ssHorizontal, System.UITypes.TScrollStyle.ssBoth]) then - UpdateHorizontalScrollBar(suoRepaintScrollBars in Options); + begin + FScrolling := True; // issue #983, see UpdateHorizontalRange + try + UpdateHorizontalScrollBar(suoRepaintScrollBars in Options); + finally + FScrolling := False; + end; + end; end; end; @@ -12289,9 +12302,11 @@ function TBaseVirtualTree.GetMaxRightExtend(): TDimension; var Node, - NextNode: PVirtualNode; + NextNode, + PrevNode: PVirtualNode; TopPosition: TDimension; CurrentWidth: TDimension; + ScrollBarOffset: TDimension; begin Node := GetNodeAt(0, 0, True, TopPosition); @@ -12299,12 +12314,26 @@ function TBaseVirtualTree.GetMaxRightExtend(): TDimension; if not Assigned(Node) then exit; + if (FScrolling) and ((GetWindowLong(Handle, GWL_STYLE) and WS_HSCROLL) <> 0) then + begin + ScrollBarOffset := GetSystemMetricsForDpi(SM_CYHSCROLL, CurrentPPI); + PrevNode := GetPreviousSibling(Node); + while (ScrollBarOffset > 0) and Assigned(PrevNode) do + begin + CurrentWidth := GetOffset(TVTElement.ofsRightOfText, PrevNode); + if Result < CurrentWidth then + Result := CurrentWidth; + Dec(ScrollBarOffset, PrevNode.NodeHeight); + PrevNode := GetPreviousSibling(PrevNode); + end; + end; + while Assigned(Node) do begin if not (vsInitialized in Node.States) then InitNode(Node); CurrentWidth := GetOffset(TVTElement.ofsRightOfText, Node); - if Result < (CurrentWidth) then + if Result < CurrentWidth then Result := CurrentWidth; Inc(TopPosition, NodeHeight[Node]); if TopPosition > Height then