Описанный здесь способ синхронизации работы нитей появился, начиная с шестой версии Delphi. В более ранних версиях списка методов для синхронизации не было. Вместо этого в главной нити создавалось специальное невидимое окно, а метод
TThread.Synchronize
с помощью
SendMessage
посылал этому окну сообщение
CM_EXECPROC
с адресом объекта, метод которого нуждался в синхронизации. Метод выполнялся в оконной процедуре данного окна при обработке этого сообщения. Такой механизм также позволял осуществить синхронизацию в приложениях без VCL. но требовал обязательного наличия петли сообщений в главной нити и не давал возможности выполнять синхронизацию, пока главная нить находилась в локальной петле сообщений. Из-за смены механизма синхронизации могут возникнуть проблемы при переносе в новые версии старых приложений: если раньше для обеспечения работы синхронизации было достаточно
организовать петлю сообщений, то теперь необходимо найти место для вызова
CheckSynchronize
. Разумеется, при переносе полноценных VCL-приложений эти проблемы не возникают, т.к. все, что нужно, содержится в методах класса
TApplication
.
Принятый в Delphi 6 способ синхронизации получил дальнейшее развитие в BDS 2006. В классе TThread появился метод
Queue
для передачи в код главной нити вызов метода для асинхронного выполнения, т.е. такого, когда нить вызвавшая
Queue
, после этого продолжает работать, не дожидаясь, пока главная нить выполнит требуемый код. Главная нить выполняет этот код параллельно тогда, когда для этого предоставляется случай (информация получена из анализа исходных кодов модулей VCL, т.к. справка Delphi, к сожалению не описывает данный метод: в справке BDS 2006 он вообще не упомянут, в справке Delphi 2007 упомянут, но все описание состоит из одной фразы "This is Queue, а member of class TThread"). Метод
Queue
использует тот же список методов синхронизации, что и
Synchronize
, только элементы этого списка пополнились признаком асинхронного выполнения и процедура
CheckSynchronize
не уведомляет нить, поместившую метод в список, о его выполнении, если метод помещен в список синхронизации методом
Queue
. А метод
TThread.RemoveQueuedEvents
позволяет удалять из списка методов синхронизации асинхронные вызовы, если нужда в их выполнении отпала.
При показе VCL-формы в модальном режиме выборка сообщений из очереди осуществляется особым образом. Модальные окна в VCL — это не то же самое, что модальные диалоги с точки зрения API. Диалог может быть создан только на основе шаблона, и его модальность обеспечивается самой операционной системой, a VCL допускает модальность для любой формы, позволяя разработчику не быть ограниченным возможностями предусмотренного системой шаблона. Достигается это следующим образом: при вызове метода
ShowModal
все окна запрещаются средствами VCL, затем окно показывается обычным образом, как немодальное, но из-за того, что все остальные окна запрещены, создается эффект модальности.
Внутри
ShowModal
создается своя петля сообщений. В этой петле в цикле вызывается метод
Application.HandleMessage
до тех пор, пока не будет установлено свойство
ModalResult
или не придет сообщение
WM_QUIT
. После завершения этой петли вновь разрешаются все окна, которые были разрешены до вызова
ShowModal
, а "модальная" форма закрывается. В отличие от системных модальных диалогов модальная форма VCL во время своей активности не посылает родительскому окну сообщение
WM_ENTERIDLE
, но благодаря тому, что "модальная" петля сообщений использует
HandleMessage
, будет вызываться
Idle
, а значит, будет возникать событие
Application.OnIdle
, которое позволит выполнять фоновые действия.
Теперь рассмотрим, как VCL обрабатывает извлеченные из очереди сообщения. Как уже было сказано ранее, для каждого класса формы VCL регистрирует одноименный оконный класс, а все окна, принадлежащие одному оконному классу, имеют общую оконную процедуру. С другой стороны, логика работы VCL требует, чтобы события обрабатывались тем экземпляром oбъекта, который инкапсулирует окно-адресат. Таким образом, возникает вопрос о том, как передать сообщение заданному экземпляру класса VCL. VCL решает эту задачу следующим образом. Модуль
Classes
содержит недокументированную функцию
MakeObjectInstance
, описанную так:
type TWndMethod = procedure(var Message: TMessage) of object;
function MakeObjectInstance(Method: TWndMethod): Pointer;
Тип
TMessage
хранит информацию о сообщении. Все методы VCL-компонентов, связанные с обработкой сообщения, используют этот тип (чуть позже мы рассмотрим его более подробно).
Функция
MakeObjectInstance
динамически формирует новую оконную процедуру и возвращает указатель на нее (следовательно, любое VCL-приложение содержит самомодифицирующийся код). Задача этой динамически созданной процедуры — передать управление тому методу, который был указан при вызове
MakeObjectInstance
(таким образом, различные оконные процедуры, сформированные этой функцией, отличаются только тем, метод
MainWndProc
какого экземпляра класса они вызывают).
Каждый экземпляр оконного компонента создает свою оконную процедуру, которая передает обработку сообщения его методу
MainWndProc
. Указатель на эту процедуру записывается в поле
FObjectInstance
. Как мы уже говорили в предыдущем разделе, при регистрации оконного класса в качестве оконной процедуры указывается
InitWndProc
, которая при получении первого сообщения создает подкласс, и оконной процедурой назначается та, указатель на которую хранится в поле
FObjectInstance
, т.е. функция, созданная с помощью
MakeObjectInstance
(см. листинг 1.12). Таким образом, каждый экземпляр получает свою оконную процедуру, а обработку сообщения начинает метод
MainWndProc
.
MainWndProc
— это невиртуальный метод, обеспечивающий решение технических вопросов: удаление "мусора", оставшегося при обработке сообщения и обработку исключений. Собственно обработку сообщения он передает методу, на который указывает свойство
WindowProc
. Это свойство имеет тип
TWndMethod
и по умолчанию указывает на виртуальный метод
WndProc
. Таким образом, если разработчик не изменял значения свойства
WindowProc
, обработкой сообщения занимается
WndProc
.
Метод
WndProc
обрабатывает только те сообщения, которые должны быть обработаны специальным образом, чтобы поддержать функциональность VCL. Особым образом метод
WndProc
обрабатывает сообщения от мыши: он следит, в границы какого визуального компонента попадают координаты "мышиных" сообщений, и если этот компонент отличается от того, в чью область попало предыдущее сообщение, компоненту из предыдущего сообщения дается команда обработать сообщение
CM_MOUSELEAVE
, а новому — сообщение
CM_MOUSENTER
. Это обеспечивает реакцию визуальных компонентов на приход и уход мыши (в частности, генерирование событий
OnMouseEnter
и
OnMouseExit
). Необходимость реализации такого способа отслеживания прихода и ухода мыши вместо использования системных сообщений
WM_MOUSEHOVER
и
WM_MOUSELEAVE
связана с тем, что системные сообщения пригодны только для работы с окнами, а VCL отслеживает приход и уход мыши и на неоконные визуальные компоненты. Впрочем,
WM_MOUSELEAVE
в
WndProc
тоже служит дополнительным средством проверки ухода мыши.
Примечание
Описанный здесь способ отслеживание ухода и прихода мыши реализован, начиная с BDS 2006. В более ранних версиях Delphi за это отвечал метод
Application.Idle
, который, как мы помним, вызывается только тогда когда в очереди нет сообщений. Из-за этого иногда (например, при быстром движении мышью) события ухода и прихода мыши пропускались, нарушая логику работы программы. Поэтому в BDS 2006 способ контроля прихода и ухода мыши был изменен, и ответственность за это возложена на метод
TWinControl.WndProc
. Это позволило избавиться от одного недостатка — потери событий, но породило другой: теперь перехват и самостоятельная обработка "мышиных" сообщений до того, как это сделает метод
WndProc
, может привести к потере возможности отслеживания прихода и ухода мыши. Впрочем, эта проблема проявляется только при выполнении программистом определенных осмысленных действий по внедрению кода в оконную процедуру, поэтому она гораздо менее серьезна, чем та от которой удалось избавиться.
События мыши метод
WndProc
диспетчеризует самостоятельно, без помощи функции
DispatchMessage
. Это связано с тем, что
DispatchMessage
передаёт сообщение тому оконному компоненту, которому оно предназначено с точки зрения системы. Однако с точки зрения VCL этот компонент может являться родителем для неоконных визуальных компонентов, и если сообщение от мыши связано с их областью, то оно должно обрабатываться соответствующим неоконным компонентом, а не его оконным родителем.
DispatchMessage
ничего о неоконных компонентах не "знает" и не может передать им сообщения, поэтому разработчикам VCL пришлось реализовывать свой способ. Те сообщения, которые метод
WndProc
не обрабатывает самостоятельно (а их — подавляющее большинство), он передает в метод Dispatch, который объявлен и реализован в классе
TObject
. На первый взгляд может показаться странным, что в самом базовом классе реализована функциональность, использующаяся только в визуальных компонентах. Эта странность объясняется тем, что разработчики Delphi встроили поддержку обработки сообщений непосредственно в язык. Методы класса, описанные с директивой message, служат специально для обработки сообщений. Синтаксис описания такого метода следующий: