How to direct the mouse wheel input to control under cursor instead of focused?
Asked Answered
C

8

42

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.

Corrigan answered 12/2, 2010 at 10:13 Comment(2)
#5297734Tunny
Related: #34146452Penthea
K
24

Try overriding your form's MouseWheelHandler method like this (I have not tested this thoroughly):

procedure TMyForm.MouseWheelHandler(var Message: TMessage);
var
  Control: TControl;
begin
  Control := ControlAtPos(ScreenToClient(SmallPointToPoint(TWMMouseWheel(Message).Pos)), False, True, True);
  if Assigned(Control) and (Control <> ActiveControl) then
  begin
    Message.Result := Control.Perform(CM_MOUSEWHEEL, Message.WParam, Message.LParam);
    if Message.Result = 0 then
      Control.DefaultHandler(Message);
  end
  else
    inherited MouseWheelHandler(Message);

end;
Kutaisi answered 12/2, 2010 at 12:7 Comment(6)
Almost working. ControlAtPos() gets the immediate child, so if the control is in panel, it returns the panel. FindVCLWindow(Mouse.CursorPos) returns the correct control. Just the DevExpress TcxTreeList scrolls too much - seems to do 3x the scroll.Corrigan
This turned out to be the solution that worked for me. The solution to the excessive scrolling was setting Message.Result := 1. Will watch out for limitations of FindVCLWindow. Thanks for the help.Corrigan
I've submitted QC report 82143 about what seems to be a bug in ControlAtPos implementation which fails to find nested child controls: qc.embarcadero.com/wc/qcmain.aspx?d=82143Kutaisi
For some reason this code produces StackOverflow when I scroll above TMyFormSelfwinding
This code can produce StackOverflow in the right circumstances when the MouseWheelHandler function is the MouseWheelHandler for the Control that gets passed to. I solved this by adding a "ScrollControl" variable in my form that gets set before calling "Perform", and checked along with the ActiveControl so it doesn't infinitely recurse. Should be set to nil at the end too.Lovesick
This gave me a Stack Overflow, and also did not do the trick. The answer below by Zoe worked, but not completely. It wouldn't scroll a TScrollBox and when a TDBGrid had focus, it captured the focus anyway.Penthea
W
30

Scrolling origins

An action with the mouse wheel results in a WM_MOUSEWHEEL message being sent:

Sent to the focus window when the mouse wheel is rotated. The DefWindowProc function propagates the message to the window's parent. There should be no internal forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it.

A mouse wheel's odyssey 1)

  1. The user scrolls the mouse wheel.
  2. The system places a WM_MOUSEWHEEL message into the foreground window’s thread’s message queue.
  3. The thread’s message loop fetches the message from the queue (Application.ProcessMessage). This message is of type TMsg which has a hwnd member designating the window handle the message is ment for.
  4. The Application.OnMessage event is fired.
    1. Setting the Handled parameter True stops further processing of the message (except for the next to steps).
  5. The Application.IsPreProcessMessage method is called.
    1. If no control has captured the mouse, the focused control's PreProcessMessage method is called, which does nothing by default. No control in the VCL has overriden this method.
  6. The Application.IsHintMsg method is called.
    1. The active hint window handles the message in an overriden IsHintMsg method. Preventing the message from further processing is not possible.
  7. DispatchMessage is called.
  8. The TWinControl.WndProc method of the focused window receives the message. This message is of type TMessage which lacks the window (because that is the instance this method is called upon).
  9. The TWinControl.IsControlMouseMsg method is called to check whether the mouse message should be directed to one of its non-windowed child controls.
    1. If there is a child control that has captured the mouse or is at the current mouse position2), then the message is sent to the child control's WndProc method, see step 10. (2) This will never happen, because WM_MOUSEWHEEL contains its mouse position in screen coordinates and IsControlMouseMsg assumes a mouse position in client coordinates (XE2).)
  10. The inherited TControl.WndProc method receives the message.
    1. When the system does not natively supports mouse wheel (< Win98 or < WinNT4.0), the message is converted to a CM_MOUSEWHEEL message and is send to TControl.MouseWheelHandler, see step 13.
    2. Otherwise the message is dispatched to the appropriate message handler.
  11. The TControl.WMMouseWheel method receives the message.
  12. The WM_MOUSEWHEEL window message (meaningful to the system and often to the VCL too) is converted to a CM_MOUSEWHEEL control message (meaningful only to the VCL) which provides for the convenient VCL's ShiftState information instead of the system's keys data.
  13. The control's MouseWheelHandler method is called.
    1. If the control is a TCustomForm, then the TCustomForm.MouseWheelHandler method is called.
      1. If there is a focused control on it, then CM_MOUSEWHEEL is sent to the focused control, see step 14.
      2. Otherwise the inherited method is called, see step 13.2.
    2. Otherwise the TControl.MouseWheelHandler method is called.
      1. If there is a control that has captured the mouse and has no parent3), then the message is sent to that control, see step 8 or 10, depending on the type of the control. (3) This will never happen, because Capture is gotten with GetCaptureControl, which checks for Parent <> nil (XE2).)
      2. If the control is on a form, then the control's form's MouseWheelHandler is called, see step 13.1.
      3. Otherwise, or if the control ís the form, then CM_MOUSEWHEEL is sent to the control, see step 14.
  14. The TControl.CMMouseWheel method receives the message.
    1. The TControl.DoMouseWheel method is called.
      1. The OnMouseWheel event is fired.
      2. If not handled, then TControl.DoMouseWheelDown or TControl.DoMouseWheelUp is called, depending on the scroll direction.
      3. The OnMouseWheelDown or the OnMouseWheelUp event is fired.
    2. If not handled, then 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 setting Message.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 to nil, the VCL ensures a focused control with TCustomForm.SetWindowFocus, which defaults to the previously active form. (With Windows.SetFocus(0), indeed the message is never sent.)

Due to the bug in IsControlMouseMsg 2), a TControl can only receive the WM_MOUSEWHEEL message if it has captured the mouse. This can manually be achieved by setting Control.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 its MouseWheelHandler 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 in TControl? Component writers may have implemented their own MouseWheelHandler 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. Sending CM_MOUSEWHEEL to such a control has no effect by default. These subclassed controls scroll as a result of the WM_MOUSEWHEEL message sent to the with the subclass associated API window procedure with CallWindowProc, which the VCL takes care of in TWinControl.DefaultHandler. Oddly enough, this routine does not check Message.Result before calling CallWindowProc, and once the message is sent, scrolling cannot be prevented. The message comes back with its Result set depending on whether the control normally is capable of scrolling or on the type of control. (E.g. a TMemo returns <> 0, and TEdit returns 0.) Whether it actually scrolled has no influence on the message result.

VCL controls rely on the default handling as implemented in TControl and TWinControl, as layed out above. They act on wheel events in DoMouseWheel, DoMouseWheelDown or DoMouseWheelUp. For as far I know, no control in the VCL has overriden MouseWheelHandler 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:

  • Make the mouse wheel affect the control, pane, or window that the pointer is currently over. Doing so avoids unintended results.
  • Make the mouse wheel take effect without clicking or having input focus. Hovering is sufficient.
  • Make the mouse wheel affect the object with the most specific scope. For example, if the pointer is over a scrollable list box control in a scrollable pane within a scrollable window, the mouse wheel affects the list box control.
  • Don't change the input focus when using the mouse wheel.

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 is TApplication.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.

procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;
  var Handled: Boolean);
var
  Window: HWND;
begin
  if Msg.message = WM_MOUSEWHEEL then
  begin
    Window := WindowFromPoint(Msg.pt);
    if Window <> 0 then
    begin

      Handled := True;
    end;
  end;
end;

With FindControl we get a reference to the VCL control. If the result is nil, 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 down TDateTimePicker). In that case the message needs to be forwarded back to the API, and its result we are not interested in.

  WinControl: TWinControl;
  WndProc: NativeInt;

      WinControl := FindControl(Window);
      if WinControl = nil then
      begin
        WndProc := GetWindowLongPtr(Window, GWL_WNDPROC);
        CallWindowProc(Pointer(WndProc), Window, Msg.message, Msg.wParam,
          Msg.lParam);
      end
      else
      begin

      end;

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 a CM_MOUSEWHEEL message because that control is definitely the foreground control. The message is to be constructed from the WM_MOUSEWHEEL message and translated into its VCL equivalent. Secondly, the WM_MOUSEWHEEL message has to be send to the control's DefaultHandler method to allow handling for native controls. And at last, again the CM_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.

  Point: TPoint;
  Message: TMessage;

        Point := WinControl.ScreenToClient(Msg.pt);
        Message.WParam := Msg.wParam;
        Message.LParam := Msg.lParam;
        TCMMouseWheel(Message).ShiftState :=
          KeysToShiftState(TWMMouseWheel(Message).Keys);
        Message.Result := WinControl.ControlAtPos(Point, False).Perform(
          CM_MOUSEWHEEL, Message.WParam, Message.LParam);
        if Message.Result = 0 then
        begin
          Message.Msg := Msg.message;
          Message.WParam := Msg.wParam;
          Message.LParam := Msg.lParam;
          WinControl.DefaultHandler(Message);
        end;
        if Message.Result = 0 then
        begin
          Message.WParam := Msg.wParam;
          Message.LParam := Msg.lParam;
          TCMMouseWheel(Message).ShiftState :=
            KeysToShiftState(TWMMouseWheel(Message).Keys);
          Message.Result := WinControl.Perform(CM_MOUSEWHEEL, Message.WParam,
            Message.LParam);
        end;

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 (see TDragObject.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.

    Window := GetCapture;
    if Window <> 0 then
    begin
      Message.Result := GetCaptureControl.Perform(CM_MOUSEWHEEL, Message.WParam,
        Message.LParam);
      if Message.Result = 0 then
        Message.Result := SendMessage(Window, Msg.message, Msg.wParam,
          Msg.lParam);
    end;

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:

  • The possibility to preview a wheel action in the main form, the active form, or the active control.
  • Registration of control classes for which their MouseWheelHandler method has to be called.
  • The possibility to bring this TApplicationEvents object in front of all others.
  • The possibility to cancel dispatching the OnMessage event to all other TApplicationEvents objects.
  • The possibility to still allow for default VCL handling afterwards for analytical or testing purposes.

ScrollAnywhere.pas

unit ScrollAnywhere;

interface

uses
  System.Classes, System.Types, System.Contnrs, Winapi.Windows, Winapi.Messages,
  Vcl.Controls, Vcl.Forms, Vcl.AppEvnts;

type
  TWheelMsgSettings = record
    MainFormPreview: Boolean;
    ActiveFormPreview: Boolean;
    ActiveControlPreview: Boolean;
    VclHandlingAfterHandled: Boolean;
    VclHandlingAfterUnhandled: Boolean;
    CancelApplicationEvents: Boolean;
    procedure RegisterMouseWheelHandler(ControlClass: TControlClass);
  end;

  TMouseHelper = class helper for TMouse
  public
    class var WheelMsgSettings: TWheelMsgSettings;
  end;

procedure Activate;

implementation

type
  TWheelInterceptor = class(TCustomApplicationEvents)
  private
    procedure ApplicationMessage(var Msg: tagMSG; var Handled: Boolean);
  public
    constructor Create(AOwner: TComponent); override;
  end;

var
  WheelInterceptor: TWheelInterceptor;
  ControlClassList: TClassList;

procedure TWheelInterceptor.ApplicationMessage(var Msg: tagMSG;
  var Handled: Boolean);
var
  Window: HWND;
  WinControl: TWinControl;
  WndProc: NativeInt;
  Message: TMessage;
  OwningProcess: DWORD;

  procedure WinWParamNeeded;
  begin
    Message.WParam := Msg.wParam;
  end;

  procedure VclWParamNeeded;
  begin
    TCMMouseWheel(Message).ShiftState :=
      KeysToShiftState(TWMMouseWheel(Message).Keys);
  end;

  procedure ProcessControl(AControl: TControl;
    CallRegisteredMouseWheelHandler: Boolean);
  begin
    if (Message.Result = 0) and CallRegisteredMouseWheelHandler and
      (AControl <> nil) and
      (ControlClassList.IndexOf(AControl.ClassType) <> -1) then
    begin
      AControl.MouseWheelHandler(Message);
    end;
    if Message.Result = 0 then
      Message.Result := AControl.Perform(CM_MOUSEWHEEL, Message.WParam,
        Message.LParam);
  end;

begin
  if Msg.message <> WM_MOUSEWHEEL then
    Exit;
  with Mouse.WheelMsgSettings do
  begin
    Message.Msg := Msg.message;
    Message.WParam := Msg.wParam;
    Message.LParam := Msg.lParam;
    Message.Result := LRESULT(Handled);
    // Allow controls for which preview is set to handle the message
    VclWParamNeeded;
    if MainFormPreview then
      ProcessControl(Application.MainForm, False);
    if ActiveFormPreview then
      ProcessControl(Screen.ActiveCustomForm, False);
    if ActiveControlPreview then
      ProcessControl(Screen.ActiveControl, False);
    // Allow capturing control to handle the message
    Window := GetCapture;
    if (Window <> 0) and (Message.Result = 0) then
    begin
      ProcessControl(GetCaptureControl, True);
      if Message.Result = 0 then
        Message.Result := SendMessage(Window, Msg.message, Msg.wParam,
          Msg.lParam);
    end;
    // Allow hovered control to handle the message
    Window := WindowFromPoint(Msg.pt);
    if (Window <> 0) and (Message.Result = 0) then
    begin
      WinControl := FindControl(Window);
      if WinControl = nil then
      begin
        // Window is a non-VCL window (e.g. a dropped down TDateTimePicker), or
        // the window doesn't belong to this process
        WndProc := GetWindowLongPtr(Window, GWL_WNDPROC);
        Message.Result := CallWindowProc(Pointer(WndProc), Window,
          Msg.message, Msg.wParam, Msg.lParam);
      end
      else
      begin
        // Window is a VCL control
        // Allow non-windowed child controls to handle the message
        ProcessControl(WinControl.ControlAtPos(
          WinControl.ScreenToClient(Msg.pt), False), True);
        // Allow native controls to handle the message
        if Message.Result = 0 then
        begin
          WinWParamNeeded;
          WinControl.DefaultHandler(Message);
        end;
        // Allow windowed VCL controls to handle the message
        if not ((MainFormPreview and (WinControl = Application.MainForm)) or
          (ActiveFormPreview and (WinControl = Screen.ActiveCustomForm)) or
          (ActiveControlPreview and (WinControl = Screen.ActiveControl))) then
        begin
          VclWParamNeeded;
          ProcessControl(WinControl, True);
        end;
      end;
    end;
    // Bypass default VCL wheel handling?
    Handled := ((Message.Result <> 0) and not VclHandlingAfterHandled) or
      ((Message.Result = 0) and not VclHandlingAfterUnhandled);
    // Modify message destination for current process
    if (not Handled) and (Window <> 0) and
      (GetWindowThreadProcessID(Window, OwningProcess) <> 0) and
      (OwningProcess = GetCurrentProcessId) then
    begin
      Msg.hwnd := Window;
    end;
    if CancelApplicationEvents then
      CancelDispatch;
  end;
end;

constructor TWheelInterceptor.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  OnMessage := ApplicationMessage;
end;

procedure Activate;
begin
  WheelInterceptor.Activate;
end;

{ TWheelMsgSettings }

procedure TWheelMsgSettings.RegisterMouseWheelHandler(
  ControlClass: TControlClass);
begin
  ControlClassList.Add(ControlClass);
end;

initialization
  ControlClassList := TClassList.Create;
  WheelInterceptor := TWheelInterceptor.Create(Application);

finalization
  ControlClassList.Free;

end.

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. When VclHandlingAfterHandled or VclHandlingAfterUnhandled is set True, 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

Wiatt answered 20/12, 2015 at 22:41 Comment(5)
"Sent to the focus window when ..." vs. ".. thread’s message loop fetches the message from the queue ..." I wonder why documentation insists that the message is sent (also here) while evidently this is not the case.Executory
"The message comes back with its Result set ..." RTL sets each and every dispatched message's result to 0, in classes.StdWndProc, before calling the target window procedure.Executory
@Ser The documentation dóes explain two different message routing methods, but I believe they prefer to use a single term synonymously for the sake of simplicity, because to give a summary every time the documentation mentions sending would not benefit readability. But indeed, Windows desktop programmers should be aware of the rather big difference between e.g. PostMessage and SendMessage.Wiatt
@Ser There, I am talking about the message result type during the call of TWinControl.DefaultHandler, which is the very last routine the message passes, way past the time it was created with Classes.StdWndProc.Wiatt
Is this the longest answer on StackOverflow? I don't complain. Nice that it ends with a concrete answer.Lactase
K
24

Try overriding your form's MouseWheelHandler method like this (I have not tested this thoroughly):

procedure TMyForm.MouseWheelHandler(var Message: TMessage);
var
  Control: TControl;
begin
  Control := ControlAtPos(ScreenToClient(SmallPointToPoint(TWMMouseWheel(Message).Pos)), False, True, True);
  if Assigned(Control) and (Control <> ActiveControl) then
  begin
    Message.Result := Control.Perform(CM_MOUSEWHEEL, Message.WParam, Message.LParam);
    if Message.Result = 0 then
      Control.DefaultHandler(Message);
  end
  else
    inherited MouseWheelHandler(Message);

end;
Kutaisi answered 12/2, 2010 at 12:7 Comment(6)
Almost working. ControlAtPos() gets the immediate child, so if the control is in panel, it returns the panel. FindVCLWindow(Mouse.CursorPos) returns the correct control. Just the DevExpress TcxTreeList scrolls too much - seems to do 3x the scroll.Corrigan
This turned out to be the solution that worked for me. The solution to the excessive scrolling was setting Message.Result := 1. Will watch out for limitations of FindVCLWindow. Thanks for the help.Corrigan
I've submitted QC report 82143 about what seems to be a bug in ControlAtPos implementation which fails to find nested child controls: qc.embarcadero.com/wc/qcmain.aspx?d=82143Kutaisi
For some reason this code produces StackOverflow when I scroll above TMyFormSelfwinding
This code can produce StackOverflow in the right circumstances when the MouseWheelHandler function is the MouseWheelHandler for the Control that gets passed to. I solved this by adding a "ScrollControl" variable in my form that gets set before calling "Perform", and checked along with the ActiveControl so it doesn't infinitely recurse. Should be set to nil at the end too.Lovesick
This gave me a Stack Overflow, and also did not do the trick. The answer below by Zoe worked, but not completely. It wouldn't scroll a TScrollBox and when a TDBGrid had focus, it captured the focus anyway.Penthea
W
7

Override the TApplication.OnMessage event (or create a TApplicationEvents component) and redirect the WM_MOUSEWHEEL message in the event handler:

procedure TMyForm.AppEventsMessage(var Msg: tagMSG;
  var Handled: Boolean);
var
  Pt: TPoint;
  C: TWinControl;
begin
  if Msg.message = WM_MOUSEWHEEL then begin
    Pt.X := SmallInt(Msg.lParam);
    Pt.Y := SmallInt(Msg.lParam shr 16);
    C := FindVCLWindow(Pt);
    if C = nil then 
      Handled := True
    else if C.Handle <> Msg.hwnd then begin
      Handled := True;
      SendMessage(C.Handle, WM_MOUSEWHEEL, Msg.wParam, Msg.lParam);
    end;
   end;
end;

It works fine here, though you may want to add some protection to keep it from recursing if something unexpected happens.

Waggish answered 12/2, 2010 at 12:59 Comment(4)
I think this is the best answer. The problem is that a focused DevExpress control still intercepts this message. If I call C.Perform() instead of SendMessage(), then the DevExpress controls work but the common controls don't. Have to do some digging in the DevExpress source to disable this hook.Corrigan
I ended up abandoning this solution as it seems that the focused TControl (nothing to to do with DevExpress) always intercepts the message.Corrigan
This is the closest I can find, but still doesn't work. As mentioned, a focused control always winds up scrolling anyway. Even if, for example, a TDBGrid has focus, but the mouse is scrolling something else, it still scrolls the TDBGrid.Penthea
I've noticed this works on XE8, but don't works on 10 Seattle, at least on my env.Thumbtack
U
2

You might find this article useful: send a scroll down message to listbox using mousewheel, but listbox doesn't have focus [1], it is written in C#, but converting to Delphi shouldn't be too big a problem. It uses hooks to accomplish the wanted effect.

To find out which component the mouse is currently over, you can use the FindVCLWindow function, an example of this can be found in this article: Get the Control Under the Mouse in a Delphi application [2].

[1] http://social.msdn.microsoft.com/forums/en-US/winforms/thread/ec1fbfa2-137e-49f6-b444-b634e4f44f21/
[2] http://delphi.about.com/od/delphitips2008/qt/find-vcl-window.htm

Uniformed answered 12/2, 2010 at 11:14 Comment(0)
S
2

This is the solution I've been using:

  1. Add amMouseWheel to the uses clause of the implementation section of the unit of your form after the forms unit:

    unit MyUnit;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
      // Fix and util for mouse wheel
      amMouseWheel;
    ...
    
  2. Save the following code to amMouseWheel.pas:

    unit amMouseWheel;
    
    // -----------------------------------------------------------------------------
    // The original author is Anders Melander, [email protected], http://melander.dk
    // Copyright © 2008 Anders Melander
    // -----------------------------------------------------------------------------
    // License:
    // Creative Commons Attribution-Share Alike 3.0 Unported
    // http://creativecommons.org/licenses/by-sa/3.0/
    // -----------------------------------------------------------------------------
    
    interface
    
    uses
      Forms,
      Messages,
      Classes,
      Controls,
      Windows;
    
    //------------------------------------------------------------------------------
    //
    //      TForm work around for mouse wheel messages
    //
    //------------------------------------------------------------------------------
    // The purpose of this class is to enable mouse wheel messages on controls
    // that doesn't have the focus.
    //
    // To scroll with the mouse just hover the mouse over the target control and
    // scroll the mouse wheel.
    //------------------------------------------------------------------------------
    type
      TForm = class(Forms.TForm)
      public
        procedure MouseWheelHandler(var Msg: TMessage); override;
      end;
    
    //------------------------------------------------------------------------------
    //
    //      Generic control work around for mouse wheel messages
    //
    //------------------------------------------------------------------------------
    // Call this function from a control's (e.g. a TFrame) DoMouseWheel method like
    // this:
    //
    // function TMyFrame.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
    //   MousePos: TPoint): Boolean;
    // begin
    //   Result := ControlDoMouseWheel(Self, Shift, WheelDelta, MousePos) or inherited;
    // end;
    //
    //------------------------------------------------------------------------------
    function ControlDoMouseWheel(Control: TControl; Shift: TShiftState;
      WheelDelta: Integer; MousePos: TPoint): Boolean;
    
    implementation
    
    uses
      Types;
    
    procedure TForm.MouseWheelHandler(var Msg: TMessage);
    var
      Target: TControl;
    begin
      // Find the control under the mouse
      Target := FindDragTarget(SmallPointToPoint(TCMMouseWheel(Msg).Pos), False);
    
      while (Target <> nil) do
      begin
        // If the target control is the focused control then we abort as the focused
        // control is the originator of the call to this method.
        if (Target = Self) or ((Target is TWinControl) and (TWinControl(Target).Focused)) then
        begin
          Target := nil;
          break;
        end;
    
        // Let the target control process the scroll. If the control doesn't handle
        // the scroll then...
        Msg.Result := Target.Perform(CM_MOUSEWHEEL, Msg.WParam, Msg.LParam);
        if (Msg.Result <> 0) then
          break;
    
        // ...let the target's parent give it a go instead.
        Target := Target.Parent;
      end;
    
      // Fall back to the default processing if none of the controls under the mouse
      // could handle the scroll.
      if (Target = nil) then
        inherited;
    end;
    
    type
      TControlCracker = class(TControl);
    
    function ControlDoMouseWheel(Control: TControl; Shift: TShiftState;
      WheelDelta: Integer; MousePos: TPoint): Boolean;
    var
      Target: TControl;
    begin
      (*
      ** The purpose of this method is to enable mouse wheel messages on controls
      ** that doesn't have the focus.
      **
      ** To scroll with the mouse just hover the mouse over the target control and
      ** scroll the mouse wheel.
      *)
      Result := False;
    
      // Find the control under the mouse
      Target := FindDragTarget(MousePos, False);
    
      while (not Result) and (Target <> nil) do
      begin
        // If the target control is the focused control then we abort as the focused
        // control is the originator of the call to this method.
        if (Target = Control) or ((Target is TWinControl) and (TWinControl(Target).Focused)) then
          break;
    
        // Let the target control process the scroll. If the control doesn't handle
        // the scroll then...
        Result := TControlCracker(Target).DoMouseWheel(Shift, WheelDelta, MousePos);
    
        // ...let the target's parent give it a go instead.
        Target := Target.Parent;
      end;
    end;
    
    end.
    
Selfinductance answered 24/2, 2014 at 9:32 Comment(4)
This did absolutely nothing for me.Penthea
@JerryDodge Works fine for me everywhere I've used it and I've heard from others that it works for them too. I can''t really comment on why it doesn't work for you since you haven't described what is you've done. You should post a new question with details about your particular requirements and problems.Selfinductance
I spoke too soon and forgot to come back and edit, sorry. It works, but not completely. The core issue is that if another control currently has focus, it still scrolls, for example a TDBGrid (which is widely used in our app). So I end up with two controls scrolling at the same time. I actually started a bounty on this question, because asking a new question would just get marked as a duplicate of this one.Penthea
@JerryDodge What version of Delphi? Are you using the TForm.MouseWheelHandler solution or the ControlDoMouseWheel() solution? Try placing a breakpoint at the Perform(CM_MOUSEWHEEL)/DoMouseWheel() call. Does the target return the correct value (i.e. a value that indicates it handled the event)? If the target returns an incorrect value then you will get the symptom you're seeing.Selfinductance
D
0

I had the same problem and solved it with some little hack, but it works.

I didn't want to mess around with messages and decided just to call DoMouseWheel method to control I need. Hack is that DoMouseWheel is protected method and therefore not accessible from form unit file, that's why I defined my class in form unit:

TControlHack = class(TControl)
end;  //just to call DoMouseWheel

Then I wrote TForm1.onMouseWheel event handler:

procedure TForm1.FormMouseWheel(Sender: TObject; Shift: TShiftState;
    WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var i: Integer;
    c: TControlHack;
begin
  for i:=0 to ComponentCount-1 do
    if Components[i] is TControl then begin
      c:=TControlHack(Components[i]);
      if PtInRect(c.ClientRect,c.ScreenToClient(MousePos)) then 
      begin
        Handled:=c.DoMouseWheel(shift,WheelDelta,MousePos);
        if Handled then break;
      end;
   end;
end;

As you see, it search for all the controls on form, not only immediate children, and turns out to search from parents to children. It would be better (but more code) to make recursive search at children, but code above works just fine.

To make only one control respond to mousewheel event, you should always set Handled:=true when it's implemented. If for example you have listbox inside panel, then panel will execute DoMouseWheel first, and if it didn't handle event, listbox.DoMouseWheel will execute. If no control under mouse cursor handled DoMouseWheel, the focused control will, it seems rather adequate behavior.

Dunite answered 7/12, 2015 at 14:14 Comment(2)
Thanks, but this didn't help. Same problems as described in my comments on the other answers.Penthea
At least I presume that you don't get stack overflow (it's just impossible in this code). Does scrollbox work the way it should?Dunite
D
0

Only for using with DevExpress controls

It works on XE3. It was not tested on other versions.

procedure TMainForm.DoApplicationMessage(var AMsg: TMsg; var AHandled: Boolean);
var
  LControl: TWinControl;
  LMessage: TMessage;
begin

  if AMsg.message <> WM_MOUSEWHEEL then
    Exit;

  LControl := FindVCLWindow(AMsg.pt);
  if not Assigned(LControl) then
    Exit;

  LMessage.WParam := AMsg.wParam;
  // see TControl.WMMouseWheel
  TCMMouseWheel(LMessage).ShiftState := KeysToShiftState(TWMMouseWheel(LMessage).Keys);
  LControl.Perform(CM_MOUSEWHEEL, LMessage.WParam, AMsg.lParam);

  AHandled := True;

end;

if you don't use DevExpress controls, then Perform -> SendMessage

SendMessage(LControl.Handle, AMsg.message, AMsg.WParam, AMsg.lParam);
Dwight answered 5/11, 2016 at 6:6 Comment(0)
E
-2

In the OnMouseEnter event for each scrollable control add a respective call to SetFocus

So for ListBox1:

procedure TForm1.ListBox1MouseEnter(Sender: TObject);  
begin  
    ListBox1.SetFocus;  
end;  

Does this achieve the desired effect?

Edin answered 12/2, 2010 at 16:18 Comment(3)
No, that would be bad behaviour for a program.Corrigan
This will change the user experience serverly. Not everybody has wrked with the X windows manager where you move the mouse to give focus to different windows..Mouth
This would be a horrible user experience. Focus means a lot. Only the user should be the one to decide when to set focus.Penthea

© 2022 - 2024 — McMap. All rights reserved.