Jump to content
Henry Olive

ScrollBox ScrollBar Mouse Tracking

Recommended Posts

Good Day,

I have a form,  respectively   panel, scrollbox, another panel and some components on the last panel

ScrollBox's  VertScrollBar  properties as below

Range = 800

Visible = True

Tracking = True

The scrollbar in the scrollbox works w/o any problem, if i mouse click (up & down)

but when i try mouse tracking,  scrollbar doesnt move ( doesnt move up or down)

I want to control the scrollbar with a mouse.

What is the problem

Thank You
 

 

 

Share this post


Link to post
5 hours ago, Henry Olive said:

Good Day,

I have a form,  respectively   panel, scrollbox, another panel and some components on the last panel

ScrollBox's  VertScrollBar  properties as below

Range = 800

Visible = True

Tracking = True

The scrollbar in the scrollbox works w/o any problem, if i mouse click (up & down)

but when i try mouse tracking,  scrollbar doesnt move ( doesnt move up or down)

I want to control the scrollbar with a mouse.

What is the problem

 

Hard to say with the info you give us. A scrollbox (VCL) sets its range automatically based on the position and dimensions of the controls it contains. If it is empty or the content is aligned alClient there is nothing to scroll. If all content fits in the client area of the scrollbox there is nothing to scroll either.

Share this post


Link to post

Thank You so much Peter, Edwin

Edwin, exactly yes

 

I wrote below code ( Delphi 10.3)  and everything is OK mouse wheel works

but to me, there should not need below code, mouse wheel should work

without any code.


procedure TMeetNote2.ScrollBox1MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  aPos: SmallInt;
begin
  aPos:= ScrollBox1.VertScrollBar.Position - WheelDelta div 10;
  aPos:= Max(aPos, 0);
  aPos:= Min(aPos, ScrollBox1.VertScrollBar.Range);
  ScrollBox1.VertScrollBar.Position := aPos;
  Handled := True;
end;

Share this post


Link to post
3 hours ago, Henry Olive said:

I wrote below code ( Delphi 10.3)  and everything is OK mouse wheel works

but to me, there should not need below code, mouse wheel should work

without any code. 

The VCL controls do not implement a default handling of the mouse wheel, unless a Windows control implements it on the API level. TScrollbox is not based on an API control, so you have to code this yourself.  You only have to do this once, though: create a descendant of TScollbox that implements the behaviour you want and the use that. Note that when creating a component you do not use the parent component's events, those are for the component user. Instead you overwrite the virtual or dynamic methods that fire the events.

  • Like 1

Share this post


Link to post
19 hours ago, Henry Olive said:

Thank You so much Peter, Edwin

Edwin, exactly yes

 

I wrote below code ( Delphi 10.3)  and everything is OK mouse wheel works

but to me, there should not need below code, mouse wheel should work

without any code.


procedure TMeetNote2.ScrollBox1MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  aPos: SmallInt;
begin
  aPos:= ScrollBox1.VertScrollBar.Position - WheelDelta div 10;
  aPos:= Max(aPos, 0);
  aPos:= Min(aPos, ScrollBox1.VertScrollBar.Range);
  ScrollBox1.VertScrollBar.Position := aPos;
  Handled := True;
end;

Yes, you have to write such code, and such code only works if the scrollbox has the focus.

Share this post


Link to post
Posted (edited)

I just spent an unreasonable amount of time looking into this myself 😄

 

1) The stock TScrollBox didn't support reacting to the mouse wheel.  That changed in Delphi 11, but they did a poor job of it and broke it so it kind of scrolls, but TScrollBox no longer calls the DoMouseWheel/OnMouseWheel events. [RSP-35333] (My comment there is outdated compared to the rest of this post)

2) The WM_MOUSEWHEEL messages used to require focus.  In a recent Windows 10 update (and Windows 11), Microsoft introduced a new mouse setting, "Scroll inactive windows when I hover over them" that instead sends the message to the control that the mouse is over.  AFAICT it's turned on by default now.  If it's turned off it reverts to the old behavior of sending it to the focused control.

3) Delphi used to forward the CM_MOUSEWHEEL messages to the focused control in TCustomForm.MouseWheelHandler, which meant that it could still require focus even if Windows was sending the messages elsewhere.  That code was removed sometime between XE7 and 10.4, so the CM_MOUSEWHEEL wheels now go to the control the message is sent to.

4) TControl.IsControlMouseMsg incorrectly treats the X/Y coordinates for WM_MOUSEWHEEL events as if they're in client coordinates like the other WM_MOUSE* events are, rather than in screen space coordinates.  This has the effect that if you have a TScrollBox with a big TImage on it, whether the scrollbox scrolls or not depends on where the form is on the screen.  Likewise, TControl.IsControlMouseMessage doesn't pay attention to the WM_MOUSEWHEEL return values, so if you fix the first issue, it actually breaks TScrollBox scrolling if it's over the image because the image swallows the WM_MOUSEWHEEL message and TScrollBox's handler never gets called.

5) If you don't have TScrollBox.VertScrollBar.Smooth := true, as you're scrolling there will be a smear as the nested controls don't redraw before the scrollbox scrolls again.  It will clear up once the apps been idle a bit, and for simple scrolls this isn't noticeable, but it can look terrible, especially if the scrollbox redraw slowly (e.g., lots of nested controls with VCL styles enabled) and you use a free-spinning mouse.

6) Once you fix all of the above, if you have scrollable controls nested in a TScrollBox, the lack of focus requirement means that if the mouse cursor goes over the top of the nested control as a result of the TScrollBox's scroll, it'll start scrolling the nested one instead.  I can provide a workaround for that, but it's a little out of scope of just "Fix the bugs".

 

On to the fixes!

 

1) Patch VCL.Forms.pas (fixes issue #1). 

 

The least invasive fix is to change TScrollBox's WMMouseWheel handler to this, which at least fixes the OnMouseWheel handlders getting called.

 

procedure TScrollBox.WMMouseWheel(var Message: TWMMouseWheel);
const
  LScrollBarValues: array[Boolean] of WPARAM = (SB_LINEDOWN, SB_LINEUP);
begin
  inherited;
  if (Message.Result <> 0) or not VertScrollBar.IsScrollBarVisible then
    Exit;
  Message.Result := 1;
  var LScrollBarValue := LScrollBarValues[Message.WheelDelta > 0];
  for var LLineToScroll := 1 to Mouse.WheelScrollLines do
    Perform(WM_VSCROLL, LScrollBarValue, 0);
end;
 

A better fix is to instead remove TScrollBox.WMMouseWheel entirely (if you're on Delphi 11) and put the code in a newly overridden DefaultHandler:

 

  TScrollBox = class(TScrollingWinControl)
  public
    ...
    procedure DefaultHandler(var Message); override;
  end;

procedure TScrollBox.DefaultHandler(var Message);
const
  LScrollBarValues: array[Boolean] of WPARAM = (SB_LINEDOWN, SB_LINEUP);
var
  LScrollBarValue: WPARAM;
  LLineToScroll: Integer;
begin
  if (WindowHandle <> 0) and (TMessage(Message).Msg = WM_MOUSEWHEEL) and
    VertScrollBar.IsScrollBarVisible then
  begin
    LScrollBarValue := LScrollBarValues[TWMMouseWheel(Message).WheelDelta > 0];
    for LLineToScroll := 1 to Mouse.WheelScrollLines do
      Perform(WM_VSCROLL, LScrollBarValue, 0);
    TMessage(Message).Result := 1;
    Exit;
  end;
  inherited;
end;

The advantage of using DefaultHandler instead of a WM_MOUSEWHEEL message handler is that the scrollbox now works just like native Win32 controls do, where the parents all the way up to the form get the OnMouseWheel messages, and the innermost scrollbox scrolls if none of them handle it.

 
2) Patch VCL.Controls.pas  (fixes issue #4):
function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
var
  Control: TControl;
  P: TPoint;
  IsWheelMsg: Boolean;
begin
  IsWheelMsg := (Message.Msg = WM_MOUSEWHEEL) or (Message.Msg = WM_MOUSEHWHEEL);
  if GetCapture = Handle then
  begin
    if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then
      Control := CaptureControl
    else
      Control := nil;
  end
  else if IsWheelMsg then
    Control := ControlAtPos(ScreenToClient(SmallPointToPoint(Message.Pos)), False)
  else
    Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);
  Result := False;
  if Control <> nil then
  begin
    if IsWheelMsg then
    begin
      Message.Result := Control.Perform(Message.Msg, Message.Keys, TMessage(Message).LParam);
      Result := (Message.Result <> 0);
    end
    else
    begin
      P.X := Message.XPos - Control.Left;
      P.Y := Message.YPos - Control.Top;
      Message.Result := Control.Perform(Message.Msg, Message.Keys, PointToLParam(P));
      Result := True;
    end;
  end;
end;

3) To make the mouse wheel consistently affect the control the mouse is over, regardless of which version of Windows you're running or which way the new setting is configured (which matches what official Microsoft apps do), add a TApplicationEvents object with this as the OnMessage handler. (Fixes issues #2 and #3)

 

procedure TMyForm.ApplicationEventsMessage(var Msg: tagMSG;
  var Handled: Boolean);
var
  Wnd: HWND;
begin
  if (Msg.message = WM_MOUSEWHEEL) or (Msg.message = WM_MOUSEHWHEEL) then
  begin
    Wnd := GetCapture;
    if Wnd = 0 then
      Wnd := WindowFromPoint(SmallPointToPoint(TSmallPoint(Msg.lParam)));
    if Wnd <> 0 then
      msg.hwnd := Wnd;
  end;
end;

4) To fix the smeared appearance, you can

    A) Turn on VertScrollBar.Smooth, which triggers an Update call between each scroll

    B) Call Update yourself, either in the OnMouseWheel handler or in the TScrollBox WMMouseWheel/DefaultHandler method in between Perform calls.

    C) Change TScrollBox WMMouseWheel/DefaultHandler's Perform loop to the below, to update the scrollbar position once instead of with repeated WM_VSCROLL messages.

    if VertScrollBar.Smooth then
    begin
      LScrollBarValue := LScrollBarValues[TWMMouseWheel(Message).WheelDelta > 0];
      for LLineToScroll := 1 to Mouse.WheelScrollLines do
        Perform(WM_VSCROLL, LScrollBarValue, 0);
    end
    else
      { Move the scrollbar directly to only update the position once.
        TControlScrollBar.Increment uses different defaults for smooth vs chunky
        scrolling (TControlScrollBar.Update), so this may produce smaller scrolls
        than if you set Smooth:=true }
      VertScrollBar.Position := VertScrollBar.Position +
        VertScrollBar.Increment * Mouse.WheelScrollLines *
        -TWMMouseWheel(Message).WheelDelta div WHEEL_DELTA;

And yes, none of the above should be necessary because the VCL should handle all of this.  The IsControlMouseMsg bugs have existed forever though and appear to have been reported as QC-135258 at least as far back as 2015.

Edited by Zoë Peterson
  • Like 4

Share this post


Link to post
On 3/28/2022 at 5:52 PM, Zoë Peterson said:

A better fix is to instead remove TScrollBox.WMMouseWheel entirely (if you're on Delphi 11) and put the code in a newly overridden DefaultHandler:

I can't edit my previous message anymore, but after some further testing/research, overriding TScrollBox.MouseWheelHandler is as good of a place as DefaultHandler, and a bit more obvious.  In that case the code is identical to the first block I posted for TScrollBox.WMMouseWheel.

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×