I use a number of scrolling controls: TTreeViews, TListViews, DevExpress cxGrids and cxTreeLists, etc. When the mouse wheel is spun, the control with focus receives the input no matter what control the mouse cursor is over.
How do you direct the mouse wheel input to whatever control the mouse cursor is over? The Delphi IDE works very nicely in this regard.
Best Answer
Scrolling origins
An action with the mouse wheel results in a
WM_MOUSEWHEEL
message being sent:A mouse wheel's odyssey 1)
WM_MOUSEWHEEL
message into the foreground window’s thread’s message queue.Application.ProcessMessage
). This message is of typeTMsg
which has ahwnd
member designating the window handle the message is ment for.Application.OnMessage
event is fired.Handled
parameterTrue
stops further processing of the message (except for the next to steps).Application.IsPreProcessMessage
method is called.PreProcessMessage
method is called, which does nothing by default. No control in the VCL has overriden this method.Application.IsHintMsg
method is called.IsHintMsg
method. Preventing the message from further processing is not possible.DispatchMessage
is called.TWinControl.WndProc
method of the focused window receives the message. This message is of typeTMessage
which lacks the window (because that is the instance this method is called upon).TWinControl.IsControlMouseMsg
method is called to check whether the mouse message should be directed to one of its non-windowed child controls.WndProc
method, see step 10. (2) This will never happen, becauseWM_MOUSEWHEEL
contains its mouse position in screen coordinates andIsControlMouseMsg
assumes a mouse position in client coordinates (XE2).)TControl.WndProc
method receives the message.CM_MOUSEWHEEL
message and is send toTControl.MouseWheelHandler
, see step 13.TControl.WMMouseWheel
method receives the message.WM_MOUSEWHEEL
window message (meaningful to the system and often to the VCL too) is converted to aCM_MOUSEWHEEL
control message (meaningful only to the VCL) which provides for the convenient VCL'sShiftState
information instead of the system's keys data.MouseWheelHandler
method is called.TCustomForm
, then theTCustomForm.MouseWheelHandler
method is called.CM_MOUSEWHEEL
is sent to the focused control, see step 14.TControl.MouseWheelHandler
method is called.Capture
is gotten withGetCaptureControl
, which checks forParent <> nil
(XE2).)MouseWheelHandler
is called, see step 13.1.CM_MOUSEWHEEL
is sent to the control, see step 14.TControl.CMMouseWheel
method receives the message.TControl.DoMouseWheel
method is called.OnMouseWheel
event is fired.TControl.DoMouseWheelDown
orTControl.DoMouseWheelUp
is called, depending on the scroll direction.OnMouseWheelDown
or theOnMouseWheelUp
event is fired.CM_MOUSEWHEEL
is sent to the parent control, see step 14. (I believe this is against the advice given by MSDN in the quote above, but that undoubtedly is a thoughtful decision made by the developers. Possibly because that would start this very chain al over.)Remarks, observations and considerations
At almost every step in this chain of processing the message can be ignored by doing nothing, altered by changing the message parameters, handled by acting on it, and canceled by setting
Handled := True
or settingMessage.Result
to non-zero.Only when some control has focus, this message is received by the application. But even when
Screen.ActiveCustomForm.ActiveControl
is forcefully set tonil
, the VCL ensures a focused control withTCustomForm.SetWindowFocus
, which defaults to the previously active form. (WithWindows.SetFocus(0)
, indeed the message is never sent.)Due to the bug in
IsControlMouseMsg
2), aTControl
can only receive theWM_MOUSEWHEEL
message if it has captured the mouse. This can manually be achieved by settingControl.MouseCapture := True
, but you have to take special care of releasing that capture expeditiously, otherwise it will have unwanted side effects like the need for an unnecessary extra click to get something done. Besides, mouse capture typically only takes place between a mouse down and a mouse up event, but this restriction does not necessarily have to be applied. But even when the message reaches the control, it is sent to itsMouseWheelHandler
method which just sends it back to either the form or the active control. Thus non-windowed VCL controls can never act on the message by default. I believe this is another bug, otherwise why would all wheel handling have been implemented inTControl
? Component writers may have implemented their ownMouseWheelHandler
method for this very purpose, and whatever solution comes to this question, there has to be taken care of not breaking this kind of existing customization.Native controls which are capable of scrolling with the wheel, like
TMemo
,TListBox
,TDateTimePicker
,TComboBox
,TTreeView
,TListView
, etc. are scrolled by the system itself. SendingCM_MOUSEWHEEL
to such a control has no effect by default. These subclassed controls scroll as a result of theWM_MOUSEWHEEL
message sent to the with the subclass associated API window procedure withCallWindowProc
, which the VCL takes care of inTWinControl.DefaultHandler
. Oddly enough, this routine does not checkMessage.Result
before callingCallWindowProc
, and once the message is sent, scrolling cannot be prevented. The message comes back with itsResult
set depending on whether the control normally is capable of scrolling or on the type of control. (E.g. aTMemo
returns<> 0
, andTEdit
returns0
.) Whether it actually scrolled has no influence on the message result.VCL controls rely on the default handling as implemented in
TControl
andTWinControl
, as layed out above. They act on wheel events inDoMouseWheel
,DoMouseWheelDown
orDoMouseWheelUp
. For as far I know, no control in the VCL has overridenMouseWheelHandler
in order to handle wheel events.Looking at different applications, there seems to be no conformity on which wheel scroll behaviour is the standard. For example: MS Word scrolls the page that is hovered, MS Excel scrolls the workbook that is focused, Windows Eplorer scrolls the focused pane, websites implement scroll behaviour each very differently, Evernote scrolls the window that is hovered, etc... And Delphi's own IDE tops everything by scrolling the focused window as well as the hovered window, except when hovering the code editor, then the code editor steals focus when you scroll (XE2).
Luckily Microsoft offers at least user experience guidelines for Windows-based desktop applications:
So the question's requirement to only scroll the hovered control has enough grounds, but Delphi's developers haven't made it easy to implement it.
Conclusion and solution
The preferred solution is one without subclassing windows or multiple implementations for different forms or controls.
To prevent the focused control from scrolling, the control may not receive the
CM_MOUSEWHEEL
message. Therefore,MouseWheelHandler
of any control may not be called. Therefore,WM_MOUSEWHEEL
may not be send to any control. Thus the only place left for intervention isTApplication.OnMessage
. Furthermore, the message may not escape from it, so all handling should take place in that event handler and when all default VCL wheel handling is bypassed, every possible condition is to be taken care of.Let's start simple. The enabled window which currently is hovered is gotten with
WindowFromPoint
.With
FindControl
we get a reference to the VCL control. If the result isnil
, then the hovered window does not belong to the application's process, or it is a window not known to the VCL (e.g. a dropped downTDateTimePicker
). In that case the message needs to be forwarded back to the API, and its result we are not interested in.When the window ís a VCL control, multiple message handlers are to be considered calling, in a specific order. When there is an enabled non-windowed control (of type
TControl
or descendant) on the mouse position, it first should get aCM_MOUSEWHEEL
message because that control is definitely the foreground control. The message is to be constructed from theWM_MOUSEWHEEL
message and translated into its VCL equivalent. Secondly, theWM_MOUSEWHEEL
message has to be send to the control'sDefaultHandler
method to allow handling for native controls. And at last, again theCM_MOUSEWHEEL
message has to be send to the control when no previous handler took care of the message. These last two steps cannot take place in reversed order because e.g. a memo on a scroll box must be able to scroll too.When a window has captured the mouse, all wheel messages should be sent to it. The window retrieved by
GetCapture
is ensured to be a window of the current process, but it does not have to be a VCL control. E.g. during a drag operation, a temporary window is created (seeTDragObject.DragHandle
) that receives mouse messages. All messages? Noooo,WM_MOUSEWHEEL
is not sent to the capturing window, so we have to redirect it. Furthermore, when the capturing window does not handle the message, all other previously covered processing should take place. This is a feature which is missing in the VCL: on wheeling during a drag operation,Form.OnMouseWheel
indeed is called, but the focused or hovered control does not receive the message. This means for example that a text cannot be dragged into a memo's content on a location that is beyond the visible part of the memo.This essentially does the job, and it was the basis for the unit presented below. To get it working, just add the unit name to one of the uses clauses in your project. It has the following additional features:
MouseWheelHandler
method has to be called.TApplicationEvents
object in front of all others.OnMessage
event to all otherTApplicationEvents
objects.ScrollAnywhere.pas
Disclaimer:
This code intentionally does not scroll anything, it only prepares the message routing for VCL's
OnMouseWheel*
events to get the proper opportunity to get fired. This code is not tested on third-party controls. WhenVclHandlingAfterHandled
orVclHandlingAfterUnhandled
is setTrue
, then mouse events may be fired twice. In this post I made some claims and I considered there to be three bugs in the VCL, however, that is all based on studying documentation and testing. Please do test this unit and comment on findings and bugs. I apologize for this rather long answer; I simply do not have a blog.1) Naming cheeky taken from A Key’s Odyssey
2) See my Quality Central bug report #135258
3) See my Quality Central bug report #135305