Все права на текст принадлежат автору: А Б Григорьев.
Это короткий фрагмент для ознакомления с книгой.
О чём не пишут в книгах по DelphiА Б Григорьев

А.Б. Григорьев О чём не пишут в книгах по Delphi

Благодарности
□ Елене Филипповой, создательнице и бессменному главному администратору сайта "Королевство Delphi".

□ Алексею Ковязину (российское отделение CodeGear) за помощь в выпуске этой книги.

□ Юрию Зотову, натолкнувшему меня на идею четвертой главы и некоторых разделов первой и третьей глав.

□ Многочисленным посетителям сайта "Королевство Delphi", при общении с которыми выяснялись интересные факты и рождались идеи, нашедшие себе место в этой книге.

□ Товарищу Witchking за сканирование

□ Товарищу DarkArt за подготовку электронного варианта 

Введение

Среда программирования Delphi заслуженно приобрела свою популярность. Этот удобный и красивый инструмент, основанный на не менее красивом языке Паскаль, первым избавил программиста от рутинных операций, сохранив при этом гибкость, присущую универсальным языкам программирования. Не удивительно, что об этой среде написано много книг, а в Интернете можно найти множество различных сайтов, посвященных Delphi.

Автор данной книги с 1999 года является постоянным посетителем (а с 2004 года — еще и модератором) сайта "Королевство Delphi" (www.delphikingdom.ru; подробнее этот сайт описан в Приложении 1), одного из самых известных русскоязычных ресурсов по Delphi. За это время изучено, какие вопросы интересуют посетителей сайта и какие ответы они хотели бы получить. Наблюдения показывают, что существует целый ряд тем, традиционно вызывающих большой интерес, но информацию по которым найти сложно. Авторы книг по Delphi стараются как можно быстрее перейти к описанию возможностей различных библиотек, оставляя в стороне другие важные вопросы.

"За бортом" остается множество тем, касающихся более низкоуровневых средств, которые в большинстве своем неплохо документированы и описаны в книгах, проиллюстрированы готовыми примерами, которые, правда, обычно даются на C++. Конечно, немного опыта — и код переведен с C++ на Delphi. Только обидно программировать в Delphi, никак не задействуя ее высокоуровневые библиотеки — хочется как-то "сшить" гибкость низкоуровневых вещей и удобство библиотек, использовав библиотеки везде, где это возможно, а низкоуровневый код — только там, где без него никак не обойтись. И вот как раз по этим вопросам и наблюдается острая нехватка информации.

Природа не терпит пустоты, поэтому в Интернете время от времени появляются рекомендации по поводу того, как же все-таки "впрячь в одну упряжку" библиотеки и низкоуровневый код. Но эти советы, за редким исключением, страдают тем, что дают готовое решение с минимальными объяснениями, почему надо делать это именно так (иногда авторы этих рекомендаций при всем желании не смогли бы дать такое объяснение, т.к. сами дошли до этого "методом тыка"). Так что такие советы могут оказаться очень полезными в конкретной ситуации, но не дают общего понимания проблемы.

Эта книга призвана заполнить информационный вакуум по некоторым из таких вопросов. Но самое главное — это то, что мы здесь принципиально будем избегать изложения в стиле "делай так, и будет тебе счастье", т.е. уклон будет не в сторону готовых рецептов, а в сторону объяснения, как это все устроено, чтобы читатель потом сам был в состоянии искать решения для своих проблем. Для этого мы будем разбирать стандартные средства Delphi с позиции "а как оно все работает".

Первая глава книги посвящена интеграции библиотеки VCL и Windows API, прежде всего той части Windows API, которая управляет окнами на экране. Не секрет, что в VCL отсутствуют многие возможности по управлению окнами, которые предоставляет операционная система Windows, но их можно задействовать и в VCL-приложении, если знать, как и куда можно вставить код, использующий API, так, чтобы это не нарушило работу VCL. В первой главе даются базовые сведения о Windows API и о том, как VCL использует API. Здесь приведен ряд примеров программ различной степени сложности, которые демонстрируют методы применения Windows API совместно с VCL в различных ситуациях. Особое внимание уделяется тому, как использовать Windows API, не нарушая работу VCL.

Вторая глава посвящена применению сокетов в Delphi. Сокеты — это самые низкоуровневые сетевые средства Windows, своего рода ассемблер сетевого программирования. Они хорошо документированы, но из-за обилия возможностей эта документация оказывается практически "неподъемной" для человека, который только-только начал знакомится с сокетами и которому не нужны все эти возможности, а требуется просто научиться передавать данные с помощью TCP/IP. Книг по сокетам очень мало, а по использованию сокетов в Delphi — вообще ни одной. Между тем сокеты в Delphi имеют свою специфику из-за отличий языка С, на который ориентирована библиотека сокетов, и Delphi: макросы заменены функциями, изменены параметры некоторых функций, определения типов приспособлены к возможностям Delphi. Кроме того, стандартный модуль Delphi для импорта функций из библиотеки сокетов импортирует библиотеку не полностью и содержит некоторые неточности. Все это делает освоение сокетов "с нуля" очень сложным делом. Вторая глава книги ориентирована на человека, который не имеет опыта сетевого программирования и не знаком с терминологией. Даются все необходимые определения и пояснения, чтобы можно было полностью понять, как работают примеры, но при этом читатель не перегружается избыточной на данном этапе информацией. Попутно излагаются особенности работы с сокетами, присущие именно Delphi. После прочтения данной главы читатель получает достаточно полное представление о протоколах стека TCP/IP и об основных режимах работы сокетов, и после этого способен дальше читать документацию самостоятельно.

Третья глава посвящена ситуациям, в которых стандартные средства ведут себя не так, как этого ожидает программист. Иногда это объясняется ошибками в реализации этих средств, но чаще тем, что программист просто не знает некоторых особенностей данной реализации. Конечно, детально изучив документацию, можно не только дать объяснение ошибкам, но и научиться предсказывать, где они могут возникнуть, но такой метод имеет очевидные сложности. В третьей главе выбран другой подход — "от ошибки". Мы сначала будем рассматривать ситуацию, в которой возникает ошибка, а потом объяснять, какие особенности реализации приводят к этому. Такой порядок изложения позволяет одновременно и рассказать о том, как соответствующие средства реализованы, и предостеречь от неверного их использования.

Четвертая глава целиком посвящена одному очень популярному в форумах вопросу: как вычислить арифметическое выражение, которое становится известным только во время выполнения программы (т. е., например, у нас есть арифметическое выражение в строковой переменной, а требуется вычислить его значение). С одной стороны, в Интернете можно найти немало самодельных библиотек, решающих эту задачу. Но не все из них работаю достаточно корректно, потому что их авторы зачастую реализуют доморощенные методы анализа выражений, дающие в некоторых случаях сбои. С другой стороны, существует литература, в которой довольно полно изложена формальная теория создания трансляторов. Но эта теория выходит далеко за рамки простого вычисления арифметических выражений и требует соответствующих усилий для ее освоения. Мы же, с одной стороны, ограничимся только той частью теоретических сведений, которая необходима для построения вычислителя, но с другой, — будем этой теории строго следовать, создавая действительно работоспособное решение. И хотя на выходе мы получим несколько вполне работоспособных примеров, не они будут нашей главной целью. Мы будем стремиться к тому, чтобы читатель понял сам принцип построения таких анализаторов и при необходимости смог вносить в них изменения. В дальнейшем это поможет при изучении теории синтаксического анализа по специализированным книгам.

Книга ориентирована на Delphi для Win32. О Delphi для .NET мы здесь говорить не будем. При написании книги были исследованы особенности всех версий Delphi от 3-й до 2007-й, за исключением BDS 2005, и если какая-то особенность или ошибка, описанная в данной книге, присутствует только в некоторых из этих версий, то это обстоятельство обязательно отмечается. Но из-за того, что с BDS 2005 автор книги не имел возможности ознакомиться, фраза "до Delphi 7 включительно" может означать "до BDS 2005" включительно, а фраза "начиная с BDS 2006" — "начиная с BDS 2005".

Автор надеется, что книга действительно окажется полезной читателю. Актуальность изложенных в ней тем подтверждается многочисленными вопросами в форумах, а полезность сведений — многочисленными ответами. 

Глава 1 Windows API и Delphi

Библиотека VCL, делающая создание приложений в Delphi таким быстрым и удобным, все же не позволяет разработчику задействовать все возможности операционной системы. Полный доступ к ним дает API (Application Programming Interface) — интерфейс, который система предоставляет программам. С его помощью можно получить доступ ко всем документированным возможностям системы.

Программированию в Windows на основе API посвящено много книг, а также материалов в Интернете. Но если все делать только с помощью API, то даже для того, чтобы создать пустое окно, потребуется написать несколько десятков строк кода, а о визуальном проектировании такого окна придется вообще забыть. Поэтому желательно как-то объединить мощность API и удобство VCL. О том, как это сделать, мы и поговорим в этой главе. В первой части главы рассматриваются общие принципы использования API и интеграции этого интерфейса с VCL. Во второй части разбираются простые примеры, иллюстрирующие теорию. В третьей части представлено несколько обобщающих примеров использования API — небольших законченных приложений, использующих различные функции API для решения комплексных задач.

1.1. Основы работы Windows API в VCL-приложениях

В данном разделе будет говориться о том. как совместить Windows API и компоненты VCL. Предполагается, что читатель владеет основными методами создания приложений с помощью VCL а также синтаксисом языка Delphi, поэтому на этих вопросах мы останавливаться не будем. Так как "официальные" справка и примеры работы с API предполагают работу на С или C++, и это может вызвать трудности у человека, знакомого только с Delphi, здесь также будет уделено внимание тому, как правильно читать справку и переводить содержащийся в ней код с C/C++ на Delphi.

1.1.1. Что такое Windows API

Windows API — это набор функций, предоставляемых операционной системой каждой программе. Данные функции находятся в стандартных динамически компонуемых библиотеках (Dynamic Linked Library. DLL), таких как kernel32.dll, user32.dll, gdi32.dll. Указанные файлы располагаются в системной директории Window. Вообще говоря, каждая программа должна самостоятельно заботиться о том. чтобы подключить эти библиотеки. DLL могут подключаться к программе статически и динамически. В первом случае связь с библиотекой прописывается в исполняемом файле программы, и система при запуске этой программы сразу же загружает в ее адресное пространство и библиотеку. Если требуемая библиотека на диске не найдена, запуск программы будет невозможен. В случае динамического подключения программа загружает библиотеку в любой удобный для нее момент времени с помощью функции LoadLibrary. Если при этом возникает ошибка из-за того, что библиотека не найдена на диске, программа может самостоятельно решить, как на это реагировать.

Статическая загрузка проще динамической, но динамическая гибче. При динамической загрузке программист может, во-первых, выгрузить библиотеку, не дожидаясь окончания работы программы. Во-вторых, программа может продолжить работу, даже если библиотека не найдена. В-третьих, возможна загрузка тех DLL, имена которых неизвестны на момент компиляции. Это позволяет расширять функциональность приложения после передачи его пользователю с помощью дополнительных библиотек (в англоязычной литературе такие библиотеки обычно называются plug-in).

Стандартные библиотеки необходимы самой системе и всем программам, они всегда находятся в памяти, и поэтому обычно они загружаются статически. Чтобы статически подключить в Delphi некоторую функцию Windows API. например, функцию GetWindowDC из модуля user32.dll, следует написать конструкцию вида

function GetWindowDC(Wnd: HWnd); HDC; stdcall;

 external 'user32.dll' name 'GetWindowDC';

В результате в специальном разделе исполняемого файла, который называется таблицей импорта, появится запись, что программа импортирует функцию GetWindowDC из библиотеки user32.dll. После такого объявления компилятор будет знать, как вызывать эту функцию, хотя ее реальный адрес будет внесен в таблицу импорта только при запуске программы. Обратите внимание, что функция GetWindowDC, как и все функции Windows API, написана в соответствии с моделью вызова stdcall, а в Delphi по умолчанию принята другая модель — register (модель вызова определяет, как функции передаются параметры). Поэтому при импорте функций из стандартных библиотек необходимо явно указывать эту модель (подчеркнем, что это относится именно к стандартным библиотекам; другие библиотеки могут использовать любую другую модель вызова, разработчик библиотеки свободен в своем выборе). Далее указывается, из какой библиотеки импортируется функция и какое название в ней она имеет. Дело в том, что имя функции в библиотеке может не совпадать с тем, под которым она становится известной компилятор). Это может помочь разрешить конфликт имен при импорте одноименных функций из разных библиотек, а также встречается в других ситуациях, которые мы рассмотрим позже. Главным недостатком DLL следует считать то. что в них сохраняется информация только об именах функций, но не об их параметрах. Поэтому если при импорте функции указать не те параметры, которые подразумевались автором DLL, то программа будет работать неправильно (вплоть до зависания), а ни компилятор, ни операционная система не смогут указать на ошибку.

Обычно программе требуется много различных функций Windows API. Декларировать их все довольно утомительно. К счастью. Delphi избавляет программиста от этой работы: многие из этих функций уже описаны в соответствующих модулях, достаточно упомянуть их имена в разделе uses. Например, большинство общеупотребительных функций описаны в модулях Windows и Messages.

Функции API, которые присутствуют не во всех версиях Windows, предпочтительнее загружать динамически. Например, если программа статически импортирует функцию SetLayeredWindowsAttributes, она не запустится в Windows 9x, где этой функции нет — система, встретив ее упоминание в таблице импорта, прервет загрузку программы. Поэтому, если требуется, чтобы программа работала и в Windows 9x, эту функцию следует импортировать динамически. Отметим, что компоновщик в Delphi помещает в таблицу импорта только те функции, которые реально вызываются программой. Поэтому наличие декларации SetLayeredWindowsAttributes в модуле Windows не помешает программе запускаться в Windows 9x, если она не вызывает эту функцию.

1.1.2. Как получить справку по функциям Windows API

Для тех, кто решит работать с Windows API, самым необходимым инструментом становится какая-либо документация по этим функциям. Их так много, что запомнить все совершенно нереально, поэтому работа без справочника под рукой просто невозможна.

Первоисточник информации по технологиям Microsoft для разработчика Microsoft Developer's Network (MSDN). Это отдельная справочная система, не входящая в комплект поставки Delphi. MSDN можно приобрести отдельно или воспользоваться online-версией, находящейся по адресу: http://msdn.microsoft.com (доступ к информации свободный, регистрация не требуется). MSDN содержит не только информацию об API, но и все, что может потребоваться программисту, использующему различные средства разработки от Microsoft. Кроме справочного материала, MSDN включает в себя спецификации стандартов и технологий, связанных с Windows, статьи из журналов, посвященных программированию, главы из некоторых книг. И вся эта информация крайне полезна разработчику. Кроме того, MSDN постоянно обновляется, информация в нем наиболее актуальна. Пример справки из MSDN показан на рис. 1.1.

Рис. 1.1. Online-вариант MSDN (показана справка по функции DeleteObject)


Примечание
Отметим, что MSDN содержит также описание функций операционной системы Windows СЕ. Интерфейс Windows СЕ API на первый взгляд очень похож на Windows API, но различия между ними есть, и иногда весьма значительные. Поэтому при использовании MSDN не следует выбирать раздел API Reference — он целиком посвящен WinCE API.

В комплект поставки Delphi входит справочная система, содержащая описание функций Windows API. Справочная система в Delphi до 7-й версии включительно была построена на основе hlp-файлов. Применительно к справке по Windows API это порождало две проблемы. Во-первых, hlp-файлы имеют ограничение по числу разделов в справочной системе, поэтому объединить в одной справке информацию и по Delphi, и по Windows API было невозможно, — эти две справки приходилось читать по очереди. Чтобы открыть файл справки по Windows API, нужно было в редакторе кода поставить курсор на название какой-либо функции API и нажать клавишу <F1>— в этом случае вместо справки по Delphi открывалась справка по Windows API. Второй вариант — в меню Программы найти папку Delphi, а в ней — папку Help\MS SDK Files и выбрать требуемый раздел. Можно также вручную открыть файл MSTools.hlp. В ранних версиях Delphi он находится в каталоге $(Delphi)\Help, в более поздних его нужно искать в $(Program Files)\Common Files. Окно старой справки показано на рис. 1.2.

Вторая проблема, связанная со справкой на основе hlp-файлов,— это то обстоятельство, что разработчики Delphi, разумеется, не сами писали эту справку, а взяли ту, которую предоставила Microsoft. Microsoft же последнюю версию справки в формате НLР выпустила в тот момент, когда уже вышла Windows 95, но еще не было Windows NT 4. Поэтому про многие функции, прекрасно работающие в NT 4, там написано, что в Windows NT они не поддерживаются, т.к. в более ранних версиях они действительно не поддерживались. В справке, поставляемой с Delphi 7 (и, возможно, с некоторыми более ранними версиями), эта информация подправлена, но даже и там отсутствуют функции, которые появились только в Windows NT 4 (как, например, CoCreateInstanceEx). И уж конечно, бесполезно искать в этой справке информацию о функциях, появившихся в Windows 98, 2000, XР. Соответственно, при работе в этих версиях Delphi даже не возникает вопрос, что предпочесть для получения информации о Windows API, — справку, поставляемую с Delphi, или MSDN. Безусловно, следует выбрать MSDN. Справка, поставляемая с Delphi, имеет только одно преимущество по сравнению с MSDN: ее можно вызывать из среды нажатием клавиши <F1>. Но риск получить неверные сведения слишком велик, чтобы это преимущество могло быть серьезным аргументом. Единственная ситуация, когда предпочтительна справка, поставляемая с Delphi, — это случай, если у вас нет достаточно быстрого доступа к Интернету для работы с online-версией MSDN и нет возможности приобрести и установить его offline-версию.

Рис. 1.2. Старая (на основе hlp-файлов) справка по Windows API (показана функция DeleteObject)


Начиная с BDS 2006, Borland/CodeGear реализовала новую справочную систему Borland Help (рис. 1.3). По интерфейсу она очень напоминает offline версию MSDN, а также использует файлы в том же формате, поэтому технологических проблем интеграции справочных систем по Delphi и по Windows API больше не существует. В справку BDS 2006 интегрирована справка по Windows API от 2002–2003 годов (разные разделы имеют разную дату) Справка Delphi 2007 содержит сведения по Windows API от 2006 года, т.е. совсем новые. Таким образом, при работе с Delphi 2007 наконец-то можно полностью отказаться от offline-версии MSDN, а к online-версии обращаться лишь изредка, когда требуется информация о самых последних изменениях в Windows API (например, о тех, которые появились в Windows Vista).

Примечание
Несмотря на очень высокое качество разделов MSDN, относящихся к Window API, ошибки иногда бывают и там. Со временем их исправляют. Поэтому, если вы столкнулись с ситуацией, когда есть подозрение, что какая-либо функция Windows API ведёт себя не так, как это описано в вашей offline-справке, есть смысл заглянуть в online-справку — возможно, там уже появились дополнительные сведения по данной функции.

Рис. 1.3. Окно справки Delphi 2007 (функция DeleteObject)


Система Windows написана на C++, поэтому все описания функций Windows API, а также примеры их использования приведены на этом языке (это касается как MSDN, так и справки, поставляемой с Delphi). При этом, прежде всего, необходимо разобраться с типами данных. Большинство типов, имеющихся в Windows API. определены в Delphi. Соответствие между ними показано в табл. 1.1.


Таблица 1.1. Соответствие типов Delphi системным типам

Тип Windows API Тип Delphi
INT INT
UINT LongWord
WORD Word
SHORT SmallInt
USHORT Word
CHAR Чаще всего соответствует типу Char, но может трактоваться также как ShortInt, т.к. в C++ нет разницы между символьным и целочисленным типами
UCHAR Чаще всего соответствует типу Byte, но может трактоваться также как Char
DWORD LongWord
BYTE Byte
WCHAR WideChar
BOOL LongBool
int Integer
long LongInt
short SmallInt
unsigned int Cardinal
Название типов указателей имеет префикс P или LP (Pointer или Long Pointer, в 16-разрядных версиях Windows были короткие и длинные указатели. В 32-разрядных все указатели длинные, поэтому оба префикса имеют одинаковый смысл). Например, LPDWORD эквивалентен типу ^DWORD, PUCHAR^Byte. Иногда после префикса P или LP стоит еще префикс C — он означает, что это указатель на константу. В C++ возможно объявление таких указателей, которые указывают на константное содержимое, т.е. компилятор разрешает это содержимое читать, но не модифицировать. В Delphi такие указатели отсутствуют, и при портировании эти типы заменяются обычными указателями, т.е. префикс C игнорируется.

Типы PVOID и LPVOID соответствуют нетипизированным указателям (Pointer).

Для передачи символов чаще всего используется тип TCHAR. Windows поддерживает две кодировки: ANSI (1 байт на символ) и Unicode (2 байта на символ; о поддержке Unicode в Windows мы будем говорить далее). Тип CHAR соответствует символу в кодировке ANSI, WCHAR — Unicode. Для программ, которые используют ANSI, тип TCHAR эквивалентен типу CHAR, для использующих Unicode — WCHAR. В Delphi нет прямого аналога типу TCHAR. Программист сам должен следить за тем, какой символьный тип требуется в данном месте. Строки в Windows API передаются как указатели на цепочку символов, завершающихся нулем. Поэтому указатель на TCHAR может указывать как на единичный символ, так и на строку. Чтобы было легче разобраться, где какой указатель, в Windows API есть типы LPTCHAR и LPTSTR. Они эквивалентны друг другу, но первый принято использовать там, где требуется указатель на одиночный символ, а второй — на строку. Если строка передается в функцию только для чтения, обычно используют указатель на константу, т.е. тип LPCTSTR. В Delphi это соответствует PChar для ANSI и PWideChar для Unicode. Здесь следует отметить особенность записи строковых литералов в языках C/C++. Символ \ в литерале имеет специальное значение: после него идет один или несколько управляющих символов. Например, \n означает перевод строки, \t — символ табуляции и т.п. В Delphi таких последовательностей нет, поэтому при переводе примеров из MSDN следует явно писать коды соответствующих символов. Например, литерал "а\nb" в Delphi превращается в 'a\'#13'b'. После символа \ может идти число — в этом случае оно трактуется как код символа, т.е. литерал "a\0b9" в C/C++ эквивалентен литералу 'а'#0'b'#9 в Delphi. Если нужно, чтобы строковый литерал включал в себя сам символ \, его удваивают, т.е. литерал "\\" в C++ соответствует '\' в Delphi. Кроме того, в примерах кода, приведенных в MSDN, можно нередко увидеть, что строковые литералы обрабатываются макросами TEXT или _T, которые служат для унификации записи строковых литералов в кодировках ANSI и Unicode. При переводе такого кола на Delphi эти макросы можно просто опустить. С учетом сказанного такой, например, код (взят из примера использования Named pipes):

LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");

на Delphi будет выглядеть так:

var

 lpszPipeName: PChar;

...

lpszPipeName:= '\\.\pipe\mynamedpipe';

Большинство названий типов из левой части табл. 1.1 в целях совместимости описаны в модуле Windows, поэтому они допустимы наравне с обычными типами Delphi. Кроме этих типов общего назначения существуют еще специальные. Например, дескриптор окна имеет тип HWND, первый параметр сообщения — тип WPARAM (в старых 16-разрядных Windows он был эквивалентен типу Word, в 32-разрядных — LongInt). Эти специальные типы также описаны в модуле Windows.

Записи (record) в C/C++ называются структурами и объявляются с помощью слова struct. Из-за особенностей описания структур на языке С структуры в Windows API получают два имени: одно основное имя, составленное из главных букв, которое затем и используется, и одно вспомогательное, получающееся из основного добавлением префикса tag. Начиная с четвертой версии Delphi приняты следующие правила именования таких типов: простое и вспомогательное имена остаются без изменений и еще добавляется новое имя, получающееся из основного присоединением общеупотребительного в Delphi префикса T. Например, в функции CreatePenIndirect одни из параметром имеет тип LOGPEN. Это основное имя данного типа, а вспомогательное — tagLOGPEN. Соответственно, в модуле Windows определена запись tagLOGPEN и ее синонимы — LOGPEN и TLogPen. Эти три идентификатора в Delphi взаимозаменяемы. Вспомогательное имя встречается редко, программисты, в зависимости от личных предпочтений, выбирают либо основное имя типа, либо имя с префиксом T.

Описанные здесь правила именования типов могут внести некоторую путаницу при использовании VCL. Например, для описания растра в Windows API определен тип BITMAP (он же— tagBITMAP). В Delphi соответствующий тип имеет еще одно имя — TBitmap. Но такое же имя имеет класс TBitmap, описанный в модуле Graphics. В коде, который Delphi создает автоматически, модуль Graphics находится в списке uses после модуля Windows, поэтому идентификатор TBitmap воспринимается компилятором как Graphics.TBitmap, а не как Windows.TBitmap. Чтобы использовать Windows.ТBitmap, нужно явно указать имя модуля или воспользоваться одним из альтернативных имен. В более ранних версиях Delphi были другие правила именования типов. Например. в Delphi 2 существовал тип BITMAP, но не было TBitmap и tagBITMAP, а в Delphi 3 из этих трех типов был только TBitmap.

Все структуры в Windows API описаны без выравнивания, т.е. компилятор не вставляет между полями неиспользуемые байты, чтобы границы полей приходились на начало двойного или четверного слова, поэтому в Delphi для описания соответствующих структур предусмотрено слово packed, запрещающее выравнивание.

При описании структур Windows API можно иногда встретить ключевое слово union (см., например, структуру in_addr). Объединение нескольких полей с помощью этого слова означает, что все они будут размещены по одному адресу. В Delphi это соответствует вариантным записям (т. е. использованию сазе в record). Объединения в C/C++ гибче, чем вариантные записи Delphi, т.к. позволяют размещать вариантную часть в любом месте структуры, а не только в конце. При переносе таких структур в Delphi иногда приходится вводить дополнительные типы.

Теперь рассмотрим синтаксис описания самой функции в C++ (листинг 1.1).

Листинг 1.1. Синтаксис описания функции на C++
<Тип функции> <Имя функции> ' ('

 [<Тип параметра> {<Имя параметра>}

  (',' <Тип параметра> {<Имя параметра>} }

 ]

')';

Как видно из листинга 1.1, при объявлении функции существует возможность указывать только типы параметров и не указывать их имена. Однако это считается устаревшим и применяется крайне редко (если не считать "параметров" типа VOID, о которых написано далее).

Необходимо помнить, что в C/C++ различаются верхний и нижний регистры, поэтому HDC, hdc, hDC и т.д. — это разные идентификаторы (автор С очень любил краткость и хотел, чтобы можно было делать не 26, а 52 переменные с именем из одной буквы). Поэтому часто можно встретить, что имя параметра и его тип совпадают с точностью до регистра. К счастью, при описании функции в Delphi мы не обязаны сохранять имена параметров, значение имеют лишь их типы и порядок следования. С учетом всего этого функция, описанная в справке как

HMETAFILE CopyMetaFile(HMETAFILE hmfSrc, LPCTSTR lpszFile);

в Delphi имеет вид

function СоруМеtaFile(hnfSrc: HMETAFILE; lpszFile: LPCTSTR): HMETAFILE;

или, что то же самое.

function CopyMetaFile(hnfSrc: HMETAFILE; lpszFile: PChar): HMETAFILE;

Примечание
Компилятор Delphi допускает, чтобы имя параметра процедуры или функции совпадало с именем типа, поэтому мы в дальнейшем увидим, что иногда имя параметра и его тип совпадают, только записываются в разном регистре, чтобы прототип функции на Delphi максимально соответствовал исходному прототипу на C/C++. При этом следует учитывать что соответствующий идентификатор внутри функции будет рассматриваться как имя переменной, а не типа, поэтому, например, объявлять локальную переменную данного типа придется с явным указанием имени модуля, в котором данный тип объявлен.

Несколько особняком стоит тип VOID (или void, что то же самое, но в Windows API этот идентификатор встречается существенно реже). Если функции имеет такой тип, то в Паскале она описывается как процедура. Если вместо параметров у функции в скобках указан void, это означает, что функция не имеет параметров. Например, функция

VOID CloseLogFile(VOID);

в Delphi описывается как

procedure CloseLogFile;

Примечание
Язык C++, в отличие от С, допускает объявление функций без параметров, т.е. функцию CloseLogFile можно было бы объявить так: VOID CloseLogFile(); В C++ эти варианты объявления эквивалентны, но в Windows API варианту явного параметра встречается существенно реже из-за несовместимости с C.

Когда тип параметра является указателем на другой тип (обычно начинается с букв LP), при описании этой функции в Delphi можно пользоваться параметром-переменной, т.к. в этом случае функции передается указатель. Например, функция

int GetRgnBox(HRGN hrgn, LPRECT lprc);

в модуле Windows описана как

function GetRgnBox(RGN: HRGN; var p2: TRec): Integer;

Такая замена целесообразна в том случае, если значение параметра не может быть нулевым указателем, потому что при использовании var передать такой указатель будет невозможно. Нулевой указатель в C/C++ обозначается константой NULL. NULL и 0 в этих языках взаимозаменяемы, поэтому в справке можно и про целочисленный параметр встретить указание, что он может быть равен NULL.

И наконец, если не удается понять, как функция, описанная в справке, должна быть переведена на Паскаль, можно попытаться найти описание этой функции в исходных текстах модулей, поставляемых вместе с Delphi. Эти модули находятся в каталоге $(DELPHI)\Source\RTL\Win (до Delphi 7) или $(BDS)\Source\Win32\RTL\Win (BDS 2006 и выше). Можно также воспользоваться подсказкой, которая всплывает в редакторе Delphi после того, как будет набрано имя функции.

Если посмотреть справку, например, по функции GetSystemMetrics, то видно, что эта функция должна иметь один целочисленный параметр. Однако далее в справке предлагается при вызове этой функции подставлять в качестве параметра не числа, a SM_ARRANGE, SM_CLEANBOOT и т.д. Подобная ситуация и со многими другими функциями Windows API. Все эти SM_ARRANGE, SM_CLEANBOOT и т.д. являются именами числовых констант. Эти константы описаны в том же модуле, в котором описана функция, использующая их, поэтому можно не выяснять численные значения этих констант, а указывать при вызове функций их имена, например, GetSystemMetrics(SM_ARRANGE);. Если по каким-то причинам все-таки потребовалось выяснить численные значения, то в справочной системе их искать не стоит — их там нет. Их можно узнать из исходных текстов модулей Delphi, в которых эти константы описаны. Так, например, просматривая Windows.pas, можно узнать, что SM_ARRANGE = 56.

В справке, поставляемой вместе с Delphi до 7-й версии включительно, в описании многих функций Windows API вверху можно увидеть три ссылки: QuickInfo, Overview и Group. Первая дает краткую информацию о функции: какой библиотекой реализуется, в каких версиях Windows работает и т.п. (напоминаю, что к информации о версиях в этой справке нужно относиться очень критично). Overview — это обзор какой-то большой темы. Например, для любой функции, работающей с растровыми изображениями, обзор будет объяснять, зачем в принципе нужны эти самые растровые изображения и как они устроены. Страница, на которую ведет ссылка Overview обычно содержит весьма лаконичные сведения, но, нажав кнопку >>, расположенную в верхней части окна, можно получить продолжение обзора. И, наконец, Group. Эта ссылка приводит к списку всех функций, родственных данной. Например, для функции CreateRectRgn группу будут составлять все функции, имеющие отношение к регионам. Если теперь нажимать на кнопку <<, то будут появляться страницы с кратким описанием возможных применений объектов, с которыми работают функции (в приведенном примере — описание возможностей регионов). Чтобы читать их в нормальной последовательности, лучше всего нажать на кнопку << столько раз, сколько возможно, а затем пойти в противоположном направлении с помощью кнопки >>.

MSDN (а также справка BDS 2006 и выше) предоставляет еще больше полезной информации. В нижней части описания каждой функции есть раздел Requirements, в котором написано, какая библиотека и какая версия Windows требуется для ее использования. В самом низу описания функции расположены ссылки See also. Первая ссылка — обзор соответствующей темы (например, для уже упоминавшейся функции CreateRectRgn — она называется Regions Overview). Вторая список родственных функций (Region Functions в данном случае). Она ведет на страницу, где перечислены все функции, родственные выбранной. После этих двух обязательных ссылок идут ссылки на описание функций и типов, которые обычно используются совместно с данной функцией.

Основные типы, константы и функции Windows API объявлены в модулях Windows и Messages. Но многие функции объявлены в других модулях, которые не подключаются к программе по умолчанию, программист должен сам выяснить, в каком модуле находится требуемый ему идентификатор, и подключить этот модуль. Ни справка, поставляемая с Delphi, ни MSDN, разумеется, не могут дать необходимую информацию. Чтобы выяснить, в каком модуле объявлен нужный идентификатор, можно воспользоваться поиском по всем файлам с расширением pas, находящимся в папке с исходными кодами стандартных модулей. Этим методом можно, например, выяснить, что весьма популярная функция ShellExecute находится в модуле ShellAPI, CoCreateInstance — в модуле ActiveX (а также в модуле Ole2, оставленном для совместимости со старыми версиями Delphi).

Еще несколько слов о числовых константах. В справке можно встретить числа вида, например, 0xC56F или 0x3341. Префикс в C/C++ означает шестнадцатеричное число. В Delphi его следует заменить на $, т.е. эти числа должны быть записаны как $C56F и $3341 соответственно.

1.1.3. Дескрипторы вместо классов

Программируя в Delphi, мы быстро привыкаем к тому, что каждый объект реализуется экземпляром соответствующего класса. Например, кнопка реализуется экземпляром класса TButton, контекст устройства — классом TCanvas. Но когда создавались первые версии Windows, объектно-ориентированный метод программирования еще не был общепризнанным, поэтому он не был реализован. Современные версии Windows частично унаследовали этот недостаток, поэтому в большинстве случаев приходится работать "по старинке", тем более что DLL могут экспортировать только функции, но не классы. Когда мы будем говорить об объектах, создаваемых через Windows API, будем подразумевать не объекты в терминах ООП, а некоторую сущность, внутренняя структура которой скрыта от нас, поэтому с этой сущностью мы можем оперировать только как с единым и неделимым (атомарным) объектом.

Каждому объекту, созданному с помощью Windows API, присваивается уникальный номер (дескриптор). Его конкретное значение не несет для программиста никакой полезной информации и может быть использовано только для того, чтобы при вызове функций из Windows API указывать, с каким объектом требуется выполнить операцию. В большинстве случаен дескрипторы представляют собой 32-значные числа, а значит, их можно передавать везде, где требуются такие числа. В дальнейшем мы увидим, что Windows API несколько вольно обращается с типами, т.е. один и тот же параметр в различных ситуациях может содержать и число, и указатель, и дескриптор, поэтому знание двоичного представления дескриптора все-таки приносит программисту пользу (хотя если бы система Windows была "спроектирована по правилам", тип дескриптора вообще не должен был интересовать программиста).

Таким образом, главное различие между методами класса и функциями Windows API заключается в том. что первые связаны с тем экземпляром класса, через который они вызываются, и поэтому не требуют явного указания на объект. Вторым необходимо указание объекта через его дескриптор, т.к. они сами по себе никак не связаны ни с одним объектом. Компоненты VCL нередко являются оболочками над объектами Delphi. В этом случае они имеют свойство (которое обычно называется Handle), содержащее дескриптор соответствующего объекта. Иногда класс Delphi инкапсулирует несколько объектов Windows. Например, класс TBitmap включает в себя HBITMAP и HPALETTE — картинку и палитру к ней. Соответственно, он хранит два дескриптора: в свойствах Handle и Palettе.

Следует учитывать, что внутренние механизмы VCL не могут включиться, если изменение объекта происходит через Windows API. Например, если спрятать окно не с помощью метода Hide, а путем вызова функции Windows API ShowWindow(Handle, SW_HIDE), не возникнет событие OnHide, потому что оно запускается теми самыми внутренними механизмами VCL. Но такие недоразумения случаются обычно только тогда, когда функциями Windows API дублируется то, что можно сделать и с помощью VCL.

Все экземпляры классов, созданные в Delphi, должны удаляться. В некоторых случаях это происходит автоматически, а иногда программист должен сам позаботиться о "выносе мусора". Аналогичная ситуация и с объектами, создаваемыми в Windows API. Если посмотреть справку по функции, создающей какой-то объект, то там обязательно будет информация о том. какой функцией можно удалить объект и нужно ли это делать вручную, или система сделает это автоматически. Во многих случаях совершенно разные объекты могут удаляться одной и той же функцией. Так, функция DeleteObject удаляет косметические перья, геометрические перья, кисти, шрифты, регионы, растровые изображения и палитры. Обращайте внимание на возможные исключения. Например, регионы не удаляются системой автоматически, однако если вызвать для региона функцию SetWindowRgn, то он переходит в собственность операционной системы. Никакие дальнейшие операции с ним, в том числе и удаление, совершать нельзя.

Если системный объект используется только одним приложением, то он будет удален при завершении работы приложения. Тем не менее хороший стиль программирования требует, чтобы программа удаляла объекты явно, а не полагалась на систему.

1.1.4. Формы VCL и окна Windows

Под словом "окно" обычно подразумевается некоторая форма наподобие тех, что можно создать с помощью класса TForm. Однако это понятие существенно шире. В общем случае окном называется любой объект, который имеет экранные координаты и может реагировать на мышь и клавиатуру. Например, кнопка, которую можно создать с помощью класса TButton, — это тоже окно. VCL вносит некоторую путаницу в это понятие. Некоторые визуальные компоненты VCL не являются окнами, а только имитируют их, как, например, TImage. Это позволяет экономить ресурсы системы и повысить быстродействие программы. Механизм этой имитации мы рассмотрим позже, а пока следует запомнить, что окнами являются только те визуальные компоненты которые имеют в числе предков класс TWinControl. Разработчики VCL постарались, чтобы разница между оконными и неоконными визуальными компонентами была минимальной. Действительно, на первый взгляд неоконный TLabel и оконный TStaticText кажутся практически близнецами. Разница становится заметной тогда, когда используется Windows API. С неоконными компонентами можно работать только средствами VCL, они даже не имеют свойства Handle, в то время как оконными компонентами можно управлять с помощью Windows API.

Отметим также еще одно различие между оконными и неоконными компонентами: неоконные компоненты рисуются непосредственно на поверхности родительского компонента, в то время как оконные как бы "кладутся" на родителя сверху . В частности, это означает, что неоконный TLabel, размещенный на форме, не может закрывать собой часть кнопки TButton, потому что TLabel рисуется на поверхности формы, а кнопка — это независимый объект, лежащий на форме и имеющий свою поверхность. A TStaticText может оказаться над кнопкой, потому что он тоже находится над формой.

Примечание
Чтобы разместить неоконный визуальный компонент над оконным, если в этом есть необходимость, можно поступить следующим образом. Положить на форму панель (TPanel) — она является оконным компонентом и может быть размещена поверх других оконных элементов. На панель теперь можно положить любой неоконный визуальный компонент, и он будет рисоваться не на поверхности формы, а на поверхности панели. Если теперь убрать у панели рамку и уменьшить ее до размеров содержащегося в ней неоконного компонента, панель станет незаметной, и все вместе это будет выглядеть так, будто неоконный компонент находится над оконным.

Каждое окно принадлежит к какому-то оконному классу. Не следует путать оконный класс с классами Delphi. Это некий шаблон, определяющий базовые свойства окна. Каждому такому шаблону присваивается имя, уникальное в его области видимости. Перед использованием класс необходимо зарегистрировать (функция RegisterClassEx). В качестве параметра эта функция принимает запись типа TWndClassEx, поля которой содержат параметры класса. 

С каждым окном должна быть связана специальная функция, называющаяся оконной процедурой (подробнее мы рассмотрим ее чуть позже). Она является параметром не отдельного окна, а всего оконного класса, т. е. все окна, принадлежащие данному классу, будут использовать одну и ту же оконную процедуру. Эта процедура может размещаться либо в самом исполняемом модуле, либо в одной из загруженных им DLL. При создании класса указывается дескриптор модуля, в котором находится оконная процедура.

Примечание
Здесь следует отметить некоторую путаницу в терминах. В англоязычной справке есть слово module, служащее для обозначения файла, отображенного в адресное пространство процесса, т.е., в первую очередь, exe-файла, породившего процесс, и загруженных им DLL. И есть слово unit, которое обозначает модуль в Delphi и которое также переводится как модуль. Ранее мы говорили о модулях как об отображаемых в адресное пространство файлах — это они имеют дескрипторы. Модули Delphi не являются системными объектами и дескрипторов не имеют.

Дескриптор модуля, загруженного в память, можно получить с помощью функции GetModuleHandle. Функция LoadLibrary в случае успешного завершения также возвращает дескриптор загруженной DLL. Кроме того, Delphi предоставляет две переменные: MainInstance из модуля System и HInstance из модуля SysInit (оба этих модуля подключаются к программе автоматически, без явного указания в списке uses). MainInstance содержит дескриптор exe-файла, породившего процесс, HInstance — текущего модуля. В исполняемом файле MainInstance и HInstance равны между собой, в DLL HInstance содержит дескриптор самой библиотеки, а MainIstance — загрузившего ее главного модуля.

Каждое окно в 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 этой ошибки уже нет).

1.1.6. Сообщения Windows

Человеку, знакомому с Delphi, должна быть ясна схема событийного управления. Программист пишет только методы реакции на различные события, а затем этот код получает управление тогда, когда соответствующее событие произойдет. Простые программы в Delphi состоят исключительно из методов реакции на события (например, OnCreate, OnClick, OnCloseQuery). Причем событием называется не только событие в обычном смысле этого слова, т.е. когда происходит что-то внешнее, но и ситуация, когда событие используется просто для передачи управления коду, написанному разработчиком программы, в тех случаях, когда VCL не может сама справиться с какой-то задачей. Пример такого события — TListBox.OnDrawItem. Устанавливая стиль списка в lbOwnerDrawFixed или lbOwnerDrawVariable, программист указывает, что ему требуется нестандартный вид элементов списка, поэтому их рисование он берет на себя. И каждый раз, когда возникает необходимость в рисовании элемента, VCL передает управление специально написанному коду. На самом деле разница между двумя типами событий весьма условна. Можно сказать, что когда пользователь нажимает клавишу, VCL не "знает", что делать, и поэтому передает управление обработчику OnKeyPress.

Событийное управление не есть изобретение авторов Delphi. Такой подход заложен в самой системе Windows. Только здесь события называются сообщениями (message), что иногда даже лучше отражает ситуацию. Windows посылает программе сообщения, связанные либо с тем, что произошло внешнее событие (нажатие кнопки мыши, клавиши на клавиатуре и т.п.), либо с тем, что самой системе потребовались от программы какие-то действия. Самое распространенное действие — предоставление информации. Например, при необходимости узнать текст заголовка окна Windows посылает этому окну специальное сообщение, в ответ на которое окно должно сообщить системе свой заголовок. Еще бывают сообщения, которые просто уведомляют программу о начале какого-то действия (например, о начале перетаскивания окна) и предоставляют возможность вмешаться. Но это вмешательство необязательно.

В Delphi для реакции на каждое событие обычно создается свой метод. В Windows одна процедура, называемая оконной, обрабатывает все сообщения, адресованные конкретному окну. (В C/C++ нет понятия "процедура", там термин "оконная процедура" не вызывает путаницы, а вот в Delphi четко определено, что такое процедура. И здесь можно запутаться: то, что в системе называется оконной процедурой, с точки зрения Delphi будет не процедурой, а функцией. Тем не менее мы будем употреблять общепринятый термин "оконная процедура".) Каждое сообщение имеет свой уникальный номер, а оконная процедура обычно целиком состоит из оператора case, и каждому сообщению соответствует своя альтернатива этого оператора. Номера сообщений знать не обязательно, потому что можно использовать константы, описанные в модуле Messages. Эти константы начинаются с префикса, указывающего на принадлежность сообщения к какой-то группе. Например, сообщения общего назначения начинаются с WM_: WM_PAINT, WM_GETTEXTLENTH. Сообщения, специфичные, например, для кнопок, начинаются с префикса BM_. Остальные группы сообщений также связаны либо с теми или иными элементами управления, либо со специальными действиями, например, с динамическим обменом данными (Dynamic Data Exchange, DDE). Обычной программе приходится обрабатывать довольно много сообщений, поэтому оконная процедура бывает, как правило, очень длинной и громоздкой. Оконная процедура описывается программистом как функция обратного вызова и указывается при создании оконного класса. Таким образом, все окна данного класса имеют одну и ту же оконную процедуру. Впрочем, существует возможность породить так называемый подкласс, т.е. новый класс, наследующий все свойства существующего, за исключением оконной процедуры. Несколько подробнее об этом будет сказано далее.

Кроме номера, каждое сообщение содержит два параметра: wParam и lParam. Префиксы w и l означают "Word" и "Long", т.е. первый параметр 16-разрядный, а второй — 32-разрядный. Однако так было только в старых, 16-разрядных версиях Windows. В 32-разрядных версиях оба параметра 32-разрядные, несмотря на их названия. Конкретный смысл каждого параметра зависит от сообщения. В некоторых сообщениях один или оба параметра могут вообще не использоваться, в других — наоборот, двух параметров даже не хватает. В этом случае один из параметров (обычно lParam) содержит указатель на дополнительные данные. После обработки сообщения оконная процедура должна вернуть какое-то значение. Обычно это значение просто сигнализирует, что сообщение не нуждается в дополнительной обработке, но в некоторых случаях оно более осмысленно, например, WM_SETICON должно вернуть дескриптор иконки, которая была установлена ранее. Прототип оконной процедуры выглядит следующим образом:

LRESULT CALLBACK WindowProc(

 HWND hwnd, // дескриптор окна

 UINT uMsg, // номер сообщения

 WPARAM wParam, // первый параметр соообщения

 LPARAM lParam // второй параметр сообщения

); 

В Delphi оконная процедура объявляется следующим образом:

function WindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;

Все, что "умеет" окно, определяется тем. как его оконная процедура реагирует на сообщения. Чтобы окно можно было, например, перетаскивать мышью, его оконная процедура должна обрабатывать целый ряд сообщений, связанных с мышью. Чтобы не заставлять программиста каждый раз реализовывать стандартную для всех окон обработку событий, в системе предусмотрена функция DefWindowProc. Разработчик приложения в своей оконной процедуре должен предусмотреть только специфическую для данного окна обработку сообщений, а обработку всех остальных сообщений передать этой функции. Существуют также аналоги функции DefWindowProc для специализированных окон: DefDlgProc для диалоговых окон, DefFrameProc для родительских MDI окон, DefChildMDIProc для дочерних MDI-окон.

Сообщение окну можно либо послать (post), либо отправить (send). Каждая нить, вызвавшая хоть одну функцию из библиотеки user32.dll или gdi32.dll, имеет свою очередь сообщений, в которую помещаются все сообщения, посланные окнам, созданным данной нитью (послать сообщение окну можно например, с помощью функции PostMessage). Соответственно, кто-то должен извлекать эти сообщения из очереди и передавать их окнам-адресатам. Это делается с помощью специального цикла, который называется петлей сообщений (message loop). В этом непрерывном цикле, который должен реализовать разработчик приложения, сообщения извлекаются из очереди с помощью функции GetMessage (реже — PeekMessage) и передаются в функцию DispatchMessage. Эта функция определяет, какому окну предназначено сообщение, и вызывает его оконную процедуру. Таким образом, простейший цикл обработки сообщений выглядит так, как показано в листинге 1.5.

Листинг 1.5. Простейшая петля сообщений
var

 Msg: TMsg;

 ...

while GetMessage(Msg, 0, 0, 0) do

begin

 TranslateMessage(Msg);

 DispatchMessage(Msg);

end;

Блок-схема петли сообщений показана на рис. 1.4.

Рис 1.4. Блок-схема петли сообщений


Функция GetMessage возвращает True до тех пор, пока не будет получено сообщение WM_QUIT, указывающее на необходимость завершения программы. Обычная программа для Windows, выполнив предварительные действия (регистрация класса и создание окна), входит в петлю сообщений, которую выполняет до конца своей работы. Все остальные действия выполняются в оконной процедуре при реакции на соответствующие сообщения.

Примечание
Если нить не имеет петли сообщений, сообщения, которые посылаются нам, не будут обработаны. Это следует учитывать при создании таких компонентов, как, например, TTimer и TClientSocket. Эти компоненты создают невидимые окна для получения сообщений, которые необходимы им для работы. Если нить, создавшая эти объекты, не будет иметь петли сообщений, они будут неработоспособными

Сообщение, извлеченное из очереди, GetMessage помещает в первый параметр-переменную типа TMsg. Последние три параметра служат для фильтрации сообщений, позволяя извлекать из очереди только те сообщения, которые соответствуют определенным критериям. Если эти параметры равны нулю, как это обычно бывает, фильтрация при извлечении сообщений не производится.

Функция TranslateMessage, которая обычно вызывается в петле сообщений, служит для трансляции клавиатурных сообщении (если петля сообщений реализуется только для обработки сообщении невидимым окнам, которые использует, например, COM/DCOM, или по каким-то другим причинам ввод с клавиатуры не обрабатывается или обрабатывается нестандартным образом, вызов TranslateMessage можно опустить). Когда пользователь нажимает какую-либо клавишу на клавиатуре, система посылает окну, находящему в фокусе, сообщение WM_KEYDOWN. Через параметры этого сообщения передаётся виртуальный код нажатой клавиши — двухбайтное число, которое определяется только положением нажатой клавиши на клавиатуре и не зависит от текущей раскладки, состояния клавиш <CapsLock> и т.п. Функция TranslateMessage. обнаружив такое сообщение, добавляет в очередь (причем не в конец, а в начало) сообщение WM_CHAR, в параметрах которого передается код символа, соответствующего нажатой клавише, с учетом раскладки, состояния клавиш <CapsLock>, <Shift> и т. п. Именно функция TranslateMessage по виртуальному коду клавиши определяет код символа. При этом нажатие любой клавиши приводит к генерации WM_KEYDOWN, а вот WM_CHAR генерируется не для всех клавиш, а только для тех, которые соответствуют какому-то символу (например, не генерирует WM_CHAR нажатие таких клавиш, как <Shift> <Ctrl>, <Insert>, функциональных клавиш).

Примечание
У многих компонентов VCL есть события OnKeyDown и OnKeyPress. Первое возникает при получении компонентом сообщения WM_KEYDOWN, второе — сообщения WM_CHAR.

Если очередь сообщений пуста, функция GetMessage ожидает, пока там не появится хотя бы одно сообщение, и только после этого завершает работу. Во время этого ожидания нить не загружает процессор.

Петля сообщений может извлечь и отправить на обработку следующее сообщение только тогда, когда оконная процедура закончила обработку предыдущего. Таким образом, сообщение, обработка которого занимает много времени, блокирует обработку следующих сообщений, и все окна, созданные данной нитью, перестают реагировать на действия пользователя. Именно этим объясняется временное зависание программы, которая в одном из своих обработчиков сообщений делает математические расчеты или выполняет длительный запрос к базе данных: сообщения накапливаются в очереди, но не извлекаются из нее и не обрабатываются. Как только обработка текущего сообщения закончится, все остальные сообщения будут извлечены из очереди и обработаны.

В некоторых случаях избежать временного зависания программы помогает организация локальной петли сообщений. Если обработчик сообщения, для работы которого требуется много времени, содержит цикл, в него можно вставить вызовы функции PeekMessage, которая позволяет проверить, есть ли в очереди сообщения. Если сообщения обнаружены, нужно вызвать DispatchMessage для передачи их требуемому окну. В этом случае сообщения будут извлекаться из очереди и обрабатываться до завершения работы обработчика. Блок-схема программы, содержащей локальную петлю сообщений, показана на рис. 1.5 (для краткости в главной петле сообщений показаны только две самых важных функции: GetMessage и DispatchMessage, хотя и в этом случае главная петля целиком выглядит так же, как на рис. 1.4).

При использовании локальной петли сообщений существует опасность бесконечной рекурсии. Рассмотрим это на простом примере: предположим, что сложный код, содержащий локальную петлю сообщений, выполняется при нажатии некоторой кнопки на форме приложения. Пока обработчик выполняется, нетерпеливый пользователь может снова нажать кнопку, запустив вторую активацию обработчика нажатия кнопки, и так несколько раз. Конечно, организовать таким образом очень глубокую рекурсию пользователь вряд ли сможет (терпения не хватит), но часто даже то, что несколько активаций обработчика вызваны рекурсивно, может привести к неприятным последствиям. А если программа организует локальную петлю сообщений в обработчике сообщений таймера, то здесь рекурсия действительно может углубляться до переполнения стека. Поэтому при организации петли сообщений следует принимать меры против рекурсии. Например, в случае с кнопкой в обработчике ее нажатие можно запретить (Enabled := False), и вновь разрешить только после окончания обработки, тогда пользователь не сможет нажать кнопку во время работы локальной петли сообщений. В очередь можно поставить сообщение, не привязанное ни к какому окну. Это делается с помощью функции PostThreadMessage. Такие сообщения необходимо самостоятельно обрабатывать в петле сообщений, потому что функция DispatchMessage их просто игнорирует.

Рис. 1.6. Блок-схема программы с локальной петлей сообщений


Существуют также широковещательные сообщения, которые посылаются сразу нескольким окнам. Проще всего послать такое сообщение с помощью функции PostMessage, указав в качестве адресата не дескриптор конкретного окна, а константу HWND_BROADCAST. Такое сообщение получат все окна, расположенные непосредственно на рабочем столе и не имеющие при этом владельцев (в терминах системы). Существует также специальная функция BroadcastSystemMessage (начиная с Windows ХР — ее расширенный вариант BroadcastSystemMessageEx), которая позволяет уточнить, каким конкретно окнам будет отправлено широковещательное сообщение.

Кроме параметров wParam и lParam, каждому сообщению приписывается время отправки и координаты курсора в момент возникновения. Соответствующие поля есть в структуре TMsg, которую используют функции GetMessage и DispatchMessage, но у оконной процедуры не предусмотрены параметры для их передачи. Получить время отправки сообщения и координаты курсора при обработке сообщения можно с помощью функций GetMessageTime и GetMessagePos соответственно.

Существует также ряд функций, которые могут обрабатывать сообщения без участия DispatchMessage и оконной процедуры. Если эти функции распознают сообщение, извлеченное из очереди, как "свое", они сами выполняют все необходимые действия по его обработке, и тогда TranslateMessage и DispatchMessage вызывать не нужно. К этим функциям, в частности, относятся следующие:

□ TranslateAccelerator — на основе загруженной из ресурсов таблицы распознает нажатие "горячих" клавиш меню и вызывает оконную процедуру, передавая ей сообщение WM_COMMAND или WM_SYSCOMMAND, аналогичное тому, которое посылается при выборе соответствующего пункта меню пользователем;

□ TranslateMDISysAccel — аналог предыдущей функции за исключением того, что распознает "горячие" клавиши системного меню MDI-окон;

□ IsDialogMessage — распознает сообщения, имеющие особый смысл для диалоговых окон (например, нажатие клавиши <Tab> для перехода между элементами управления). Используется для немодальных диалоговых окон и окон, не являющихся диалоговыми (т.е. созданными без помощи функций CreateDialogXXXX), но требующими аналогичной функциональности.

Перечисленные функции при необходимости вставляются в петлю сообщений. Листинг 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).

Листинг 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, которое позволит выполнять фоновые действия. ...



Все права на текст принадлежат автору: А Б Григорьев.
Это короткий фрагмент для ознакомления с книгой.
О чём не пишут в книгах по DelphiА Б Григорьев