Антон Григорьев дата публикации 22-09-1999 00:00 Основы работы с Win API в VCL-приложениях
Более полный вариант этой статьи вошёл в книгу "О чём не пишут в книгах по Delphi"
В данной статье будет говориться о том, как совместить использование Win API (Application Programming Interface) и компоненты VCL. Предполагается, что читатель владеет основными методами создания приложений с помощью VCL, поэтому на них мы останавливаться не будем. Написание приложений исключительно с помощью Win API также не является предметом данной статьи, т.к. подобные примеры легко найти в документации Microsoft и во многих книгах. Статья ориентирована на человека, который уже умеет писать приложения с помощью VCL Delphi и хочет научиться расширять их функциональность с помощью API. Так как примеры использования API, а также справка, предполагают использование C или C++, и это может вызвать трудности у человека, знакомого только с Delphi, здесь также будет уделено внимание тому, как правильно читать справку и переводить содержащийся в ней код с C/C++ на Delphi.
К статье прилагаются примеры – файл WinAPISamples.zip. Эти примеры сопровождаются комментариями, которые содержат дополнительные сведения, отсутствующие в основной статье, поэтому рекомендуется прочитать не только текст статьи, но и текст комментариев к примерам. Все примеры проверены на работоспособность в Delphi 5-ой, 6-ой и 7-ой версий.
Windows API - это набор функций, предоставляемых операционной системой каждой программе. Эти функции находятся в стандартных динамически компонуемых библиотеках (Dynamic Linked Library, DLL), таких как kernel32.dll, user32.dll, gdi32.dll. Эти файлы находятся в системной директории Window. Вообще говоря, каждая программа должна самостоятельно заботится о том, чтобы подключить эти библиотеки. DLL могут подключаться к программе статически и динамически. В первом случае связь с библиотекой прописывается в исполняемом файле программы, и система при запуске этой программы сразу же загружает в её адресное пространство и библиотеку. Если требуемая библиотека на диске не найдена, запуск программы будет невозможен. В случае динамического подключения программа загружает библиотеку в любой удобный ей момент с помощью функции LoadLibrary. Если при этом возникает ошибка из-за того, что библиотека не найдена на диске, программа может самостоятельно решить, как на это реагировать.
Статическая загрузка проще динамической, но динамическая гибче. При использовании динамической загрузки программист может, во-первых, выгрузить библиотеку, не дожидаясь окончания работы программы. Во-вторых, динамическая загрузка позволяет программе продолжить работу, даже если библиотека не найдена. В-третьих, динамическая загрузка позволяет загружать DLL, имена которых неизвестны на момент компиляции. Это позволяет использовать динамически загружаемые 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, если она не вызывает эту функцию.
Для тех, кто решил использовать Win API, самым необходимым инструментом становится какая-либо документация по этим функциям. Их так много, что запомнить все совершенно нереально, поэтому работа без справочника под рукой просто невозможна.
В комплект поставки Delphi входит справочная система, содержащая описание функций Win API. Использование этой справки не настолько удобное, как по стандартным функциям Delphi. Если набрать в редакторе Delphi имя какой-нибудь функции Win API, поставить курсор в начало этой функции и нажать F1, то откроется справка по ней, как и в случае обычных функций и процедур. Однако функции Win API не появляются в предметном указателе справочной системы Delphi. Это объясняется ограничениями, накладываемыми самой справочной системой WinHelp, используемой в Delphi. Чтобы иметь возможность пользоваться предметным указателем, нужно открыть файл MSTools.hlp. В ранних версиях Delphi он находится в директории $(Delphi)\Help, в более поздних его надо искать в $(Program Files)\Common Files.
Справка, поставляемая вместе со старыми версиями Delphi, во многих отношениях уже не актуальна - она содержит информацию на тот момент, когда уже вышла Windows 95, но ещё не было Windows NT 4. Поэтому про многие функции, прекрасно работающие в NT 4, там написано, что в Windows NT они не поддерживаются, т.к. в более ранних версиях они действительно не поддерживались. В справке, поставляемой с Delphi 7 (и, возможно, с некоторыми более ранними версиями), эта информация подправлена, но даже в этой справке отсутствуют функции, которые появились только в Windows NT 4 (как, например, CoCreateInstanceEx). И уж конечно, бесполезно искать в этой справке информацию о функциях, появившихся в Windows 98, 2000, XP.
Полноценной справкой по всем функциям API является Microsoft Developer's Network (MSDN). Это отдельная справочная система, не входящая в комплект поставки Delphi. MSDN можно приобрести отдельно или воспользоваться online-версией, находящейся по адресу http://msdn.microsoft.com (доступ к информации свободный, регистрация не требуется). MSDN содержит информацию не только об API, но и всё, что может потребоваться программисту, использующему различные средства разработки от Microsoft. Кроме справочного материала, MSDN включает в себя спецификации стандартов и технологий, использующихся в Windows, статьи из журналов, посвящённых программированию, главы из некоторых книг. И вся эта информация крайне полезна разработчику. Кроме того, MSDN постоянно обновляется, информация в нём наиболее актуальна.
Таким образом, по содержанию MSDN настолько превосходит справку по API, поставляемую с Delphi, что их даже сравнивать не совсем уместно. Достоинства справки по сравнению с MSDN - то, что она поставляется вместе с Delphi, и то, что она может вызываться непосредственно из среды с помощью F1. Но информация в ней настолько устарела, что порекомендовать её можно только человеку, использующему API лишь изредка и для решения простейших задач. А для того, кто собирается серьёзно заниматься API, даже вопрос такой не может стоять - что предпочесть. Безусловно, MSDN.
Отметим, что MSDN содержит также описание функций операционной системы Windows CE. Интерфейс WinCE API на первый взгляд очень похож на Win API, но различия между ними есть, и иногда весьма значительные. Поэтому при использовании MSDN не следует выбирать раздел API Reference - он целиком посвящён WinCE API.
Система Windows написана на C++, поэтому все описания функций Win API, а также примеры их использования приведены на этом языке (это касается как MSDN, так и справки, поставляемой с Delphi). Во-первых, необходимо разобраться с типами. Большинство типов, использующихся в Win API, определены в Delphi. Соответствие между типами показано в таблице.
Тип Win 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" - он означает, что это указатель на константу. В С++ возможно объявление таких указателей, которые указывают на константное содержимое, т.е. компилятор разрешает это содержимое читать, но не модифицировать. В Delphi такие указатели отсутствуют, и при портировании эти типы заменяются обычными указателями, т.е. префикс "C" игнорируется.
Типы PVOID и LPVOID соответствуют нетипизированным указателям (Pointer).
Для передачи символов чаще всего используется тип TCHAR. Windows поддерживает две кодировки - ANSI (1 байт на символ) и Unicode (2 байта на символ; о поддержке Unicode в Windows мы будем говорить ниже). Тип CHAR соответствует символу в кодировке ANSI, WCHAR - Unicode. Там, где используется ANSI, тип TCHAR эквивалентен типу CHAR, там, где Unicode - WCHAR. В Delphi нет аналога типу TCHAR, программист сам должен следить за тем, какой тип требуется в данном месте.
Строки в Win API передаются как указатели на цепочку символов, завершающихся нулём. Поэтому указатель на TCHAR может означать указатель как на единичный символ, так и на строку. Чтобы было легче разобраться, где какой указатель, в Win API есть типы LPTCHAR и LPTSTR. Они эквивалентны друг другу, но первый принято использовать там, где требуется указатель на одиночный символ, а второй - на строку. Если строка передаётся в функцию только для чтения, обычно используется указатель на константу, т.е. тип LPCTSTR. В Delphi это соответствует PChar для ANSI и PWideChar для Unicode.
Большинство названий типов из левой части таблицы в целях совместимости описаны в модуле Windows, поэтому их можно использовать наравне с обычными типами Delphi. Кроме этих типов общего назначения существуют ещё специальные. Например, дескриптор окна имеет тип HWND, первый параметр сообщения - тип WPARAM (в старых 16-разрядных Windows он был эквивалентен типу Word, в 32-разрядных - LongInt). Эти специальные типы также описаны в модуле Windows.
Записи (record) в C/C++ называются структурами и обозначаются словом struct. Из-за особенностей описания структур на языке C структуры в Win API получают два имени: одно основное имя, составленное из заглавных букв, которое затем и используется, и одно вспомогательное, получающееся из основного добавлением префикса tag. Начиная с четвёртой версии Delphi приняты следующие правила именования таких типов: простое и вспомогательное имя остаются без изменений, и ещё добавляется новое имя, получающееся из основного добавлением общеупотребительного в Delphi префикса "T". Например, в функции CreatePenIndirect один из параметров имеет тип LOGPEN. Это основное имя данного типа, а вспомогательное - tagLOGPEN. Соответственно, в модуле Windows определена запись tagLOGPEN и её синонимы - LOGPEN и TLogPen. Эти три идентификатора в Delphi взаимозаменяемы. Вспомогательное имя используется редко, программисты, в зависимости от личных предпочтений, используют либо основное имя типа, либо имя с префиксом "T".
Описанные здесь правила именования типов могут внести некоторую путаницу при использовании VCL. Например, для описания растра в Win API определен тип BITMAP (он же - tagBITMAP). В Delphi соответствующий тип имеет ещё одно имя - TBitmap. Но такое же имя имеет класс TBitmap, описанный в модуле Graphics. В коде, который Delphi создаёт автоматически, модуль Graphics стоит в списке uses после модуля Windows, поэтому идентификатор TBitmap воспринимается компилятором как Graphics.TBitmap, а не как Windows.TBitmap. Чтобы использовать Windows.TBitmap, нужно явно указать имя модуля или воспользоваться одним из альтернативных имён.
В более ранних версиях Delphi были другие правила именования типов. Например, в Delphi 2 был тип BITMAP, но не было TBitmap и tagBITMAP, а в Delphi 3 из этих трёх типов был только TBitmap.
Все структуры в Win API описаны без выравнивания, т.е. компилятор не вставляет между полями неиспользуемые байты, чтобы границы полей приходились на начало двойного слова, поэтому в Delphi при описании соответствующих структур используется слово packed, запрещающее выравнивание.
При описании структур Win API можно иногда встретить ключевое слово union (см., например, структуру in_addr). Объединение нескольких полей с помощью этого слова означает, что все они будут размещены по одному адресу. В Delphi это соответствует вариантным записям (см. использование case в record). Объединения в C/C++ гибче, чем вариантные записи Delphi, т.к. позволяют размещать вариантную часть в любом месте структуры, а не только в конце. При переносе таких структур в Delphi иногда приходится вводить дополнительные типы.
Теперь о синтаксисе описания самой функции в C++. Оно имеет вид
<Тип функции> <Имя функции> '('
<Тип параметра> <Имя параметра>
{','<Тип параметра> <Имя параметра>}
')';
Еще в C/C++ различается верхний и нижний регистр, поэтому идентификаторы HDC, hdc, hDC и т. д. - разные идентификаторы (автор С очень любил краткость и хотел, чтобы можно было делать не 26, а 52 переменные с именем из одной буквы). Поэтому часто можно встретить, что имя параметра и его тип совпадают с точностью до регистра. К счастью, при описании функции в Delphi мы не обязаны сохранять имена параметров, значение имеют лишь их типы и порядок следования. С учётом всего этого функция, описанная в справке как
HMETAFILE CopyMetaFile(HMETAFILE hmfSrc, LPCTSTR lpszFile);
в Delphi имеет вид
function CopyMetaFile(hmfSrc: HMETAFILE; lpszFile: LPCTSTR): HMETAFILE;
или, что то же самое,
function CopyMetaFile(hmfSrc: HMetaFile; lpszFile: PChar): HMetaFile;
В дальнейшем мы будем изменять регистр символов в идентификаторах так, чтобы они были наиболее удобны для чтения, т.е., например, вместо HMETAFILE писать HMetaFile.
Примечание: компилятор Delphi допускает, чтобы имя параметра процедуры или функции совпадало с именем типа, поэтому мы в дальнейшем увидим, что иногда имя параметра и его тип совпадают, только записываются в разном регистре, чтобы прототип функции на Delphi максимально соответствовал исходному прототипу на C/C++. При использовании этой возможности компилятора следует учитывать, что соответствующий идентификатор внутри функции будет рассматриваться как имя переменной, а не типа, поэтому, например, объявить локальную переменную данного типа уже не удастся.
Несколько особняком стоит тип VOID. Если функция имеет такой тип, то в Паскале она описывается как процедура. Если вместо параметров у функции в скобках стоит VOID, это означает, что функция не имеет параметров. Например, функция
VOID CloseLogFile(VOID);
в Delphi описывается как
procedure CloseLogFile;
Не путайте VOID и PVOID. PVOID - это нетипизированный указатель, соответствующий типу Pointer.
В тех случаях, когда тип параметра является указателем на другой тип (обычно начинается с букв LP), при описании этой функции в Delphi можно пользоваться параметром-переменной, так как в этом случае функции передаётся указатель. Например, функция
int GetRgnBox(HRGN hrgn, LPRECT lprc);
в файле Windows.pas описана
function GetRgnBox(RGN: HRGN; var p2: TRect): Integer;
Такая замена целесообразна в том случае, если значение параметра не может быть нулевым указателем, потому что при использовании var передать такой указатель будет невозможно. Нулевой указатель в C/C++ обозначается константой NULL. NULL и 0 в этих языках взаимозаменяемы, поэтому в справке можно и про целочисленный параметр встретить указание, что он может быть равен NULL.
И, наконец, если не удаётся понять, как функция, описанная в справке, должна быть переведена на Паскаль, можно попытаться найти описание этой функции в исходных текстах модулей, поставляемых вместе с Delphi. Эти модули находятся в директории $(DELPHI)\Source\RTL\Win. Можно также воспользоваться подсказкой, которая всплывает в редакторе Delphi после того, как будет набрано имя функции.
Если посмотреть справку, например, по функции GetSystemMetrics, то видно, что эта функция должна иметь один целочисленный параметр. Однако далее в справке предлагается при вызове этой функции подставлять в качестве параметра не числа, а SM_ARRANGE, SM_CLEANBOOT и т. д. Подобная ситуация и со многими другими функциями Win API. Все эти SM_ARRANGE, SM_CLEANBOOT и т. д. являются именами числовых констант. Эти константы описаны в том же модуле, в котором описана функция, использующая их, поэтому можно не выяснять численные значения этих констант, а указывать при вызове функций их имена, например, GetSystemMetrics(SM_Arrange); Если по каким-то причинам всё-таки потребовалось выяснить численные значения, то в справочной системе их искать не стоит - их там нет. Их можно узнать из исходных текстов модулей Delphi, в которых эти константы описаны. Так, например, просматривая Windows.pas, можно узнать, что SM_ARRANGE = 56.
В справке, поставляемой вместе с Delphi, в описании многих функций Win API вверху можно увидеть три ссылки: QuickInfo, Overview и Group. Первая даёт краткую информацию о функции: какой библиотекой реализуется, в каких версиях Windows работает и т.п. (напоминаю, что к информации о версиях в этой справке нужно относиться очень критично). Overview - это обзор какой-то большой темы. Например, для любой функции, работающей с растровыми изображениями, обзор будет объяснять, зачем в принципе нужны эти самые растровые изображения и как они устроены. Страница, на которую ведёт ссылка Overview, обычно содержит весьма лаконичные сведения, но, нажав кнопку >>, расположенную в верхней части окна, можно получить продолжение обзора. И, наконец, Group. Эта ссылка приводит к списку всех функций, родственных данной. Например, для функции CreateRectRgn группу будут составлять все функции, имеющие отношение к регионам. Если теперь нажимать на кнопку << будут появляться страницы с кратким описанием возможных применений объектов, с которыми работают функции (в приведённом примере - описание возможностей регионов). Чтобы читать их в нормальной последовательности, лучше всего нажать на << столько раз, сколько возможно, а затем пойти в противоположном направлении с помощью кнопки >>.
MSDN, естественно, предоставляет больше полезной информации. В нижней части описания каждой функции есть раздел Requirements, в котором написано, какая библиотека и какая версия Windows требуется для её использования. В самом низу описания функции расположены ссылки "See also". Первая ссылка - обзор соответствующей темы (например, для уже упоминавшейся функции CreateRectRgn она называется Regions Overview). Вторая - список родственных функций (Region Functions в данном случае). Она ведёт на страницу, где перечислены все функции, родственные выбранной. После этих двух обязательных ссылок идут ссылки на описание функций и типов, которые обычно используются совместно с данной функцией.
Основные типы, константы и функции Win API объявлены в модулях Windows и Messages. Но многие функции объявлены в других модулях, которые не подключаются к программе по умолчанию, программист должен сам выяснить, в каком модуле находятся требуемый ему идентификатор, и подключить этот модуль. Ни справка, поставляемая с Delphi, ни MSDN, разумеется, не могут дать необходимую информацию. Чтобы выяснить, в каком модуле объявлен нужный идентификатор, можно воспользоваться поиском по файлам *.pas, находящимся в папке $(DELPHI)\Source\RTL\Win. Этим методом можно, например, выяснить, что весьма популярная функция ShellExecute находится в модуле ShellAPI, а CoCreateInstance - в модуле ActiveX (а также в модуле Ole2, оставленном для совместимости со старыми версиями Delphi).
Ещё несколько слов о числовых константах. В справке можно встретить числа вида, например, 0xC56F или 0x3341. Префикс "0x" в Си означает шестнадцатеричное число. В Delphi надо его заменить на "$", то есть вышеназванные числа должны быть записаны как $C56F и $3341 соответственно.
Программируя в Delphi, мы быстро привыкаем к тому, что каждый объект реализуется экземпляром соответствующего класса. Например, кнопка реализуется экземпляром класса TButton, контекст устройства - классом TCanvas. Но когда создавались первые версии Windows, объектно-ориентированный метод программирования ещё не был общепризнанным, поэтому он не был реализован. Современные версии Windows частично унаследовали этот недостаток, поэтому в большинстве случаев приходится работать по старинке, тем более что DLL могут экспортировать только функции, но не классы. Поэтому, когда мы будем говорить об объектах, создаваемых через Win API, мы будем иметь ввиду не объекты в терминах ООП, а некоторую сущность, внутренняя структура которой скрыта от нас, поэтому с этой сущностью мы можем оперировать только как с единым и неделимым (атомарным) объектом.
Каждому объекту, созданному с помощью Win API, присваивается уникальный номер (дескриптор). Конкретное значение этого дескриптора не несёт для программиста никакой полезной информации и может быть использовано только для того, чтобы при вызове функций из Win API указывать, с каким объектом требуется выполнить операцию. В большинстве случаев дескрипторы представляют собой 32-значные числа, а значит, их можно передавать везде, где требуются такие числа. В дальнейшем мы увидим, что Win API несколько вольно обращается с типами, т.е. один и тот же параметр в различных ситуациях может содержать и число, и указатель, и дескриптор, поэтому знание двоичного представления дескриптора всё-таки приносит программисту пользу (хотя если бы система Windows была "спроектирована по правилам", тип дескриптора не должен был бы интересовать программиста).
Таким образом, главное различие между методами класса и функциями Win API заключается в том, что первые связаны с тем экземпляром класса, через который они вызываются, и поэтому не требуют явного указания на объект. Вторым необходимо указание объекта через его дескриптор, так как они сами по себе никак не связаны ни с одним объектом.
Не следует думать, что при работе с Win API следует полностью отказываться от VCL. Эти методы прекрасно работают вместе. Правда, внутренние механизмы VCL не могут включиться, если изменение объекта происходит через Win API. Например, если спрятать окно не с помощью метода Hide, а с помощью вызова функции Win API ShowWindow(Handle, SW_Hide), не возникнет событие OnHide, потому что оно запускается теми самыми внутренними механизмами VCL. Но такие недоразумения случаются обычно только тогда, когда функциями Win API дублируется то, что можно сделать и с помощью VCL. Дескриптор системного объекта, оболочкой которого является класс VCL, обычно хранится в свойстве Handle.
В некоторых случаях класс Delphi инкапсулирует несколько объектов Windows. Например, класс TBitmap включает в себя HBitmap и HPalette - картинку и палитру к ней. Соответственно, он хранит два дескриптора: в свойствах Handle и Palette.
Все экземпляры классов, созданные в Delphi, должны удаляться. В некоторых случаях это происходит автоматически, в некоторых программист должен сам позаботиться о "выносе мусора". Аналогичная ситуация и с объектами, создаваемыми в Win API. Если посмотреть справку по функции, создающей какой-то объект, то там обязательно будет информация о том, какой функцией можно удалить объект и может ли система сделать это автоматически. Во многих случаях совершенно разные объекты могут удаляться одной и той же функцией. Так, функция DeleteObject удаляет косметические карандаши, геометрические карандаши, кисти, шрифты, регионы, растровые изображения и палитры. Обращайте внимание на возможные исключения. Например, регионы не удаляются системой автоматически, однако если вызвать для региона функцию SetWindowRgn, то этот регион переходит в собственность операционной системы. Никакие дальнейшие операции с ним, в том числе и удаление, совершать нельзя.
Если системный объект используется только одним приложением, то он будет удалён при завершении работы приложения. Тем не менее, хороший стиль программирования требует, чтобы программа удаляла объекты явно, а не полагалась на систему.
Под словом "Окно" обычно подразумевается некоторая форма наподобие тех, что можно создать с помощью класса TForm. Однако это понятие существенно шире. В общем случае окном называется любой объект, который имеет экранные координаты и может реагировать на мышь и клавиатуру. Например, кнопка, которую можно создать с помощью класса TButton, - это тоже окно.
VCL вносит некоторую путаницу в это понятие. Часть визуальных компонентов VCL не является окнами, а только имитируют их, как, например, TImage. Это позволяет экономить ресурсы системы и повысить быстродействие программы. Механизм этой имитации мы рассмотрим позже, а пока следует запомнить, что окнами являются только те визуальные компоненты, которые имеют в числе предков класс TWinControl. Разработчики VCL постарались, чтобы разница между оконными и неоконными визуальными компонентами была минимальна. Действительно, на первый взгляд неоконный TLabel и оконный TStaticText кажутся практически близнецами. Разница становится заметной тогда, когда используется Win API. С неоконными компонентами можно работать только средствами VCL, они даже не имеют свойства Handle, в то время как к оконным компонентам можно взаимодействовать с помощью Win API.
Отметим также ещё одно различие между оконными и неоконными компонентами: неоконные компоненты рисуются непосредственно на поверхности родительского компонента, в то время как оконные как бы кладутся на родителя сверху. В частности, это означает, что неоконный TLabel, размещённый на форме, не может закрывать собой часть кнопки TButton, потому что TLabel рисуется на поверхности формы, а кнопка - это независимый объект, лежащий на форме и имеющий свою поверхность. А TStaticText может оказаться над кнопкой, потому что он тоже находится над формой.
Каждое окно принадлежит к какому-то оконному классу. Не следует путать оконный класс с классами 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 содержит дескриптор самой библиотеки, а MainInstance - загрузившего её главного модуля.
Каждое окно в Windows привязывается к какому-либо модулю (в Windows 9x/ME необходимо явно указать дескриптор этого модуля, NT/2000/XP определяет модуль, из которого вызвана функция создания окна, автоматически). Соответственно, оконные классы делятся на локальные и глобальные: окна локальных классов может создавать только тот модуль, в котором находится оконная процедура класса, глобальных - любой модуль данного приложения. Будет ли класс локальным или глобальным, зависит от значений полей TWndClassEx при регистрации класса.
Оконный класс, к которому принадлежит окно, указывается при его создании. Это может быть зарегистрированный ранее класс или один из системных классов. Системные классы - это 'BUTTON', 'COMBOBOX', 'EDIT', 'LISTBOX', 'MDICLIENT', 'SCROLLBAR' и 'STATIC'. Назначение этих классов понятно из их названий (класс 'STATIC' реализует статические, то есть не реагирующие на мышь и клавиатуру, но имеющие дескриптор элементы, текстовые или графические). Кроме этих классов существуют также классы из библиотеки ComCtl32.dll, они тоже доступны всем приложениям без предварительной регистрации (подробнее об этих классах можно узнать в MSDN, в разделе Common Controls Reference).
Для окон в обычном понимании этого слова готовых классов не существует, их приходится регистрировать самостоятельно. В частности, VCL для форм регистрирует оконные классы, имена которых совпадают с именами соответствующих классов VCL.
Кроме имени, класс включает в себя другие параметры, такие как стиль, кисть и т. д. Они подробно перечислены в справке.
Для создания окна используются функции CreateWindow и CreateWindowEx. При создании окна в числе других параметров задаётся модуль, к которому оно привязано, имя оконного класса, стиль и расширенный стиль. Последние два параметра определяют поведение конкретного окна и не имеют ничего общего со стилем класса. Результатом работы этих функций является дескриптор созданного ими окна.
Ещё один важный параметр этих функций - дескриптор родительского окна. Окно является подчинённым по отношению к своему родителю. Например, если дочернее окно - это кнопка или иной элемент управления, визуально оно располагается в другом окне, которое является для него родительским. Если дочернее окно - это MDIChild, родительским для него будет MDIForm. Другими словами, отношения "родительское-дочернее окно" отражают принадлежность одного окна другому, визуальную связь между ними. Окна, родитель которых не задан (т.е. в качестве дескриптора родителя передан ноль), располагаются непосредственно на рабочем столе. Если при создании окна задан стиль WS_Child, его координаты отсчитываются от левого верхнего угла клиентской области родительского окна, и при перемещении родительского окна все дочерние окна будут перемещаться вместе с ним. Окно, имеющее стиль WS_Child, не может располагаться на рабочем столе - попытка создать такое окно окончится неудачей.
Визуальные компоненты Delphi имеют два свойства, которые иногда путают: Owner и Parent. Свойство Parent указывает на объект, реализующий окно, являющееся родительским для данного визуального компонента (компоненты, не являющиеся наследником TWinControl, также имеют это свойство - VCL для них имитирует эту взаимосвязь, однако сами они не могут быть родителями других визуальных компонентов). Свойство Owner указывает на владельца компонента. Отношения "владелец-принадлежащий" реализуются полностью внутри VCL. Свойство Owner есть у любого наследника TComponent, в т.ч. и у невизуального, и владельцем других компонентов также может быть невизуальный компонент (например, TDataModule). При уничтожении компонента он автоматически уничтожает все компоненты, владельцем которых он является (здесь, впрочем, есть некоторое дублирование функций, т.к. оконный компонент также при уничтожении уничтожает все визуальные компоненты, родителем которых он является). Ещё владелец отвечает за загрузку всех установленных в design-time свойств принадлежащих ему компонентов.
Свойство Owner доступно только для чтения. Владелец компонента задаётся один раз при вызове конструктора и остаётся неизменным на протяжении всего жизненного цикла компонента. Свойство Parent задаётся отдельно и может быть впоследствии изменено (визуально это будет выглядеть как "перепрыгивание" компонента из одного окна в другое).
Визуальный компонент может не иметь владельца - это означает, что ответственность за его удаление лежит на программисте, создавшем его. Но большинство визуальных компонентов не может функционировать, если свойство Parent не задано. Например, невозможно создать компонент TButton, у которого не установлено свойство Parent. Это связано с тем, что большинство оконных компонентов имеет стиль WS_Child, который, напомню, не позволяет разместить окно на рабочем столе. Окнами без родителя могут быть только наследники TCustomForm.
Впрочем, сделать кнопку, не имеющую родителя, можно средствами Win API. Например, такой командой:
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 и в терминах системы мы в дальнейшем всегда будем оговаривать, в каком смысле будет использовано слово "владелец".
Параметры, которые оконные компоненты VCL передают функции CreateWindowEx, устанавливаются в методе TWinControl.CreateParams. Это виртуальный метод, и наследник при необходимости может перекрыть его и изменить эти параметры.
Создание окон через Win API требует кропотливой работы. VCL справляется с этой задачей замечательно, поэтому создавать окна самостоятельно приходится только тогда, когда использование VCL нежелательно, например, если необходимо написать как можно более компактное приложение. Во всех остальных случаях приходится только слегка подправлять работу VCL. Например, с помощью Win API можно изменить форму окна или убрать из него заголовок, оставив рамку. Подобные действия не требуют от программиста создания нового окна, можно воспользоваться тем, что уже создано VCL.
Другой случай, когда могут понадобиться функции Win API для окон - если приложение должно что-то делать с чужими окнами. Например, хотя бы просто перечислить все окна, открытые в данный момент, как это делает WinSight32. Но в этом случае также не приходится самому создавать окна, работа идёт с уже имеющимися.
Прежде чем двигаться дальше, необходимо разобраться с тем, что такое функции обратного вызова (callback functions; этот термин иногда также переводят "функции косвенного вызова"). Эти функции в программе описываются, но обычно не вызываются напрямую, хотя ничто не запрещает сделать это. В этом они похожи на те методы класса, которые связаны с событиями. Ничто не мешает вызывать напрямую, например, метод FormCreate, но делать это приходится крайне редко. С другой стороны, даже если этот метод не вызывается явно, он всё равно выполняется, потому что VCL автоматически вызывает его без прямого указания программиста. Еще одно общее свойство - конкретное имя метода при косвенном вызове не важно. Можно изменить его, но если этот метод по-прежнему будет связан с событием OnCreate, он так же будет успешно вызываться. Разница заключается только в том, что такие методы вызываются внутренними механизмами VCL, а функции обратного вызова - самой системой Windows. Соответственно, на эти функции налагаются следующие требования: во-первых, эти функции должны быть именно функциями, а не методами класса; во-вторых, эти функции должны быть написаны в соответствии с моделью вызова stdcall (справочная система предлагает использовать модель 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);
Функции с таким именем не существует в Win API. Это так называемый прототип функции, согласно которому следует описывать функцию обратного вызова. На самом деле этот прототип предоставляет большую свободу, чем это может показаться на первый взгляд. Во-первых, имя может быть любым. Во-вторых, система не накладывает строгих ограничений на имена и типы параметров - они могут быть любыми, при условии что новые типы совпадают по размерам с теми, которые указываются (тип TFNWndEnumProc, описанный в модуле Windows - это не процедурный тип, а просто нетипизированный указатель, поэтому компилятор Delphi не будет контролировать соответствие передаваемой функции обратного вызова её прототипу). Что касается типа функции и типа первого параметра, то они имеют определённый смысл, и изменение их типа вряд ли может быть полезным. Но второй параметр предназначен специально для передачи значения, которое программист волен использовать по своему усмотрению, система просто передаёт через него в функцию обратного вызова то значение, которое имел параметр lParam при вызове функции EnumWindows. А программисту может показаться удобнее работать не с типом LPARAM (то есть LongInt), а, например, с указателем или же с массивом из четырёх байт. Лишь бы были именно четыре байта, а не восемь, шестнадцать или ещё какое-то число. Можно даже превратить этот параметр в параметр-переменную, так как при этом функции будут передаваться всё те же четыре байта - адрес переменной. Впрочем, тем, кто не очень хорошо разбирается с тем, как используется стек для передачи параметров при различных моделях вызова, лучше не экспериментировать с изменением типа параметра, а строго следовать заявленному прототипу, при необходимости выполняя требуемые преобразования внутри функции обратного вызова.
Функция EnumWindows работает так: после вызова она начинает по очереди перебирать все имеющиеся в данный момент окна верхнего уровня, т.е. те, у которых нет родителя. Для каждого такого окна вызывается заданная функция обратного вызова, в качестве первого параметра ей передаётся дескриптор данного окна (каждый раз, естественно, новый), в качестве второго - то, что было передано самой функции EnumWindows в качестве второго параметра (каждый раз одно и то же). Получая по очереди дескрипторы всех окон верхнего уровня, функция обратного вызова может выполнить с каждым из них определённое действие (закрыть, минимизировать и т.п.). Или можно проверять все эти окна на соответствие какому-то условию, пытаясь найти нужное. А значение, возвращаемое функцией обратного вызова, влияет на работу EnumWindows. Если она возвращает False, значит, всё, что нужно, уже сделано, можно не перебирать остальные окна.
Окончательный код для того случая, когда второй параметр имеет тип Pointer, выглядит так:
function MyCallbackFunction(Wnd: HWnd; P: Pointer):Bool; stdcall;
begin
end;
..............
var MyPointer:Pointer;
..............
EnumWindows(@MyCallbackFunction, LongInt(MyPointer));
Что бы мы ни делали с типом второго параметра функции обратного вызова, тип соответствующего параметра EnumWindows не меняется. Поэтому необходимо явное приведение передаваемого параметра к типу LongInt. Обратное преобразование типов при вызове MyCallbackFunction осуществляется автоматически.
Использование EnumWindows и функций обратного вызова демонстрируется примером EnumWnd.
Отметим, что функции обратного вызова будет вызываться до того, как завершит работу функция EnumWindows. Однако это не является распараллеливанием работы. Чтобы проиллюстрировать это, рассмотрим ситуацию, когда программа вызывает некоторую функцию A, которая, в свою очередь, вызывает функцию B. Функция B, очевидно, начнёт свою работу до того, как завершит работу функция A. То же самое произойдёт и с функцией обратного вызова, переданной в EnumWindows: она будет вызываться из кода EnumWindows так же, как и функция B из кода функции A. Поэтому код функции обратного вызова получит управление (и не один раз, т.к. EnumWindows будет вызывать эту функцию в цикле) до завершения работы EnumWindows.
Однако это правило действует не во всех ситуациях. В некоторых случаях система запоминает адрес переданной ей функции обратного вызова, чтобы использовать её потом. Примером такой функции является оконная процедура: её адрес передаётся системе один раз при регистрации класса, и затем система многократно вызывает эту функцию в случае необходимости.
В 16-разрядных версиях Windows вызов функций обратного вызова осложнялся тем, что для них необходимо было делать специальный код, называемый прологом. Пролог создавался с помощью функции MakeProcInstance, удалялся после завершения с помощью FreeProcInstance. То есть вызов EnumWindows должен был бы выглядеть так:
var MyProcInstnace:TFarProc;
...................
MyProcInstance:=MakeProcInstance(@MyCallbackFunction,HInstance);
EnumWindows(MyProcInstance,LongInt(MyPointer));
FreeProcInstance(MyProcInstance);
В Delphi этот код будет работоспособным, так как для совместимости MyProcInstance и FreeProcInstance оставлены. Но они ничего не делают (в чём легко убедиться, просмотрев исходный файл Windows.pas), поэтому можно обойтись и без них. Другой способ, с помощью которого в 16-разрядных версиях можно сделать пролог - описать функцию с директивой export. Эта директива сохранена для совместимости и в Delphi, но в 32-разрядных версиях она также ничего не делает (несмотря на то, что справка, например, по Delphi 3 утверждает обратное; в справке по Delphi 4 этой ошибки уже нет).
Человеку, знакомому с Delphi, должна быть ясна схема событийного управления. Программист пишет только методы реакции на различные события, а затем этот код получает управление тогда, когда соответствующее событие произойдёт. Простые программы в Delphi состоят исключительно из методов реакции на события (например, OnCreate, OnClick, OnCloseQuery). Причём событием называется не только событие в обычном смысле этого слова, т.е. когда происходит что-то внешнее, но и ситуация, когда событие используется просто для передачи управления коду, написанному разработчиком программы, в тех случаях, когда VCL не может сама справиться с какой-то задачей. Примером такого события является TListBox.OnDrawItem. Устанавливая стиль списка в lbOwnerDrawFixed или lbOwnerDrawVariable, программист указывает, что ему требуется нестандартный вид элементов списка, поэтому их рисование он берёт на себя. И каждый раз, когда возникает необходимость в рисовании элемента, VCL передаёт управление специально написанному коду. На самом деле разница между двумя типами событий весьма условна. Можно так же сказать, что когда пользователь нажимает клавишу, VCL не знает, что делать, и поэтому передаёт управление обработчику OnKeyPress.
Событийное управление не есть изобретение авторов Delphi. Такой подход использует сама система Windows. Только здесь события называются сообщениями (message), что иногда даже лучше отражает ситуацию. Windows посылает программе сообщения, связанные либо с тем, что произошло внешнее событие (мышь, клавиатура…), либо с тем, что самой системе потребовались от программы какие-то действия. Самым распространённым таким действием является предоставление информации. Например, при необходимости узнать текст заголовка окна Windows посылает этому окну специальное сообщение, в ответ на которое окно должно сообщить системе свой заголовок. Ещё бывают сообщения, которые просто уведомляют программу о начале какого-то действия (например, о начале перетаскивания окна) и предоставляют возможность вмешаться. Но это вмешательство необязательно.
В Delphi для реакции на каждое событие обычно создаётся свой метод. В Windows одна процедура, называемая оконной, обрабатывает все сообщения, адресованные конкретному окну. В C/C++ нет понятия "процедура", поэтому при использовании Delphi может возникнуть путаница. Дело в том, что то, что называется оконной процедурой, на самом деле является функцией. Тем не менее, мы будем использовать общепринятый термин "оконная процедура". Каждое сообщение имеет свой уникальный номер, а оконная процедура обычно целиком состоит из оператора case, и каждому сообщению соответствует своя альтернатива этого оператора. Номера сообщений знать не обязательно, потому что можно использовать константы, описанные в модуле Messages. Эти константы начинаются с префикса, указывающего на принадлежность сообщения к какой-то группе. Например, сообщения общего назначения начинаются с WM_: например, WM_Paint, WM_GetTextLength. Сообщения, специфичные, например, для кнопок, начинаются с префикса 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 (реже - PickMessage) и передаются в функцию DispatchMessage. Эта функция определяет, какому окну предназначено сообщение, и вызывает его оконную процедуру. Таким образом, простейший цикл обработки сообщений выглядит так:
var Msg:TMsg;
...
while GetMessage(Msg, 0, 0, 0) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg)
end;
Функция 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 для передачи их требуемому окну. В этом случае сообщения будут извлекаться из очереди и обрабатываться до завершения работы обработчика.
В очередь можно поставить сообщение, не привязанное ни к какому окну. Это делается с помощью функции PostThreadMessage. Такие сообщения необходимо самостоятельно обрабатывать в петле сообщений, потому что функция DispatchMessage их просто игнорирует.
Существуют также широковещательные сообщения, которые посылаются сразу нескольким окнам. Проще всего послать такое сообщение с помощью функции PostMessage, указав в качестве адресата не дескриптор конкретного окна, а константу HWnd_Broadcast. Такое сообщение получат все окна, расположенные непосредственно на рабочем столе и не имеющие при этом владельцев (в терминах системы). Существует также специальная функция BroadcastSystemMessage (а начиная с Windows XP - её расширенный вариант BroadcastSystemMessageEx), которая позволяет более гибко определить, каким именно окнам будет отправлено широковещательное сообщение.
Кроме параметров WParam и LParam, каждому сообщению приписывается время отправки и координаты курсора в момент возникновения. Соответствующие поля есть в структуре TMsg, которую используют функции GetMessage и DispatchMessage, но у оконной процедуры не предусмотрены параметры для их передачи. Получить время оправки сообщения и координаты курсора при обработке сообщения можно с помощью функций GetMessageTime и GetMessagePos соответственно.
Существует также ряд функций, которые могут обрабатывать сообщения без участия DispatchMessage и оконной процедуры. Если эти функции распознают сообщение, извлечённое из очереди, как "своё", они сами выполняют все необходимые действия по его обработке, и тогда TranslateMessage и DispatchMessage вызывать не нужно. К этим функциям, в частности, относятся следующие:
- TranslateAccelerator. На основе загруженной из ресурсов таблицы распознаёт нажатие горячих клавиш меню и вызывает оконную процедуру, передавая ей сообщение WM_Command или WM_SysCommand, аналогичное тому, которое посылается при выборе соответствующего пункта меню пользователем.
- TranslateMDISysAccel. Аналог предыдущей функции за исключением того, что распознаёт горячие клавиши системного меню MDI-окон.
- IsDialogMessage. Распознаёт сообщения, имеющие особый смысл для диалоговых окон (например, нажатие Tab для перехода между элементами управления). Используется для немодальных диалоговых окон и окон, не являющихся диалоговыми (т.е. созданными без помощи функций CreateDialogXXXX), но требующими аналогичную функциональность.
Перечисленные выше функции при необходимости вставляются в петлю сообщений. Так, например, будет выглядеть петля сообщений, содержащая вызов TranslateAccelerator для родительской MDI-формы и TranslateMDISysAccell для дочерней:
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.
Все диалоговые окна имеют т.н. диалоговую процедуру - функцию, указатель на которую передаётся в качестве одного из параметров функциям DialogBoxXXXX и CreateDialogXXXX. Прототип диалоговой процедуры совпадает с прототипом оконной процедуры. Функция DefDlgProc начинает свою работу с того, что вызывает диалоговую процедуру. Если та не обработала переданное ей сообщение (о чём сигнализирует возвращаемое нулевое значение), функция DefDlgProc обрабатывает его сама. Таким образом, с помощью одного оконного класса можно реализовывать различные диалоговые окна, используя разные диалоговые процедуры.
Функции DialogBoxXXXX создают диалоговое окно и сразу же его показывают в модальном режиме. Данные функции завершают своё выполнение только тогда, когда модальное окно будет закрыто. Внутри модальных функций организуется собственная петля сообщений. Все прочие окна на время показа модального диалога запрещаются, т.е. перестают реагировать на сообщения от мыши и клавиатуры. При этом они сохраняют способность реагировать на другие сообщения, благодаря чему могут, например, обновлять своё содержимое по таймеру (в справке написано, что ничто не мешает программисту вставить в диалоговую процедуру вызов функций, разрешающих запрещённые системой окна, но при этом теряется смысл модальных диалогов).
Если в очереди нет сообщений, модальная петля посылает родительскому окну диалога сообщение WM_EnterIdle, обработка которого позволяет этому окну выполнять фоновые действия. Разумеется, что обработчик WM_EnterIdle не должен выполняться слишком долго, иначе модальное окно зависнет.
Обычно окно использует оконную процедуру, которая задана при создании соответствующего оконного класса. Однако допускается создание т.н. подклассов - переопределение оконной процедуры после того, как окно создано. Это переопределение касается только заданного окна и не оказывает влияния на остальные окна, принадлежащие данному оконному классу. Осуществляется оно с помощью функции SetWindowLong с параметром GWL_WndProc (другие значения этого параметра позволяют менять другие свойства окна, такие как стиль и расширенный стиль). Изменять оконную процедуру можно только у окон, созданных самим процессом.
Новая оконная процедура, которая устанавливается при создании подкласса, все необработанные сообщения должна передавать не функции DefWindowProc, а той оконной процедуре, которая была установлена ранее. SetWindowLong при изменении оконной процедуры возвращает дескриптор старой процедуры (этот же дескриптор можно получить, заранее вызвав функцию GetWindowLong с аргументом GWL_WndProc). Обычно значение дескриптора численно совпадает с адресом старой оконной процедуры, поэтому в некоторых источниках можно встретить рекомендации использовать этот дескриптор непосредственно как указатель процедурного типа. И это даже будет работать для оконных классов, созданных самой программой. Но безопаснее всё же для вызова старой оконной процедуры использовать системную функцию CallWindowProc, предоставив ей разбираться, является ли дескриптор указателем.
В качестве примера рассмотрим создание подкласса для некоторого окна, дескриптор которого содержится в переменной Wnd. Пусть нам потребовалось для этого окна нестандартным образом обрабатывать сообщение WM_KillFocus. Тогда код новой оконной процедуры и код её установки будет выглядеть так:
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. Однако до 7-ой версии Delphi включительно эти функции отсутствуют в модуле Windows, и при необходимости их следует импортировать самостоятельно.
Переопределять оконную процедуру с помощью SetWindowLong можно и у тех окон, чья оконная процедура была переопределена ранее. Таким образом создаются цепочки оконных процедур, каждая из которых вызывает предыдущую.
При использовании VCL в простых случаях самостоятельно работать с оконными сообщениями нет нужды - практически всё можно сделать с помощью свойств, методов и событий компонентов. Тем не менее, некоторые сообщения приходится обрабатывать вручную. Чаще всего это приходится делать при разработке собственных компонентов, но и в обычных приложениях это также может быть полезным.
Кроме сообщений, предусмотренных в системе, компоненты VCL обмениваются сообщениями, созданными авторами этой библиотеки. Эти сообщения имеют префиксы СМ_ и CN_. Они нигде не документированы, разобраться с ними можно только по исходным кодам VCL. При разработке собственных компонентов приходится обрабатывать эти сообщения. Мы здесь не будем полностью описывать эти сообщения, но некоторые из них будут упоминаться в описании работы VCL с событиями.
В Win API нет понятия главного окна - все окна, не имеющие родителя (или владельца в терминах системы), равноценны, и приложение может продолжать работу после закрытия любых окон. Но в VCL введено понятие главной формы - форма, которая создаётся первой, становится главной, и её закрытие означает закрытие всего приложения.
Чтобы кнопка окна появилась на панели задач, это окно должно быть создано со стилем WS_EX_AppWindow. Обычно этот стиль назначается тому окну, которое играет роль главного в приложении, хотя система не запрещает нескольким окнам одного приложения иметь такой стиль (примеры - Internet Explorer, MS Word). Разработчики VCL пошли по другому пути: этот стиль имеет окно, создаваемое объектом Application, именно оно ответственно за появление кнопки на панели задач. Дескриптор этого окна хранится в Application.Handle, а само оно невидимо, т.к. имеет нулевые размеры. Как и любое другое окно, это окно имеет оконную процедуру и может обрабатывать сообщения. Главная форма - это отдельное окно, не имеющее, с формальной точки зрения, никакого отношения к кнопке на панели задач. Видимость связи между этой кнопкой и главной формой обеспечивается взаимодействием объекта Application и объекта главной формы внутри VCL. Таким образом, даже простейшее VCL-приложение создаёт два окна: невидимое окно объекта Application и окно главной формы. Окно, создаваемое объектом Application, мы будем называть невидимым окном приложения. Невидимое окно приложения является владельцем (в терминах системы) всех форм, у которых явно не установлено свойство Parent, в т.ч. и главной формы.
При обработке сообщений VCL решает две задачи: выборка сообщений из очереди и передача сообщения конкретному компоненту. Рассмотрим сначала первую задачу.
Выборкой сообщений из очереди занимается объект Application. Его метод ProcessMessage с помощью PeekMessage проверяет, есть ли в очереди сообщения (напоминаю, что при этом диспетчеризуются сообщения, отправленные из других нитей). Если извлечено сообщение WM_Quit, PeekMessage устанавливает в True поле FTerminate и завершает свою работу. Обработка всех остальных сообщений, извлечённых из очереди, состоит из следующих основных этапов:
- Если назначен обработчик Application.OnMessage, сообщение передаётся ему. В этом обработчике можно установить параметр-переменную Handled в True, что означает, что сообщение не нуждается в дополнительной обработке.
- Затем, если на экране присутствует всплывающая подсказка (hint), проверяется, должно ли пришедшее сообщение прятать эту подсказку, и если да, то она убирается с экрана. Список сообщений, которые должны прятать окно подсказки, зависит от класса этого окна (здесь имеется ввиду класс VCL, а не оконный класс) и определяется виртуальным методом THintWindow.IsHintMsg. Стандартная реализация этого метода рассматривает как "прячущие" сообщения все сообщения от мыши, клавиатуры, сообщения об активации и деактивации программы и сообщения о действиях пользователя с меню или визуальными компонентами.
- Далее проверяется значение параметра Handled, установленное в обработчике OnMessage (если он назначен). Если это значение равно True, метод ProcessMessage завершает свою работу, и обработка сообщения на этом заканчивается. Таким образом, обработка сообщения по событию OnMessage не может отменить исчезновение всплывающей подсказки.
- Если главная форма приложения имеет стиль MDIForm, и одно из его дочерних MDI-окон в данный момент активно, сообщение передаётся функции TranslateMDISysAccel. Если эта функция вернёт True, обработка сообщения на этом завершается.
- Затем, если получено клавиатурное сообщение, определяется окно, которое должно осуществить его предварительную обработку. Обычно предварительную обработку осуществляет то окно, которому предназначено это сообщение, но если окно-адресат не является VCL-окном, производится поиск VCL-окна по цепочке родителей. Кроме того, если какое-либо окно захватит ввод мыши, предварительную обработку клавиатурных сообщений будет осуществлять именно оно. Предварительная обработка клавиатурного сообщения начинается с попытки найти полученную комбинацию клавиш среди горячих клавиш контекстно-зависимого меню и выполнить соответствующую команду. Если контекстно-зависимое меню не распознало сообщение как свою горячую клавишу, вызывается обработчик события OnShortCut окна, осуществляющего предварительную обработку (если это окно не является формой и не имеет этого события, вызывается OnShortCut его родительской формы). Если обработчик OnShortCut не установил свой параметр Handled в True, полученная комбинация клавиш ищется среди горячих клавиш сначала главного меню, а потом - среди компонентов TActionList. Если и здесь искомая комбинация не находится, возникает событие Application.OnShortCut, которое также имеет параметр Handled, позволяющий указать, что сообщение в дополнительной обработке не нуждается. Если обработчик не установил этот параметр, сообщение передаётся главной форме приложения, которое пытается найти нажатую комбинацию среди горячих клавиш своего контекстного меню, передаёт его обработчику OnShortCut, ищет среди горячих клавиш главного меню и компонентов TActionList. Если нажатая клавиша не является горячей, но относится к клавишам, использующимся для управления диалоговыми окнами (Tab, стрелки, Esc и т.п.), форме передаётся сообщение об этом, и при необходимости сообщение обрабатывается. Таким образом, на данном этапе средствами VCL эмулируются функции TranslateAccelerator и IsDialogMessage.
- Если на экране присутствует один из стандартных диалогов (в VCL они реализуются классами TOpenDialog, TSaveDialog и т.п.), вызывается функция IsDialogMessage, чтобы эти диалоги могли нормально функционировать.
- Если ни на одном из предыдущих этапов сообщение не было обработано, вызываются функции TranslateMessage и DispatchMessage, которые завершают обработку сообщения путём направления его соответствующей оконной функции.
Примечание: если внимательно проанализировать пятый этап обработки сообщения, видно, что нажатая комбинация клавиш проверяется на соответствие горячим клавишам меню сначала активной формы, затем - главной. При этом сначала возникает событие OnShortCut активной формы, затем - Application.OnShortCut, затем - OnShortCut главной формы. Если в момент получения сообщения главная форма является активной, она дважды будет проверять соответствие клавиши горячим клавишам своих меню, и событие OnShortCut тоже возникнет дважды (первый раз поле Msg.Msg равно CN_KeyDown, второй - CM_AppKeyDown). Эта проверка осуществляется дважды только в том случае, если комбинация клавиш не распознаётся как горячая клавиша - в противном случае цепочка проверок обрывается при первой проверке.
Метод ProcessMessage возвращает True, если сообщение извлечено и обработано, и False, если очередь была пуста. Этим пользуется метод HandleEvent, который вызывает ProcessMessage и, если тот вернёт False, вызывает метод Application.Idle для выполнения низкоприоритетных действий, которые должны выполняться только при отсутствии сообщений в очереди. Метод Idle, во-первых, проверяет, над каким компонентом находится курсор мыши, и, если этот компонент изменился со времени последней проверки, посылает старому компоненту сообщение CM_MouseLeave, а новому - CM_MouseEnter (Delphi не использует системные сообщения WM_MouseHover и WM_MouseLeave для выяснения того, над каким компонентом находится мышь, т.к. эти сообщения могут быть полезными только для оконных компонентов, а неоконные визуальные компоненты их получить не смогут). Затем, при необходимости, прячется старая всплывающая подсказка, показывается новая. После этого вызывается обработчик Application.OnIdle, если он назначен. Этот обработчик имеет параметр Done, по умолчанию равный True. Если в коде обработчика он не меняется на False, метод Idle инициирует события OnUpdate у всех объектов TAction, у которых они назначены (если Done после вызова принял значение False, HandleMessage не тратит время на инициацию событий OnUpdate). Затем, независимо от значения Done, с помощью процедуры CheckSynchronize проверяется, есть ли записи в списке методов, ожидающих синхронизации (эти методы помещаются в указанный список при вызове TThread.Synchronize). Если список не пуст, выполняется первый из этих методов (при том он, разумеется, удаляется из списка). Затем, если Done остался равным True, а список методов для синхронизации был пуст (т.е. никаких дополнительных действий выполнять не надо), HandleMessage вызывает функцию Win 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 до тех пор, пока очередь не окажется пустой. Вызов этого метода рекомендуется вставлять в обработчики событий, которые работают долго, чтобы в это время программа не теряла способности реагировать на действия пользователя.
Примечание: при использовании ProcessMessages следует соблюдать некоторую осторожность. Рассмотрим ситуацию, когда на форме расположена кнопка Button1, обработчик OnClick которой выполняется долго и содержит вызовы Application.ProcessMessages. Во время выполнения данного обработчика пользователь может вновь нажать на эту кнопку, и локальная петля сообщений снова вызовет Button1Click - получится что-то типа рекурсии, которая может стать очень глубокой, если пользователь будет продолжать нажимать на кнопку. Чтобы избежать этой ситуации, необходимо предпринять меры, исключающие повторный вызов Button1Click из локальной петли - например, при начале обработки сделать Button1.Enabled:=False;
Из сказанного выше может показаться, что главная нить проверяет список методов синхронизации только в главной петле сообщений, когда вызывается метод OnIdle. На самом деле это не так. Модуль Classes содержит переменную WakeMainThread, хранящую указатель на метод, который вызывается при помещении нового метода в список синхронизации. В конструкторе TApplication этой переменной присваивается указатель на метод TApplication.WakeMainThread, который посылает сообщение WM_Null невидимому окну приложения. Сообщение WM_Null - это "пустое" сообщение, на которое окно не должно реагировать (оно используется, например, при перехвате сообщений ловушкой: ловушка не может запретить передачу окну сообщения, но может изменить его на WM_Null, чтобы окно проигнорировало сообщение). Невидимое окно приложения, тем не менее, не игнорирует это сообщение, а вызывает при его получении CheckSynchronize. Таким образом, синхронное выполнение метода не откладывается до вызова OnIdle, а выполняется достаточно быстро, в т.ч. и в локальной петле сообщений. Более того, если главная нить перешла в режим ожидания получения сообщения (через вызов WaitMessage), вызов Synchronize в другой нити прервёт это ожидание, т.к. в очередь будет поставлено сообщение WM_Null.
Использование процедуры CheckSynchronize и переменной WakeMainThread позволяет обеспечить синхронизацию и в тех приложениях, которые не используют VCL в полном объёме. Разработчику приложения необходимо обеспечить периодические вызовы функции CheckSynchronize из главной нити, чтобы можно было использовать TThread.Synchronize в других нитях. При этом в главной нити можно обойтись без петли сообщений. Присвоение переменной WakeMainThread собственного метода позволяет использовать специфичный для данного приложения способ ускорения вызова метода в главной нити.
Примечание: описанный здесь способ синхронизации работы нитей используется, начиная с шестой версии Delphi. В более ранних версиях списка методов для синхронизации не было. Вместо этого в главной нити создавалось специальное невидимое окно, а метод TThread.Synchronizе с помощью SendMessage посылал этому окну сообщение CM_ExecProc с адресом объекта, метод которого нуждался в синхронизации. Метод выполнялся в оконной процедуре данного окна при обработки этого сообщения. Такой механизм также позволял использовать синхронизацию в приложениях без VCL, но требовал обязательного наличия петли сообщений в главной нити. Из-за смены механизма синхронизации могут возникнуть проблемы при переносе в новые версии старых приложений: если раньше для обеспечения работы синхронизации было достаточно организовать петлю сообщений, то теперь необходимо найти место для вызова CheckSynchronize. Разумеется, при переносе полноценных VCL-приложений эти проблемы не возникают, т.к. всё, что нужно, содержится в методах класса TApplication.
При показе VCL-формы в модальном режиме выборка сообщений из очереди осуществляется особым образом. Модальные окна в VCL - это не то же самое, что модальные диалоги с точки зрения API. Диалог может быть создан только на основе шаблона, и его модальность обеспечивается самой операционной системой, а VCL допускает модальность для любой формы, позволяя разработчику не быть ограниченным возможностями предусмотренного системой шаблона. Достигается это следующим образом: при вызове метода ShowModal все окна запрещаются средствами VCL, затем окно показывается обычным образом, как немодальное, но из-за того, что все остальные окна запрещены, создаётся эффект модальности.
Внутри ShowModal создаётся своя петля сообщений. В этой петле в цикле вызывается метод Application.HandleMessage до тех пор, пока не будет установлено свойство ModalResult или не придёт сообщение WM_Quit. После завершения этой петли вновь разрешаются все окна, которые были разрешены до вызова ShowModal, а "модальная" форма закрывается. В отличие от системных модальных диалогов модальная форма VCL во время своей активности не посылает родительскому окну сообщение WM_EnterIdle, но благодаря тому, что "модальная" петля сообщений использует HandleMessage, будет вызываться Idle, а значит, будет возникать событие Application.OnIdle, которое позволит выполнять фоновые действия.
Теперь рассмотрим, как VCL обрабатывает извлечённые из очереди сообщения. Как уже было сказано выше, для каждого класса формы VCL регистрирует одноимённый оконный класс, а все окна, принадлежащие одному оконному классу, имеют общую оконную процедуру. С другой стороны, логика работы VCL требует, чтобы события обрабатывались тем экземпляром объекта, который инкапсулирует окно-адресат. Таким образом, встаёт задача о том, как передать сообщение заданному экземпляру класса VCL.
VCL решает эту задачу следующим образом. Модуль Classes содержит недокументированную функцию MakeObjectInstance, описанную следующим образом:
type TWndMethod = procedure(var Message: TMessage) of object;
function MakeObjectInstance(Method: TWndMethod): Pointer;
Тип TMessage хранит информацию о сообщении. Все методы VCL-компонентов, связанные с обработкой сообщения, используют этот тип (чуть позже мы рассмотрим его более подробно).
Функция MakeObjectInstance динамически формирует новую оконную процедуру и возвращает указатель на неё (таким образом, любое VCL-приложение содержит самомодифицирующийся код). Задача этой динамически созданной процедуры - передать управление тому методу, который был указан при вызове MakeObjectInstance (таким образом, различные оконные процедуры, сформированные этой функцией, отличаются только тем, какой метод они вызывают).
Каждый экземпляр оконного компонента создаёт свою оконную процедуру, которая передаёт обработку сообщения его методу MainWndProc. При регистрации оконного класса в качестве оконной процедуры указывается InitWndProc, а при создании экземпляра компонента создаётся подкласс: с помощью SetWindowLong и параметра GWL_WndProc оконной процедурой назначается та функция, которая была создана с помощью MakeObjectInstance. Таким образом, каждый экземпляр получает свою оконную процедуру, а обработку сообщения начинает метод MainWndProc.
MainWndProc - это невиртуальный метод, обеспечивающий решение технических вопросов: удаление "мусора", оставшегося при обработке сообщения и обработку исключений. Собственно обработку сообщения он передаёт методу, на который указывает свойство WindowProc. Это свойство имеет тип TWndMethod и по умолчанию указывает на виртуальный метод WndProc. Таким образом, если разработчик не изменял значения свойства WindowProc, обработкой сообщения занимается WndProc.
WndProc обрабатывает только те сообщения, которые должны быть обработаны специальным образом, чтобы поддержать функциональность VCL. Остальные сообщения (а их - подавляющее большинство), WndProc передаёт в метод Dispatch.
Метод Dispatch объявлен и реализован в классе TObject. На первый взгляд может показаться странным, что в самом базовом классе реализована функциональность, использующаяся только в визуальных компонентах. Эта странность объясняется тем, что разработчики Delphi встроили поддержку обработки сообщений непосредственно в язык. Методы класса, описанные с директивой message, служат специально для обработки сообщений. Синтаксис описания такого метода следующий:
procedure (var Message: ); message ;
- это номер сообщения, для обработки которого предназначен метод. Имя метода может быть любым, но традиционно оно совпадает с именем константы сообщения за исключением того, что в нём отсутствует символ "_" (например, метод для обработки WM_Size будет называться WMSize).
В качестве типа параметра компилятор разрешает использовать любой тип, но на практике имеет смысл только использование типа TMessage или "совместимого" с ним. Тип TMessage описан следующим образом:
TMessage = packed record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
Поле Msg содержит номер сообщения, поля WParam и LParam - значение одноимённых параметров сообщения. Поле Result - выходное: метод, осуществляющий окончательную обработку сообщения, заносит в него то значение, которое должна вернуть оконная процедура. Поля с суффиксами Lo и Hi позволяют обращаться отдельно к младшему и старшему словам соответствующих полей, что может быть очень полезно, когда эти параметры содержат пару 16-разрядных значений. Например, у сообщения WM_NCMouseMove младшее слово параметра LParam содержит X-координату мыши, старшее - Y-координату. В случае обработки этого сообщения поле LParamLo будет содержать X-координату, LParamHi - Y-коодинату.
"Совместимыми" с TMessage можно назвать структуры, которые имеют такой же размер, а также параметр Msg, задающий сообщение. Эти структуры учитывают специфику конкретного сообщения. Их имена образуются из имени сообщения путём отбрасывания символа "_" и добавления префикса "Т". Для уже упоминавшегося сообщения WM_NCMouseMove соответствующий тип будет выглядеть следующим образом:
TWMNCMouseMove = packed record
Msg: Cardinal;
HitTest: Longint;
XCursor: Smallint;
YCursor: Smallint;
Result: Longint;
end;
Параметр WParam переименован в HitTest, что лучше отражает его смысл в данном случае, а параметр LParam разбит на две 16-разрядных части: XCursor и YCursor.
Параметр метода для обработки сообщения имеет тип, соответствующий обрабатываемому сообщению (при необходимости можно описать свой тип), или тип TMessage. Таким образом, обработчик сообщения WM_NCMouseMove будет выглядеть следующим образом:
type TSomeForm = class(TForm)
................
procedure WMNCMouseMove(var Message: TWMNCMouseMove); message WM_NCMouseMove;
................
end;
procedure TSomeForm.WMNCMouseMove(var Message: TWMNCMouseMove);
begin
..............
inherited;
end;
Метод для обработки сообщения может выполнить эту обработку полностью самостоятельно - тогда он не должен вызывать унаследованный метод обработки сообщения. Если же реакция предка на сообщение в целом устраивает разработчика, но нуждается только в дополнении, ключевое слово inherited позволяет вызвать унаследованный обработчик для данного сообщения. Таким образом, может образовываться целая цепочка вызовов унаследованных обработчиков одного и того же сообщения, каждый из которых выполняет свою часть обработки. Если у предков класса нет обработчика данного сообщения, директива inherited передаёт управление методу TObject.DefaultHandler.
Вернёмся к функции Dispatch. Она ищет среди обработчиков сообщения класса (собственных или унаследованных) метод для обработки сообщения, заданного полем Msg параметра Message и, если находит, передаёт управление ему. Если ни сам класс, ни его предки не содержат обработчик данного сообщения, обработка передаётся методу DefaultHandler.
Метод DefaultHandler является виртуальным. В классе TObject он не выполняет никаких действий, но наследники его переопределяют. Впервые он переопределяется в классе TControl. для обработки сообщений, связанных с получением и установкой заголовка окна - WM_GetText, WM_GetTextLength и WM_SetText. Напомним, что класс TControl является предком для всех визуальных компонентов, а не только оконных, и появление обработчика системных сообщений в этом классе является частью той имитации обработки сообщений неоконными компонентами, о которой мы уже говорили.
В классе TWinControl метод DefaultHandler также переопределён. Помимо передачи некоторых сообщений дочерним окнам (об этом мы будем подробнее говорить чуть ниже) и обработки некоторых внутренних сообщений он вызывает оконную процедуру, адрес которой хранится в свойстве DefWndProc. Это свойство содержит адрес, который был присвоен полю WindowClass.lpfnWndProc структуры TCreateParams в методе CreateParams. По умолчанию это поле содержит адрес стандартной оконной процедуры DefWindowProc. Как было сказано выше, обработка сообщений при использовании API обычно завершается вызовом этой процедуры.
В классе TCustomForm метод DefaultHandler также переопределён: если форма является MDI-формой, сообщения, присланные ей, передаются в процедуру DefFrameProc (за исключением WM_Size, которое передаётся в DefWindowProc) независимо от того, какое значение имеет свойство DefWndProc. Для всех остальных типов форм вызывается унаследованный от TWinControl DefaultHandler.
Повторим ещё раз всю цепочку обработки сообщений оконными компонентами VCL. Для каждого компонента создаётся уникальная оконная процедура, которая передаёт управление методу MainWndProc. MainWndProc передаёт управление методу, указатель на который хранится в свойстве WindowProc. По умолчанию это - метод компонента WndProc. Он осуществляет обработку некоторых сообщений, но в большинстве случаев передаёт управление методу Dispatch, который ищет среди методов компонента или его предков обработчик данного сообщения. Если обработчик не найден, управление получает метод DefaultHandler (он может также получить управление и в том случае, если обработчик найден, но он вызывает inherited). DefaultHandler самостоятельно обрабатывает некоторые сообщения, но большинство из них передаётся оконной процедуре, адрес которой хранится в свойстве DefWndProc (по умолчанию это стандартная функция Win API DefWindowProc).
Класс TControl имеет метод Perform, с помощью которого можно заставить визуальный компонент выполнить обработку конкретного сообщения в обход оконной процедуры и системного механизма передачи сообщений. Perform приводит к непосредственному вызову метода, указатель на который хранится в свойстве WindowProc. Дальше цепочка обработки сообщений такая же, как и при получении сообщения через оконную процедуру. Для оконных компонентов вызов Perform по своим последствиям практически эквивалентен передачи сообщения с помощью SendMessage с двумя исключениями. Во-первых, при использовании SendMessage система обеспечивает переключение между нитями, и сообщение будет выполнено в той нити, которая создала окно, а Perform никакого переключения не производит, и обработка сообщения будет выполнена той нитью, которая вызвала Perform. Поэтому Perform, в отличие от SendMessage, можно использовать только в главной нити (напомним, что VCL - принципиально однонитевая библиотека, и создание форм вне главной нити с её помощью недопустимо). Во-вторых, Perform выполняется чуть быстрее, так как оконная процедура и метод MainWndProc исключаются из цепочки обработки сообщения.
Но основное преимущество Perform перед SendMessage заключается в том, что Perform можно использовать со всеми визуальными компонентами, а не только с оконными. Неоконные визуальные компоненты не могут иметь оконной процедуры, но цепочка обработки сообщений у них есть. В ней отсутствует оконная процедура и метод MainWndProc, а DefaultHandler не вызывает никаких стандартных оконных процедур, но во всём остальном эта цепочка полностью эквивалентна цепочке оконных компонентов. Таким образом, цепочка обработки сообщений оконных компонентов имеет две точки входа - оконную процедуру и метод Perform, а цепочка неоконных компонентов - только метод Perform. И метод Perform, следовательно, является универсальным: он одинаково хорошо подходит как для оконных, так и для неоконных компонентов. Он широко используется в VCL, т.к. позволяет единообразно работать с любыми визуальными компонентами.
Неоконным визуальным компонентам сообщения посылает их родительское окно. Например, обработка сообщений, связанных с мышью, в классе TWinControl начинается с того, что выполняется проверка, не попадают ли координаты курсора в область какого-либо из дочерних неоконных компонентов. И если попадает, оконный компонент не обрабатывает это сообщение самостоятельно, а транслирует его соответствующему неоконному компоненту с помощью Perform. Эта трансляция и обеспечивает получение сообщений неоконными компонентами.
Сообщения в VCL транслируются не только неоконным, но и оконным компонентам. В Windows все сообщения, информирующие об изменении состояния стандартных элементов управления, получает их родительское окно, а не сам элемент. Например, при нажатии на кнопку уведомительное сообщение об этом получает не сама кнопка, а окно, её содержащее. Сама кнопка получает и обрабатывает только те сообщения, которые обычно разработчику неинтересны. Это упрощает работу программиста, т.к. не требуется для каждого элемента управления писать свою оконную процедуру, все значимые сообщения получает оконная процедура родительского окна.
Рассмотрим, что происходит при нажатии кнопки на форме. Окно получает сообщение WM_Command, уведомляющее о возникновении события среди оконных компонентов. Параметры сообщения позволяют определить, какое именно событие и с каким элементом управления произошло (в данном случае событие будет BN_Clicked). Обработчик WM_Command класса TWinControl находит компонент, вызвавший сообщение, и посылает ему сообщение CN_Command (внутреннее сообщение VCL) с теми же параметрами. В нашем примере это будет экземпляр класса TButton, реализующий кнопку, которую нажал пользователь. Получив CN_Command, компонент начинает обработку произошедшего с ним события (в частности, TButton инициирует событие OnClick).
Примечание: к переопределению обработчика WM_Command нужно относиться осторожно, чтобы не нарушить механизм трансляции сообщений. Примером неправильного переопределения может служить класс TCustomDrawGrid. В форумах нередко встречаются вопросы, почему элементы управления, родителем которых является TDrawGrid или TStringGrid, некорректно ведут себя: кнопки при нажатии не генерируют событие OnClick, выпадающие списки остаются пустыми и т.п. Это связано с тем, что обработчик WM_Command в TCustomGrid учитывает возможность существования только одного дочернего компонента - внутреннего редактора, возникающего при включенной опции goEditing. Остальным дочерним элементам WM_Command не транслируются, и они лишены возможности корректно реагировать на происходящие с ними события. Выходом из ситуации может стать либо создание наследника от TDrawGrid или TStringGrid, который правильно транслирует WM_Command, либо назначение родительским окном компонента, вставляемого в сетку, формы, панели или иного оконного компонента, который правильно транслирует это сообщение.
Рассмотрим все методы, с помощью которых можно встроить свой код в цепочку обработки сообщений оконным компонентом и перехватить сообщения. Всего существует шесть способов сделать это.
- Как и у всякого окна, у оконного компонента VCL можно изменить оконную процедуру с помощью функции SetWindowLong. Этот способ лучше не применять, т.к. код VCL не будет ничего знать об этом переопределении, и сообщения, получаемые компонентом не через оконную процедуру, а с помощью Perform, не будут перехвачены. Другим недостатком данного способа является то, что изменение некоторых свойств компонента (например, FormStyle и BorderStyle у формы) невозможно без уничтожения окна и создания нового. Для программиста это пересоздание окна выглядит прозрачно, но новое окно получит новую оконную процедуру, и нужно будет выполнять перехват заново. Отследить момент пересоздания окна можно с помощью сообщения CM_RecreateWnd. Обработчик этого сообщения уничтожает старое окно, a создание нового окна откладывается до момента, когда будет в первый раз использовано свойство Handle. Если перехватить это сообщение, то, в принципе, после выполнения стандартного обработчика можно заново установить перехват с помощью SetWindowLong, но так как этот способ не даёт никаких преимуществ перед другими, им всё равно лучше не пользоваться.
- Можно создать собственный метод обработки сообщения и поместить указатель на него в свойство WindowProc. При этом старый указатель обычно запоминается, т.к. новый обработчик обрабатывает лишь некоторые сообщения, а остальные передаёт старому. Достоинством этого способа является то, что метод, указатель на который помещается в WindowProc, не обязан принадлежать тому компоненту, сообщения которого перехватываются. Это позволяет, во-первых, создавать компоненты, которые влияют на обработку сообщений родительскими формами, а во-вторых, реализовывать нестандартную обработку сообщений стандартными компонентами, не порождая от них наследника.
- При написании нового компонента можно перекрыть виртуальный метод WndProc и реализовать обработку нужных сообщений в нём. Это позволяет компоненту перехватывать сообщения в самом начале цепочки (за исключением внешних обработчиков, установленных с помощью свойства WindowProc - здесь разработчик компонента не властен).
- Наиболее удобный способ самостоятельной обработки событий - написание их методов-обработчиков. Этот способ используется чаще всего. Его недостатком является то, что номера обрабатываемых сообщений должны быть известны на этапе компиляции. Для системных сообщений и внутренних сообщений VCL это условие выполняется, но далее мы будем говорить об определяемых пользователем сообщениях, номера которых в некоторых случаях на этапе компиляции неизвестны. Обрабатывать такие сообщения с помощью методов с директивой message невозможно.
- Для перехвата сообщений, которые не были обработаны с помощью методов-обработчиков, можно перекрыть виртуальный метод DefaultHandler.
- И наконец, можно написать оконную процедуру и поместить указатель на неё в свойство DefWndProc. Этот способ по своим возможностям практически эквивалентен предыдущему, но менее удобен. Однако предыдущим свойством можно воспользоваться только при создании собственного компонента, в то время как DefWndProc можно изменять у экземпляров существующих классов. Напомним, что этот способ не подходит для форм, у которых FormStyle=fsMDIForm, т.к. такие формы игнорируют значение свойства DefWndProc.
Для перехвата сообщений неоконных визуальных компонентов можно использовать все перечисленные выше способы, за исключением первого и последнего.
Метод WndProc оконного компонента транслирует сообщения от мыши неоконным визуальным компонентам, родителем которых он является. Например, если положить на форму компонент TImage и переопределить у этой формы метод для обработки сообщения WM_LButtonDown, то нажатие кнопки мыши над TImage не приведёт к вызову этого метода, т.к. WndProc передаст это сообщение в TImage, и Dispatch не будет вызван. Но если переопределить WndProc или изменить значение свойства WindowProc (т.е. использовать второй или третий метод перехвата), то можно получать и обрабатывать и те "мышиные" сообщения, которые должны транслироваться неоконным дочерним компонентам. Это общее правило: чем раньше встраивается собственный код в цепочку обработки сообщений, тем больше у него возможностей.
Различные способы перехвата сообщений иллюстрируются рядом примеров: использование свойства WindowProc показано в примерах Line, CoordLabel и PanelMsg, перекрытие метода WndProc - в примере NumBrodcast, создание метода для обработки сообщения - в примере ButtonDel.
Использование сообщений очень удобно в тех случаях, когда нужно заставить окно выполнить какое-то действие. Поэтому Windows предоставляет возможность программисту создавать свои сообщения. Существуют три типа пользовательских сообщений: сообщения оконного класса, сообщения приложения и глобальные (строковые) сообщения. Для каждого из них выделен отдельный диапазон номеров. Номера стандартных сообщений лежат в диапазоне от 0 до WM_User-1 (WM_User - константа, для 32-разрядных версий Windows равная 1024).
Сообщения оконного класса имеют номера в диапазоне от WM_User до WM_App-1 (WM_App имеет значение 32768). Программист может выбирать произвольные номера для своих сообщений в этом диапазоне. Каждое сообщение должно иметь смысл только для конкретного оконного класса. Для различных оконных классов можно определять сообщения, имеющие одинаковые номера. Система никак не следит за тем, чтобы сообщения, определённые для какого-либо оконного класса, посылались только окнам этого класса - программист должен сам об этом заботиться. В этом же диапазоне лежат сообщения, специфические для стандартных оконных классов 'BUTTON', 'EDIT', 'LISTBOX', 'COMBOBOX' и т.п.
Использование сообщений из этого диапазона иллюстрируется примером ButtonDel.
Диапазон от WM_App до 49151 предназначен для сообщений приложения. Номера этих сообщений также выбираются программистом произвольно. Система гарантирует, что ни один из стандартных оконных классов не использует сообщения из этого диапазона. Это позволяет выполнять их широковещательную в пределах приложения рассылку. Ни один из стандартных классов не откликнется на такое сообщение и не выполнит нежелательных действий.
Упоминавшиеся ранее внутренние сообщения VCL с префиксами CM_ и CN_ имеют номера в диапазоне от 45056 до 49151, т.е. используют часть диапазона сообщений приложения. Таким образом, при использовании VCL диапазон сообщений приложения сокращается до WM_App..45055.
Сообщения оконного класса и приложения можно использовать и для взаимодействия с другими приложениями, но при этом отправитель должен быть уверен, что адресат правильно его поймёт. Широковещательная рассылка при этом исключена - реакция других приложений, которые также получат это сообщение, может быть непредсказуемой. Если всё же необходимо рассылать широковещательные сообщения между приложениями, нужно воспользоваться глобальными сообщениями, для которых зарезервирован диапазон номеров от 49152 до 65535.
Глобальное сообщение обязано иметь имя (именно поэтому такие сообщения называются также строковыми), под которым оно регистрируется в системе с помощью функции RegisterWindowMessage. Эта функция возвращает уникальный номер регистрируемого сообщения. Если сообщение с таким именем регистрируется впервые, номер выбирается из числа ещё не занятых. Если же сообщение с таким именем уже было зарегистрировано, то возвращается тот же самый номер, который был присвоен ему при первой регистрации. Таким образом разные программы, регистрирующие сообщения с одинаковыми именами, получат одинаковые номера и смогут понимать друг друга. Для прочих же окон это сообщение не будет иметь никакого смысла.
Создание и использование оконных сообщений демонстрируется примером NumBroadcast.
Разумеется, существует вероятность, что два разных приложения выберут для своих глобальных сообщений одинаковые имена, и это приведёт к проблемам при широковещательной рассылке этих сообщений. Но, если давать своим сообщениям осмысленные имена, а не что-то вроде WM_MyMessage1, вероятность такого совпадения будет очень мала. В особо критических ситуациях можно в качестве имени сообщения можно использовать GUID, уникальность которого гарантируется.
Номера глобальных сообщений становятся известны только на этапе выполнения программы. Это означает, что для их обработки нельзя использовать методы с директивой message, вместо этого следует перекрывать методы WndProc или DefaultHandler.
Отправка и обработка некоторых сообщений производится не по общим правилам, а с различными исключениями. Приведённый ниже список таких сообщений не претендует на полноту, но всё-таки может дать представление о таких исключениях.
Сообщение WM_CopyData используется для передачи блока данных от одного процесса к другому. В 32-разрядных версиях Windows память, выделенная процессу, недоступна для всех остальных процессов. Поэтому просто передать указатель другому процессу нельзя - он не сможет получить доступ к этой области памяти. При передачи сообщения WM_CopyData система копирует указанный блок из адресного пространства отправителя в адресное пространство получателя, передаёт получателю указатель на этот блок, и при завершении обработки сообщения освобождает блок. Всё это требует определённой синхронности действий, которой невозможно достичь при посылке сообщения, поэтому WM_CopyData можно только отправлять, но не посылать (т.е. можно использовать SendMessage, но не PostMessage).
Сообщение WM_Paint предназначено для перерисовки клиентской области окна. Если изображение сложное, перерисовка занимает много времени, поэтому в Windows предусмотрены механизмы, минимизирующие количество перерисовок. Перерисовывать своё содержимое окно должно при получении сообщения WM_Paint. С каждым таким сообщением связан регион, нуждающийся в обновлении. Этот регион может совпадать с клиентской областью окна или быть её частью. В последнем случае программа может ускорить перерисовку, рисуя не всё окно, а только нуждающуюся в этом часть (VCL игнорирует возможность перерисовки только части окна, поэтому при использовании этой библиотеки окно всегда перерисовывается полностью). Послать сообщение WM_Paint с помощью PostMessage окну нельзя - оно не ставится в очередь. Вместо этого можно пометить регион как нуждающийся в обновлении с помощью функций InvalidateRect и InvalidateRgn. Если на момент вызова этих функций регион, нуждающийся в обновлении, не был пуст, новый регион объединяется со старым. Функции GetMessage и PeekMessage, если очередь сообщений пуста, а регион, требующий обновления, не пуст, возвращают сообщение WM_Paint. Таким образом, перерисовка окна откладывается до того момента, когда все остальные сообщения будут обработаны. Отправить WM_Paint с помощью SendMessage тоже нельзя. Если требуется немедленная перерисовка окна, следует использовать функции UpdateWindow или RedrawWindow, которые не только отправляют сообщение окну, но и выполняют сопутствующие действия, связанные с регионом обновления.
Обработка сообщения WM_Paint также имеет некоторые особенности. Обработчик должен получить контекст устройства окна (см. раздел "Графика в Win API") с помощью функции BeginPaint и по окончанию работы освободить его с помощью EndPaint. Эти функции должны вызываться только один раз при обработке сообщения. Соответственно, если сообщение обрабатывается поэтапно несколькими обработчиками, как это бывает при перехвате сообщений, получать и освобождать контекст устройства должен только первый из них, а остальные должны пользоваться тем контекстом, который он получил. Система не накладывает обязательных требований, которые могли бы решить проблему, но предлагает решение, которое используют все предопределённые системные классы. Когда сообщение WM_Paint извлекается из очереди, его параметр WParam равен нулю. Если же обработчик получает сообщение с WParam<>0, он рассматривает значение этого параметра как дескриптор контекста устройства и использует его, вместо того чтобы получать дескриптор через BeginPaint. Первый в цепочке обработчиков должен передать вниз по цепочке сообщение с изменённым параметром WParam. Компоненты VCL также пользуются этим решением. При перехвате сообщения WM_Paint это нужно учитывать.
Примеры PanelMsg и Line демонстрируют, как правильно перехватывать сообщение WM_Paint.
Простые таймеры, создаваемые системой с помощью функции SetTimer, сообщают об истечении интервала посредством сообщения WM_Timer. Проверка того, истёк ли интервал, осуществляется внутри функций GetMessage и PeekMessage. Таким образом, если эти функции долго не вызываются, сообщение WM_Timer не ставится в очередь, даже если положенный срок истёк. Если за время обработки других сообщений срок истёк несколько раз, в очередь ставится только одно сообщение WM_Timer. Если в очереди уже есть сообщение WM_Timer, новое в очередь не добавляется, даже если срок истёк. Таким образом, часть сообщений WM_Timer теряется, т.е., например, если интервал таймера установить равным одной секунде, то за час будет получено не 3600 сообщений WM_Timer, а меньшее число, и разница будет тем больше, чем интенсивнее программа использует процессор.
Примечание: класс TTimer инкапсулирует таймер, работающий через WM_Timer. Сообщения получает невидимое окно, создающееся специально для этого. Поэтому событие OnTimer за час при секундном интервале также возникнет меньше, чем 3600 раз.
Некоторую специфику имеют и сообщения от клавиатуры. При обработке таких сообщений можно использовать функцию GetKeyState, которая возвращает состояние любой клавиши (нажата-отпущена) в момент возникновения данного события. Именно в момент возникновения, а не в момент вызова функции. Если функцию GetKeyState использовать при обработке не клавиатурного сообщения, оно вернёт состояние клавиши на момент последнего извлечённого из очереди клавиатурного сообщения.
Та часть Win API, которая служит для работы с графикой, обычно называется GDI (Graphic Device Interface). Ключевым в GDI является понятие контекста устройства (Device Context, DC). Контекст устройства - это специфический объект, хранящий информацию о возможностях устройства, о способе работы с ним и о разрешённой для изменения области. В Delphi контекст устройства представлен классом TCanvas, свойство Handle которого содержит дескриптор контекста устройства. TCanvas универсален в том смысле, что с его помощью рисование в окне, на принтере или в метафайле выглядит одинаково. То же самое справедливо и для контекста устройства. Разница заключается только в том, как получить в разных случаях дескриптор контекста.
Большинство методов класса TCanvas являются "калькой" с соответствующих (в большинстве случаев одноимённых) функций GDI. Но в некоторых случаях (прежде всего в методах вывода текста и рисования многоугольников) параметры методов TCanvas имеют более удобный тип, чем функции GDI. Например, метод TCanvas.Polygon требует в качестве параметра открытый массив элементов типа TPoint, а соответствующая функция GDI - указатель на область памяти, содержащую координаты точек, и число точек. Это означает, что до вызова функции надо выделить память, а потом - освободить её. Ещё нужен код, который заполнит эту область памяти нужными значениями. И ни в коем случае нельзя ошибаться в количестве элементов массива. Если зарезервировать память для одного числа точек, а при вызове функции указать другое, программа будет работать неправильно. Но для простых функций работа через GDI ничуть не сложнее, чем через TCanvas.
Для получения дескриптора контекста устройства существует много функций. Только для того, чтобы получить дескриптор контекста обычного окна, существуют четыре функции: BeginPaint, GetDC, GetWindowDC и GetDCEx. Первая из них может использоваться только при обработке сообщения WM_Paint. Вторая даёт контекст клиентской области окна. Третья позволяет получить контекст всего окна, вместе с неклиентской частью. Последняя же позволяет получить контекст определённой области клиентской части окна.
После того, как дескриптор контекста получен, можно воспользоваться преимуществами класса TCanvas. Для этого надо создать экземпляр такого класса, и присвоить его свойству Handle полученный дескриптор. Освобождение ресурсов нужно проводить в следующем порядке: сначала свойству Handle присваивается нулевое значение, затем уничтожается экземпляр класса TCanvas, затем с помощью подходящей функции GDI освобождается контекст устройства.
Использование класса TCanvas для рисования на контексте устройства, для которого имеется дескриптор, показано в примере PanelMsg.
Разумеется, можно вызывать функции GDI при работе через TCanvas. Для этого им просто надо передать в качестве дескриптора контекста Canvas.Handle. Коротко перечислим те возможности GDI, которые разработчики VCL почему-то не сочли нужным включать в TCanvas: работа с путями и регионами; выравнивание текста по любому углу или по центру; установка собственной координатной системы; получение детальной информации об устройстве; использование геометрических карандашей; вывод текста под углом к горизонтали; расширенные возможности вывода текста; ряд возможностей по рисованию нескольких кривых и многоугольников одной функцией; поддержка режимов заливки. Доступ ко всем этим возможностям может быть осуществлён только через API. Отметим также, что Windows NT/2000/XP поддерживает большее число графических функций, чем 9x/ME. Функции, которые не поддерживаются в 9x/ME, также не имеют аналогов среди методов TCanvas, иначе программы, написанные с использованием данного класса, нельзя было бы запустить в этих версиях Windows.
Некоторые возможности GDI, которым нет аналогов в TCanvas, демонстрируются примером GDIDraw.
Для задания цвета в GDI предусмотрен тип COLORREF (в модуле Windows определён также его синоним для Delphi - TColorRef). Это 4-байтное беззнаковое целое, старший байт которого определяет формат представления цвета. Если этот байт равен нулю, первый, второй и третий байты представляют собой интенсивности красного, зелёного и синего цветов соответственно. Если старший байт равен 1, два младших байта хранят индекс цвета в текущей палитре устройства, третий байт не используется и должен быть равен нулю. Если старший байт равен 2, остальные байты, как и в нулевом формате, показывают интенсивность цветовых компонент.
Тип TColorRef позволяет менять глубину каждого цветового канала от 0 до 255, обеспечивая кодирование 16777216 различных оттенков (это соответствует режиму True Color). Если цветовое разрешение устройства невелико, GDI подбирает ближайший возможный цвет из палитры. Если старший байт TColorRef равен нулю, цвет выбирается из текущей системной палитры (по умолчанию эта палитра содержит всего 20 цветов, поэтому результаты получаются далёкими от совершенства). Если же старший байт равен 2, GDI выбирает ближайший цвет из палитры устройства. В этом случае результаты получаются более приемлемыми. Если устройство имеет большую цветовую глубину и не использует палитру, разницы между нулевым и вторым форматом COLORREF нет.
Примечание: хотя режимы HighColor (32768 или 65536 цветов) не обладают достаточной цветовой глубиной, чтобы передать все возможные значения TColorRef, палитра в этих режимах не используется, и ближайший цвет выбирается не из палитры, а из всех цветов, которые способно отобразить устройство. Поэтому использование нулевого формата в этих режимах даёт хорошие результаты.
В API определены макросы (а в Windows, соответственно, одноимённые функции) RGB, PaletteIndex и PaletteRGB. RGB принимает три параметра - интенсивности красного, зелёного и синего компонентов и строит из них значение типа TColorRef нулевого формата. PaletteIndex принимает в качестве параметра номер цвета в палитре и на его основе конструирует значение первого формата. Макрос PaletteRGB эквивалентен RGB, за исключением того, что устанавливает старший байт возвращаемого значения равным двум. Для извлечения интенсивностей отдельных цветовых компонент из значения типа TColorRef можно воспользоваться функциями GetRValue, GetGValue и GetBValue.
В системе определены два специальных значения цвета: CLR_NONE ($1FFFFFFF) и CLR_DEFAULT ($20000000). Они используются только в списках рисунков (image lists) для задания фонового и накладываемого цветов при выводе рисунка. CLR_NONE задаёт отсутствие фонового или накладываемого цвета (в этом случае соответствующий визуальный эффект не применяется), CLR_DEFAULT - использование цвета, заданного для всего списка.
В VCL для передачи цвета используется тип TColor, определённый в модуле Graphics. Это 4-байтное число, множество значений которого является надмножеством значений типа TColorRef. К системным форматам 0, 1 и 2 добавлен формат 255. Если старший байт значения типа TColor равен 255, то младший байт интерпретируются как индекс системного цвета. (второй и третий байт при этом не используются). Системные цвета - это цвета, используемые системой для рисования различных элементов интерфейса пользователя. конкретные RGB-значения этих цветов зависят от версии Windows и от текущей цветовой схемы. RGB-значение системного цвета можно получить с помощью функции GetSysColor. 255-ый формат TColor освобождает от необходимости вызова данной функции.
Для типа TColor определён ряд констант, облегчающих использование данного типа. Среди них есть как те, которые соответствуют определённому RGB-цвету (clWhite, clBlack, clRed и т.п.), так и те, которые соответствуют определённому системному цвету (clWindow, clHighlight, clBtnFace и т.п.). Значения RGB-цветов определены в нулевом формате. Это не приведёт к потере точности цветопередачи в режимах с палитрой, т.к. константы определены только для 16-ти основных цветов, которые обязательно присутствуют в системной палитре. Значениям CLR_NONE и CLR_DEFAULT соответствуют константы clNone и clDefault. Они используются (помимо списков рисунков) при задании прозрачного цвета в растровом изображении. Если этот цвет равен clNone, изображение считается непрозрачным, если clDefault, в качестве прозрачного цвета берётся цвет левого нижнего пикселя.
Везде, где требуется значение типа TColor, можно подставлять TColorRef, т.е. всем свойствам и параметрам методов класса TCanvas, имеющим тип TColor, можно присваивать те значения TColorRef, которые сформированы функциями API. Обратное неверно: API-функции не умеют обращаться с 255-ым форматом TColor. Преобразование из TColor в TColorRef осуществляется с помощью функции ColorToRGB. Значения нулевого, первого и второго формата, а также clNone и clDefault она оставляет без изменения, а значения 255-ого формата приводит к нулевому с помощью функции GetSysColor. Эту функцию следует использовать при передаче значений типа TColor в функции GDI.
Использование кистей, карандашей и шрифтов в GDI принципиально отличается от того, как это делается в VCL. Класс TCanvas имеет свойства Brush, Pen и Font, изменение свойств которых приводит к выбору того или иного карандаша, шрифта, кисти. В GDI эти объекты самостоятельны, должны создаваться, получать свой дескриптор, "выбираться" в нужный контекст устройства с помощью функции SelectObject и уничтожаться после использования. Причём удалять можно только те объекты, которые не выбраны ни в одном контексте. Есть также несколько стандартных объектов, которые не надо ни создавать, ни удалять. Их дескрипторы можно получить с помощью функции GetStockObject. Для примера рассмотрим фрагмент программы, рисующей на контексте с дескриптором DC две линии - синюю и красную. В этом фрагменте используется то, что функция SelectObject возвращает дескриптор объекта, родственного выбираемому, который был выбран ранее. Так, при выборе нового карандаша она вернёт дескриптор того карандаша, который был выбран до этого.
SelectObject(DC, CreatePen(PS_Solid, 1, RGB(255, 0, 0)));
MoveToEx(DC, 100, 100, nil);
LineTo(DC, 200, 200);
DeleteObject(SelectObject(DC, CreatePen(PS_Solid, 1, RGB(0, 0, 255))));
MoveToEx(DC, 200, 100, nil);
LineTo(DC, 100, 200);
DeleteObject(SelectObject(DC, GetStockObject(Black_Pen)));
При переходе на 32-разрядную версию Windows многие функции были исключены из GDI и заменены новыми. Список устаревших функций и соответствующих им новых можно найти в справке в разделе 'Graphics Functions'.
Ещё одно отличие от 16-разрядных версий заключается в том, что ранее дескрипторы графических объектов были глобальными, то есть объект, созданный одной программой, можно было использовать в другой, если эти программы могли передавать друг другу дескрипторы. В 32-разрядных версиях дескрипторы GDI-объектов, созданные одним процессом, не имеют смысла для другого.
Для хранения растровых изображений в Windows существуют три формата: DDB, DIB и DIB-секция. DDB - это Device Dependent Format, формат, который определяется графическим устройством, на которое идёт вывод. DIB - это Device Independent Bitmap, формат, единый для всех устройств. Формат DIB - это устаревший формат, который не позволяет использовать графические функции GDI для модификации картинки, модифицировать изображение можно, только вручную изменяя цвета отдельных пикселей. В 32-разрядных версиях появился ещё один формат - DIB-секция. По сути дела это тот же самый DIB, но дополненный возможностями рисовать на нём с помощью GDI-функций. Все различия между этими тремя форматами можно прочитать в замечательной книге [1]; мы же здесь ограничимся только кратким их обзором.
Формат DDB поддерживается самой видеокартой (или другим устройством вывода), поэтому при операциях с таким изображением задействуется аппаратный ускоритель графики. DDB-изображение хранится в выгружаемом системном пуле памяти (Windows NT/2000/XP) или в куче GDI (Windows 9x/ME). При этом размер DDB-растра не может превышать 16 Мб в Windows 9x/ME и 48 Мб в Windows NT/2000/XP. Формат DDB непереносим с одного устройства на другое, он должен использоваться только в рамках одного устройства. Прямой доступ к изображению и его модификация вручную невозможны, т.к. формат хранения изображения конкретным устройством непредсказуем. Модифицировать DDB можно только с помощью функций GDI. Цветовая глубина DDB-изображений определяется устройством.
DIB-секция может храниться в любой области памяти, их размер ограничивается только размером доступной приложению памяти, функции GDI для рисования на таком изображении используют чисто программные алгоритмы, никак не задействуя аппаратный ускоритель, поэтому графические операции с DIB-секцией выполняются медленнее, чем с DDB (не все видеокарты поддерживают аппаратные алгоритмы построения линий, кривых, заполнения областей и т.п., поэтому рисование на DIB-изображении может выполняться быстрее, чем на DDB; но вывод DDB на экран всегда существенно быстрее, чем DIB). DIB-секция поддерживает различную цветовую глубину и прямой доступ к области памяти, в которой хранится изображение. DIB-секция переносима с одного устройства на другое. BMP-файлы хранят изображение как DIB.
Класс TBitmap может хранить изображение как в виде DDB, так и в виде DIB-секции - это определяется значением свойства PixelFormat. Значение pfDevice означает использование DDB, остальные значения - DIB-секции с различной цветовой глубиной. По умолчанию TBitmap создаёт изображение с форматом pfDevice, но программист может изменить формат в любой момент. При этом создаётся новое изображение нового формата, старое копируется в него и уничтожается.
Со свойством PixelFormat тесно связано свойство HandleType, которое может принимать значение bmDIB и bmDDB. Изменение свойства PixelFormat приводит к изменению свойства HandleType и наоборот.
При загрузке изображения из файла, ресурса или потока класс TBitmap обычно создаёт изображение в формате DIB-секции, соответствующее источнику по цветовой глубине. Исключение составляют сжатые файлы (формат BMP поддерживает сжатие только для 16- и 256-цветных изображений) - в этом случае создаётся DDB. В файле Graphics определена глобальная переменная DDBsOnly, которая по умолчанию равна False. Если изменить её значение на True, загружаемое изображение всегда будет иметь формат DDB.
Примечание: в справке сказано, что когда DDBsOnly=False, вновь создаваемые изображения по умолчанию хранятся в виде DIB-секции. На самом деле из-за ошибки в модуле Graphics (как минимум до 7-ой версии Delphi включительно) вновь созданное изображение всегда хранится как DDB независимо от значения DDBsOnly.
Класс TBitmap имеет свойство ScanLine, через которое можно получить прямой доступ к массиву пикселей, составляющих изображение. В справке написано, что это свойство можно использовать только с DIB-изображениями. Но на самом деле DDB-изображения тоже позволяют использовать это свойство, хотя и с существенными ограничениями. Если изображение хранится в DDB-формате, при обращении к ScanLine создаётся его DIB-копия, и ScanLine возвращает указатель на массив этой копии. Поэтому, во-первых, ScanLine работает с DDB-изображениями очень медленно, а во-вторых, работает не с изображением, а с его копией, откуда вытекают следующие ограничения:
- Копия создаётся на момент обращения к ScanLine, поэтому изменения, сделанные на изображении с помощью GDI-функций после этого, будут недоступны.
- Каждое обращение к ScanLine создаёт новую копию изображения, а старая при этом уничтожается. Гарантии, что новая копия будет располагаться в той же области памяти, нет, поэтому указатель, полученный при предыдущем обращении к ScanLine, больше нельзя использовать.
- Изменения, сделанные в массиве пикселей, затрагивают только копию изображения, но само изображение при этом не меняется. Поэтому при использовании DDB свойство ScanLine даёт возможность прочитать, но не изменить изображение.
Следует отметить, что TBitmap иногда создаёт DIB-секции, даже если свойства HandleType и PixelFormat явно указывают на использование DDB. Особенно часто это наблюдается для изображений большого размера. По всей видимости, это происходит в тех случаях, когда в системном пуле нет места для хранения DDB-изображения такого размера, и разработчики TBitmap решили, что в таком случае лучше создать DIB-изображение, чем не создавать никакого.
Пример BitmapSpeed позволяет сравнить скорость выполнения различных операций с DDB- и DIB-изображениями.
Windows поддерживает две кодировки: ANSI и Unicode. В кодировке ANSI (American National Standard Institute) каждый символ кодируется однобайтным кодом. Коды от 0 до 127 совпадают с кодами ASCII, коды от 128 до 255 могут означать разные символы разных языков в зависимости от выбранной кодовой страницы. Кодовые страница позволяют уместить многочисленные символы различных языков в однобайтный код, но при этом можно работать только с одной кодовой страницей, т.е. с одним языком. Неверно выбранная кодовая страница приводит к появлению непонятных символов (в интернете их обычно называют кракозябрами) вместо осмысленного текста.
В кодировке Unicode используется 2 байта на символ, что даёт возможность закодировать 65536 символов. Этого хватает для символов латиницы и кириллицы, греческого алфавита, китайских иероглифов, арабских и еврейских букв, а также многочисленных дополнительных (финансовых, математических и т.п.) символов. Кодовых страниц в Unicode нет.
Примечание: кодовая страница русского языка в ANSI имеет номер 1251. Кодировка символов в ней отличается от принятой в DOS т.н. альтернативной кодировки. В целях совместимости для DOS-программ, а также для консольных приложений Windows использует альтернативную кодировку. Именно поэтому при выводе русского текста в консольных приложениях получаются те самые "кракозябры". Чтобы избежать этого, следует перекодировать символы из кодировки ANSI в DOS при выводе, и наоборот - при вводе. Это можно сделать с помощью функций CharToOem и OemToChar.
Windows NT/2000/XP поддерживает ANSI и Unicode в полном объёме. Это значит, что любая функция, работающая со строками, представлена в двух вариантах: для ANSI и для Unicode. Windows 9x/ME в полном объёме поддерживает только ANSI. Unicode-варианты в этих системах есть у относительно небольшого числа функций. Каждая страница MSDN, посвящённая функции, работающей со строками (или со структурами, содержащими строки), в нижней части содержит надпись, показывающую, реализован ли Unicode-вариант этой функции только для NT/2000/XP или для всех платформ.
Примечание: в качестве примера рассмотрим функции вывода текста на экран. Unicode-версию на всех платформах имеют только две из них: TextOut и ExtTextOut. Функции DrawText и DrawTextEx имеют Unicode-варианты только в Windows NT/2000/XP. Если же смотреть функции для работы с окнами, то среди них нет ни одной, которая имела бы Unicode-вариант в Windows 9x/ME.
Рассмотрим, как сосуществуют два варианта функций на примере функции RegisterWindowMessage. Согласно справке, она экспортируется библиотекой user32.dll. Однако, если посмотреть список функций, экспортируемых этой библиотекой (это можно сделать, например, при помощи утилиты TDump.exe, входящей в состав Delphi), то там этой функции не будет, зато будут функции RegisterWindowMessageA и RegisterWindowMessageW. Первая из них - это ANSI-вариант функции, вторая - Unicode-вариант (буква W означает Wide - широкий; символы кодировки Unicode часто называются широкими из-за того, что на один символ приходится не один, а два байта).
Сначала разберёмся с тем, как используются два варианта одной функции в Microsoft Visual C++. В стандартных заголовочных файлах предусмотрено использование макроопределения UNICODE. Есть два символьных типа - CHAR для ANSI и WCHAR для Unicode. Если макрос UNICODE определён, тип TCHAR соответствует типу WCHAR, если не определён - типу CHAR (после этого производные от TCHAR типы, такие как LPCTSTR, автоматически начинают соответствовать кодировке, определяемой наличием или отсутствием определения UNICODE). В заголовочных файлах импортируются оба варианта функции, а также определяется макрос RegisterWindowMessage. Его смысл также зависит от макроса UNICODE: если он определён, RegisterWindowMessage эквивалентен RegisterWindowMessageW, если не определён - RegisterWindowMessageA. Все функции, поддерживающие два варианта кодировки, импортируются точно так же. Таким образом, вставляя или убирая макроопределение UNICODE, можно, не меняя ни единого символа в программе, компилировать её ANSI- или Unicode-версию.
Разработчики Delphi не стали полностью копировать этот механизм - видимо, этому помешала существующая в Delphi раздельная компиляция модулей, из-за которой невозможно определением одного символа заставить все модули перекомпилироваться (тем более что часть из них может не иметь исходных кодов). Поэтому в Delphi нет типа, аналогичного TCHAR.
Рассмотрим, как та же функция RegisterWindowMessage импортируется модулем Windows.
interface
...
function RegisterWindowMessage(lpString: PChar): UINT; stdcall;
function RegisterWindowMessageA(lpString: PAnsiChar): UINT; stdcall;
function RegisterWindowMessageW(lpString: PWideChar): UINT; stdcall;
...
implementation
...
function RegisterWindowMessage; external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageA; external user32 name 'RegisterWindowMessageA';
function RegisterWindowMessageW; external user32 name 'RegisterWindowMessageW';
Видно, что функция RegisterWindowMessageA импортируется дважды: один раз под своим настоящим именем, а второй раз - под именем RegisterWindowMessage. Любое из этих имён может быть использовано для вызова ANSI-варианта этой функции (напоминаю, что типы PChar и PAsniChar эквивалентны). Чтобы вызвать Unicode-вариант функции, нужно использовать функцию RegisterWindowMessageW.
Структуры, содержащие строковые данные, также имеют ANSI- и Unicode-вариант. Например, структура WNDCLASS в модуле Windows представлена типами TWndClassA (с синонимами WNDCLASSA и tagWNDCLASSA) и TWndClassW (с синонимами WNDCLASSW и tagWNDCLASSW). Тип TWndClass (и его синонимы WNDCLASS и tagWNDCLASS) эквивалентен типу TWndClassA.
Unicode используется редко, т.к. программы, использующие эту кодировку, не работают в Windows 9x/ME. Библиотека VCL также игнорирует возможность использования Unicode, ограничиваясь ANSI. Поэтому далее мы будем говорить только об использовании ANSI. Кодировку Unicode можно использовать аналогично с точностью до замены PChar на PWideChar и string на WideString.
Для работы со строками в Delphi чаще всего используется тип AnsiString, обычно называемый просто string. Переменная типа string является указателем на строку, хранящуюся в динамической памяти. Этот указатель указывает на первый символ строки. По отрицательному смещению хранится число символов в строке и счётчик ссылок.
Счётчик ссылок позволяет избежать ненужных копирований строки, реализуя т.н. "копирование при необходимости". Если присвоить одной строковой переменной значение другой строковой переменной, то строка не копируется, а просто обе переменные начинают указывать на одну и ту же строку. Счётчик ссылок при этом увеличивается на единицу. Когда строка модифицируется, проверяется счётчик ссылок: если он не равен единице, то строка копируется, счётчик ссылок старой копии уменьшается на единицу, у новой копии счётчик ссылок будет равен единице, и переменная, которая меняется, будет указывать на новую копию. Таким образом, строка копируется только тогда, когда одна из ссылающихся на неё переменных начинает изменять эту строку, чтобы изменения не коснулись остальных переменных. При любых модификациях строки в её конец автоматически добавляется нулевой символ (при подсчёте длины строки с помощью функции Length он игнорируется). Но если присвоить строковой переменной пустую строку, то эта переменная станет нулевым указателем (nil), память для хранения одного символа #0 выделена не будет. При выходе строковой переменной из области видимости (т.е., например, при завершении процедуры, в которой она является локальной переменной, или при уничтожении объекта, полем которого она является) она автоматически финализируется, т.е. счётчик ссылок уменьшается на единицу и, если он оказывается равен нулю, память, выделенная для строки, освобождается.
Механизм выделения и освобождения памяти и подсчёта ссылок прозрачен для программы. От разработчика требуется только не вмешиваться в работу этого механизма с помощью низкоуровневых операций с указателями, чтобы менеджер памяти не запутался.
Примечание: в отличие от string, тип WideString не имеет счётчика ссылок, и любое присваивание переменных этого типа приводит к копированию строки. Это сделано в целях совместимости с системным типом BSTR, использующимся в COM/DCOM и OLE.
Функции Win API не поддерживают тип string. Они работают со строками, оканчивающимися на #0 (нуль-терминированные строки, null-terminated strings). Это означает, что строкой называется указатель на цепочку символов. Признаком конца такой цепочки является символ с кодом 0. Раньше для таких строк использовали термин ASCIIZ. ASCII - название кодировки, Z - zero. Сейчас кодировка ASCII в чистом виде не используется, поэтому этот термин больше не применяется, хотя это те же самые по своей сути строки. Как уже говорилось выше, в Delphi ко всем строкам типа string неявно добавляется нулевой символ, не учитывающийся при подсчёте числа символов. Это сделано для совместимости с нуль-терминированными строками. Однако эта совместимость ограничена.
Для работы с нуль-терминированными строками в Delphi обычно используется тип PChar. Формально это указатель на один символ типа Char, но подразумевается, что это только первый символ строки, и за ним следуют остальные символы. Где будут эти символы размещены и какими средствами для них будет выделена память, программист должен решить самостоятельно. Он же должен позаботиться о том, чтобы в конце цепочки символов стоял #0.
Строку, на которую указывает PChar, можно использовать везде, где можно использовать string - компилятор сам выполнит необходимые преобразования. Обратное неверно. Фактически, string - это указатель на начало строки, завершающейся нулём, т.е. тот самый указатель, который требуется при использовании PChar. Однако, как уже отмечалось выше, некорректные манипуляции с этим указателем могут привести к нежелательным эффектам, поэтому компилятор требует явного приведения переменных и выражений типа string к PChar. В свою очередь, программист, использующий это приведение, должен ясно представлять, к каким последствиям это может привести.
Если посмотреть описание функций API, имеющих строковые параметры, в справке, можно заметить, что в некоторых случаях строковые параметры имеют тип LPCTSTR (как, например, у функции SetWindowText), а в некоторых - LPTSTR (GetWindowText). Выше мы говорили, что появление префикса "C" после "LP" указывает на то, что это указатель на константу, т.е. то, на что указывает такой указатель, не может быть изменено. Тип LPCTSTR имеют те строковые параметры, которые содержимое которых функция только читает, но не модифицирует. С такими параметрами работать проще всего. Рассмотрим на примере функции SetWindowText, как можно работать с такими параметрами.
SetWindowText(Handle,'Строка');
SetWindowText(PChar(S));
SetWindowText(PChar('Выполнено '+IntToStr(X)+'%'));
В первом варианте компилятор размещает строковую константу в сегменте кода, а в функцию передаёт указатель на эту константу. Так как функция не модифицирует строку, а только читает, передача такого указателя не вызывает проблем.
Во втором варианте функции передаётся указатель, хранящийся в переменной S. Такое приведение string к PChar безопасно, т.к. строка, на которую ссылается переменная S, не будет модифицироваться. Но здесь существует одна тонкость: конструкция PChar(S) - это не просто приведение типов, при её использовании неявно вызывается функция _LStrToPChar. Как мы уже говорили выше, когда string хранит пустую строку, указатель просто имеет значение nil. Функция _LStrToPChar проверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещён в сегменте кода. Поэтому даже если S содержит пустую строку, в функцию будет передан ненулевой указатель.
Вычисление строковых выражений требует перераспределения памяти, а это компилятор делает только с выражениями типа string. Поэтому результат выражения, приведённого в третьем варианте, также имеет тип string. Но его можно привести к PChar. Память для хранения результата выражения выделяется динамически, как и для обычных переменных типа string. Чтобы передать указатель на это выражение в функцию, нужно привести его к PChar. В эпилог функции, вызывающей SetWindowText или иную функцию с подобным аргументом, добавляется код, который освобождает динамически сформированную строку, поэтому утечек памяти не происходит.
Разумеется, существуют и другие способы формирования параметра типа LPCTSTR, кроме предложенных здесь. Можно, например, выделить память для нуль-терминированной строки с помощью StrNew или родственной ей функции из модуля SysUtils. Можно использовать массив типа Char. Можно выделять память какими-либо другими способами. Но предложенные здесь три варианта в большинстве случаев наиболее удобны.
Параметры типа LPTSTR используются в тех случаях, когда функция может не только читать, но и модифицировать передаваемое ей значение. В большинстве случаев такие параметры чисто выходные, т.е. функция не интересуется, какое значение имел параметр при вызове, используя его только для возврата значения. При возврате строкового значения всегда возникает проблема: где, кем и как будет выделена память, в которую будет записана строка? Функции Win API, за очень редким исключением, решают эту проблему следующим образом: память должна выделить вызывающая программа, а в функцию передаётся указатель на этот заранее выделенный блок. Сама функция только копирует строку в этот блок.
Таким образом, перед программой встаёт задача узнать, какой объём памяти следует выделить под возвращаемую строку. Здесь API не предлагает универсального решения - разные функции по-разному решают эту проблему. Например, при получении заголовка окна с помощью GetWindowText размер этого заголовка можно узнать, вызвав предварительно GetWindowTextLength. Функции типа GetCurrentDirectory возвращают длину строки. Если при первом вызове этой функции памяти выделено недостаточно, можно увеличить буфер и вызвать функцию ещё раз. И, наконец, есть функции типа SHGetSpecialFolderPath, в описании которых написано, каков минимальный размер буфера, необходимый для корректной работы функции. Следует также отметить, что большинство API-функций, возвращающих строки, в качестве одного из параметров принимают размер буфера, чтобы не скопировать больше байт, чем буфер может принять.
Выделять буфер для получения строки можно многими способами. На практике удобнее всего бывает использовать статические массивы, тип string или динамически выделять память для нуль-терминированных строк.
Статические массивы могут использоваться, если размер буфера известен на этапе компиляции. Массивы типа Char с начальным индексом 0 рассматриваются компилятором как нуль-терминированные строки, поэтому с ними удобно выполнять дальнейшие операции. Этот способ удобен тем, что не нужно заботиться о выделении и освобождении памяти, поэтому он часто применяется там, где формально длина строки на этапе неизвестна, но "исходя из здравого смысла" можно сделать вывод, что в подавляющем большинстве случаев эта длина не превысит некоторой величины, которая и берётся в качестве размера массива.
Строки типа string также можно использовать как буфер для получения строковых значений от системы. Для этого нужно предварительно установить требуемую длину строки с помощью SetLength, а затем передать указатель на начало строки в функцию API. Здесь следует соблюдать осторожность: если длина строки окажется равной нулю, переменная типа string будет иметь значение nil, а система попытается записать по этому указателю пустую строку, состоящую из единственного символа #0. Это приведёт к ошибке Access violation.
Третий способ - выделение памяти для буфера с помощью StrAlloc или аналогичной ей функции. Память, выделенную таким образом, следует обязательно освобождать с помощью StrDispose. При этом крайне желательно использовать конструкцию try/finally, чтобы возникновение исключений не привело к утечкам памяти.
Все три способа получения данных строковых данных от функций Win API используются в примере EnumWnd.
Замечательная библиотека VCL позволяет делать очень сложные вещи, не задумываясь об их внутренней реализации. Ограничиваясь возможностями VCL, можно писать достаточно сложные программы и даже получать за них неплохие деньги. Но настоящему программисту должно быть тесно в рамках VCL. Использование API позволяет существенно раздвинуть эти рамки. Я надеюсь, что в данной статье мне удалось в общих чертах рассказать о том, что такое API, и слегка приподнять завесу таинственности над тем, как API используется в VCL. Чтобы двигаться дальше, нужно работать с тремя источниками информации: MSDN, справкой по VCL и с исходными кодами VCL. Это не всегда бывает легко, особенно сначала, но информация оттуда позволяет легко делать такие вещи, о которых обычный "компоненто-на-форму-кидатель" (язык не поворачивается называть таких людей программистами) даже мечтать не смеет. Так что результат вполне стоит труда, на него затраченного.
- Фень Юань Программирование графики для Windows // СПб.: Питер, 2002
К материалу прилагаются файлы:
[Ядро, структуры и механизмы Windows, использование API]
Обсуждение материала [ 15-10-2010 04:10 ] 56 сообщений |