А. Григорьев - О чём не пишут в книгах по Delphi
Рассмотрим все методы, с помощью которых можно встроить свой код в цепочку обработки сообщений оконным компонентом и перехватить сообщения. Всего существует шесть способов сделать это.
1. Как и у всякого окна, у оконного компонента VCL можно изменить оконную процедуру с помощью функции SetWindowLong. Этот способ лучше не применять, поскольку код VCL не будет ничего "знать" об этом переопределении, и сообщения, получаемые компонентом не через оконную процедуру, а с помощью Perform, не будут перехвачены. Другой недостаток данного способа — то, что изменение некоторых свойств компонента (например, FormStyle и BorderStyle у формы) невозможно без уничтожения окна и создания нового. Для программиста это пересоздание окна выглядит прозрачно, но новое окно получит новую оконную процедуру, и нужно будет выполнять перехват заново. Отследить момент пересоздания окна можно с помощью сообщения CM_RECREATEWND, обработчик которого уничтожает старое окно, а создание нового окна откладывается до момента первого обращения к свойству Handle. Если перехватить по сообщение, то, в принципе, после выполнения стандартного обработчика можно зaново установить перехват с помощью SetWindowLong, но т.к. этот способ не дает никаких преимуществ перед другими, более простыми, им все равно лучше не пользоваться.
2. Можно создать собственный метод обработки сообщения и поместить указатель на него в свойство WindowProc. При этом старый указатель обычно запоминается, т.к. новый обработчик обрабатывает лишь некоторые сообщения, а остальные передает старому. Достоинство этого способа — то, что метод, указатель на который помещается в WindowProc, не обязан принадлежать тому компоненту, сообщения которого перехватываются. Это позволяет, во-первых, создавать компоненты, которые влияют на обработку сообщений родительскими формами, а во-вторых, реализовывать нестандартную обработку сообщений стандартными компонентами, не порождая от них наследника.
3. При написании нового компонента можно перекрыть виртуальный метод WndProc и реализовать обработку нужных сообщений в нем. Это позволяет компоненту перехватывать сообщения в самом начале цепочки (за исключением внешних обработчиков, установленных с помощью свойства WindowProc — здесь разработчик компонента не властен).
4. Наиболее удобный способ самостоятельной обработки событий — написание их методов-обработчиков. Этот способ встречается чаще всего. Его недостатком является то, что номера обрабатываемых сообщений должны быть известны на этапе компиляции. Для системных сообщений и внутренних сообщений VCL это условие выполняется, но далее мы будем говорить об определяемых пользователем сообщениях, номера которых в некоторых случаях на этапе компиляции неизвестны. Обрабатывать такие сообщения с помощью методов с директивой невозможно.
5. Для перехвата сообщений, которые не были обработаны с помощью методов-обработчиков, можно перекрыть виртуальный метод.
6. И наконец, можно написать оконную процедуру и поместить указатель на нее в свойство DefWndProc. Этот способ по своим возможностям практически эквивалентен предыдущему, но менее удобен. Однако предыдущий способ пригоден только для создания собственного компонента, в то время как DefWndProc можно изменять у экземпляров существующих классов. Напомним, что этот способ не подходит для форм, у которых FormStyle = fsMDIForm, т.к. такие формы игнорируют значение свойства DefWndProc.
Для перехвата сообщений неоконных визуальных компонентов допустимы все перечисленные способы, за исключением первого и последнего.
Метод WndProc оконного компонента транслирует сообщения от мыши неоконным визуальным компонентам, родителем которых он является. Например. если положить на форму компонент TImage и переопределить у этой формы метод для обработки сообщения WM_LBUTTONDOWN, то нажатие кнопки мыши над TImage не приведет к вызову этого метода, т.к. WndProc передаст это сообщение в TImage, и Dispatch не будет вызван. Но если переопределить WndProc или изменить значение свойства WindowProc (т.е. использовать второй или третий метод перехвата), то можно получать и обрабатывать и те "мышиные" сообщения, которые должны транслироваться неоконным дочерним компонентам. Это общее правило: чем раньше встраивается собственный код в цепочку обработки сообщений, тем больше у него возможностей. Как мы уже говорили, начиная с BDS 2006 появился еще один способ перехвата сообщений — перекрытие метода PreProcessMessage. Этот способ нельзя ставить в один ряд с перечисленными ранее шестью способами, т.к. он имеет два существенных отличия от них. Во-первых, с помощью этого способа перехватываются все сообщения, попавшие в петлю сообщений, а не только те, которые посланы конкретному компоненту, из-за чего может понадобиться дополнительная фильтрация сообщений. Во-вторых, метод PreProcessMessage перехватывает сообщения, попавшие в петлю сообщений, а не в оконную процедуру компонента. С одной стороны, это даёт возможность перехватывать те сообщения, которые метод Аррlication.ProcessMessage не считает нужным передавать в оконную процедуру, но с другой стороны, не позволяет перехватывать те сообщения, которые окно получает, минуя петлю сообщений (например, те, которые отправлены с помощью SendMessage или Perform). По этим причинам область применения данного способа совсем другая, чем у способов, связанных с внедрением кода в оконную процедур. Перекрытие PreProcessMessage сопоставимо, скорее, с использованием события Application.OnMessage.
Различные способы перехвата сообщений иллюстрируются рядом примеров на прилагающемся к книге компакт-диске: использование свойства WindowProc показано в примерах Line, CoordLabel и PanelMsg, перекрытие метода WndProc — в примере NumBroadcast, создание метода для обработки сообщения — в примере ButtonDel.
1.1.9. Сообщения, определяемые пользователем
Сообщения очень удобны в тех случаях, когда нужно заставить окно выполнить какое-то действие. Поэтому Windows предоставляет возможность программисту создавать свои сообщения. Существуют три типа пользовательских сообщений:
□ сообщения оконного класса;
□ сообщения приложения;
□ глобальные (строковые) сообщения.
Для каждого из них выделен отдельный диапазон номеров. Номера стандартных сообщений лежат в диапазоне от 0 до WM_USER-1 (WM_USER — константа, для 32-разрядных версий Windows равная 1024).
Сообщения оконного класса имеют номера в диапазоне от WM_USER до WM_APP-1 (WM_APP имеет значение 32 768). Программист может выбирать произвольные номера для своих сообщений в этом диапазоне. Каждое сообщение должно иметь смысл только для конкретного оконного класса. Для различных оконных классов можно определять сообщения, имеющие одинаковые номера. Система никак не следит за тем, чтобы сообщения, определенные для какого-либо оконного класса, посылались только окнам этого класса — программист должен сам об этом заботиться. В этом же диапазоне лежат сообщения, специфические для стандартных оконных классов 'BUTTON', 'EDIT', 'LISTBOX', 'COMBOBOX' и т.п.
Использование сообщений из этого диапазона иллюстрируется примером ButtonDel.
Диапазон от WM_APP до 49 151 (для этого значения константа не предусмотрена) предназначен для сообщений приложения. Номера этих сообщений также выбираются программистом произвольно. Система гарантирует, что ни один из стандартных оконных классов не задействует сообщения из этого диапазона. Это позволяет выполнять их широковещательную в пределах приложения рассылку. Ни один из стандартных классов не откликнется на такое сообщение и не выполнит нежелательных действий.
Упоминавшиеся ранее внутренние сообщения VCL с префиксами CM_ и CN_ имеют номера в диапазоне от 45 056 до 49 151, т.е. используют часть диапазона сообщений приложения. Таким образом, при использовании VCL диапазон сообщений приложения сокращается до WM_APP..45055. Сообщения оконного класса и приложения пригодны и для взаимодействия с другими приложениями, но при этом отправитель должен быть уверен, что адресат правильно его поймет. Широковещательная рассылка при этом исключена — реакция других приложений, которые также получат это сообщение, может быть непредсказуемой. Если все же необходимо рассылать широковещательные сообщения между приложениями, то следует воспользоваться глобальными сообщениями, для которых зарезервирован диапазон номеров от 49 152 до 65 535.
Глобальное сообщение обязано иметь имя (именно поэтому такие сообщения называются также строковыми), под которым оно регистрируется в системе с помощью функции в RegisterWindowMessage. Эта функция возвращает уникальный номер регистрируемого сообщения. Если сообщение с таким именем регистрируется впервые, номер выбирается из числа ещё не занятых. Если же сообщение с таким именем уже было зарегистрировано, то возвращается тот же самый номер, который был присвоен ему при первой регистрации. Таким образом, разные программы, регистрирующие сообщения с одинаковыми именами, получат одинаковые номера и смогут понимать друг друга. Для прочих же окон это сообщение не будет иметь никакого смысла. Создание и использование оконных сообщений демонстрируется примером NumBroadcast, содержащимся на прилагаемом компакт-диске. Разумеется, существует вероятность, что два разных приложения выберут для своих глобальных сообщений одинаковые имена, и это приведет к проблемам при широковещательной рассылке этих сообщений. Но, если давать своим сообщениям осмысленные имена, а не что-то вроде WM_MYMESSAGE1, вероятность такого совпадения будет очень мала. В особо критических ситуациях можно в качестве имени сообщения использовать GUID, уникальность которого гарантируется.