Перечисленные функции при необходимости вставляются в петлю сообщений. Листинг 1.6 показывает, как будет выглядеть петля сообщений, содержащая вызов TranslateAccelerator для родительской MDI-формы и TranslateMDISysAccel для дочерней.
Листинг 1.6. Петля сообщении с обработкой "горячих" клавиш главного меню и системного меню MDI-окон
while GetMessage(Msg, 0, 0, 0) do
if not TranslateMDISysAccel(ActiveMDIChildHandle, Msg)
and not TranslateAccelerator(MDIFormHandle, AccHandle, Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
При отправке сообщения, в отличие от посылки, оно не ставится в очередь, а передается оконной процедуре напрямую. Отправить сообщение можно, например, с помощью функции SendMessage. Если эта функция вызывается из той же нити, которой принадлежит окно-адресат, то фактически это эквивалентно прямому вызову оконной процедуры. Если окно принадлежит другой нити, данное сообщение становится в отдельную очередь, имеющую более высокий приоритет, чем очередь для посланных сообщений. Функции GetMessage и PeekMessage сначала выбирают все сообщения из этой очереди и отправляют их на обработку, и лишь затем приступают к анализу очереди посланных сообщений.
Примечание
Поскольку сообщения, отправленные окну, передаются оконной процедуре напрямую либо диспетчеризуются внутри GetMessage или PeekMessage, то эти сообщения не попадают в функции TranslateMDISysAccel, TranslateAccelerator и TranslateMessage. Это необходимо учитывать при передаче окну сообщений, эмулирующих нажатие клавиш на клавиатуре. Такие сообщения окну нужно посылать, а не отправлять, чтобы они прошли полный цикл обработки и окно правильно на них отреагировало. Для эмуляции сообщений от клавиатуры можно также воспользоваться функцией keybd_event, но она посылает сообщение не указанному окну, а активному, что не всегда удобно.
Диалоговые окна обрабатывают сообщения по-особому. Эти окна делятся на модальные (создаются и показываются с помощью функций DialogBoxXXXX) немодальные (создаются с помощью функций CreateDialogXXXX и затем показываются с помощью функции ShowWindow, использующейся и для обычных, не диалоговых, окон). И модальные, и немодальные окна создаются на основ шаблона, который может храниться в ресурсах приложения или в памяти. В шаблоне можно явно указать имя созданного вами оконного класса диалогового окна или (как это обычно бывает) не указывать его вообще, чтобы был выбран класс, предоставляемый системой для диалоговых окон по умолчанию. Оконная процедура диалогового класса должна передавать необработанные сообщения функции DefDlgProc.
Все диалоговые окна имеют так называемую диалоговую процедуру - функцию, указатель на которую передается в качестве одного из параметров функциям DialogВохХХХХ и CreateDialogXXXX. Прототипы диалоговой и оконной процедур совпадают. Функция DefDlgProc начинаем свою работу с того, что вызывает диалоговую процедуру. Если та не обработала переданное ей сообщение (о чем сигнализирует возвращаемое нулевое значение), функция DefDlgProc обрабатывает его сама. Таким образом, с помощью одного оконного класса и одной оконной процедуры можно реализовывать различные диалоговые окна, используя разные диалоговые процедуры.
Функции DialogВохХХХХ создают диалоговое окно и сразу же показывают его в модальном режиме. Данные функции завершают свое выполнение только тогда, когда модальное окно будет закрыто. Внутри модальных функций организуется собственная петля сообщений. Все прочие окна на время показа модального диалога запрещаются (как если бы для них была вызвана функция EnableWindow с параметром FALSE), т.е. перестают реагировать на сообщения от мыши и клавиатуры. При этом они сохраняют способность реагировать на другие сообщения, благодаря чему могут, например, обновлять свое содержимое по таймеру (в справке написано, что ничто не мешает программисту вставить в диалоговую процедуру вызов функций, разрешающих запрещенные системой окна, но при этом теряется смысл модальных диалогов). Если в очереди нет сообщений, модальная петля посылает родительскому окну диалога сообщение WM_ENTERIDLE, обработка которого позволяет этому окну выполнять фоновые действия. Разумеется, что обработчик WM_ENTERIDLE не должен выполняться слишком долго, иначе модальное окно зависнет. Обычно окно использует оконную процедуру, которая задана при создании соответствующего оконного класса. Однако допускается создание так называемых подклассов - переопределение оконной процедуры после того, как окно создано. Это переопределение касается только заданного окна и не оказывает влияния на остальные окна, принадлежащие данному оконному классу. Осуществляется оно с помощью функции SetWindowLong с параметром GWL_WNDPROC (другие значения этого параметра позволяют менять другие свойства окна, такие как стиль и расширенный сталь). Изменять оконную процедуру можно только у окон, созданных самим процессом.
Новая оконная процедура, которая устанавливается при создании подкласса, все необработанные сообщения должна передавать не функции DefWindowProc, а той оконной процедуре, которая была установлена ранее. SetWindowLong при изменении оконной процедуры возвращает дескриптор старой процедуры (этот же дескриптор можно получить, заранее вызвав функцию GetWindowLong с аргументом GWL_WINDOWPROC). Обычно значение дескриптора численно совпадает с адресом старой оконной процедуры, поэтому в некоторых источниках можно встретить рекомендации использовать этот дескриптор непосредственно как указатель процедурного типа. И это даже будет работать для оконных классов, созданных самой программой. Но безопаснее все же вызов старой оконной процедуры реализовать с помощью системной функции CallWindowProc, предоставив ей "разбираться", является ли дескриптор указателем.
В качестве примера рассмотрим создание подкласса для некоторого окна, дескриптор которого содержится в переменной Wnd. Пусть нам потребовалось для этого окна нестандартным образом обрабатывать сообщение WM_KILLFOCUS.
Тогда код новой оконной процедуры и код ее установки будет выглядеть так, как показано в листинге 1.7.
Листинг 1.7. Создание подкласса для особой обработки сообщения WM_KILLPFOCUS
var
OldWndProc: TFNWndProc;
function NewWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM) : LRESULT; stdcall;
begin
if Msg = WM_KILLFOCUS then
// Обработка события
else
Result := CallWindowProc(OldWndProc, hWnd, Msg, wParam, lParam);
end;
...
// Установка новой оконной процедуры окну Wnd
OldWndProc := TFNWndProc(SetWindowLong(Wnd, GWL_WNDPROC, Longint(@NewWindowProc)));
...
Примечание
MSDN называет функции GetWindowLong и SetWindowLong устаревшими и рекомендует использовать вместо них GetWindowLongPtr и SetWindowLongPtr, совместимые с 64-разрядными версиями Windows. Однако до 2007-й версии Delphi включительно эти функции отсутствуют в модуле Windows, и при необходимости их следует импортировать самостоятельно.
Переопределять оконную процедуру с помощью SetWindowLong можно и у тех окон, оконная процедура которых была переопределена ранее. Таким образом создаются цепочки оконных процедур, каждая из которых вызывает предыдущую.
1.1.7. Создание окон средствами VCL
Теперь поговорим о том, как в VCL создаются окна. Речь здесь будет идти не о написании кода для создания окна с помощью VCL (предполагается, что читатель это и так знает), а о том, какие функции API и в какой момент вызывает VCL при создании окна.
Если смотреть код методов класса TWinControl, которые вызываются при создании и отображении окна, то найти там то место, когда окно создается, удается не сразу. На первый взгляд все выглядит так, будто этот код вообще не имеет отношения к созданию окна, как будто оно создается где-то совсем в другом месте, а TWinControl получает уже готовый дескриптор. На самом деле окно создает, конечно же, сам TWinControl, а спрятано его создание в свойстве Handle. Метод GetHandle, который возвращает значение свойства Handle, выглядит следующим образом (листинг 1.8).
Листинг 1.8. Реализация метода TWinControl.GetHandle
procedure TWinControl.HandleNeeded;
begin
if FHandle = 0 then
begin
if Parent <> nil then Parent.HandleNeeded;
CreateHandle;
end;
end;
function TWinControl.GetHandle: HWnd;
begin
HandleNeeded;
Result := FHandle;
end;
При каждом обращении к свойству Handle вызывается метод HandleNeeded, который проверяет, создано ли уже окно, и если нет, создает его, попутно создавая, при необходимости, родительское окно. Таким образом, окно создается при первом обращении к свойству Handle.
Метод CreateHandle, который вызывается из HandleNeeded, выполняет непосредственно лишь несколько вспомогательных операций, а для создания окна вызывает еще один метод - CreateWnd (листинг 1.9).
Листинг 1.9. Реализация метода CreateWnd
procedure TWndControl.CreateWnd;
var
Params: TCreateParams;
TempClass: TWndClass;
ClassRegistered: Boolean;
begin
CreateParams(Params);
with Params do
begin
if (WndParent = 0) end (Style and WS_CHILD <> 0) then
if (Owner <> nil) end (csReading in Owner.ComponentState) and (Owner is TWinControl) then
WndParent TWinControl(Owner).Handle
else
raise EInvalidOperation.CreateFmt(SParentRequired, [Name]);
FDefWndProc := WindowClass.lpfnWndProc;
ClassRegistered := GetClassInfo(WindowClass.hInstance, WinClassName, TempClass);
if not ClassRegistered or (TempClass.lpfnWndProc <> @InitWndProc) then
begin
if (ClassRegistered then
Windows.UnregisterClass(WinClassName, WindowClass.hInstance);
WindowClass.lpfnWndProc := InitWndProc;
WindowClass.lpszClassName := WinClassName;
if Windows.RegisterClass(WindowClass) = 0 then RaiseLastOSError;
end;
CreationControl := Self;
CreateWindowHandle(Params);
if FHandle = 0 then RaiseLastOSError;
if (GetWindowLong(FHandle, GWL_STYLE) and WS_CHILD <> 0) and (GetWindowLong(FHandle, GWL_ID) = 0) then
SetWindowLong(FHandle, GWL_ID, FHandle);
end;
StrDispose(FText);
FText := nil;
UpdateBounds;
Perform(WM_SETFONT, FFont.Handle, 1);
if AutoSize then AdjustSize;
end;
Собственно создание окна опять происходит не здесь, а в методе CreateWindowHandle, который очень прост: он состоит из одного только вызова API-функции CreateWindowEx с параметрами, значения которых берутся из полей записи Params типа TCreateParams (листинг 1.10)
Листинг 1.10. Запись TCreateParams
TCreateParams = record
Caption: PChar;
Style: WORD;
ExStyle: DWORD;
X, Y: Integer;
Width, Height: Integer;
WndParent: HWnd;
Param: Pointer;
WindowClass: TWndClass;
WinClassName: array[0..63] of Char;
end;
В записи Params хранятся параметры как окна, передаваемые в функцию WindowCreateEx, так и оконного класса (поля WindowClass и WndClassName). Все поля инициализируются методом CreateParams на основе значений свойств оконного компонента. Данный метод виртуальный и может быть перекрыт в наследниках, что бывает полезно, когда необходимо изменить стиль создаваемого окна. Например, добавив расширенный стиль WS_EX_CLIENTEDGE (или, как вариант, WS_EX_STATICEDGE), можно получить окно с необычной рамкой (листинг 1.11).