Каждое окно в Windows привязывается к какому-либо модулю (в Windows 9х/МЕ необходимо явно указать дескриптор этого модуля. NT 2000 ХР определяет модуль, из которого вызвана функция создания окна, автоматически). Соответственно, оконные классы делятся на локальные и глобальные: окна локальных классов может создавать только тот модуль, в котором находится оконная процедура класса, глобальных - любой модуль данного приложения. Будет ли класс локальным или глобальным, зависит от значений полей TWndClassEx при регистрации класса.
Оконный класс, к которому принадлежит окно, указывается при его создании. Это может быть зарегистрированный ранее класс или один из системных классов. Системные классы - это 'BUTTON', 'COMBOBOX', 'EDIT', 'LISTBOX', 'MDICLIENT', 'SCROLLBAR' и 'STATIC'. Назначение этих классов понятно из их названий (класс 'STATIC' реализует статические текстовые или графические элементы, т.е. не реагирующие на мышь и клавиатуру, но имеющие дескриптор). Кроме этих классов существуют также классы из библиотеки ComCtl32.dll, они тоже доступны всем приложениям без предварительной регистрации (подробнее об этих классах можно узнать в MSDN в разделе Common Controls Reference).
Для окон в обычном понимании этого слова готовых классов не существует, их приходится регистрировать самостоятельно. В частности, VCL для форм регистрирует оконные классы, имена которых совпадают с именами соответствующих классов VCL.
Кроме имени, класс включает в себя другие параметры, такие как стиль, кисть и т.д. Они подробно перечислены в справке.
Для создания окна служат функции CreateWindow и CreateWindowEx. При создании окна в числе других параметров задается модуль, к которому оно привязано, имя оконного класса, стиль и расширенный стиль. Последние два параметра определяют поведение конкретного окна и не имеют ничего общего со стилем класса. Результат работы этих функций - дескриптор созданного ими окна.
Еще один важный параметр этих функций - дескриптор родительского окна. Окно является подчиненным по отношению к своему родителю. Например, если дочернее окно - это кнопка или иной элемент управления, то визуально оно располагается в другом окне, которое является для нею родительским. Если дочернее окно - это MDIChild, то родительским для него будет MDIForm (если быть до конца точным, то не сама форма MDIForm, а специальное окно класса MDICLIENT, которое является дочерним по отношению к MDIForm; дескриптор этого окна хранится в свойстве ClientHandle главной формы). Другими словами, отношения "родительское - дочернее окно" отражают принадлежность одного окна другому, визуальную связь между ними. Окна, родитель которых не задан (т.е. в качестве дескриптора родителя передан ноль), располагаются непосредственно на рабочем столе. Если при создании окна задан стиль WS_CHILD, то его координаты отсчитываются от левого верхнего угла клиентской области родительского окна, и при перемещении родительского окна все дочерние окна будут перемещаться вместе с ним. Окно, имеющее стиль WS_CHILD, не может располагаться ни рабочем столе, попытка создать такое окно окончится неудачей. Визуальные компоненты VCL имеют два свойства, которые иногда путают: Owner и Parent. Свойство Parent указывает на объект, реализующий окно, являющееся родительским для данного визуального компонента (компоненты, не являющиеся наследником TWinControl, также имеют это свойство - VCL для них имитирует эту взаимосвязь, однако сами они не могут быть родителями других визуальных компонентов). Свойство Owner указывает на владельца компонента. Отношения "владелец-принадлежащий" реализуются полностью внутри VCL. Свойство Owner есть у любого наследника TComponent, в том числе и у невизуальных компонентов, и владельцем других компонентов также может быть невизуальный компонент (например, TDataModule). При уничтожении компонента он автоматически уничтожает все компоненты, владельцем которых он является (здесь, впрочем, есть некоторое дублирование функций, т.к. оконный компонент также при уничтожении уничтожает все визуальные компоненты, родителем которых он является). Еще владелец отвечает за загрузку всех установленных во время разработки свойств принадлежащих ему компонентов.
Свойство Owner доступно только для чтения. Владелец компонента задается один раз при вызове конструктора и остается неизменным на протяжении всего жизненного цикла компонента (за исключением достаточно редких случаев явного вызова методов InsertComponent и RemoveComponent). Свойство Parent задается отдельно и может быть впоследствии изменено (визуально это будет выглядеть как "перепрыгивание" компонента из одного окна в другое).
Визуальный компонент может не иметь владельца. Это означает, что ответственность за его удаление лежит на программисте, создавшем его. Но большинство визуальных компонентов не может функционировать, если свойство Parent не задано. Например, невозможно отобразить на экране компонент TButton, у которого не установлено свойство Parent. Это связано с тем, что большинство оконных компонентов имеет стиль WS_CHILD, который, напомним. не позволяет разместить окно на рабочем столе. Окнами без родителя могут быть только наследники TCustomForm.
Впрочем, сделать кнопку, не имеющую родителя, можно средствами Windows API. Например, такой командой (листинг 1.2).
Листинг 1.2. Создание кнопки, не имеющей родительского окна
CreateWindow('BUTTON', 'Test', WS_VISIBLE or BS_PUSHBUTTON or WS_POPUP, 10, 10, 100, 50, 0, 0, HInstance, nil);
Рекомендуем в этом примере убрать стиль WS_POPUP и посмотреть, что получится - эффект достаточно забавный. Отметим, что создавать такие висящие сами по себе кнопки смысла нет, поскольку сообщения о событиях, происходящих со стандартными элементами управления, получает родительское окно, и при его отсутствии программа не может отреагировать, например, на нажатие кнопки.
Кроме обычного конструктора Create, у класса TWinControl есть конструктор CreateParented, позволяющий создавать оконные компоненты, родителями которых являются окна, созданные без использования VCL. В качестве параметра этому конструктору передается дескриптор родительского окна. У компонентов, созданных таким образом, не нужно устанавливать свойство Parent.
Примечание
Путаницу между понятием родителя и владельца усиливает то, что в MSDN по отношению к окнам тоже используются термины owner и owned (принадлежащий), однако это не имеет никакого отношения к владельцу в понимании VCL. Если окно имеет стиль WS_CHILD, то оно обязано иметь родителя, но не может иметь владельца. Если такого стиля у окна нет, оно не может иметь родителя, но может (хотя и не обязано) иметь владельца. Владельцем в этом случае становится то окно, чей дескриптор передан в качестве родительского, т.е. родитель и владелец в терминах системы - это один и тот же параметр, который по-разному интерпретируется в зависимости от стиля самого окна. Окно, имеющее владельца, уничтожается при уничтожении владельца, прячется при его минимизации и всегда находится над владельцем. Окно, имеющее стиль WS_CHILD, может быть родителем, но не может быть владельцем другого окна; если передать дескриптор такого окна в качестве владельца, то реальным владельцем станет родитель дочернего окна. Чтобы не путать владельца в терминах VCL и в терминах системы, мы в дальнейшем всегда будем оговаривать, в каком смысле будет упомянуто слово "владелец".
Создание окон через Windows API требует кропотливой работы. VCL справляется с этой задачей замечательно, поэтому создавать окна самостоятельно приходится только тогда, когда использование VCL нежелательно, например, если необходимо написать как можно более компактное приложение. Во всех остальных случаях приходится только слегка подправлять работу VCL. Например, с помощью Windows API можно изменить форму окна или убрать из нею заголовок, оставив рамку. Подобные действия не требуют от программиста создания нового окна, можно воспользоваться тем, что уже создано VCL.
Другой случай, когда могут понадобиться функции Windows API для окон, - если приложение должно что-то делать с чужими окнами. Например, хотя бы просто перечислить все окна, открытые в данный момент, как это делает входящая в состав Delphi утилита WinSight32. Но в этом случае также не приходится самому создавать окна, работа идет с уже имеющимися.
1.1.5. Функции обратного вызова
Прежде чем двигаться дальше, необходимо разобраться с тем, что такое функции обратного вызова (callback functions: этот термин иногда также переводят "функции косвенного вызова"). Эти функции в программе описываются, но обычно не вызываются напрямую, хотя ничто не запрещает сделать это. В этом они похожи на те методы класса, которые связаны с событиями.
Ничто не мешает вызывать напрямую, например, метод FormCreate, но делать это приходится крайне редко. С другой стороны, даже если этот метод не вызывается явно, он все равно выполняется, потому что VCL автоматически вызывает его без прямого указания программиста. Еще одно общее свойство - конкретное имя метода при косвенном вызове не важно. Можно изменить его, но если этот метод по-прежнему будет связан с событием OnCreate, он так же будет успешно вызываться. Разница заключается только в том, что такие методы вызываются внутренними механизмами VCL, а функции обратного вызова - самой системой Windows. Соответственно, на эти функции налагаются следующие требования: во-первых, они должны быть именно функциями, а не методами класса; во-вторых, они должны быть написаны в соответствии с моделью вызова stdcall (MSDN предлагает использовать модель callback, которая в имеющихся версиях Windows является синонимом stdcall). Что же касается того, как программист сообщает системе о том, что он написал функцию обратного вызова, то это в каждом случае будет по-своему.
В качестве примера рассмотрим перечисление окон с помощью функции EnumWindows. В справке она описана так:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
Соответственно, в Windows.pas она имеет вид
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
Параметр lpEnumFunc должен содержать указатель на функцию обратного вызова. Прототип этой функции описан так:
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
Функции с таким именем в Windows API не существует. Это так называемый прототип функции, согласно которому следует описывать функцию обратного вызова. На самом деле этот прототип предоставляет большую свободу, чем это может показаться на первый взгляд. Во-первых, имя может быть любым. Во-вторых, система не накладывает строгих ограничений на имена и типы параметров - они могут быть любыми, при условии, что новые типы совпадают по размерам с теми, которые указываются (тип TFNWndEnumProc, описанный в модуле Windows - это не процедурный тип, а просто нетипизированный указатель, поэтому компилятор Delphi не будет контролировать соответствие передаваемой функции обратного вызова ее прототипу). Что касается типа функции и типа первого параметра, то они имеют определенный смысл, и изменение их типа вряд ли может быть полезным. Но второй параметр предназначен специально для передачи значения, которое программист волен использовать но своему усмотрению, система просто передает через него в функцию обратного вызова то значение, которое имел параметр lParam при вызове функции EnumWindows. А программисту может показаться удобнее работать не с типом lParam (т.е. LongInt), а, например, с указателем или же с массивом из четырех байтов. Лишь бы были именно четыре байта, а не восемь, шестнадцать или еще какое-то число. Можно даже превратить этот параметр в параметр-переменную, т.к. при этом функции будут передаваться все те же четыре байта - адрес переменной. Впрочем, тем, кто не очень хорошо разбирается с тем, как используется стек для передачи параметров при различных моделях вызова, лучше не экспериментировать с изменением типа параметра, а строго следовать заявленному прототипу, при необходимости выполняя требуемые преобразования внутри функции обратного вызова.
Функция EnumWindows работает так: после вызова она начинает по очереди перебирать все имеющиеся в данный момент окна верхнего уровня, т.е. те, у которых нет родителя. Для каждого такого окна вызывается заданная функция обратного вызова, в качестве первого параметра ей передается дескриптор данного окна (каждый раз, естественно, новый), в качестве второго - то, что было передано самой функции EnumWindows в качестве второго параметра (каждый раз одно и то же). Получая по очереди дескрипторы всех окон верхнего уровня, функция обратного вызова может выполнить с каждым из них определенное действие (закрыть, минимизировать и т.п.). Или можно проверять все эти окна на соответствие какому-то условию, пытаясь найти нужное. А значение, возвращаемое функцией обратного вызова, влияет на работу EnumWindows. Если она возвращает False, значит, все, что нужно, уже сделано, можно не перебирать остальные окна.
Окончательный код для того случая, когда второй параметр имеет тип Pointer, иллюстрирует листинг 1.3.
Листинг 1.3. Вызов функции EnumWindows с функцией обратного вызова
function MyCallbackFunction(Wnd: HWND; Р: Pointer): BOOL; stdcall;
begin
{ что-то делаем}
end;
...............
var
MyPointer: Pointer;
...............
EnumWindows(@MyCallbackFunction, LongInt(MyPointer));
Что бы мы ни делали с типом второго параметра функции обратного вызова, тип соответствующего параметра EnumWindows не меняется. Поэтому необходимо явное приведение передаваемого параметра к типу LongInt. Обратное преобразование типов при вызове MyCallbackFunction, осуществляется автоматически.
Использование EnumWindows и функций обратного вызова демонстрируется примером EnumWnd.
Отметим, что функции обратного вызова будут вызываться до того, как завершит работу функция EnumWindows. Однако это не является распараллеливанием работы. Чтобы проиллюстрировать это, рассмотрим ситуацию, когда программа вызывает некоторую функцию А, которая, в свою очередь, вызывает функцию В. Функция В, очевидно, начнет свою работу до того, как завершит работу функция А. То же самое произойдет и с функцией обратного вызова, переданной в EnumWindows: она будет вызываться из кода EnumWindows так же, как и функция В из кода функции А. Поэтому код функции обратного вызова получит управление (и не один раз, т.к. EnumWindows будет вызывать эту функцию в цикле) до завершения работы EnumWindows.
Однако это правило действует не во всех ситуациях. В некоторых случаях система запоминает адрес переданной ей функции обратного вызова, чтобы использовать ее потом. Примером такой функции является оконная процедура: ее адрес передается системе один раз при регистрации класса, и затем система многократно вызывает эту функцию при необходимости.
В 16-разрядных версиях Windows вызов функций обратного вызова осложнялся тем, что для них необходим был специальный код. называемый прологом. Пролог создавался с помощью функции MakeProcInstance, удалялся после завершения с помощью FreeProcInstance. Таким образом, вызов EnumWindows должен был бы выглядеть так. как показано в листинге 1.4.
Листинг 1.4. Вызов функции EnumWindows в 16-разрядных версиях Windows
var
MyProcInstanсe: TFarProc;
...............
MyProcInstance := MakeProcInstance(@MyCallBackFunction, HInstance);
EnumWindows(MyProcInstance, LongInt(MyPointer));
FreeProcInstance(MyProcInstance);
В Delphi этот код будет работоспособным, т.к. для совместимости MakeProcInstance и FreeProcInstance оставлены. Но они ничего не делают (в чем легко убедиться, просмотрев исходный файл Windows.pas), поэтому можно обойтись и без них. Тем не менее эти функции иногда до сих пор используются, видимо, просто в силу привычки. Другой способ, с помощью которого и 16-разрядных версиях можно сделать пролог - описать функцию с директивой export. Эта директива сохранена для совместимости и в Delphi, но в 32-разрядных версиях она также ничего не делает (несмотря на то, что справка, например, по Delphi 3 утверждает обратное; в справке по Delphi 4 этой ошибки уже нет).