Я использую несколько элементов управления прокруткой: TTreeViews, TListViews, DevExpress cxGrids и cxTreeLists и т.д. Когда колесико мыши вращается, управление с фокусом получает вход, независимо от того, какой контроль над курсором мыши завершен.
Как вы управляете вводом колесика мыши в любое управление курсором мыши? В этом отношении Delphi IDE работает очень хорошо.
Ответ 2
Прокрутка источника
Действие с колесом мыши приводит к сообщению WM_MOUSEWHEEL
:
Отправлено в окно фокуса, когда колесико мыши повернуто. Функция DefWindowProc передает сообщение родительскому окну. Не должно быть внутренней пересылки сообщения, так как DefWindowProc распространяет его на родительскую цепочку, пока не найдет окно, которое его обрабатывает.
Одиссея колесика мыши 1)
- Пользователь прокручивает колесико мыши.
- Система помещает сообщение
WM_MOUSEWHEEL
в очередь сообщений о потоках окон переднего плана.
- Цикл сообщений потоков выбирает сообщение из очереди (
Application.ProcessMessage
). Это сообщение имеет тип TMsg
, у которого есть член hwnd
, обозначающий дескриптор окна, для которого указано сообщение.
- Событие
Application.OnMessage
запускается.
- Установка параметра
Handled
True
останавливает дальнейшую обработку сообщения (за исключением следующих шагов).
- Вызывается метод
Application.IsPreProcessMessage
.
- Если элемент управления не захватил мышь, вызывается метод сфокусированного управления
PreProcessMessage
, который по умолчанию ничего не делает. Отсутствие контроля в VCL превзошло этот метод.
- Вызывается метод
Application.IsHintMsg
.
- Активное окно подсказки обрабатывает сообщение в методе overriden
IsHintMsg
. Предотвращение сообщения от дальнейшей обработки невозможно.
-
DispatchMessage
.
- Метод
TWinControl.WndProc
сфокусированного окна получает сообщение. Это сообщение имеет тип TMessage
, которому не хватает окна (потому что это экземпляр, вызываемый этим методом).
- Вызывается метод
TWinControl.IsControlMouseMsg
, чтобы проверить, должно ли сообщение мыши быть перенаправлено на один из его дочерних элементов управления, отличных от окон.
- Если есть дочерний элемент управления, который захватил мышь или находится в текущей позиции мыши 2) тогда сообщение отправляется дочернему элементу управления
WndProc
, см. шаг 10. ( 2) Это никогда не произойдет, потому что WM_MOUSEWHEEL
содержит свою позицию мыши в координатах экрана, а IsControlMouseMsg
предполагает положение мыши в клиентских координатах (XE2).)
- Наследуемый метод
TControl.WndProc
получает сообщение.
- Если система не поддерживает колесико мыши (< Win98 или < WinNT4.0), сообщение преобразуется в сообщение
CM_MOUSEWHEEL
и отправляется на TControl.MouseWheelHandler
, см. шаг 13.
- В противном случае сообщение отправляется соответствующему обработчику сообщений.
- Метод
TControl.WMMouseWheel
получает сообщение.
-
WM_MOUSEWHEEL
w indow m Essage (значимый для системы и часто для VCL тоже) преобразуется в CM_MOUSEWHEEL
c ontrol m essage (значимый только для VCL), который обеспечивает удобную информацию VCL ShiftState
вместо данные системных клавиш.
- Вызывается метод управления
MouseWheelHandler
.
- Если элемент управления имеет значение
TCustomForm
, тогда вызывается метод TCustomForm.MouseWheelHandler
.
- Если на нем есть сфокусированное управление, тогда
CM_MOUSEWHEEL
отправляется на сфокусированное управление, см. шаг 14.
- В противном случае вызывается унаследованный метод, см. шаг 13.2.
- В противном случае вызывается метод
TControl.MouseWheelHandler
.
- Если есть элемент управления, который захватил мышь и не имеет родительского 3) тогда сообщение отправляется этому элементу управления, см. шаг 8 или 10, в зависимости от типа элемента управления. ( 3) Это никогда не произойдет, потому что
Capture
получен с помощью GetCaptureControl
, который проверяет наличие Parent <> nil
(XE2).)
- Если элемент управления находится в форме, вызывается форма управления
MouseWheelHandler
, см. шаг 13.1.
- В противном случае, или если элемент управления является формой, тогда
CM_MOUSEWHEEL
отправляется в элемент управления, см. шаг 14.
- Метод
TControl.CMMouseWheel
получает сообщение.
- Вызывается метод
TControl.DoMouseWheel
.
- Событие
OnMouseWheel
запущено.
- Если не обрабатывается, вызывается
TControl.DoMouseWheelDown
или TControl.DoMouseWheelUp
в зависимости от направления прокрутки.
- Выполняется событие
OnMouseWheelDown
или OnMouseWheelUp
.
- Если не обрабатывается, тогда
CM_MOUSEWHEEL
отправляется в родительский элемент управления, см. шаг 14. (Я считаю, что это противоречит рекомендациям MSDN в приведенной выше цитате, но это, несомненно, является продуманным решением разработчиков. Возможно, потому что это начало этой самой цепи.)
Замечания, замечания и соображения
Почти на каждом этапе этой цепочки обработки сообщение можно игнорировать, ничего не делая, изменяя путем изменения параметров сообщения, обрабатывая его действием и отменя, установив Handled := True
или установив Message.Result
в ненулевое значение.
Только когда какой-либо элемент управления имеет фокус, это сообщение принимается приложением. Но даже когда Screen.ActiveCustomForm.ActiveControl
принудительно настроено на nil
, VCL обеспечивает сфокусированное управление с TCustomForm.SetWindowFocus
, которое по умолчанию относится к ранее активной форме. (С Windows.SetFocus(0)
, действительно, сообщение никогда не отправляется.)
Из-за ошибки в IsControlMouseMsg
2)TControl
может получать только сообщение WM_MOUSEWHEEL
, если оно захватило мышь. Это можно сделать вручную, установив Control.MouseCapture := True
, но вы должны проявлять особую осторожность в том, чтобы освободить этот захват оперативно, иначе он будет иметь нежелательные побочные эффекты, такие как необходимость ненужный дополнительный щелчок, чтобы что-то сделать. Кроме того, захват мыши обычно происходит только между нажатием мыши и событием мыши, но это ограничение необязательно необходимо применять. Но даже когда сообщение достигает элемента управления, оно отправляется его методу MouseWheelHandler
, который просто отправляет его обратно либо в форму, либо в активный элемент управления. Таким образом, не-оконные элементы управления VCL никогда не могут действовать по сообщению по умолчанию. Я считаю, что это еще одна ошибка, иначе зачем все манипуляции с колесами были реализованы в TControl
? Компонент-писатели, возможно, внедрили свой собственный метод MouseWheelHandler
для этой цели, и независимо от того, что решение приходит к этому вопросу, необходимо позаботиться о том, чтобы не нарушить эту существующую настройку.
Встроенные элементы управления, которые способны прокручивать колесо, например TMemo
, TListBox
, TDateTimePicker
, TComboBox
, TTreeView
, TListView
и т.д., прокручиваются самой системой. Отправка CM_MOUSEWHEEL
в такой элемент управления по умолчанию не влияет. Эти подклассифицированные элементы управления прокручиваются в результате сообщения WM_MOUSEWHEEL
, отправленного с помощью связанной с подкладом процедуры окна API с помощью CallWindowProc
, который VCL позаботится о TWinControl.DefaultHandler
. Как ни странно, эта процедура не проверяет Message.Result
перед вызовом CallWindowProc
, и после отправки сообщения прокрутка не может быть предотвращена. Сообщение возвращается с набором Result
, зависящим от того, может ли управление нормально прокручивать или управлять типом элемента управления. (Например, a TMemo
возвращает <> 0
, а TEdit
возвращает 0
.) Независимо от того, действительно ли он прокручивается, не влияет на результат сообщения.
Элементы управления VCL полагаются на обработку по умолчанию, реализованную в TControl
и TWinControl
, как описано выше. Они воздействуют на события колеса в DoMouseWheel
, DoMouseWheelDown
или DoMouseWheelUp
. Насколько мне известно, никакой контроль в VCL не превзошел MouseWheelHandler
, чтобы обрабатывать события колес.
При взгляде на разные приложения, похоже, нет соответствия, по которому поведение прокрутки колес является стандартом. Например: MS Word прокручивает страницу, которая зависает, MS Excel прокручивает рабочую книгу, которая сфокусирована, Windows Eplorer прокручивает сфокусированную панель, веб-сайты реализуют поведение прокрутки по-разному, Evernote прокручивает окно, которое зависает, и т.д.... и Delphi собственная среда IDE вершина всего, прокручивая сфокусированное окно , а также зависающее окно, за исключением того, что при зависании редактора кода, тогда редактор кода крадет фокус при прокрутке (XE2).
К счастью Microsoft предлагает как минимум рекомендации для пользователей для настольных приложений на базе Windows:
- Сделать колесико мыши влияющим на элемент управления, панель или окно, в котором в данный момент находится указатель.. Это позволяет избежать непреднамеренных результатов.
- Сделать колесико мыши эффектом без щелчка или с фокусом ввода. Наведение достаточно.
- Сделать колесико мыши влияющим на объект с наиболее конкретной областью. Например, если указатель находится над элементом управления прокручиваемым списком в прокручиваемой панели в прокручиваемом окне, колесико мыши влияет на list box.
- Не меняйте фокус ввода при использовании колеса мыши.
Таким образом, требование к запросу только для прокрутки зависающего элемента управления имеет достаточные основания, но разработчики Delphi не упростили его реализацию.
Заключение и решение
Предпочтительным решением является один без подклассов окон или нескольких реализаций для разных форм или элементов управления.
Чтобы предотвратить прокрутку сфокусированного элемента управления, управление может не получать сообщение CM_MOUSEWHEEL
. Следовательно, MouseWheelHandler
любого элемента управления не может быть вызван. Поэтому WM_MOUSEWHEEL
не может быть отправлен на какой-либо элемент управления. Таким образом, единственное место, оставшееся для вмешательства, - это TApplication.OnMessage
. Кроме того, сообщение не может выйти из него, поэтому вся обработка должна выполняться в этом обработчике событий, и когда вся обработка колес VCL по умолчанию отключена, все возможные условия должны быть приняты во внимание.
Пусть начнется просто. Окно с включенным окном, которое в настоящее время зависает, получает 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;
С FindControl
мы получаем ссылку на элемент управления VCL. Если результатом является nil
, тогда зависающее окно не относится к приложению, или это окно, не известное VCL (например, упавшее TDateTimePicker
). В этом случае сообщение нужно переслать обратно в API, и его результат нам неинтересен.
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;
Когда окно является элементом управления VCL, несколько обработчиков сообщений должны считаться вызывающими в определенном порядке. Когда в позиции мыши есть включенное не оконное управление (типа TControl
или потомка), оно должно сначала получить сообщение CM_MOUSEWHEEL
, потому что этот элемент управления определенно является элементом управления переднего плана. Сообщение должно быть построено из сообщения WM_MOUSEWHEEL
и переведено в его эквивалент VCL. Во-вторых, сообщение WM_MOUSEWHEEL
должно быть отправлено на элемент управления DefaultHandler
, чтобы разрешить обработку для собственных элементов управления. И наконец, снова сообщение CM_MOUSEWHEEL
должно быть отправлено в элемент управления, когда предыдущий обработчик не позаботился о сообщении. Эти последние два этапа не могут иметь место в обратном порядке, например, заметка в окне прокрутки также должна прокручиваться.
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;
Когда окно захватило мышь, все сообщения колеса должны быть отправлены на него. Окно, полученное с помощью GetCapture
, будет являться окном текущего процесса, но оно не должно быть элементом управления VCL. Например. во время операции перетаскивания создается временное окно (см. TDragObject.DragHandle
), который получает сообщения мыши. Все сообщения? Noooo, WM_MOUSEWHEEL
не отправляется в окно захвата, поэтому мы должны перенаправить его. Кроме того, когда окно захвата не обрабатывает сообщение, должна выполняться вся другая предварительно обработанная обработка. Это функция, отсутствующая в VCL: при повороте во время операции перетаскивания Form.OnMouseWheel
действительно вызывается, но сфокусированный или зависающий элемент управления не получает сообщение. Это означает, например, что текст нельзя перетащить в содержимое заметки в месте, которое находится за пределами видимой части заметки.
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;
Это по существу выполняет эту работу, и это послужило основой для представленной ниже единицы. Чтобы заставить его работать, просто добавьте имя единицы в одно из предложений о применении в вашем проекте. Он имеет следующие дополнительные возможности:
- Возможность предварительного просмотра действия колеса в основной форме, активной форме или активном элементе управления.
- Регистрация классов управления, для которых должен быть вызван метод
MouseWheelHandler
.
- Возможность принести этот объект
TApplicationEvents
перед всеми остальными.
- Возможность отменить отправку события
OnMessage
ко всем остальным TApplicationEvents
объектам.
- Возможность по-прежнему разрешать обработку по умолчанию VCL для аналитических целей или тестирования.
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.
Отказ от ответственности:
Этот код намеренно не прокручивает ничего, он только готовит маршрутизацию сообщений для событий VCL OnMouseWheel*
, чтобы получить надлежащую возможность уволить. Этот код не проверяется на сторонних элементах управления. Когда VclHandlingAfterHandled
или VclHandlingAfterUnhandled
установлено True
, события мыши могут быть запущены дважды. В этом посте я сделал несколько утверждений, и я счел, что в VCL есть три ошибки, но все это основано на изучении документации и тестирования. Проведите тестирование этого устройства и прокомментируйте результаты и ошибки. Прошу прощения за этот довольно длинный ответ; У меня просто нет блога.
1) Именование нахально взято из Одиссея ключей
2) См. мой Отчет об ошибке Quality Central # 135258
3) См. мой Отчет об ошибках в Quality Central # 135305