由于几个VCL结构(无论它们是否是有意的实现选择或可能是错误1),我在中途停止。只有焦点控件及其所有父级接收鼠标滚轮消息,以及具有鼠标捕获和聚焦父级的控件。
在TControl
层面上,可以强制执行后一个条件。当鼠标进入控件的客户端空间时,控件会从VCL接收到CM_MOUSEENTER
消息。为了强制其接收鼠标滚轮消息,请将其父级设置为焦点并在该消息处理程序中捕获鼠标:
procedure TWheelControl.CMMouseEnter(var Message: TMessage);
begin
FPrevFocusWindow := SetFocus(Parent.Handle);
MouseCapture := True;
inherited;
end;
但是当鼠标离开控件时,这些设置必须撤销。由于控件现在正在捕获鼠标,因此它不会收到 CM_MOUSELEAVE
消息,所以您必须手动检查,例如在 WM_MOUSEMOVE
消息处理程序中:
procedure TWheelControl.WMMouseMove(var Message: TWMMouseMove);
begin
if MouseCapture and
not PtInRect(ClientRect, SmallPointToPoint(Message.Pos)) then
begin
MouseCapture := False;
SetFocus(FPrevFocusWindow);
end;
inherited;
end;
现在,你可能会认为控件接收到的滚轮消息会随后触发 OnMouseWheel
、OnMouseWheelDown
和 OnMouseWheelUp
事件。但不,还需要进行一次干预。该消息进入 MouseWheelHandler
内部,该处理程序恰好将消息传递给窗体或活动控件。要触发这些事件,应发送一个 CM_MOUSEWHEEL
控件消息:
procedure TWheelControl.MouseWheelHandler(var Message: TMessage);
begin
Message.Result := Perform(CM_MOUSEWHEEL, Message.WParam, Message.LParam);
if Message.Result = 0 then
inherited MouseWheelHandler(Message);
end;
这导致最终代码如下:
unit WheelControl;
interface
uses
System.Classes, Winapi.Windows, Winapi.Messages, Vcl.Controls;
type
TWheelControl = class(TGraphicControl)
private
FPrevFocusWindow: HWND;
procedure CMMouseEnter(var Message: TMessage); message CM_MOUSEENTER;
procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
public
procedure MouseWheelHandler(var Message: TMessage); override;
published
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
end;
implementation
procedure TWheelControl.CMMouseEnter(var Message: TMessage);
begin
FPrevFocusWindow := SetFocus(Parent.Handle);
MouseCapture := True;
inherited;
end;
procedure TWheelControl.MouseWheelHandler(var Message: TMessage);
begin
Message.Result := Perform(CM_MOUSEWHEEL, Message.WParam, Message.LParam);
if Message.Result = 0 then
inherited MouseWheelHandler(Message);
end;
procedure TWheelControl.WMMouseMove(var Message: TWMMouseMove);
begin
if MouseCapture and
not PtInRect(ClientRect, SmallPointToPoint(Message.Pos)) then
begin
MouseCapture := False;
SetFocus(FPrevFocusWindow);
end;
inherited;
end;
end.
如您所见,这会改变焦点控件,这与
Windows桌面应用程序的用户体验指南 相违背,并可能在焦点控件具有显式焦点状态时导致视觉干扰。
作为替代方案,您可以通过覆盖
Application.OnMessage
并在那里处理来绕过所有默认的VCL鼠标滚轮处理。操作如下:
unit WheelControl2;
interface
uses
System.Classes, Winapi.Windows, Winapi.Messages, Vcl.Controls, Vcl.AppEvnts,
Vcl.Forms;
type
TWheelControl = class(TGraphicControl)
published
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
end;
implementation
type
TWheelInterceptor = class(TCustomApplicationEvents)
private
procedure ApplicationMessage(var Msg: tagMSG; var Handled: Boolean);
public
constructor Create(AOwner: TComponent); override;
end;
procedure TWheelInterceptor.ApplicationMessage(var Msg: tagMSG;
var Handled: Boolean);
var
Window: HWND;
WinControl: TWinControl;
Control: TControl;
Message: TMessage;
begin
if Msg.message = WM_MOUSEWHEEL then
begin
Window := WindowFromPoint(Msg.pt);
if Window <> 0 then
begin
WinControl := FindControl(Window);
if WinControl <> nil then
begin
Control := WinControl.ControlAtPos(WinControl.ScreenToClient(Msg.pt),
False);
if Control <> nil then
begin
Message.WParam := Msg.wParam;
Message.LParam := Msg.lParam;
TCMMouseWheel(Message).ShiftState :=
KeysToShiftState(TWMMouseWheel(Message).Keys);
Message.Result := Control.Perform(CM_MOUSEWHEEL, Message.WParam,
Message.LParam);
Handled := Message.Result <> 0;
end;
end;
end;
end;
end;
constructor TWheelInterceptor.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
OnMessage := ApplicationMessage;
end;
initialization
TWheelInterceptor.Create(Application);
end.
请注意,要将
MouseWheel*
事件的
Handled
参数设置为
True
,否则聚焦的控件也会滚动。
有关鼠标滚轮处理和更常见的解决方案,请参见
如何将鼠标滚轮输入直接发送到光标下的控件而不是聚焦的控件?。
请参见
1)Quality Central bug report #135258和
Quality Central bug report #135305。