Henry Olive 5 Posted March 25, 2022 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
PeterBelow 238 Posted March 25, 2022 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
Edwin Yip 154 Posted March 26, 2022 Do you want to scroll TScrollBox vertically with mouse wheel? Share this post Link to post
Henry Olive 5 Posted March 26, 2022 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
PeterBelow 238 Posted March 26, 2022 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. 1 Share this post Link to post
Henry Olive 5 Posted March 27, 2022 Peter, Thank you so much for the information Share this post Link to post
Edwin Yip 154 Posted March 27, 2022 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
Zoë Peterson 15 Posted March 28, 2022 (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 March 28, 2022 by Zoë Peterson 4 Share this post Link to post
Attila Kovacs 629 Posted March 29, 2022 (edited) There is more with these scrolling components. Touch / Pan is not ok as windows translates wm_gesture/pan messages to wm_scroll if the previous pan is still running.... Also, the pan handling in the VCL is wrong https://quality.embarcadero.com/browse/RSP-37027 Edited March 29, 2022 by Attila Kovacs Share this post Link to post
Zoë Peterson 15 Posted March 30, 2022 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