Delphi VCL form Arrow Key management solution fires twice - delphi

I have a VCL form with many tframes (referred to as tcellFrame) containing components arranged in a grid. I am using mouse clicks and arrow keys for the user to navigate between them. The mouse clicks work fine, but I had trouble with the arrow keys until I discovered this question thread: Delphi XE and Trapping Arrow Key with OnKeyDown. The solution in Sertac Akyuz's answer does handle getting the arrow key messages to the form using
procedure TForm1.DialogKey(var Msg: TWMKey);
begin
case Msg.CharCode of
VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT:
if Assigned(onKeyDown) then
onKeyDown(Self, Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
else
inherited
end;
end;
but acts upon the form twice for each key stroke. Instead of moving to the left one cellframe, it moves two. Tracing the thread using the debugger demonstrates that the onkeydown event is called twice.
My onKeyDown event is structured as follows:
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
i : integer;
g, r, c, str : string;
tmpFrame : tcellFrame; //frame component containing tlabels
begin
...
case key of
VK_UP:
begin
//calc new cell location values for g,r,c
str := ('Cell'+g+r+c);
picklist.Clear; // picklist is a form-wide tstringlist variable containing currently selected cellframes.
picklist.add (str);
TmpFrame := FindComponent(picklist [0]) as TCellFrame;
tmpframe.Color := pickClr;
end;
//VK_DOWN, VK_LEFT, VK_RIGHT: defined similarly to VK_UP
end;
end;
There is more code in Formkeydown, but it is all internal calculations to determine the proper tcellframe name to place in the picklist.
My questions are:
What is causing this repeat to occur?
How do I terminate the message implementation after its first instance?

In your CM_DIALOGKEY message handler, return a non-zero value if you handle the key, then it won't be dispatched further.
procedure TForm1.DialogKey(var Msg: TWMKey);
begin
case Msg.CharCode of
VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT:
begin
if Assigned(onKeyDown) then
onKeyDown(Self, Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
Msg.Result := 1; // <-- add this
end;
else
inherited;
end;
end;
However, if you have KeyPreview=True on the Form and the arrow keys are already being dispatched normally, then there is no need to handle CM_DIALOGKEY at all, just let the Form's OnKey... events get dispatched normally. You should not be firing the Form's OnKey... events from a CM_DIALOGKEY handler.
See A Key's Odyssey for more details.

Related

OnClick Event for TListbox, Calls while using the arrow Keys to change a selected item

I Have a ListBox on my From with several items in it. When the User clicks an item (OnClick Event) the Users Status is changed and a TCP server is notified. If I use the Arrow Keys On the Keyboard, the Same Event Is called, like an OnChange Event. However There is no OnChange Event.
The Problem with using the arrow keys is that If a User rapidly moves across several items, my Notify Server Method is called several times. (this is not good)
To Get around this I Put a Timer on the OnKeyPress Event. When The arrow keys are pressed If the user stops pushing the arrow key for 2seconds, the Notify Server Method is called, Notifying the server once. (In theory)
Both OnKeyPress and OnClick are still called.
Is anyone familiar enough with TListbox to explain to me why this happens, and if there is a better way of thinking about this problem? The User Requirements are to use a Listbox, and to Not disable the arrow keys.
The OnClick event is triggered when the user clicks on the ListBox, but it is also triggered when the selection actually changes for any reason. This is a design flaw (IMHO) in how TListBox is implemented. It should have exposed actual OnChanging and OnChange events instead (since the underlying ListBox control provides such notifications), like other components do.
However, you can the use the following approach to distinguish between a mouse click and a keyboard arrow keypress:
Set a flag in the OnKeyDown event if an up/down arrow is being held down.
Clear the flag in the OnKeyUp event for the same arrow key.
You can then check for that flag in the OnClick event (or better, subclass the ListBox to intercept the LBN_SELCHANGING/LBN_SELCHANGE notification directly). If the flag is set, start your timer to delay your server action, otherwise perform your action immediately.
For example:
type
TForm1 = class(TForm)
...
private
IsArrowDown: Boolean;
...
end;
...
procedure TForm1.ListBox1Click(Sender: TObject);
begin
if IsArrowDown then
begin
Timer1.Enabled := False;
Timer1.Interval := 1000;
Timer1.Enabled := True;
end else
UpdateUserStatus;
end;
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := True;
end;
procedure TForm1.ListBox1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := False;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.UpdateUserStatus;
begin
// notify server as needed...
end;
Update: a double-click also triggers the OnClick event before the OnDblClick event. So if you need to differentiate between single-clicking and double-clicking, you will have to use a timer for that as well:
type
TForm1 = class(TForm)
...
private
IsArrowDown: Boolean;
...
end;
...
procedure TForm1.ListBox1Click(Sender: TObject);
begin
if IsArrowDown then
begin
Timer1.Enabled := False;
Timer1.Interval := 1000;
Timer1.Enabled := True;
end else
begin
Timer1.Enabled := False;
Timer1.Interval := GetDoubleClickTime() + 500;
Timer1.Enabled := True;
end;
end;
procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := True;
end;
procedure TForm1.ListBox1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [VK_DOWN, VK_UP] then
IsArrowDown := False;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
UpdateUserStatus;
end;
procedure TForm1.UpdateUserStatus;
begin
// notify server as needed...
end;
Rather than performing the action automatically each time a selection or onChange event occurs, either make the action explicit with a button, as suggested elsewhere here; or reset a timer, then when the timer goes off, if the selection is still valid, trigger the action on the current selection (effectively clicking the button in the timer handler). This approach lends itself to a nice user-configurable option where you can enable automatic notification after ___ seconds or require the button to be clicked manually.
This is documented behaviour:
This event can also occur when the user selects an item in a grid, outline, list, or combo box by pressing an arrow key.
From the perspective of the user, why should using the keyboard be discriminated against. If I want to select the item immediately below the current selection then why does it matter whether I use the mouse or the keyboard. Some users don't even have mice.
You need to design your program to be resilient to such actions. Your current approach is not unreasonable. I'd take the same approach even if the user clicked with the mouse. Users often miss and need to click again. So, wait for a short period of time after OnClick before responding.
Another approach might be to make the user actively invoke the action. So, provide a button, perhaps captioned Apply and only do work when the user presses it.
If I understand you correctly your problem is that your program can send to manny notifications to your server.
If this is true then you should not be considering of how TListBox events work but how can you prevent to manny nitifications being sent to your server.
So first thing you should do is move all the code related to nitifying your server into a seperate method if you haven't done so.
Then this method should check when the last notification to the server was sent in order to determine if another server notification is allowed.
For this you can simply store the last time that notification to the server was sent either by using Now (TTime format) to get current system time whic would be good for one second or larger intervals or GetTickCount if you are interested in intervals shorter than one second. Technically you could also use Now for less tahan a second intervals but would require you to call special methods to get the time in milliseconds format.
After you have the last notification time stored all you need to do is check if certain interval has already passed.
And if you need to really log every event you can configure your client to store them in some que and then send the whole que to the server.

OnKeyDown not working on Dialog invoked from main form (which also uses OnKeyDown)

Delphi 2010
I am using a OnFormKeyDown event on my main form, and I basically use the same event on a Dialog
//main form
procedure TfrmMain.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
procedure TfrmMain.mniShowOwnedClick(Sender: TObject);
var
I: Integer;
begin
frmMain.KeyPreview:= False;
frmOwned.KeyPreview:= True;
frmOwned.Owned2.Clear;
for I := 0 to Tags.Count - 1 do
if Owned.IndexOf(Tags.Names[I]) <> -1 then
frmOwned.Owned2.Add(Tags[I]);
if frmOwned.ShowModal = mrOK then
begin
frmMain.KeyPreview:= True;
frmOwned.KeyPreview:= False;
end;
end;
//Dialog
procedure TfrmOwned.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
case Key of
VK_DOWN: btnLast.OnClick(Self);
VK_Up: btnFirst.OnClick(Self);
VK_Left: btnPrev.OnClick(Self);
VK_Right: btnNext.OnClick(Self);
end;
end;
The Form's OnKeyDown works fine, but I can't seem to get the dialogs to work
The problem is that these keys are used as dialog navigation keys. And as such, they never make their way to the OnKeyDown event.
To be honest I had a hard time understanding why they are firing for your main form's OnKeyDown event. I could not make that happen in my test environment. That's because I had added a button to the form. That's enough to mean that the arrow keys are treated as navigation keys. Try creating a an app with a single form and adding a few buttons. Then run the app and use the arrow keys to move the focus between the buttons. That's what I mean when I say that the arrow keys are treated as navigation keys.
I expect that the difference between your two forms is that the main form has nothing that can be navigated around by arrow keys, but the modal form does.
Now, you could stop the arrow keys being treated as navigation keys. Like this:
type
TMyForm = class(TForm)
....
protected
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
....
end;
....
procedure TMyForm.CMDialogKey(var Message: TCMDialogKey);
begin
case Message.CharCode of
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
Message.Result := 0;
else
inherited;
end;
end;
However, a better solution, in my view, is to stop trying to implement shortcuts using OnKeyDown events. That seems like the wrong solution. The right solution is to use actions. Create an action list. Add actions for first, last, previous and next actions. Give them the appropriate ShortCut properties. Assign those actions to your buttons. And the job is done.
One of the benefits of this is that you can stop trying to fake button click events. For what it is worth, calling OnClick is the wrong way to do that. Call the button's Click method if ever your really need to do that. However, use an action and it's all taken care of.
Another benefit is that you'll no longer need to pfaff around with KeyPreview. Simply put, if you want to implement short cuts, use TAction.ShortCut.

Application.OnIdle Keeps executing

I am trying to handle some events when my application is idle so i created this code
procedure TForm1.ApplicationEventIdle(Sender: TObject; var Done: Boolean);
begin
Done := false;
ShowMessage('Hello');
Done := true;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnIdle := ApplicationEventIdle;
end;
The problem is the message box appears infinite times how can i display it only once ?
This behaviour happens precisely because you show the dialog. In order to close the dialog you have to click the OK button. This places an input message on the queue. After that has been processed the OnIdle event fires. And you show the message box again. And so on and so on.
You need to make sure that you don't do anything that requires user input in your OnIdle handler. In your case try outputting to a log rather than showing a dialog, e.g. OutputDebugString.
Create a private boolean field in TForm1 to indicate when the dialog has been shown, so you don't show it again.
procedure TForm1.ApplicationEventIdle(Sender: TObject; var Done: Boolean);
begin
if not FDialogShown then
begin
FDialogShown := True;
ShowMessage('Hello');
end;
end;

TMenuItem-Shortcuts overwrite Shortcuts from Controls (TMemo)

What can I do that shortcuts for menu items don't overwrite those from local controls?
Imagine this simple app in the screenshot. It has one "undo" menu item with the shortcut CTRL+Z (Strg+Z in German) assigned to it. When I edit some text in the memo and press CTRL+Z I assume that the last input in the memo is reverted, but instead the menu item is executed.
This is especially bad in this fictional application because the undo function will now delete my last added "Item 3" which properties I was editing.
CTRL+Z is just an example. Other popular shortcuts cause similar problems (Copy&Paste: CTRL+X/C/V, Select all: CTRL+A).
Mini Demo with menu item with CTRL+Z short-cut http://img31.imageshack.us/img31/9074/ctrlzproblem.png
The VCL is designed to give menu item shortcuts precedence. You can, however, write your item click handler (or action execute handler) to do some special handling when ActiveControl is TCustomEdit (call Undo, etc.)
Edit: I understand you don't like handling all possible special cases in many places in your code (all menu item or action handlers). I'm afraid I can't give you a completely satisfactory answer but perhaps this will help you find a bit more generic solution. Try the following OnShortCut event handler on your form:
procedure TMyForm.FormShortCut(var Msg: TWMKey; var Handled: Boolean);
var
Message: TMessage absolute Msg;
Shift: TShiftState;
begin
Handled := False;
if ActiveControl is TCustomEdit then
begin
Shift := KeyDataToShiftState(Msg.KeyData);
// add more cases if needed
Handled := (Shift = [ssCtrl]) and (Msg.CharCode in [Ord('C'), Ord('X'), Ord('V'), Ord('Z')]);
if Handled then
TCustomEdit(ActiveControl).DefaultHandler(Message);
end
else if ActiveControl is ... then ... // add more cases as needed
end;
You could also override IsShortCut method in a similar way and derive your project's forms from this new TCustomForm descendant.
You probably need an alike solution as below. Yes, feels cumbersome but this is the easiest way I could think of at the time. If only Delphi allowed duck-typing!
{ you need to derive a class supporting this interface
for every distinct control type your UI contains }
IEditOperations = interface(IInterface)
['{C5342AAA-6D62-4654-BF73-B767267CB583}']
function CanCut: boolean;
function CanCopy: boolean;
function CanPaste: boolean;
function CanDelete: boolean;
function CanUndo: boolean;
function CanRedo: boolean;
function CanSelectAll: Boolean;
procedure CutToClipBoard;
procedure Paste;
procedure CopyToClipboard;
procedure Delete;
procedure Undo;
procedure Redo;
procedure SelectAll;
end;
// actions....
procedure TMainDataModule.actEditCutUpdate(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
(Sender as TAction).Enabled := intf.CanCut
else
(Sender as TAction).Enabled := False;
end;
procedure TMainDataModule.actEditCutExecute(Sender: TObject);
var intf: IEditOperations;
begin
if Supports(Screen.ActiveControl, IEditOperations, intf) then
intf.CutToClipBoard;
end;
....

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

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.
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)
The user scrolls the mouse wheel.
The system places a WM_MOUSEWHEEL message into the foreground window’s thread’s message queue.
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.
The Application.OnMessage event is fired.
Setting the Handled parameter True stops further processing of the message (except for the next to steps).
The Application.IsPreProcessMessage method is called.
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.
The Application.IsHintMsg method is called.
The active hint window handles the message in an overriden IsHintMsg method. Preventing the message from further processing is not possible.
DispatchMessage is called.
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).
The TWinControl.IsControlMouseMsg method is called to check whether the mouse message should be directed to one of its non-windowed child controls.
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).)
The inherited TControl.WndProc method receives the message.
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.
Otherwise the message is dispatched to the appropriate message handler.
The TControl.WMMouseWheel method receives the message.
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.
The control's MouseWheelHandler method is called.
If the control is a TCustomForm, then the TCustomForm.MouseWheelHandler method is called.
If there is a focused control on it, then CM_MOUSEWHEEL is sent to the focused control, see step 14.
Otherwise the inherited method is called, see step 13.2.
Otherwise the TControl.MouseWheelHandler method is called.
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).)
If the control is on a form, then the control's form's MouseWheelHandler is called, see step 13.1.
Otherwise, or if the control ís the form, then CM_MOUSEWHEEL is sent to the control, see step 14.
The TControl.CMMouseWheel method receives the message.
The TControl.DoMouseWheel method is called.
The OnMouseWheel event is fired.
If not handled, then TControl.DoMouseWheelDown or TControl.DoMouseWheelUp is called, depending on the scroll direction.
The OnMouseWheelDown or the OnMouseWheelUp event is fired.
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
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;
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.
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
This is the solution I've been using:
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;
...
Save the following code to amMouseWheel.pas:
unit amMouseWheel;
// -----------------------------------------------------------------------------
// The original author is Anders Melander, anders#melander.dk, 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.
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.
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);
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?

Resources