Листинг 1.11. Перекрытие метода CreateParams
procedure TForm1.CreateParams(var Params: TCreateParams);
begin
// Вызов унаследованного метода заполнения всех полей
// записи Params
inherited CreateParams(Params);
// Добавляем флаг WS_EX_CLIENTEEDGE к расширенному стилю окна
Params.ExStyle := Params.ExStyle or WS_EX_CLIENTEDGE;
end;
Примечание
В разд. 1.1.4 мы говорили, что имя оконного класса, который VCL создает для оконного компонента, совпадает с именем класса этого компонента. Здесь мы видим, что на самом деле имя оконного класса можно сделать и другим, для этого достаточно изменить значение поля Params.WinClassName.
Обратите внимание, что всем без исключения классам метод CreateWnd назначает одну и ту же оконную процедуру — InitWndProc. Это является основой в обработке сообщений с помощью VCL, именно поэтому оконная процедура назначается не в методе CreateParams, а в методе CreateWnd, чтобы в наследниках нельзя было изменить это поведение (метод CreateWnd тоже виртуальный, но при его переопределении имеет смысл только добавлять какие-то действия, а не изменять поведение унаследованного метода).
Чтобы понять, как работает процедура InitWndProc, обратите внимание на еще одну особенность метода CreateWnd: перед вызовом CreateWindowHandle (т.е. непосредственно перед созданием окна) он записывает ссылку на текущий объект в глобальную переменную СreationСontrol. Эта переменная затем используется процедурой InitWndProc (листинг 1.12).
Листинг 1.12. Оконная процедура InitWndProc
function InitWndProc(HWindow: HWnd; Message, WParam, LParam: LongInt): LongInt;
begin
CreationControl.FHandle := HWindow;
SetWindowLong (HWindow, GWL_WNDPROC, LongInt(CreationControl.FObjectInstance));
if (GetWindowLong(HWindow, GWL_STYLE) and WS_CHILD <> 0) and (GetWindowLong(HWindow, GWL_ID) = 0) then
SetWindowLong(HWindow, GWL_ID, HWindow);
SetProp(HWindow, MakeIntAtom(ControlAtom), THandle(CreationControl));
SetProp(HWindow, MakeIntAtom(WindowAtom), THandle(CreationControl));
asm
PUSH LParam
PUSH WParam
PUSH Message
PUSH HWindow
MOV EAX, CreationControl
MOV CreationControl, 0
CALL [EAX].TWinControl.FObjectInstance
MOV Result, EAX
end;
end;
Примечание
Код функции InitWndProc в листинге 1.12 взят из Delphi 7. В более поздних версиях код включает в себя поддержку окон, работающих с кодировкой Unicode, поэтому там предусмотрен выбор между ANSI- и Unicode-вариантами функций API (подробнее об ANSI- и Unicode-вариантах см разд. 1.1.12). Такой код сложнее понять из-за этих дополнений. Кроме того, из листинга 1.12 убрано все, что относится к компиляции под LINUX, чтобы не засорять листинг.
Из листинга 1.12 видно, что оконная процедура InitWndProc не обрабатывает сама никаких сообщений, а просто переназначает оконную процедуру у окна. Таким образом, InitWndProc для каждого окна вызывается только один раз, чтобы переназначить оконную процедуру. Обработка того сообщения, которое привело к вызову InitWndProc, тоже передается в эту новую процедуру (ассемблерная вставка в конце InitWndProc делает именно это). При просмотре этого кода возникают два вопроса. Первый — зачем писать такую оконную процедуру, почему бы не назначить нужную процедуру обычным образом? Здесь все дело в том. что стандартными средствами оконная процедура назначается одна на весь оконный класс, в то время как по внутренней логике VCL каждый экземпляр компонента должен иметь свою собственную оконную процедуру. Добиться этого можно только порождением подкласса уже после создания окна. Указатель на свою уникальную оконную процедуру (откуда эта процедура берется и почему она должна быть уникальной, мы поговорим в следующем разделе) каждый экземпляр хранит в поле FObjectInstance. Значение глобальной переменной CreationControl присваивается, как мы помним, непосредственно перед созданием окна, а первое свое сообщение окно получает буквально в момент создания. Так как VCL — принципиально однонитевая библиотека, ситуация, когда другой код вклинивается между присваиванием значения переменной CreationControl и вызовом InitWndProc, невозможна, так что в InitWndProc попадает правильная ссылка на создаваемый объект.
Второй вопрос — зачем так сложно? Почему в методе CreateWnd сразу после создания окна нельзя было вызвать SetWindowLong и установить нужную оконную процедуру там, вместо того чтобы поручать это процедуре InitWndProc? Здесь ответ такой: это сделано потому, что свои первые несколько сообщений (например, сообщения WM_CREATE и WM_NCCREATE) окно получает до того, как функция CreateWindowEx завершит свою работу. Чтобы завершить создание окна, CreateWindowEx отправляет несколько сообщений окну, и только после того как окно обработает их должным образом, процесс создания окна считается завершенным. Так что назначать уникальную оконную процедуру после завершения CreateWindowEx — это слишком поздно. Именно поэтому уникальная оконная процедура назначается таким неочевидным и несколько неуклюжим способом.
1.1.8. Обработка сообщений с помощью VCL
При использовании VCL в простых случаях самостоятельно работать с оконными сообщениями нет нужды, поскольку практически все можно сделать с помощью свойств, методов и событий компонентов. Тем не менее, некоторые сообщения приходится обрабатывать вручную. Чаще всего это приходится делать при разработке собственных компонентов, но и в обычных приложениях это также может быть полезным.
Кроме сообщений, предусмотренных в системе, компоненты VCL обмениваются сообщениями, созданными авторами этой библиотеки. Эти сообщения имеют префиксы CM_ и CN_. Они нигде не документированы, разобраться с ними можно только по исходным кодам VCL. При разработке собственных компонентов приходится обрабатывать эти сообщения, которые мы здесь не будем полностью описывать, но некоторые из них будут упоминаться в описании работы VCL с событиями.
В Windows API нет понятия главного окна — все окна, не имеющие родителя (или владельца в терминах системы), равноценны, и приложение может продолжать работу после закрытия любых окон. Но в VCL введено понятие главной формы: форма, которая создается первой, становится главной, и ее закрытие означает закрытие всего приложения.
Если окно не имеет ни родителя, ни владельца в терминах системы (такие окна называются окнами верхнего уровня), то на панели задач появляется кнопка, связанная с этим окном (окно, имеющее владельца, также может обзавестись такой кнопкой, если оно создано со стилем WS_EX_APPWINDOW). Обычно в приложении одно окно главного уровня, и оно играет роль главного окна этого приложения, хотя система не запрещает приложению создавать несколько окон верхнего уровня (примеры — Internet Explorer, Microsoft Word). Разработчики VCL пошли по другому пути: окно верхнего уровня, ответственное за появление кнопки на панели задач, создается объектом Application. Дескриптор этого окна хранится в свойстве Application.Handle, а само оно невидимо, т.к. имеет нулевые размеры. Как и любое другое, это окно имеет оконную процедуру и может обрабатывать сообщения. Главная форма — это отдельное окно, не имеющее, с формальной точки зрения, никакого отношения к кнопке на панели задач. Видимость связи между этой кнопкой и главной формой обеспечивается взаимодействием объекта Application и объекта главной формы внутри VCL. Таким образом, даже простейшее VCL-приложение создает два окна: невидимое окно объекта Application и окно главной формы. Окно, создаваемое объектом Application, мы будем называть невидимым окном приложения. Невидимое окно приложения по умолчанию становится владельцем (в терминах системы) всех форм, у которых явно не установлено свойство Parent, в том числе и главной формы.
При обработке сообщений VCL решает две задачи: выборка сообщений из очереди и передача сообщения конкретному компоненту. Рассмотрим сначала первую задачу.
Выборкой сообщений из очереди занимается объект Application, непосредственно за извлечение и диспетчеризацию сообщения отвечает его метод ProcessMessage (листинг 1.13).
Листинг 1.13. Метод TApplication.ProcessMessage
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Unicode: Boolean;
Handled: Boolean;
MsgExists: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
begin
Unicode := (Msg.hwnd <> 0) and IsWindowUnicode(Msg.hwnd);
if Unicode then MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE)
else MsgExists := PeekMessage(Msg, 0, 0, 0, PM_REMOVE);
if not MsgExists then Exit;
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and not Handled and
not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
if Unicode then DispatchMessageW(Msg);
else DispatchMessage(Msg);
end;
end else FTerminate := True;
end;
end;
В этом коде отдельного комментария требует то, как используется функция PeekMessage. Сначала эта функция вызывается с параметром PM_NOREMOVE, — так выполняется проверка условия, что в очереди присутствует сообщение, а также выясняется, для какого окна предназначено первое сообщение в очереди. Само сообщение при этом остается в очереди. С помощью функции IsWindowUnicode производится проверка, использует ли окно-адресат кодировку ANSI или Unicode, и затем, в зависимости от этого, сообщение извлекается либо функцией PeekMessage, либо ее Unicode-аналогом PeekMessageW (о Unicode-аналогах функций см. разд. 1.1.12). При диспетчеризации сообщения также вызывается либо функция DispatchMessage, либо ее Unicode-аналог DispatchMessageW.
Если метод ProcessMessage с помощью PeekMessage извлекает из очереди сообщение WM_QUIT, то он устанавливает в True поле FTerminate и завершает свою работу. Обработка всех остальных сообщений, извлеченных из очереди состоит из следующих основных этапов (см. рис. 1.6):
1. Если назначен обработчик Application.OnMessage, сообщение передается ему. В этом обработчике можно установить параметр-переменную Handle в True, что означает, что сообщение не нуждается в дополнительной обработке.
2. Второй шаг —это предварительная обработка сообщения (вызов метода IsPreProcessMessage). Этот шаг появился только начиная с BDS 2006, в более ранних версиях его не было. Обычно предварительную обработку осуществляет то окно, которому предназначено это сообщение, но если окно-адресат не является VCL-окном, производится поиск VCL-окна по цепочке родителей. Кроме того, если какое-либо окно захватит ввод мыши, предварительную обработку сообщений будет осуществлять именно оно. Если оконный компонент, удовлетворяющий этим требованиям, найден, вызывается его метод PreProcessMessage, который возвращает результат логического типа. Если компонент вернул True, то на этом обработка сообщения заканчивается. Отметим, что ни один из стандартных компонентов VCL не использует эту возможность перехвата сообщений, она реализована для сторонних компонентов.
3. Затем, если на экране присутствует всплывающая подсказка (hint), проверяется, должно ли пришедшее сообщение прятать эту подсказку, и если да, то она убирается с экрана (метод IsHintMessage). Список сообщений, которые должны прятать окно подсказки, зависит от класса этого окна (здесь имеется в виду класс VCL, а не оконный класс) и определяется виртуальным методом THintWindow.IsHintMsg. Стандартная реализации этого метода рассматривает как "прячущие" все сообщения от мыши, клавиатуры, сообщения об активации и деактивации программы и о действиях пользователя с меню или визуальными компонентами. Если метод IsHintMessage возвращает False, то сообщение дальше не обрабатывается, но стандартная реализация этого метода всегда возвращает True.
4. Далее проверяется значение параметра Handled, установленное в обработчике OnMessage (если он назначен). Если это значение равно True, метод ProcessMessage завершает свою работу, и обработка сообщения на этом заканчивается. Таким образом, обработка сообщения по событию OnMessage не может отменить предварительную обработку сообщения и исчезновение всплывающей подсказки.
Рис. 1.6. Одна итерация петли сообщений VCL (блок-схема метода Application.ProcessMessage)
5. Если главная форма приложения имеет стиль MDIForm, и одно из его дочерних MDI-окон в данный момент активно, сообщение передается функции TranslateMDISysAccel. Если эта функция вернет True, то обработка сообщения на этом завершается (все эти действия выполняются в методе IsMDIMsg).
6. Затем, если получено клавиатурное сообщение, оно отправляется на предварительную обработку тому же окну, что и в пункте 2 (метод IsKeyMsg). Предварительная обработка клавиатурного сообщения начинается с попытки найти полученную комбинацию клавиш среди "горячих" клавиш контекстно-зависимого меню и выполнить соответствующую команду. Если контекстно-зависимое меню не распознало сообщение как свою "горячую" клавишу, то вызывается обработчик события OnShortCut окна, осуществляющего предварительную обработку (если это окно не является формой и не имеет этого события, то вызывается OnShortCut его родительской формы). Если обработчик OnShortCut не установил свой параметр Handled в True, полученная комбинация клавиш ищется среди "горячих" клавиш сначала главного меню, а потом — среди компонентов TActionList. Если и здесь искомая комбинация не находится, возникает событие Application.OnShortCut, которое также имеет параметр Handled, позволяющий указать, что сообщение в дополнительной обработке не нуждается. Если обработчик не установил этот параметр, то сообщение передается главной форме приложения, которое пытается найти нажатую комбинацию среди "горячих" клавиш своего контекстного меню, передает его обработчику OnShortCut, ищет среди "горячих" клавиш главного меню и компонентов TActionList. Если нажатая клавиша не является "горячей", но относится к клавишам, использующимся для управления диалоговыми окнами (<Tab>, стрелки, <Esc> и т.п.), форме передается сообщение об этом, и при необходимости сообщение обрабатывается. Таким образом, на данном этапе средствами VCL эмулируются функции TranslateAccelerator и IsDialogMessage.
7. Если на экране присутствует один из стандартных диалогов (в VCL они реализуются классами TOpenDialog, TSaveDialog и т.п.), то вызывается функция IsDialogMessage, чтобы эти диалоги могли нормально функционировать (метод IsDlgMsg).
8. Если ни на одном из предыдущих этапов сообщение не было обработано, то вызываются функции TranslateMessage и DispatchMessage, которые завершают обработку сообщения путем направления его соответствующей оконной функции.
Примечание
Если внимательно проанализировать шестой этап обработки сообщения, видно, что нажатая комбинация клавиш проверяется на соответствие "горячим" клавишам меню сначала активной формы, затем — главной. При этом сначала возникает событие OnShortCut активной формы, потом — Application.OnShortCut, затем — OnShortCut главной формы. Если в момент получения сообщения главная форма активна, то она дважды будет проверять соответствие клавиши "горячим" клавишам своих меню и событие OnShortCut тоже возникнет дважды (первый раз поле Msg.Msg равно CN_KEYDOWN, второй — CM_APPKEYDOWN). Эта проверка осуществляется дважды только в том случае, если комбинация клавиш не распознается как "горячая" клавиша — в противном случае цепочка проверок обрывается при первой проверке.
Метод ProcessMessage возвращает True, если сообщение извлечено и обработано, и False, если очередь была пуста. Этим пользуется метод HandleMessage, который вызывает ProcessMessage и, если тот вернет False, вызывает метод Application.Idle для низкоприоритетных действий, которые должны выполняться только при отсутствии сообщений в очереди. Метод Idle, во-первых, проверяет, над каким компонентом находится курсор мыши, и сохраняет ссылку на него в поле FMouseControl, которое используется при последующей проверке, нужно ли прятать всплывающую подсказку. Затем, при необходимости, прячется старая всплывающая подсказка и показывается новая. После этого вызывается обработчик Application.OnIdle, если он назначен. Этот обработчик имеет параметр Done, по умолчанию равный True. Если в коде обработчика он не меняется на False, метод Idle инициирует события OnUpdate у всех объектов TAction, у которых они назначены (если Done после вызова принял значение False, HandleMessage не тратит время на инициацию событий OnUpdate).
Примечание
В BDS 2006 появилось свойство Application.ActionUpdateDelay, позволяющее снизить нагрузку на процессор, откладывая на некоторое время обновление объектов TAction. Если значение этого свойства не равно нулю, в методе Idle вместо вызова запускается таймер и OnUpdate вызывается по его сигналу.
Затем, независимо от значения Done, с помощью процедуры CheckSynchronize проверяется, есть ли записи в списке методов, ожидающих синхронизации (эти методы помещаются в указанный список при вызове TThread.Synchronize). Если список не пуст, выполняется первый из этих методов (при этом он, разумеется, удаляется из списка). Затем, если остался равным True, а список методов для синхронизации был пуст (т. е. никаких дополнительных действий выполнять не нужно), HandleMessage вызывает функцию Windows API WaitMessage. Эта функция приостанавливает выполнение нити до тех пор, пока в ее очереди не появятся сообщения.
Примечание
Вызов Synchronize приводит к тому, что соответствующий метод будет выполнен основной нитью приложения, а нить, вызвавшая Synchronize, будет приостановлена до тех пор, пока главная нить не сделает это. Отсюда видно, насколько бредовыми являются советы (заполонившие Интернет, а также встречающиеся в некоторых книгах, например, у Архангельского) помещать весь код нити в Synchronize. В этом случае дополнительная нить вообще не будет ничего делать, все будет выполняться основной нитью, и выигрыша от создания дополнительной нити просто не будет. Поэтому в Synchronize нужно помещать только те действия, которые не могут быть выполнены неосновной нитью (например, обращения к свойствам и методам VCL-компонентов).
Главная петля сообщений в VCL реализуется методом Application.Run, вызов которого автоматически вставляется в dpr-файл VCL-проекта. Application.Run вызывает в цикле метод HandleMessage, пока поле FTerminate не окажется равным True (напомним, что значение True присваивается этому полю, когда ProcessMessage извлекает из очереди сообщение WM_QUIT, а также при обработке сообщения WM_ENDSESSION и при закрытии главной формы).
Для организации локальной петли сообщений существует метод Application.ProcessMessages. Он вызывает ProcessMessage до тех пор, пока очередь не окажется пустой. Вызов этого метода рекомендуется вставлять в обработчики событий, которые работают долго, чтобы в это время программа не теряла способности реагировать на действия пользователя.
Из сказанного может сложиться впечатление, что главная нить проверяет список методов синхронизации только в главной петле сообщений, когда вызывается метод Idle. На самом деле это не так. Модуль Classes содержит переменную WakeMainThread, хранящую указатель на метод, который вызывается при помещении нового метода в список синхронизации. В конструкторе TApplication этой переменной присваивается указатель на метод TApplication.WakeMainThread, который посылает сообщение WM_NULL невидимому окну приложения. Сообщение WM_NULL — это "пустое" сообщение, на которое окно не должно реагировать (оно используется, например, при перехвате сообщений ловушкой: ловушка не может запретить передачу окну сообщения, но может изменить его на WM_NULL, чтобы окно проигнорировало сообщение). Невидимое окно приложения, тем не менее, не игнорирует это сообщение, а вызывает при его получении CheckSynchronize. Таким образом, синхронное выполнение метода не откладывается до вызова Idle, а выполняется достаточно быстро, в том числе и в локальной петле сообщений. Более того, если главная нить перешла в режим ожидания получения сообщения (через вызов WaitMessage), то вызов Synchronize в другой нити прервет это ожидание, т.к. в очередь будет поставлено сообщение WM_NULL.
Процедура CheckSynchronize и переменная WakeMainThread позволяют обеспечить синхронизацию и в тех приложениях, которые не используют VCL в полном объеме. Разработчику приложения необходимо обеспечить периодические вызовы функции CheckSynchronize из главной нити, чтобы можно было вызывать TThread.Synchronize в других нитях. При этом в главной нити можно обойтись без петли сообщений. Присвоение переменной WakeMainThread собственного метода позволяет реализовать специфичный для данного приложения способ ускорения вызова метода в главной нити.
Примечание
Описанный здесь способ синхронизации работы нитей появился, начиная с шестой версии 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, служат специально для обработки сообщений. Синтаксис описания такого метода следующий:
procedure <Name>(var Message: <TMsgType>); message <MsgNumber>;
<MsgNumber> — это номер сообщения, для обработки которого предназначен метод. Имя метода может быть любым, но традиционно оно совпадает с именем константы сообщения за исключением того, что в нем выбран более удобный регистр символов и отсутствует символ "_" (например, метод для обработки WM_SIZE будет называться WMSize).
В качестве типа параметра <TMsgType> компилятор разрешает любой тип, но на практике имеет смысл только использование типа TMessage или "совместимого" с ним. Тип TMessage описан в листинге 1.14.
Листинг 1.14. Описание типа TMessage
TMessage = packed record
Msg: Cardinal;
case Integer of
0: (
WParam: LongInt;
LParam: LongInt;
Result: LongInt);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
Поле Msg содержит номер сообщения, поля WParam и LParam — значение одноименных параметров сообщения. Поле Result — выходное: метод, осуществляющий окончательную обработку сообщения, заносит в него то значение, которое должна вернуть оконная процедура. Поля с суффиксами Lo и Hi позволяют обращаться отдельно к младшему и старшему словам соответствующих полей, что может быть очень полезно, когда эти параметры содержат пару 16-разрядных значений. Например, у сообщения WM_MOUSEREMOVE младшее слово параметра LParam содержит X-координату мыши, старшее — Y-координату. В случае обработки этого сообщения поле LParamLo будет содержать X-координату, LParamHi — Y-координату.
"Совместимыми" с TMessage можно назвать структуры, которые имеют такой же размер, а также параметр Msg, задающий сообщение. Эти структуры учитывают специфику конкретного сообщения. Их имена образуются из имени сообщения путем отбрасывания символа и добавления префикса T. Для уже упоминавшегося сообщения WM_MOUSEMOVE соответствующий тип выглядит, как показано в листинге 1.15.
Листинг 1.15. Тип TWMNCMouseMove
TWMNCMouseMove = packed record
Msg: Cardinal;
HitTest: LongInt;
XCursor: SmallInt;
YCursor: SmallInt;
Result: LongInt;
end;
Параметр WParam переименован в HitTest, что лучше отражает его смысл в данном случае, а параметр LParam разбит на две 16-разрядных части: XCursor и YCursor.
Параметр метода для обработки сообщения имеет тип, соответствующий обрабатываемому сообщению (при необходимости можно описать свой тип), или тип TMessage. Таким образом, обработчик сообщения WM_MOUSEMOVE будет выглядеть так, как показано в листинге 1.16.
Листинг 1.16. Объявление и реализация метода для обработки сообщения WM_MOUSEMOVE
type
TSomeForm = class(TForm)
...............
procedure WMNCMouseMove(var Message: TWMNCMouseMove); message WM_NCMOUSEMOVE;
................
end;
procedure TSomeForm.WMNCMouseMove(var Message: TWMNCMouseMove);
begin
...............
inherited; // Возможно, этот вызов не будет нужен
end;
Метод для обработки сообщения может выполнить ее полностью самостоятельно, тогда он не должен вызывать унаследованный метод обработки сообщения. Если же реакция предка на сообщение в целом устраивает разработчика, но нуждается только в дополнении, ключевое слово inherited позволяет вызвать унаследованный обработчик для данного сообщения. Таким образом, может образовываться целая цепочка вызовов унаследованных обработчиков одного и того же сообщения, каждый из которых выполняет свою часть обработки. Если у предков класса нет обработчика данного сообщения, директива inherited передает управление методу TObject.DetaultHandler. Вернемся к методу Dispatch. Он ищет среди обработчиков сообщения класса (собственных или унаследованных) метод для обработки сообщения, заданного полем Msg параметра Message и, если находит, передает управление ему. Если ни сам класс, ни его предки не содержат обработчика данного сообщения, то обработка передаётся методу DefaultHandler.
Метод DefaultHandler виртуальный, в классе TObject он не выполняет никаких действий, но наследники его переопределяют. Впервые он переопределяется в классе TControl для обработки сообщений, связанных с получением и установкой заголовка окна — WM_GETTEXT, WM_GETTEXTLENGTH и WM_SETTEXT. Напомним, что класс TControl является предком для всех визуальных компонентов, а не только оконных, и появление обработчика системных сообщений в этом классе — часть той имитации обработки сообщений неоконными компонентами, о которой мы уже говорили.
В классе TWinControl метод DefaultHandler также переопределен. Помимо передачи некоторых сообщений дочерним окнам (об этом мы будем подробнее говорить чуть позже) и обработки некоторых внутренних сообщений он вызывает оконную процедуру, адрес которой хранится в свойстве DefWndProc. Это свойство содержит адрес, который был присвоен полю WindowClass.lpfnWndProc структуры TCreateParams в методе CreateParams. По умолчанию это поле содержит адрес стандартной оконной процедуры DefWindowProc. Как было сказано ранее, обработка сообщений при использовании API обычно завершается вызовом этой процедуры. В классе TCustomForm метод DefaultHandler также переопределен, если форма является MDI-формой, сообщения, присланные ей, передаются в процедуру DefFrameProc (за исключением WM_SIZE, которое передается в DefWindowProc) независимо от того, какое значение имеет свойство DefWindowProc. Для всех остальных типов форм вызывается унаследованный от TWinControl DefaultHandler.
Повторим еще раз всю цепочку обработки сообщений оконными компонентами VCL (рис. 1.7). Для каждого компонента создается уникальная оконная процедура, которая передает управление методу MainWndProc. MainWndProc передает управление методу, указатель на который хранится в свойстве WindowProc. По умолчанию это метод компонента WndProc. Он осуществляет обработку некоторых сообщений, но в большинстве случаев передает управление методу Dispatch, который ищет среди методов компонента или его предков обработчик данного сообщения. Если обработчик не найден, управление получает метод DefaultHandler (он может также получить управление и в том случае, если обработчик найден, но он вызывает inherited). DefaultHandler самостоятельно обрабатывает некоторые сообщения, но большинство из них передаётся оконной процедуре, адрес хранится в свойстве DefWndProc (по умолчанию это стандартная функция Windows API DefWindowProc).
Рис. 1.7. Блок-схема оконной процедуры оконных компонентов VCL
Класс TControl имеет метод Perform, с помощи которого можно заставить визуальный компонент выполнить обработку конкретного сообщения в обход оконной процедуры и системного механизма передачи сообщений. Perform приводит к непосредственному вызову метода, указатель на который хранится в свойстве WindowProc. Дальше цепочка обработки сообщений такая же, как и при получении сообщения через оконную процедуру. Для оконных компонентов вызов Perform по своим последствиям практически эквивалентен передаче сообщения с помощью SendMessage с двумя исключениями. Во-первых, при использовании SendMessage система обеспечивает переключение между нитями, и сообщение будет выполнено в той нити, которая создала окно, a Perform никакого переключения не производит, и обработка сообщения будет выполнена той нитью, которая вызвала Perform. Поэтому Perform, в отличие от SendMessage, можно использовать только в главной нити (напомним, что VCL — принципиально однонитевая библиотека, и создание форм вне главной нити с ее помощью недопустимо). Во-вторых, Perform выполняется чуть быстрее, т.к. оконная процедура и метод MainWndProc исключаются из цепочки обработки сообщения.
Но основное преимущество Perform перед SendMessage заключается в том, что Perform пригоден для работы со всеми визуальными компонентами, а не только с оконными. Неоконные визуальные компоненты не могут иметь оконной процедуры, но цепочка обработки сообщений у них есть. В ней отсутствует оконная процедура и метол MainWndProc, a DefaultHandler не вызывает никаких стандартных оконных процедур, но во всем остальном эта цепочка полностью эквивалентна цепочке оконных компонентов. Таким образом, цепочка обработки сообщений оконных компонентов имеет две точки входа: оконную процедуру и метод Perform, а цепочка неоконных компонентов — только метод Perform. Следовательно, метод Perform универсален: он одинаково хорошо подходит как для оконных, так и для неоконных компонентов. Он широко применяется в VCL, т.к. позволяет единообразно работать с любыми визуальными компонентами.
Неоконным визуальным компонентам сообщения посылает их родительское окно. Например, как мы уже говорили, обработка сообщений, связанных с мышью, в классе TWinControl включает в себя, не попадают ли координаты курсора в область какого-либо из дочерних неоконных компонентов. И если попадает, оконный компонент не обрабатывает это сообщение самостоятельно, а транслирует его соответствующему неоконному компоненту с помощью Perform. Эта трансляция и обеспечивает получение сообщений неоконными компонентами.
Сообщения в VCL транслируются не только неоконным, но и оконным компонентам. В Windows все сообщения, информирующие об изменении состояния стандартных элементов управления, получает их родительское окно, а не сам элемент. Например, при нажатии на кнопку уведомительное сообщение об этом получает не сама кнопка, а окно, ее содержащее. Сама кнопка получает и обрабатывает только те сообщения, которые обычно разработчику неинтересны. Это упрощает работу программиста, т.к. не требуется для каждого элемента управления писать свою оконную процедуру, все значимые сообщения получает оконная процедура родительского окна. Рассмотрим, что происходит при нажатии кнопки на форме. Окно, содержащее эту кнопку, получает сообщение WM_COMMAND, уведомляющее о возникновении события среди оконных компонентов. Параметры сообщения позволяют определить, какое именно событие и с каким элементом управления произошло (в данном случае событие будет BN_CLICKED). Обработчик WM_COMMAND класса TWinControl находит компонент, вызвавший сообщение, и посылает ему сообщение CN_COMMAND (как видно из префикса, это внутреннее сообщение VCL) с теми же параметрами. В нашем примере это будет экземпляр класса TButton, реализующий кнопку, которую нажал пользователь. Получив CN_COMMAND, компонент начинает обработку произошедшего с ним события (в частности, TButton инициирует событие OnСlick).
Примечание
К переопределению обработчика WM_COMMAND нужно относиться осторожно, чтобы не нарушить механизм трансляции сообщений. Примером неправильного переопределения может служить класс TCustomGrid. В форумах нередко встречаются вопросы, почему элементы управления, родителем которых является TDrawGrid или TStringGrid, некорректно ведут себя: кнопки при нажатии не генерируют событие OnClick, выпадающие списки остаются пустыми и т.д. Это связано с тем, что обработчик WM_COMMAND в TCustomGrid учитывает возможность существования только одного дочернего компонента — внутреннего редактора, возникающего при включенной опции goEditing. Остальным дочерним компонентам WM_COMMAND не транслируются, и они лишены возможности корректно реагировать на происходящие с ними события. Выходом из ситуации может стать либо создание наследника от TDrawGrid или TStringGrid, который правильно транслирует WM_COMMAND, либо назначение родительским окном компонента, вставляемого в сетку, формы, панели или иного оконного компонента, который правильно транслирует это сообщение.
Рассмотрим все методы, с помощью которых можно встроить свой код в цепочку обработки сообщений оконным компонентом и перехватить сообщения. Всего существует шесть способов сделать это.
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, уникальность которого гарантируется.
Номера глобальных сообщений становятся известными только на этапе выполнения программы. Это означает, что для их обработки нельзя использовать методы с директивой message, вместо этого следует перекрывать методы WndProc или DefaultHandler.
1.1.10. Особые сообщения
Отправка и обработка некоторых сообщений производится не по общим правилам, а с различными исключениями. Приведенный далее список таких сообщений не претендует на полноту, но все-таки может дать представление о таких исключениях.
Сообщение WM_COPYDATA служит для передачи блока данных от одного процесса к другому. В 32-разрядных версиях Windows память, выделенная некоторому процессу, недоступна для всех остальных процессов. Поэтому просто передать указатель другому процессу нельзя, поскольку он не сможет получить доступ к этой области памяти. При передаче сообщения WM_COPYDATA система копирует указанный блок из адресного пространства отправителя в адресное пространство получателя, передает получателю указатель на этот блок, и при завершении обработки сообщения освобождает блок. Все это требует определенной синхронности действий, которой невозможно достичь при посылке сообщения, поэтому с помощью WM_COPYDATA можно только отправлять, но не посылать (т.е. можно использовать SendMessage, но не PostMessage).
Сообщение WM_PAINT предназначено для перерисовки клиентской облаcти окна. Если изображение сложное, этот процесс занимает много времени, поэтому в Windows предусмотрены механизмы, минимизирующие количество перерисовок. Перерисовывать свое содержимое окно должно при получении сообщения WM_PAINT. С каждым таким сообщением связан регион, нуждающийся в обновлении. Этот регион может совпадать с клиентской областью окна или быть ее частью. В последнем случае программа может ускорить перерисовку, рисуя не все окно, а только нуждающуюся в этом часть (VCL игнорирует возможность перерисовки только части окна, поэтому при работе с этой библиотекой окно всегда перерисовывается полностью). Послать сообщение WM_PAINT с помощью PostMessage окну нельзя, т.к. оно не ставится в очередь. Вместо этого можно пометить регион как нуждающийся в обновлении с помощью функций InvalidateRect и InvalidateRgn. Если на момент вызова этих функций регион, который необходимо обновить, не был пуст, новый регион объединяется со старым. Функции GetMessage и PeekMessage, если очередь сообщений пуста, а регион, требующий обновления, не пуст, возвращают сообщение WM_PAINT. Таким образом, перерисовка окна откладывается до того момента, когда все остальные сообщения будут обработаны. Отправить WM_PAINT с помощью SendMessage тоже нельзя. Если требуется немедленная перерисовка окна, следует вызвать функции UpdateWindow или RedrawWindow, которые не только отправляют сообщение окну, но и выполняют сопутствующие действия, связанные с регионом обновления. Обработка сообщения WM_PAINT также имеет некоторые особенности. Обработчик должен получить контекст устройства окна (см. разд. 1.1.11 данной главы) с помощью функции BeginPaint и по окончании работы освободить его с помощью EndPaint. Эти функции должны вызываться только один раз при обработке сообщения. Соответственно, если сообщение обрабатывается поэтапно несколькими обработчиками, как это бывает при перехвате сообщений, получать и освобождать контекст устройства должен только первый из них, а остальные должны пользоваться тем контекстом, который он получил. Система не накладывает обязательных требований, которые могли бы решить проблему, но предлагает решение, которое используют все предопределенные системные классы. Когда сообщение WM_PAINT извлекается из очереди, его параметр wParam равен нулю. Если же обработчик получает сообщение с wParam <> 0, то он рассматривает значение этого параметра как дескриптор контекста устройства и использует его, вместо того чтобы получать дескриптор через BeginPaint. Первый в цепочке обработчиков должен передать вниз по цепочке сообщение с измененным параметром wParam. Компоненты VCL также пользуются этим решением. При перехвате сообщения WM_PAINT это нужно учитывать.
Примеры PanelMsg и Line, имеющиеся на прилагаемом компакт-диске, демонстрируют, как правильно перехватывать сообщение WM_PAINT.
Простые таймеры, создаваемые системой с помощью функции SetTimer, сообщают об истечении интервала посредством сообщения WM_TIMER. Проверка того, истек ли интервал, осуществляется внутри функций GetMessage, PeekMessage. Таким образом, если эти функции долго не вызываются, сообщение WM_TIMER не ставится в очередь, даже если положенный срок истек. Если за время обработки других сообщений срок истек несколько раз, в очередь ставится только одно сообщение WM_TIMER. Если в очереди уже есть сообщение WM_TIMER, новое в очередь не добавляется, даже если срок истек. Таким образом, часть сообщений WM_TIMER теряется, т.е., например, если интервал таймера установить равным одной секунде, то за час будет получено не 3600 сообщений WM_TIMER, а меньшее число, и разница будет тем больше, чем интенсивнее программа загружает процессор.
Примечание
Класс TTimer инкапсулирует таймер, работающий через WM_TIMER. Сообщения получает невидимое окно, создающееся специально для этого. Поэтому событие OnTimer за час при секундном интервале также возникнет меньше, чем 3600 раз.
Некоторую специфику имеют и сообщения от клавиатуры. При обработке таких сообщений можно использовать функцию GetKeуState, которая возвращает состояние любой клавиши (нажата-отпущена) в момент возникновения данного события. Именно в момент возникновения, а не в момент вызова функции. Если функцию GetKeyState использовать при обработке не клавиатурного сообщения, оно вернет состояние клавиши на момент последнего извлеченного из очереди клавиатурного сообщения.
1.1.11. Графика в Windows API
Та часть Windows API, которая служит для работы с графикой, обычно называется GDI (Graphic Device Interface). Ключевое понятие в GDI — контекст устройства (Device Context, DC). Контекст устройства — это специфический объект, хранящий информацию о возможностях устройства, о способе работы с ним и о разрешенной для изменения области. В Delphi контекст устройства представлен классом TCanvas, свойство Handle которого содержит дескриптор контекста устройства. TCanvas универсален в том смысле, что с его помощью рисование в окне, на принтере или в метафайле выглядит одинаково. То же самое справедливо и для контекста устройства. Разница заключается только в том, как получить в разных случаях дескриптор контекста.
Большинство методов класса TCanvas являются "калькой" с соответствующих (в большинстве случаев одноименных) функций GDI. Но в некоторых случаях (прежде всего в методах вывода текста и рисования многоугольников) параметры методов TCanvas имеют более удобный тип, чем функции GDI. Например, метод TCanvas.Polygon требует в качестве параметра открытый массив элементов типа TPoint, а соответствующая функция GDI — указатель на область памяти, содержащую координаты точек, и число точек. Это означает, что до вызова функции следует выделить память, а потом — освободить ее. Еще нужен код, который заполнит эту область памяти требуемыми значениями. И ни в коем случае нельзя ошибаться в количестве элементов массива. Если зарезервировать память для одного числа точек, а при вызове функции указать другое, программа будет работать неправильно. Но для простых функций работа через GDI ничуть не сложнее, чем через TCanvas. Для получения дескриптора контекста устройства существует много функций. Только для того, чтобы получить дескриптор контекста обычного окна, существуют четыре функции: BeginPaint, GetDC, GetWindowDC и GetDCEx. Первая из них возвращает контекст клиентской области окна при обработке сообщения WM_PAINT. Вторая дает контекст клиентской области окна, который можно использовать в любой момент времени, а не только при обработке WM_PAINT. Третья позволяет получить контекст всего окна, вместе с неклиентской частью. Последняя же дает возможность получить контекст определенной области клиентской части окна.
После того как дескриптор контекста получен, можно воспользоваться преимуществами класса TCanvas. Для этого необходимо создать экземпляр такого класса и присвоить его свойству Handle полученный дескриптор. Освобождение ресурсов нужно проводить в следующем порядке сначала свойству Handle присваивается нулевое значение, затем уничтожается экземпляр класса TCanvas, после этого с помощью подходящей функции GDI освобождается контекст устройства. Пример такого использования класса TCanvas демонстрируется листингом 1.17.
Листинг 1.17. Использование класса TCanvas для работы с произвольным контекстом устройства
var
DC: HDC;
Canvas: TCanvas;
begin
DC := GetDC(...); // Здесь возможны другие способы получения DC
Canvas := TCanvas.Create;
try
Canvas.Handle := DC; // Здесь рисуем, используя Canvas
finally
Canvas.Free;
end;
// Освобождение объекта Canvas не означает освобождения контекста DC
// DC необходимо удалить вручную
ReleaseDC(DC);
end;
Использование класса TCanvas для рисования на контексте устройства, для которого имеется дескриптор, показано в примере PanelMsg на прилагающемся компакт-диске.
Разумеется, можно вызывать функции GDI при работе через TCanvas. Для этого им просто нужно передать в качестве дескриптора контекста значение свойства Canvas.Handle. Коротко перечислим те возможности GDI, которые разработчики VCL почему-то не сочли нужным включать в TCanvas: работа с регионами и траекториями; выравнивание текста по любому углу или по центру; установка собственной координатной системы; получение детальной информации об устройстве; использование геометрических перьев; вывод текста под углом к горизонтали; расширенные возможности вывода текста; ряд возможностей по рисованию нескольких кривых и многоугольников одной функцией; поддержка режимов заливки. Доступ ко всем этим возможностям может быть осуществлен только через API. Отметим также, что Windows NT/2000/XP поддерживает больше графических функций, чем 9x/МЕ. Функции, которые не поддерживаются в 9x/ME, также не имеют аналогов среди методов TCanvas, иначе программы, написанные с использованием данного класса, нельзя было бы запустить в этих версиях Windows.
GDI предоставляет очень интересные возможности по преобразованию координат, но только в Windows NT/2000/XP; в Windows 9x/ME они не поддерживаются. С помощью функции SetWorldTransform можно задать произвольную матрицу преобразования координат, и все дальнейшие графические операции будут работать в новой координатной системе. Матрица позволяет описать такие преобразования координат, как поворот, сдвиг начала координат и масштабирование, т.е. возможности открываются очень широкие. Также существует менее гибкая, но тоже полезная функция преобразования координат — SetMapMode, которая поддерживается во всех версиях Windows. С ее помощью можно установить такую систему координат, чтобы во всех функциях задавать координаты, например, в миллиметрах, а не пикселах. Это позволяет использовать один и тот же код для вывода на устройства с разными разрешениями.
Некоторые возможности GDI, которым нет аналогов в TCanvas, демонстрируются примером GDI Draw.
Для задания цвета в GDI предусмотрен тип COLORREF (в модуле Windows определен также его синоним для Delphi — TColorRef). Это 4-байтное беззнаковое целое, старший байт которого определяет формат представления цвета. Если этот байт равен нулю (будем называть этот формат нулевым), первый, второй и третий байты представляют собой интенсивности красного, зеленого и синего цветов соответственно. Если старший байт равен 1, два младших байта хранят индекс цвета в текущей палитре устройства, третий байт не используется и должен быть равен нулю. Если старший байт равен 2, остальные байты, как и в нулевом формате, показывают интенсивность цветовых составляющих.
Тип TColorRef позволяет менять глубину каждого цветового канала от 0 до 255, обеспечивая кодирование 16 777 216 различных оттенков (это соответствует режиму TrueColor). Если цветовое разрешение устройства невелико, GDI подбирает ближайший возможный цвет из палитры. Если старший байт TColorRef равен нулю, цвет выбирается из текущей системной палитры (по умолчанию эта палитра содержит всего 20 цветов, поэтому результаты получаются далекими от совершенства). Если же старший байт равен 2, то GDI выбирает ближайший цвет из палитры устройства. В этом случае результаты получаются более приемлемыми. Если устройство имеет большую цветовую глубину и не задействует палитру, разницы между нулевым и вторым форматом COLORREF нет.
Примечание
Хотя режимы HighColor (32 768 или 65 536 цветов) не обладают достаточной цветовой глубиной, чтобы передать все возможные значения TColorRef, палитра в этих режимах не используется и ближайший цвет выбирается не из палитры, а из всех цветов, которые способно отобразить устройство. Поэтому выбор нулевого формата в этих режимах дает хорошие результаты.
В Windows API определены макросы (а в модуле Windows, соответственно, одноименные функции) RGB, PaletteIndex и PaletteRGB. RGB принимает три параметра — интенсивности красного, зеленого и синего компонентов и строит из них значение типа TColorRef нулевого формата. PaletteIndex принимает в качестве параметра номер цвета в палитре и на его основе конструирует значение первого формата. Макрос PaletteRGB эквивалентен RGB, за исключением того, что устанавливает старший байт возвращаемого значения равным двум. Для извлечения интенсивностей отдельных цветовых компонентов из значения типа TColorRef можно воспользоваться функциями GetRValue, GetGValue и GetBValue.
В системе определены два специальных значения цвета: CLR_NONE ($1FFFFFFF) и CLR_DEFAULT ($20000000). Они используются только в списках рисунков (image lists) для задания фонового и накладываемого цветов при выводе рисунка. CLR_NONE задаёт отсутствие фонового или накладываемого цвета (в этом случае соответствующий визуальный эффект не применяется). CLR_DEFAULT — установка цвета, заданного для всего списка.
В VCL для передачи цвета предусмотрен тип TColor, определенный в модуле Graphics. Это 4-байтное число, множество значений которого является множеством значений типа TColorRef. К системным форматам 0, 1 и 2 добавлен формат 255. Если старший байт значения типа TColor равен 255, то младший байт интерпретируется как индекс системного цвета (второй и третий байт при этом не учитываются). Системные цвета — это цвета, используемые системой для рисования различных элементов интерфейса пользователя. Конкретные RGB-значения этих цветов зависят от версии Windows и от текущей цветовой схемы. RGB-значение системного цвета можно получить с помощью функции GetSysColor. 255-й формат TColor освобождает от необходимости явного вызова данной функции.
Для типа TColor определен ряд констант, облегчающих его использование. Некоторые из них соответствуют определенному RGB-цвету (clWhite, clBlack, clRed и т.п.), другие — определенному системному цвету (clWindow, clHighlight, clBtnFace и т.п.). Значения RGB-цветов определены в нулевом формате. Это не приведет к потере точности цветопередачи в режимах с палитрой, т.к. константы определены только для 16-ти основных цветов, которые обязательно присутствуют в системной палитре. Значениям CLR_NONE и CLR_DEFAULT соответствуют константы clNone и clDefault. Они служат (помимо списков рисунков) для задания прозрачного цвета в растровом изображении. Если этот цвет равен clNone, изображение считается непрозрачным, если clDefault, в качестве прозрачного цвета берется цвет левого нижнего пиксела.
Везде, где требуется значение типа TColor, можно подставлять TColorRef, т.е. всем свойствам и параметрам методов класса TCanvas, имеющим тип TColor можно присваивать те значения TColorRef, которые сформированы функциями API. Обратное неверно: API-функции не умеют обращаться с 255-м форматом TColor. Преобразование из TColor в TColorRef осуществляется с помощью функции ColorToRGB. Значения нулевого, первого и второго формата, а также clNone и clDefault она оставляет без изменения, а значения 255-го формата приводит к нулевому с помощью функции GetSysColor. Эту функцию следует использовать при передаче значении типа TColor в функции GDI.
Применение кистей, перьев и шрифтов в GDI принципиально отличается от того, как это делается в VCL. Класс TCanvas имеет свойства Pen, Brush, и Font, изменение свойств которых приводит к выбору того или иного пера, кисти, шрифта. В GDI эти объекты самостоятельны, должны создаваться, получать свой дескриптор, "выбираться" в нужный контекст устройства с помощью функции SelectObject и уничтожаться после использования. Причем удалять можно только те объекты, которые не выбраны ни в одном контексте. Есть также несколько стандартных объектов, которые не нужно ни создавать, ни удалять. Их дескрипторы можно получить с помощью функции GetStockObject. Для примера рассмотрим фрагмент программы, рисующей на контексте с дескриптором DC две линии: синюю и красную (листинг 1.18). В этом фрагменте используется то, что функция SelectObject возвращает дескриптор объекта, родственного выбираемому, который был выбран ранее. Так, при выборе нового пера она вернет дескриптор того пера, которое было выбрано до этого.
Листинг 1.18. Рисование разными перьями с использованием GDI
SelectObject(DC, CreatePen(PS_SOLID, 1, RGB(255, 0, 0)));
MoveToEx(DC, 100, 100, nil);
LineTo(DC, 200, 200);
DeleteObject(SelectObject(DC, CreatePen(PS_SOLID, 1, RGB(0, 0, 255))));
MoveToEx(DC, 200, 100, nil);
LineTo(DC, 100, 200);
DeleteObject(SelectObject(DC, SetStockObject(BLACK_PEN)));
Дескрипторы объектов GDI имеют смысл только в пределах того процесса, который их создал, передавать их между процессами нельзя. Тем не менее изредка можно встретить утверждения, что такая передача возможна. Источник этой ошибки в том. что дескрипторы объектов GDI можно было передавать между процессами в старых, 16-разрядных версиях Windows, так что все утверждения о возможности такой передачи просто основываются на устаревших сведениях.
Для хранения растровых изображений в Windows существуют три формата: DDB, DIB и DIB-секция. DDB — это Device Dependent Format, формат, определяемый графическим устройством, на которое идет вывод. DIB — это Device Independent Bitmap, формат, единый для всех устройств. Формат DIB — это устаревший формат, который не позволяет использовать графические функции GDI для модификации картинки, модифицировать изображение можно, только одним способом: вручную изменяя цвета отдельных пикселов. В 32-разрядных версиях появился еще один формат — DIB-секция. По сути дела это тот же самый DIB, но дополненный возможностями рисовать на нем с помощью GDI-функций. Все различия между этими тремя форматами можно прочитать в замечательной книге [1]; мы же здесь ограничимся только кратким их обзором.
Формат DDB поддерживается самой видеокартой (или другим устройством вывода), поэтому при операциях с таким изображением задействуется аппаратный ускоритель графики. DDB-изображение хранится в выгружаемом системном пуле памяти (Windows NT/2000/XP) или в куче GDI (Windows 9x/ME). При этом размер DDB-растра не может превышать 16 Мбайт в Windows 9x/ME и 48 Мбайт в Windows NT/2000/XP. Формат DDB не переносим с одного устройства на другое, он должен использоваться только в рамках одного устройства. Прямой доступ к изображению и его модификация вручную невозможны, т.к. формат хранения изображения конкретным устройством непредсказуем. Модифицировать DDB можно только с помощью функций GDI. Цветовая глубина DDB-изображений определяется устройством.
DIB-секция может храниться в любой области памяти, ее размер ограничивается только размером доступной приложению памяти, функции GDI для рисования на таком изображении используют чисто программные алгоритмы, никак не задействуя аппаратный ускоритель. DIB-секция поддерживает различную цветовую глубину и прямой доступ к области памяти, в которой хранится изображение. DIB-секция переносима с одного устройства на другое. BMP-файлы хранят изображение как DIB.
Скорость работы с изображением в формате DIB-секции зависит только от производительности процессора, памяти и качества реализации графических алгоритмов системой (а они, надо сказать, реализованы в Windows очень неплохо). Скорость работы с изображением в формате DDB зависит еще и от драйвера и аппаратного ускорителя видеокарты. Во-первых, аппаратный ускоритель и драйвер могут поддерживать или не поддерживать рисование графических примитивов (в последнем случае эти примитивы рисует система: то, какие операции поддерживает драйвер, можно узнать с помощью функции GetDeviceCaps). До недавнего времени была характерной ситуация, когда рисование картинки на DDB-растре и вывод такого растра на экран были заметно (иногда — в два-три раза) быстрее, чем аналогичные операции с DIB-секцией. Однако сейчас разница стала существенно меньше, производительность системы в целом выросла сильнее, чем производительность двумерных аппаратных ускорителей (видимо, разработчики видеокарт больше не считают двумерную графику узким местом и поэтому сосредоточили свои усилия на развитии аппаратных ускорителей 3D-графики). На некоторых мощных компьютерах можно даже столкнуться с ситуацией, когда DDB-изображение отстает по скорости от DIB.
Класс TBitmap может хранить изображение как в виде DDB, так и в виде DIB- секции — это определяется значением свойства PixelFormat. Значение pfDevice означает использование DDB, остальные значения — DIB-секции с различной цветовой глубиной. По умолчанию TBitmap создает изображение с форматом pfDevice, но программист может изменить формат в любой момент. При этом создается новое изображение требуемого формата, старое копируется в него и уничтожается.
Со свойством PixelFormat тесно связано свойство HandleType, которое может принимать значения bmDIB и bmDDB. Изменение свойства PixelFormat приводит к изменению свойства HandleType, и наоборот.
Примечание
Если вы собираетесь распечатывать изображение, содержащееся в TBitmap, то вы должны установкой свойств PixelFormat или HandleType обеспечить, чтобы изображение хранилось в формате DIB. Попытка вывести DDB-изображение на принтер приводит к непредсказуемым результатам (чаще всего просто ничего не выводится) из-за того, что драйвер принтера не понимает формат, совместимый с видеокартой.
При загрузке изображения из файла, ресурса или потока класс TBitmap обычно создает изображение в формате DIB-секции, соответствующее источнику по цветовой глубине. Исключение составляют сжатые файлы (формат BMP поддерживает сжатие только для 16- и 256-цветных изображений) — в этом случае создается DDB. В файле Graphics определена глобальная переменная DDBsOnly, которая по умолчанию равна False. Если изменить ее значение на True, загружаемое изображение всегда будет иметь формат DDB.
Примечание
В справке сказано, что когда DDBsOnly = False, вновь создаваемые изображения по умолчанию хранятся в виде DIB-секций. На самом деле из-за ошибки в модуле Graphics (как минимум до 2007-й версии Delphi включительно) вновь созданное изображение всегда хранится как DDB независимо от значения DDBsOnly.
Класс TBitmap имеет свойство ScanLine, через которое можно получить прямой доступ к массиву пикселов, составляющих изображение. В справке написано, что это свойство можно использовать только с DIB-изображениями. Но на самом деле DDB-изображения тоже позволяют использовать это свойство, хотя и с существенными ограничениями. Если изображение хранится в DDB- формате, при обращении к ScanLine создастся его DIB-копия, и ScanLine возвращает указатель на массив этой копии. Поэтому, во-первых, ScanLine работает с DDB-изображениями очень медленно, а во-вторых, работает не с изображением, а с его копией, откуда вытекают следующие ограничения:
1. Копия создаётся на момент обращения к ScanLine, поэтому изменения, сделанные на изображении с помощью GDI-функций после этого, будут недоступными.
2. Каждое обращение к ScanLine создает новую копию изображения, а старая при этом уничтожается. Гарантии, что новая копия будет располагаться в той же области памяти, нет, поэтому указатель, полученный при предыдущем обращении к ScanLine, больше нельзя использовать.
3. Изменения, сделанные в массиве пикселов, затрагивают только копию изображения, но само изображение при этом не меняется. Поэтому в случае DDB свойство ScanLine дает возможность прочитать, но не изменить изображение.
Следует отметить, что TBitmap иногда создает DIB-секции, даже если свойства HandleType и PixelFormat явно указывают на использование DDB. Особенно часто это наблюдается для изображений большого размера. По всей видимости, это происходит, когда в системном пуле нет места для хранения DDB-изображения такого размера, и разработчики TBitmap решили, что в таком случае лучше создать DIB-изображение, чем не создавать никакого. Пример BitmapSpeed на прилагаемом компакт-диске позволяет сравнить скорость выполнения различных операций с DDB- и DIB-изображениями.
1.1.12. ANSI и Unicode
Windows поддерживает две кодировки: ANSI и Unicode. В кодировке ANSI (American National Standard Institute) каждый символ кодируется однобайтным кодом. Коды от 0 до 127 совпадают с кодами ASCII, коды от 128 до 255 могут означать разные символы различных языков в зависимости от выбранной кодовой страницы. Кодовые страницы позволяют уместить многочисленные символы различных языков в однобайтный код, но при этом можно работать только с одной кодовой страницей, т.е. с одним языком. Неверно выбранная кодовая страница приводит к появлению непонятных символов (в Интернете их обычно называют "кракозябрами") вместо осмысленного текста.
В кодировке Unicode используется 2 байта на символ, что даёт возможность закодировать 65 536 символов. Этого хватает для символов латиницы и кириллицы, греческого алфавита, китайских иероглифов, арабских и еврейских букв, а также многочисленных дополнительных (финансовых, математических и т. п.) символов. Кодовых страниц в Unicode нет.
Примечание
Кодовая страница русского языка в ANSI имеет номер 1251. Кодировка символов в ней отличается от принятой в DOS так называемой альтернативной кодировки. В целях совместимости для DOS-программ, а также для консольных приложений Windows использует альтернативную кодировку. Именно поэтому при выводе русского текста в консольных приложениях получаются те самые "кракозябры". Чтобы избежать этого, следует перекодировать символы из кодировки ANSI в DOS при выводе, и наоборот — при вводе. Это можно сделать с помощью функций CharToOem и OemToChar.
Windows NT/2000/XP поддерживает ANSI и Unicode в полном объеме. Это значит, что любая функция, работающая со строками, представлена в двух вариантах: для ANSI и для Unicode. Windows 9x/МЕ в полном объеме поддерживает только ANSI. Unicode-варианты в этих системах есть у относительно небольшого числа функций. Каждая страница MSDN, посвященная функции, работающей со строками (или со структурами, содержащими строки), в нижней части содержит надпись, показывающую, реализован ли Unicode-вариант этой функции только для NT/2000/XP или для всех платформ.
Примечание
В качестве примера рассмотрим функции вывода текста на экран. Unicode-версию на всех платформах имеют только две из них TextOut и ExtTextOut. Функции DrawText и DrawTextEx имеют Unicode-варианты только в Windows NT/2000/XP. Если же смотреть функции для работы с окнами, то среди них нет ни одной, которая имела бы Unicode-вариант в Windows 9х/МЕ. Следует отметить, что с относительно недавнего времени Microsoft предоставляет расширение для Windows 9x/МЕ которое позволяет добавить полную поддержку Unicode в эти системы. Это расширение называется MSLU (Microsoft Layer for Unicode), его можно скачать с официального сайта Microsoft.
Рассмотрим, как сосуществуют два варианта на примере функции RegisterWindowMessage. Согласно справке, она экспортируется библиотекой user32.dll. Однако если посмотреть список функций, экспортируемых этой библиотекой (это можно сделать, например, при помощи утилиты TDump.exe, входящей в состав Delphi), то там этой функции не будет, зато будут функции RegisterWindowMessageA и RegisterWindowMessageW. Первая из них — это ANSI-вариант функции, вторая — Unicode-вариант (буква W означает Wide — широкий; символы кодировки Unicode часто называются широкими из-за того, что на один символ приходится не один, а два байта).
Сначала разберемся с тем, как используются два варианта одной функции в Microsoft Visual C++. В стандартных заголовочных файлах учитывается наличие макроопределения UNICODE. Есть два символьных типа — CHAR для ANSI и WCHAR для Unicode. Если макрос UNICODE определен, тип TCHAR соответствует типу WCHAR, если не определен — типу CHAR (после этого производные от TCHAR типы, такие как LPCTSTR автоматически начинают соответствовать кодировке, определяемой наличием или отсутствием определения UNICODE). В заголовочных файлах импортируются оба варианта функции, а также определяется макрос RegisterWindowMessage. Его смысл также зависит от макроса UNICODE: если он определен, RegisterWindowMessage эквивалентен RegisterWindowMessageW, если не определен — RegisterWindowMessageA. Все функции, поддерживающие два варианта кодировки, импортируются точно так же. Таким образом, вставляя или убирая макроопределение UNICODE, можно, не меняя ни единого символа в программе, компилировать ее ANSI- или Unicode-версию.
Разработчики Delphi не стали полностью копировать этот механизм, видимо, этому помешала существующая в Delphi раздельная компиляция модулей, из-за которой невозможно определением одного символа заставить все модули перекомпилироваться (тем более что часть из них может не иметь исходных кодов). Поэтому в Delphi нет типа, аналогичного TCHAR.
Рассмотрим, как та же функция RegisterWindowMessage импортируется модулем Windows (листинг 1.19).
Листинг 1.19. Импорт функции RegisterWindowMessage
interface
...
function RegisterWindowMessage(lpString: PChar): UINT; stdcall;
function RegisterWindowMessageA(lpString: PAnsiChar): UINT;
stdcall; function RegisterWindowMessageW(lpString: PWideChar): UINT; stdcall;
...
implementation
...
function RegisterWindowMessage;
external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageA;
external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageW;
external user32 name 'RegisterWindowMessageW';
Видно, что функция RegisterWindowMessageA импортируется дважды: один раз под своим настоящим именем, а второй раз — под именем RegisterWindowMessage. Любое из этих имен подходит для вызова ANSI-варианта этой функции (напоминаю, что типы PChar и PAnsiChar эквивалентны). Чтобы вызвать Unicode-вариант функции, потребуется функция RegisterWindowMessageW.
Структуры, содержащие строковые данные, также имеют ANSI- и Unicode-вариант. Например, структура WNDCLASS в модуле Windows представлена типами TWndClassA (с синонимами WNDCLASSA и tagWNDCLASSA) и TWndClassW (с синонимами WNDCLASSW и tagWHDCLASSW). Тип TWndClass (и его синонимы WNDCLASS и tagWNDCLASS) эквивалентен типу TWndClassA. Соответственно, при вызове функций RegisterClassA и RegisterClassExA используется тип TWndClassA, при вызове RegisterClassW и RegisterClassExW — тип TWndClassW.
1.1.13. Строки в Windows API
Unicode в Delphi встречается редко, т.к. программы, использующие эту кодировку, не работают в Windows 9x/МЕ. Библиотека VCL также игнорирует возможность работы с Unicode, ограничиваясь ANSI. Поэтому далее мы будем говорить только об ANSI. С кодировкой Unicode можно работать аналогично, заменив PChar на PWideChar и string на WideString.
Для работы со строками в Delphi наиболее распространен тип AnsiString, обычно называемый просто string (более детально отношения между этими типами рассмотрены в главе 3). Переменная типа string является указателем на строку, хранящуюся в динамической памяти. Этот указатель указывает на первый символ строки. По отрицательному смещению хранится число символов в строке и счетчик ссылок, который позволяет избежать ненужных копирований строки, реализуя так называемое "копирование при необходимости". Если присвоить одной строковой переменной значение другой строковой переменной, то строка не копируется, а просто обе переменные начинают указывать на одну и ту же строку. Счетчик ссылок при этом увеличивается на единицу. Когда строка модифицируется, проверяется счетчик ссылок: если он не равен единице, то строка копируется, счетчик ссылок старой копии уменьшается на единицу, у новой копии счетчик ссылок будет равен единице, и переменная, которая меняется, будет указывать на новую копию. Таким образом, строка копируется только тогда, когда одна из ссылающихся на нее переменных начинает изменять эту строку, чтобы изменения не коснулись остальных переменных. При любых модификациях строки в ее конец автоматически добавляется нулевой символ (при подсчете длины строки с помощью функции Length он игнорируется). Но если присвоить строковой переменной пустую строку, то эта переменная станет нулевым указателем (nil), память для хранения одного символа #0 выделена не будет. При выходе строковой переменной из области видимости (т. е., например, при завершении процедуры, в которой она является локальной переменной, или при уничтожении объекта, полем которого она является) она автоматически финализируется, т.е. счетчик ссылок уменьшается на единицу и, если он оказывается равен нулю, память, выделенная для строки, освобождается. (О внутреннем устройстве AnsiString см. также разд. 3.3.)
Механизм выделения и освобождения памяти и подсчета ссылок прозрачен для программы. От разработчика требуется только не вмешиваться в его работу с помощью низкоуровневых операций с указателями, чтобы менеджер памяти не запутался.
Примечание
В отличие от string, тип WideString не имеет счетчика ссылок, и любое присваивание переменных этого типа приводит к копированию строки. Это сделано в целях совместимости с системным типом BSTR, использующимся в COM/DCOM и OLE.
Функции Windows API не поддерживают тип string. Они работают со строками, оканчивающимися на #0 (нуль-терминированные строки, null-terminated strings). Это означает, что строкой называется указатель на цепочку символов. Признаком конца такой цепочки является символ с кодом 0. Раньше для таких строк существовал термин ASCIIZ. ASCII — название кодировки, Z — zero. Сейчас кодировка ASCII в чистом виде не встречается, поэтому этот термин больше не применяется, хотя это те же самые по своей сути строки. Как уже говорилось, в Delphi ко всем строкам типа string неявно добавляется нулевой символ, не учитывающийся при подсчете числа символов. Это сделано для совместимости с нуль-терминированными строками. Однако эта совместимость ограничена.
Для работы с нуль-терминированными строками в Delphi обычно служит тип PChar. Формально это указатель на один символ типа Char, но подразумевается, что это только первый символ строки, и за ним следуют остальные символы. Где будут эти символы размещены и какими средствами для них будет выделена память, программист должен решить самостоятельно. Он же должен позаботиться о том, чтобы в конце цепочки символов стоял #0.
Строку, на которую указывает PChar, можно использовать везде, где требуется string — компилятор сам выполнит необходимые преобразования. Обратное неверно. Фактически, string — это указатель на начало строки, завершающейся нулем, т.е. тот самый указатель, который требуется при работе с PChar. Однако, как уже отмечалось, некорректные манипуляции с этим указателем могут привести к нежелательным эффектам, поэтому компилятор требует явного приведения переменных и выражений типа string к PChar. В свою очередь, программист должен ясно представлять, к каким последствиям это может привести.
Если посмотреть описание функций API, имеющих строковые параметры, в справке, можно заметить, что в некоторых случаях строковые параметры имеют тип LPCTSTR (как, например, у функции SetWindowText), а в некоторых — LPTSTR (GetWindowText). Ранее мы говорили, что появление префикса C после LP указывает на то, что это указатель на константу, т.е. то, на что указывает такой указатель, не может быть изменено. Тип LPCTSTR имеют те строковые параметры, содержимое которых функция только читает, но не модифицирует. С такими параметрами работать проще всего. Рассмотрим на примере функции SetWindowText, как можно работать с такими параметрами (листинг 1.20).
Листинг 1.20. Вызов функции с параметром типа LPCTSTR
{ Вариант 1 }
SetWindowText(Handle, 'Строка');
{ Вариант 2
S -переменная типа string }
SetWindowText(PChar(S));
{ Вариант 3
X - переменная типа Integer }
SetWindowText(PChar('Выполнено ' + IntToStr(X) + '%'));
В первом варианте компилятор размещает строковый литерал в сегменте кода, а в функцию передает указатель на эту область памяти. Поскольку функция не модифицирует строку, а только читает, передача такого указателя не вызывает проблем.
Во втором варианте функции передается указатель, хранящийся в переменной S. Такое приведение string к PChar безопасно, т.к. строка, на которую ссылается переменная S, не будет модифицироваться. Но здесь существует одна тонкость: конструкция PChar(S) — это не просто приведение типов, при ее использовании неявно вызывается функция _LStrToPChar. Как мы уже говорили, когда string хранит пустую строку, указатель просто имеет значение nil. Функция _LStrToPChar проверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещен в сегменте кода. Поэтому даже если содержит пустую строку, в функцию будет передан ненулевой указатель.
Вычисление строковых выражений требует перераспределения памяти, а это компилятор делает только с выражениями типа string. Поэтому результат выражения, приведенного в третьем варианте, также имеет тип string. Но его можно привести к PChar. Память для хранения результата выражения выделяется динамически, как и для обычных переменных типа string. Чтобы передать указатель на но выражение в функцию, следует привести его к PChar. В эпилог процедуры, вызывающей функцию SetWindowText или иную функцию с подобным аргументом, добавляется код, который освобождает динамически сформированную строку, поэтому утечек памяти не происходит. Разумеется, существуют и другие способы формирования параметра типа LPCTSTR, кроме предложенных здесь. Можно, например, выделить память для нуль-терминированной строки с помощью StrNew или родственной ей функции из модуля SysUtils. Можно использовать массив типа Char. Можно выделять память какими-либо другими способами. Но предложенные здесь три варианта в большинстве случаев наиболее удобны.
Параметры типа LPTSTR применяются в тех случаях, когда функция может не только читать, но и модифицировать передаваемое ей значение. В большинстве случаев такие параметры чисто выходные, т.е. функция не интересуется, какое значение имел параметр при вызове, используя его только для возврата значения. При возврате строкового значения всегда возникает проблема: где, кем и как будет выделена память, в которую будет записана строка? Функции Windows API, за очень редким исключением, решают эту проблему следующим образом: память должна выделить вызывающая программа, а в функцию передается указатель на этот заранее выделенный блок. Сама функция только копирует строку в этот блок.
Таким образом, перед программой встает задача узнать, какой объем памяти следует выделить под возвращаемую строку. Здесь API не предлагает универсального решения, разные функции по-разному решают эту проблему. Например, при получении заголовка окна с помощью GetWindowText размер этого заголовка можно узнать, вызвав предварительно GetWindowTextLength. Функции типа GetCurrentDirectory возвращают длину строки. Если при первом вызове этой функции памяти выделено недостаточно, можно увеличить буфер и вызвать функцию еще раз. И наконец, есть функции типа SHGetSpecialFolderPath, в описании которых написано, каков минимальный размер буфера, необходимый для гарантированной передачи полной строки этой функцией (это, разумеется, возможно только в том случае, когда размер возвращаемой строки имеет какое-то естественное ограничение). Следует также отметить, что большинство API-функций, возвращающих строки, в качестве одного из параметров принимают размер буфера, чтобы не скопировать больше байтов, чем буфер может принять.
Выделять буфер для получения строки можно многими способами. На практике удобнее всего статические массивы, тип string или динамическое выделение памяти для нуль-терминированных строк.
Статические массивы могут использоваться, если размер буфера известен на этапе компиляции. Массивы типа Char с начальным индексом 0 рассматриваются компилятором как нуль-терминированные строки, поэтому с ними удобно выполнять дальнейшие операции. Этот способ удобен тем, что не нужно заботиться о выделении и освобождении памяти, поэтому он часто применяется там, где формально длина строки на этапе неизвестна, но "исходя из здравого смысла" можно сделать вывод, что в подавляющем большинстве случаев эта длина не превысит некоторой величины, которая и берется в качестве размера массива.
Строки типа string также могут служить буфером для получения строковых значений от системы. Для этого нужно предварительно установить требуемую длину строки с помощью SetLength, а затем передать указатель на начало строки в функцию API. Здесь следует соблюдать осторожность: если длина строки окажется равной нулю, переменная типа string будет иметь значение nil, а система попытается записать по этому указателю пустую строку, состоящую из единственного символа #0. Это приведет к ошибке Access violation.
Третий способ — выделение памяти для буфера с помощью StrAlloc или аналогичной ей функции. Память, выделенную таким образом, следует обязательно освобождать с помощью StrDispose. При этом крайне желательно использовать конструкцию try/finally, чтобы возникновение исключений не привело к утечкам памяти.
Все три способа получения строковых данных от функций Windows API показаны в примере EnumWnd, находящемся на прилагаемом компакт-диске.