Capturing vertical and horizontal scroll from mousewheel - delphi

I need to capture mousewheel events in my application to move the view area like modern UI's since my application will run mostly on laptops.
I have looked into Windows messages and apparently only controls inherited from TWinControl can recieve mousewheel messages.
I'm using TApplicationEvents, that can also capture those mesagges. I tried handling the WM_MOUSEWHEEL message, but it works only for vertical scrolling. I also tried handling WM_HSCROLL and WM_HSCROLLCLIPBOARD messages, but they were not captured at all.
How can I capture both vertical and especially horizontal mousewheel messages and use them in my software?

You simply need to respond to WM_MOUSEHWHEEL messages. For instance, here's an extract from a class of mine which adds horizontal mouse wheel scrolling to a scroll box:
procedure TMyScrollBox.WndProc(var Message: TMessage);
begin
if Message.Msg=WM_MOUSEHWHEEL then begin
(* For some reason using a message handler for WM_MOUSEHWHEEL doesn't work.
The messages don't always arrive. It seems to occur when both scroll bars
are active. Strangely, if we handle the message here, then the messages
all get through. Go figure! *)
if TWMMouseWheel(Message).Keys=0 then begin
HorzScrollBar.Position := HorzScrollBar.Position
+ TWMMouseWheel(Message).WheelDelta;
Message.Result := 0;
end else begin
Message.Result := 1;
end;
end else begin
inherited;
end;
end;

First you need to handle the message WM_MOUSEHWHEEL.
Notice that the letter "H" is in there (WM_MOUSE H WHEEL).
I added a TApplicationEvent componenet and added the following code
to the OnMessage event:
uses VCL.Controls;
.....
procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var ctrl: TWinControl;
begin
ctrl := FindVCLWindow(Mouse.CursorPos); //first I need to find the control under the mouse
if ctrl is T3DWorldViewerComponent then //then I need to make sure
//that the control under the mouse
//is the 3D World Viewer contains my graphics
if msg.message = WM_MOUSEHWHEEL then //then I need to make sure that I want to scroll Horizontally
begin
if msg.wParam=4287102976 then //this indicates that I'm scrolling to the left
world.CameraMoveTo(MyCamera.Position.X+0.03, MyCamera.Position.Y, MyCamera.Position.Z)
else
if msg.wParam=7864320 then //and this indicates that I'm scrolling to the right
world.CameraMoveTo(MyCamera.Position.X-0.03, MyCamera.Position.Y, MyCamera.Position.Z);
end;
end;
Done!

Related

How to be notified at the END of Horizontal Scrolling in TListView?

In a VCL Application, I am trying to be notified when I END the horizontal scrolling in a TListView with this interposer class code:
type
TListView = class(Vcl.ComCtrls.TListView)
private
procedure WMNotify(var AMessage: TWMNotify); message WM_NOTIFY; // used for other purposes
procedure CNCommand(var Message: TWMCommand); message CN_COMMAND;
procedure WMVScroll(var Msg: TWMHScroll); message WM_VSCROLL;
procedure WMHScroll(var Msg: TWMHScroll); message WM_HSCROLL;
protected
procedure CreateWnd; override;
end;
implementation
procedure TListView.CNCommand(var Message: TWMCommand);
begin
case Message.NotifyCode of
EN_VSCROLL: CodeSite.Send('TListView.CNCommand: EN_VSCROLL'); // does not work
EN_HSCROLL: CodeSite.Send('TListView.CNCommand: EN_HSCROLL'); // does not work
end;
inherited ;
end;
procedure TListView.WMHScroll(var Msg: TWMHScroll);
begin
CodeSite.Send('TListView.WMHScroll: WM_HSCROLL'); // does work
inherited;
end;
procedure TListView.WMVScroll(var Msg: TWMHScroll);
begin
CodeSite.Send('TListView.WMVScroll: WM_VSCROLL'); // does work
inherited;
end;
However, only WHILE scrolling I get constantly notified by WM_HSCROLL and WM_VSCROLL generating a lot of messages.
But I need to be notified only at the END of the horizontal scrolling! Is this possible?
The comments given to the Q are very relevant.
First, as Remy Lebeau stated, the WM_HSCROLL message tells you if the operation is done:
procedure TListView.WMHScroll(var Msg: TWMHScroll);
begin
inherited;
if Msg.ScrollCode = SB_ENDSCROLL then
ShowMessage('End scroll')
end;
However, this only lets you know when a scrolling operation initiated by the horizontal scroll bar is done. Currently, this includes these scroll-bar operations:
Thumb released
Scroll bar LEFT or RIGHT button clicked
Scroll bar empty area clicked (for page scroll)
Scroll bar context menu item selected
But there are many other ways the list view control can be scrolled, which have nothing to do with the scroll bar:
Using your mouse's horizontal scrolling wheel (or the standard vertical wheel if there is only a horizontal scroll bar and no vertical one)
Using your keyboard's left and right arrow keys (or Ctrl+Left/Right for page scroll)
With MultiSelect = True, making a selection rectangle with the mouse (start dragging outside any list-view item)
Hence, by reacting only to WM_HSCROLL, you will not detect these scrolling events. Almost certainly, you want to react when the scroll position has changed, no matter how it was changed.
And, as AmigoJack wrote, it is not absolutely clear what "end" means (other than when you release the mouse button after having dragging the scroll bar thumb). For instance, if you scroll using your mouse wheel, is the result a single large scroll operation or several small ones? After all, in any case, even thumb tracking, the control repaints itself at every small step.
So probably your best option is to use
procedure TListView.CNNotify(var Message: TMessage);
begin
inherited;
if PNMHDR(Message.lParam).code = LVN_ENDSCROLL then
// Scrolled
end;
According to the documentation,
Notifies a list-view control's parent window when a scrolling operation ends.
Notice that the documentation says that the notification is sent when the operation ends. Still, you will find that it is sent for every small update while you drag the scroll bar thumb. As mentioned above, this is reasonable: scrolling has indeed been performed after every such small step.

How to prevent focused control from scrolling when the mouse isn't over it?

Refer to this prior related question. While the answers there do work, I have further issues when it comes to certain types of controls such as a TDBGrid. If the TDBGrid currently has focus, but the mouse is pointed over another control to scroll, the TDBGrid scrolls anyway, thus resulting in two different controls scrolling at the same time. This happens in every single solution which I've found so far related to scrolling the control underneath the mouse.
How can I prevent this behavior and ensure that only the control under the mouse scrolls, and nothing else?
This code works fine for me.
procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var
Control: TWinControl;
begin
if Msg.message = WM_MOUSEWHEEL then
begin
// Find control at mouse cursor
Control := FindVCLWindow(Msg.pt);
if Assigned(Control) then
begin
// Try to scroll
if Control.Perform(CM_MOUSEWHEEL, Msg.wParam, Msg.lParam) <> 0 then
Handled := True
else
// If no scroll was performed by control,
// then detrmine if message control is at mouse cursor.
// If not, then supress message
if Control.Handle <> Msg.hwnd then
Handled := True;
end;
end;
end;

Delphi TrackBar On Stop

I am making a basic music player and am using a TTrackBar as the progress in the song. As well I want to make it so u can drag the bar and fast forward the song.
Currently I have an OnChange event with the following line:
MediaPlayer1.position := TrackBar1.value... (with proper casting)
but what happens is that it skips the song along as I drag making a choppy sound as it plays the song at certain random points along the way.
What I really want is for when the user stops dragging the song should change. What event is this? The onStopDrop even doesn't do the trick..
The scroll notification messages arrive though WM_HSCROLL or WM_VSCROLL, depending on the orientation of your track bar. These surface in the VCL control as CN_HSCROLL and CN_VSCROLL. You need to handle these messages and ignore message for which the scroll code is TB_THUMBTRACK to prevent the control to fire the OnChange event when the user drags the slider.
For example, here is an interposer control that does what you need:
type
TTrackBar = class(Vcl.ComCtrls.TTrackBar)
protected
procedure CNHScroll(var Message: TWMHScroll); message CN_HSCROLL;
procedure CNVScroll(var Message: TWMVScroll); message CN_VSCROLL;
end;
implementation
procedure TTrackBar.CNHScroll(var Message: TWMHScroll);
begin
if Message.ScrollCode = TB_THUMBTRACK then
Message.Result := 0
else
inherited;
end;
procedure TTrackBar.CNVScroll(var Message: TWMVScroll);
begin
if Message.ScrollCode = TB_THUMBTRACK then
Message.Result := 0
else
inherited;
end;

How to disable MouseWheel if mouse is not over VirtualTreeView (TVirtualStringTree)

TVirtualStringTree behaves by default if it is focused - it will scroll on mouse wheel even if mouse is not over control (except if it is over another TVirtualStringTree).
Is there a quick and elegant way to disable this behaviour?
I already did this with OnMouseWheel event and checking with PtInRect if Mouse.CursorPos if it is over a control but I have a feeling that there is a better way to do the same because this way I'd have to define a new event for each TreeView I add and also handle when to focus/unfocus the control so I hope there must be a better way to disable this.
So to be clear, I want mousewheel function to work as usual, but only when mouse is over VirtualTreeView.
Drop down a TApplicationEvents control to the form
in TApplicationEvents onMessage
procedure TForm5.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var
pnt: TPoint;
ctrl: TWinControl;
begin
if Msg.message = WM_MOUSEWHEEL then
begin
if not GetCursorPos(pnt) then Exit;
ctrl := FindVCLWindow(pnt);
if Assigned(ctrl) then
Msg.hwnd := ctrl.Handle;
end;
end;
Or you might try to modify the VirtualTree a bit. In the following example is used the interposed class. If you paste this code into your unit, then all of your VirtualTrees will behave this way in the form.
uses
VirtualTrees;
type
TVirtualStringTree = class(VirtualTrees.TVirtualStringTree)
private
FMouseInside: Boolean;
procedure CMMouseEnter(var Message: TMessage); message CM_MOUSEENTER;
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure CMMouseWheel(var Message: TCMMouseWheel); message CM_MOUSEWHEEL;
end;
implementation
procedure TVirtualStringTree.CMMouseEnter(var Message: TMessage);
begin
inherited;
// SetFocus will set the focus to the tree which is entered by mouse
// but it's probably what you don't want to, if so, just remove the
// following line. If you want to scroll the tree under mouse without
// stealing the focus from the previous control then this is not the
// right way - the tree must either be focused or you can steal it by
// the SetFocus. This only resolves the case when you have a focused
// tree and leave it with the mouse, then no scrolling is performed,
// if you enter it, you can scroll again.
SetFocus;
// set the flag which tells about mouse inside
FMouseInside := True;
end;
procedure TVirtualStringTree.CMMouseLeave(var Message: TMessage);
begin
// reset the flag about mouse inside
FMouseInside := False;
inherited;
end;
procedure TVirtualStringTree.CMMouseWheel(var Message: TCMMouseWheel);
begin
// if mouse is inside then let's wheel the mouse otherwise nothing
if FMouseInside then
inherited;
end;

TPageControl tab area OnMouseEnter OnMouseLeave events

I need to catch the "OnMouseEnter" and "0nMouseLeave" for a certain area of the TPageControl component. With that specific area I mean the whole "tab header" rectangle.
The problem is, that the page control doesn't catch the messages (I'm catching internal control messages CM_MOUSEENTER and CM_MOUSELEAVE) in the "empty" space.
The aim for me is to draw a small arrow in the right empty side when user hovers in the red framed area (and drawing is just piece of cake) and erase it when leaves this area. And I'm don't care about the overflow of the tabs (which causes to draw scrolling double button) - that will never happen.
Here is the working piece of code, but it's not the clear solution and I don't like it. There must be another (more clean) way to do it.
type
TPageControl = class(ComCtrls.TPageControl)
protected
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
end;
procedure TPageControl.CMMouseLeave(var Message: TMessage);
begin
inherited;
Canvas.TextOut(Width - 130, 5, 'CMMouseLeave'); // display result
end;
procedure TPageControl.WMNCHitTest(var Message: TWMNCHitTest);
var TabHeaderRect: TRect;
begin
if Message.Result = 0 then // if Message.Result = HTNOWHERE ...
begin
TabHeaderRect := ClientRect;
TabHeaderRect.Bottom := Top + 21;
if PtInRect(TabHeaderRect, ScreenToClient(Point(Message.XPos, Message.YPos))) then
Canvas.TextOut(Width - 130, 5, 'WMNCHitTest '); // display result
Message.Result := HTCLIENT;
end
else
inherited;
end;
Obviously, the empty space does not belong to the control's client area and so the control doesn't get any mouse-related Windows messages for that area. You will have to use the form's mouse events. Or put the page control inside a panel (using alClient) and use the panel's mouse events.
If you need this more than once, you could create a new component that does exactly that (combine a panel and a page control to achieve the desired behaviour).
Are you sure you're handling OnMouseEnter/OnMouseLeave for the page control itself, and not for the TTabSheet instance that it contains?

Resources