, которое уведомляет программу о том, что пользователь нажал на кнопку компонента
TUpDown
. Именно при обработке этого сообщения VCL вызывает событие
TUpDown.OnClick
, в котором открывается модальное окно. Всё это происходит очень быстро, поэтому кнопку мыши пользователь отпускает тогда, когда модальное окно уже оказалось на экране. В результате сообщение
WM_LBUTTONUP
либо попадает в очередь открывшегося диалогового окна, если мышь находилась над ним, либо вообще никуда не попадает, если мышь была вне модального окна. На время существования модального окна система "забывает" о том, что мышь захвачена для монопольного использования, но "вспоминает"
об этом, как только модальное окно закрывается. Монопольное использование мыши компонентом
TUpDown
должно отменяться при обработке сообщения
WM_LBUTTONUP
, но оно, как было сказано ранее, в очередь не попадает, поэтому после закрытия окна мышь остается захваченной данным компонентом. Поэтому любое нажатие кнопки мыши воспринимается системой как относящееся к
UpDown1
, и снова приводит к помещению в очередь сообщений
WM_LBUTTONDOWN
и
WM_NOTIFY
, которые обрабатываются описанным образом. Так получается порочный круг, из которого при нормальной работе программы нет выхода. Этот круг может быть разорван, например, отладчиком, который отменяет монопольное использование мыши компонентами программы, чтобы иметь возможность работать.
В этой проблеме виновата VCL, которая зачем-то назначает компоненту
TUpDown
стиль
csCaptureMouse
. Данный компонент реализуется не средствами VCL, — это стандартное окно системного класса
UPDOWN_CLASS
, а компонент
TUpDown
— это только оболочка для него. Поэтому все необходимые перехваты мыши выполняются самой системой. VCL нет нужды в это вмешиваться. Чтобы избавиться от проблемы, нужно убрать
Этот код достаточно выполнить один раз (например, в обработчике события
OnCreate
формы), и проблемы с зацикливанием исчезнут (в примере UpDownDlg эта строка закомментирована).
Отметим, что в Windows предусмотрено специальное сообщение —
WM_CANCELMODE
, — посылаемое при открытии диалогового окна тому окну, которое захватило мышь, чтобы оно ее освободило. Один из способов решения проблемы — добавление в
UpDown1
обработчика этого сообщения (для этого можно написать наследника
TUpDown
или же воспользоваться свойством
WindowProc
— см. разд. 1.1.8), который отменит захват мыши. Отсутствие этого обработчика — тоже явная ошибка VCL.
3.4.3. Access violation при закрытии формы с перекрытым методом WndProc
Чтобы увидеть этот "подводный камень", создадим проект, содержащий две формы: главную
Form1
и вспомогательную
Form2
. В
Form1
добавим код, который по нажатию кнопки открывает
Form2
.
Во второй форме напишем обработчик события
OnClose
таким образом, чтобы он устанавливал по закрытию действие
caFree
. Добавим поле строкового типа, перекроем конструктор и метод
WndProc
так, чтобы окончательный код выглядел следующим образом (листинг 3.52, пример CloseAV на компакт- диске).
Листинг 3.52. Код класса
TForm2
type
TForm2 = class(TForm)
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
Обратите
внимание, что в конструкторе сначала присваивается значение полю
S
, и лишь потом вызывается унаследованный конструктор. Это сделано потому, что по умолчанию
S
содержит пустую строку, т.е.
nil
, а уже при вызове унаследованного конструктора окно получит сообщения, для обработки которых будет вызван метод
WndProc
. Если в этот момент
S
будет по-прежнему
nil
, попытка обратиться ко второму символу строки вызовет Access violation. Поэтому еще до начала работы унаследованного конструктора поле
S
должно получить подходящее значение.
Запустим программу и попытаемся закрыть второе окно. Возникнет исключение Access Violation: Write of address 00000001. Проблема будет в строке, отмеченной
{*}
. При этом любые другие манипуляции с окном никаких исключений вызывать не будут.
При
Action = caFree
после завершения работы метода FormClose VCL вызывает метод
TCustomForm.Release
. Проблема именно в нем: если попытаться закрыть
Form2
с помощью
Release
, возникнет то же самое исключение. В справке
Release
позиционируется как безопасный способ удаления формы из ее собственного метода. К сожалению, в действительности это не так: реализация этого удаления оставляет желать лучшего и может приводить к попыткам работать с объектом тогда, когда его уже не существует.
При вызове
Release
в очередь помещается сообщение
CM_RELEASE
, адресатом которого является сама удаляемая форма. В очередном цикле петли сообщений
CM_RELEASE
извлекается из очереди и передается на обработку. Так как сообщение адресовано форме, она же его и обрабатывает. Рассмотрим более подробно, как это происходит. (Детально механизм обработки сообщений в VCL описан в разд. 1.1.8; мы здесь рассмотрим только ту часть, которая относится к обработке
CM_RELEASE
.)
Система передает управление оконной процедуре. Для каждого экземпляра визуального компонента VCL создает свою оконную процедуру с помощью
MakeObjectInstance
. Эта процедура вызывает метод объекта
MainWndProc
, передающий управление тому методу, на который указывает свойство
WindowProc
. По умолчанию это
WndProc
.
WndProc
не обрабатывает
CM_RELEASE
самостоятельно, а передает его методу
Dispatch
.
Dispatch
пытается найти для этого сообщения специальный обработчик (метод с директивой