|
||||||||||||||||||||||||||||||||||
|
Глава 1Windows API и Delphi Библиотека VCL, делающая создание приложений в Delphi таким быстрым и удобным, все же не позволяет разработчику задействовать все возможности операционной системы. Полный доступ к ним дает API (Application Programming Interface) — интерфейс, который система предоставляет программам. С его помощью можно получить доступ ко всем документированным возможностям системы. Программированию в Windows на основе API посвящено много книг, а также материалов в Интернете. Но если все делать только с помощью API, то даже для того, чтобы создать пустое окно, потребуется написать несколько десятков строк кода, а о визуальном проектировании такого окна придется вообще забыть. Поэтому желательно как-то объединить мощность API и удобство VCL. О том, как это сделать, мы и поговорим в этой главе. В первой части главы рассматриваются общие принципы использования API и интеграции этого интерфейса с VCL. Во второй части разбираются простые примеры, иллюстрирующие теорию. В третьей части представлено несколько обобщающих примеров использования API — небольших законченных приложений, использующих различные функции API для решения комплексных задач. 1.1. Основы работы Windows API в VCL-приложенияхВ данном разделе будет говориться о том. как совместить Windows API и компоненты VCL. Предполагается, что читатель владеет основными методами создания приложений с помощью VCL а также синтаксисом языка Delphi, поэтому на этих вопросах мы останавливаться не будем. Так как "официальные" справка и примеры работы с API предполагают работу на С или C++, и это может вызвать трудности у человека, знакомого только с Delphi, здесь также будет уделено внимание тому, как правильно читать справку и переводить содержащийся в ней код с C/C++ на Delphi. 1.1.1. Что такое Windows APIWindows API — это набор функций, предоставляемых операционной системой каждой программе. Данные функции находятся в стандартных динамически компонуемых библиотеках (Dynamic Linked Library. DLL), таких как kernel32.dll, user32.dll, gdi32.dll. Указанные файлы располагаются в системной директории Window. Вообще говоря, каждая программа должна самостоятельно заботиться о том. чтобы подключить эти библиотеки. DLL могут подключаться к программе статически и динамически. В первом случае связь с библиотекой прописывается в исполняемом файле программы, и система при запуске этой программы сразу же загружает в ее адресное пространство и библиотеку. Если требуемая библиотека на диске не найдена, запуск программы будет невозможен. В случае динамического подключения программа загружает библиотеку в любой удобный для нее момент времени с помощью функции LoadLibrary. Если при этом возникает ошибка из-за того, что библиотека не найдена на диске, программа может самостоятельно решить, как на это реагировать. Статическая загрузка проще динамической, но динамическая гибче. При динамической загрузке программист может, во-первых, выгрузить библиотеку, не дожидаясь окончания работы программы. Во-вторых, программа может продолжить работу, даже если библиотека не найдена. В-третьих, возможна загрузка тех DLL, имена которых неизвестны на момент компиляции. Это позволяет расширять функциональность приложения после передачи его пользователю с помощью дополнительных библиотек (в англоязычной литературе такие библиотеки обычно называются plug-in). Стандартные библиотеки необходимы самой системе и всем программам, они всегда находятся в памяти, и поэтому обычно они загружаются статически. Чтобы статически подключить в Delphi некоторую функцию Windows API. например, функцию GetWindowDCиз модуля user32.dll, следует написать конструкцию вида function GetWindowDC(Wnd: HWnd); HDC; stdcall; external 'user32.dll' name 'GetWindowDC'; В результате в специальном разделе исполняемого файла, который называется таблицей импорта, появится запись, что программа импортирует функцию GetWindowDCиз библиотеки user32.dll. После такого объявления компилятор будет знать, как вызывать эту функцию, хотя ее реальный адрес будет внесен в таблицу импорта только при запуске программы. Обратите внимание, что функция GetWindowDC, как и все функции Windows API, написана в соответствии с моделью вызова stdcall, а в Delphi по умолчанию принята другая модель — register(модель вызова определяет, как функции передаются параметры). Поэтому при импорте функций из стандартных библиотек необходимо явно указывать эту модель (подчеркнем, что это относится именно к стандартным библиотекам; другие библиотеки могут использовать любую другую модель вызова, разработчик библиотеки свободен в своем выборе). Далее указывается, из какой библиотеки импортируется функция и какое название в ней она имеет. Дело в том, что имя функции в библиотеке может не совпадать с тем, под которым она становится известной компилятор). Это может помочь разрешить конфликт имен при импорте одноименных функций из разных библиотек, а также встречается в других ситуациях, которые мы рассмотрим позже. Главным недостатком DLL следует считать то. что в них сохраняется информация только об именах функций, но не об их параметрах. Поэтому если при импорте функции указать не те параметры, которые подразумевались автором DLL, то программа будет работать неправильно (вплоть до зависания), а ни компилятор, ни операционная система не смогут указать на ошибку. Обычно программе требуется много различных функций Windows API. Декларировать их все довольно утомительно. К счастью. Delphi избавляет программиста от этой работы: многие из этих функций уже описаны в соответствующих модулях, достаточно упомянуть их имена в разделе uses. Например, большинство общеупотребительных функций описаны в модулях Windowsи Messages. Функции API, которые присутствуют не во всех версиях Windows, предпочтительнее загружать динамически. Например, если программа статически импортирует функцию SetLayeredWindowsAttributes, она не запустится в Windows 9x, где этой функции нет — система, встретив ее упоминание в таблице импорта, прервет загрузку программы. Поэтому, если требуется, чтобы программа работала и в Windows 9x, эту функцию следует импортировать динамически. Отметим, что компоновщик в Delphi помещает в таблицу импорта только те функции, которые реально вызываются программой. Поэтому наличие декларации SetLayeredWindowsAttributesв модуле Windows не помешает программе запускаться в Windows 9x, если она не вызывает эту функцию. 1.1.2. Как получить справку по функциям Windows APIДля тех, кто решит работать с Windows API, самым необходимым инструментом становится какая-либо документация по этим функциям. Их так много, что запомнить все совершенно нереально, поэтому работа без справочника под рукой просто невозможна. Первоисточник информации по технологиям Microsoft для разработчика Microsoft Developer's Network (MSDN). Это отдельная справочная система, не входящая в комплект поставки Delphi. MSDN можно приобрести отдельно или воспользоваться online-версией, находящейся по адресу: http://msdn.microsoft.com (доступ к информации свободный, регистрация не требуется). MSDN содержит не только информацию об API, но и все, что может потребоваться программисту, использующему различные средства разработки от Microsoft. Кроме справочного материала, MSDN включает в себя спецификации стандартов и технологий, связанных с Windows, статьи из журналов, посвященных программированию, главы из некоторых книг. И вся эта информация крайне полезна разработчику. Кроме того, MSDN постоянно обновляется, информация в нем наиболее актуальна. Пример справки из MSDN показан на рис. 1.1. Рис. 1.1. Online-вариант MSDN (показана справка по функции DeleteObject) Примечание В комплект поставки Delphi входит справочная система, содержащая описание функций Windows API. Справочная система в Delphi до 7-й версии включительно была построена на основе hlp-файлов. Применительно к справке по Windows API это порождало две проблемы. Во-первых, hlp-файлы имеют ограничение по числу разделов в справочной системе, поэтому объединить в одной справке информацию и по Delphi, и по Windows API было невозможно, — эти две справки приходилось читать по очереди. Чтобы открыть файл справки по Windows API, нужно было в редакторе кода поставить курсор на название какой-либо функции API и нажать клавишу <F1>— в этом случае вместо справки по Delphi открывалась справка по Windows API. Второй вариант — в меню Программы найти папку Delphi, а в ней — папку Help\MS SDK Files и выбрать требуемый раздел. Можно также вручную открыть файл MSTools.hlp. В ранних версиях Delphi он находится в каталоге $(Delphi)\Help, в более поздних его нужно искать в $(Program Files)\Common Files. Окно старой справки показано на рис. 1.2. Вторая проблема, связанная со справкой на основе hlp-файлов,— это то обстоятельство, что разработчики Delphi, разумеется, не сами писали эту справку, а взяли ту, которую предоставила Microsoft. Microsoft же последнюю версию справки в формате НLР выпустила в тот момент, когда уже вышла Windows 95, но еще не было Windows NT 4. Поэтому про многие функции, прекрасно работающие в NT 4, там написано, что в Windows NT они не поддерживаются, т.к. в более ранних версиях они действительно не поддерживались. В справке, поставляемой с Delphi 7 (и, возможно, с некоторыми более ранними версиями), эта информация подправлена, но даже и там отсутствуют функции, которые появились только в Windows NT 4 (как, например, CoCreateInstanceEx). И уж конечно, бесполезно искать в этой справке информацию о функциях, появившихся в Windows 98, 2000, XР. Соответственно, при работе в этих версиях Delphi даже не возникает вопрос, что предпочесть для получения информации о Windows API, — справку, поставляемую с Delphi, или MSDN. Безусловно, следует выбрать MSDN. Справка, поставляемая с Delphi, имеет только одно преимущество по сравнению с MSDN: ее можно вызывать из среды нажатием клавиши <F1>. Но риск получить неверные сведения слишком велик, чтобы это преимущество могло быть серьезным аргументом. Единственная ситуация, когда предпочтительна справка, поставляемая с Delphi, — это случай, если у вас нет достаточно быстрого доступа к Интернету для работы с online-версией MSDN и нет возможности приобрести и установить его offline-версию. Рис. 1.2. Старая (на основе hlp-файлов) справка по Windows API (показана функция DeleteObject) Начиная с BDS 2006, Borland/CodeGear реализовала новую справочную систему Borland Help (рис. 1.3). По интерфейсу она очень напоминает offline версию MSDN, а также использует файлы в том же формате, поэтому технологических проблем интеграции справочных систем по Delphi и по Windows API больше не существует. В справку BDS 2006 интегрирована справка по Windows API от 2002–2003 годов (разные разделы имеют разную дату) Справка Delphi 2007 содержит сведения по Windows API от 2006 года, т.е. совсем новые. Таким образом, при работе с Delphi 2007 наконец-то можно полностью отказаться от offline-версии MSDN, а к online-версии обращаться лишь изредка, когда требуется информация о самых последних изменениях в Windows API (например, о тех, которые появились в Windows Vista). Примечание Рис. 1.3. Окно справки Delphi 2007 (функция DeleteObject) Система Windows написана на C++, поэтому все описания функций Windows API, а также примеры их использования приведены на этом языке (это касается как MSDN, так и справки, поставляемой с Delphi). При этом, прежде всего, необходимо разобраться с типами данных. Большинство типов, имеющихся в Windows API. определены в Delphi. Соответствие между ними показано в табл. 1.1. Таблица 1.1. Соответствие типов Delphi системным типам
Название типов указателей имеет префикс P или LP (Pointer или Long Pointer, в 16-разрядных версиях Windows были короткие и длинные указатели. В 32-разрядных все указатели длинные, поэтому оба префикса имеют одинаковый смысл). Например, LPDWORDэквивалентен типу ^DWORD, PUCHAR— ^Byte. Иногда после префикса P или LP стоит еще префикс C — он означает, что это указатель на константу. В C++ возможно объявление таких указателей, которые указывают на константное содержимое, т.е. компилятор разрешает это содержимое читать, но не модифицировать. В Delphi такие указатели отсутствуют, и при портировании эти типы заменяются обычными указателями, т.е. префикс C игнорируется. Типы PVOIDи LPVOIDсоответствуют нетипизированным указателям ( Pointer). Для передачи символов чаще всего используется тип TCHAR. Windows поддерживает две кодировки: ANSI (1 байт на символ) и Unicode (2 байта на символ; о поддержке Unicode в Windows мы будем говорить далее). Тип CHARсоответствует символу в кодировке ANSI, WCHAR— Unicode. Для программ, которые используют ANSI, тип TCHARэквивалентен типу CHAR, для использующих Unicode — WCHAR. В Delphi нет прямого аналога типу TCHAR. Программист сам должен следить за тем, какой символьный тип требуется в данном месте. Строки в Windows API передаются как указатели на цепочку символов, завершающихся нулем. Поэтому указатель на TCHARможет указывать как на единичный символ, так и на строку. Чтобы было легче разобраться, где какой указатель, в Windows API есть типы LPTCHARи LPTSTR. Они эквивалентны друг другу, но первый принято использовать там, где требуется указатель на одиночный символ, а второй — на строку. Если строка передается в функцию только для чтения, обычно используют указатель на константу, т.е. тип LPCTSTR. В Delphi это соответствует PChar для ANSI и PWideChar для Unicode. Здесь следует отметить особенность записи строковых литералов в языках C/C++. Символ \ в литерале имеет специальное значение: после него идет один или несколько управляющих символов. Например, \n означает перевод строки, \t — символ табуляции и т.п. В Delphi таких последовательностей нет, поэтому при переводе примеров из MSDN следует явно писать коды соответствующих символов. Например, литерал "а\nb"в Delphi превращается в 'a\'#13'b'. После символа \может идти число — в этом случае оно трактуется как код символа, т.е. литерал "a\0b9"в C/C++ эквивалентен литералу ' а'#0'b'#9в Delphi. Если нужно, чтобы строковый литерал включал в себя сам символ \, его удваивают, т.е. литерал "\\"в C++ соответствует '\'в Delphi. Кроме того, в примерах кода, приведенных в MSDN, можно нередко увидеть, что строковые литералы обрабатываются макросами TEXTили _T, которые служат для унификации записи строковых литералов в кодировках ANSI и Unicode. При переводе такого кола на Delphi эти макросы можно просто опустить. С учетом сказанного такой, например, код (взят из примера использования Named pipes): LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); на Delphi будет выглядеть так: var lpszPipeName: PChar; ... lpszPipeName:= '\\.\pipe\mynamedpipe'; Большинство названий типов из левой части табл. 1.1 в целях совместимости описаны в модуле Windows, поэтому они допустимы наравне с обычными типами Delphi. Кроме этих типов общего назначения существуют еще специальные. Например, дескриптор окна имеет тип HWND, первый параметр сообщения — тип WPARAM(в старых 16-разрядных Windows он был эквивалентен типу Word, в 32-разрядных — LongInt). Эти специальные типы также описаны в модуле Windows. Записи ( record) в C/C++ называются структурами и объявляются с помощью слова struct. Из-за особенностей описания структур на языке С структуры в Windows API получают два имени: одно основное имя, составленное из главных букв, которое затем и используется, и одно вспомогательное, получающееся из основного добавлением префикса tag. Начиная с четвертой версии Delphi приняты следующие правила именования таких типов: простое и вспомогательное имена остаются без изменений и еще добавляется новое имя, получающееся из основного присоединением общеупотребительного в Delphi префикса T. Например, в функции CreatePenIndirectодни из параметром имеет тип LOGPEN. Это основное имя данного типа, а вспомогательное — tagLOGPEN. Соответственно, в модуле Windows определена запись tagLOGPENи ее синонимы — LOGPENи TLogPen. Эти три идентификатора в Delphi взаимозаменяемы. Вспомогательное имя встречается редко, программисты, в зависимости от личных предпочтений, выбирают либо основное имя типа, либо имя с префиксом T. Описанные здесь правила именования типов могут внести некоторую путаницу при использовании VCL. Например, для описания растра в Windows API определен тип BITMAP(он же— tagBITMAP). В Delphi соответствующий тип имеет еще одно имя — TBitmap. Но такое же имя имеет класс TBitmap, описанный в модуле Graphics. В коде, который Delphi создает автоматически, модуль Graphicsнаходится в списке usesпосле модуля Windows, поэтому идентификатор TBitmapвоспринимается компилятором как Graphics.TBitmap, а не как Windows.TBitmap. Чтобы использовать Windows.ТBitmap, нужно явно указать имя модуля или воспользоваться одним из альтернативных имен. В более ранних версиях Delphiбыли другие правила именования типов. Например. в Delphi 2 существовал тип BITMAP, но не было TBitmapи tagBITMAP, а в Delphi 3 из этих трех типов был только TBitmap. Все структуры в Windows API описаны без выравнивания, т.е. компилятор не вставляет между полями неиспользуемые байты, чтобы границы полей приходились на начало двойного или четверного слова, поэтому в Delphi для описания соответствующих структур предусмотрено слово packed, запрещающее выравнивание. При описании структур Windows API можно иногда встретить ключевое слово union(см., например, структуру in_addr). Объединение нескольких полей с помощью этого слова означает, что все они будут размещены по одному адресу. В Delphi это соответствует вариантным записям (т. е. использованию сазе в record). Объединения в C/C++ гибче, чем вариантные записи Delphi, т.к. позволяют размещать вариантную часть в любом месте структуры, а не только в конце. При переносе таких структур в Delphi иногда приходится вводить дополнительные типы. Теперь рассмотрим синтаксис описания самой функции в C++ (листинг 1.1). Листинг 1.1. Синтаксис описания функции на C++<Тип функции> <Имя функции> ' (' [<Тип параметра> {<Имя параметра>} (',' <Тип параметра> {<Имя параметра>} } ] ')'; Как видно из листинга 1.1, при объявлении функции существует возможность указывать только типы параметров и не указывать их имена. Однако это считается устаревшим и применяется крайне редко (если не считать "параметров" типа VOID, о которых написано далее). Необходимо помнить, что в C/C++ различаются верхний и нижний регистры, поэтому HDC, hdc, hDCи т.д. — это разные идентификаторы (автор С очень любил краткость и хотел, чтобы можно было делать не 26, а 52 переменные с именем из одной буквы). Поэтому часто можно встретить, что имя параметра и его тип совпадают с точностью до регистра. К счастью, при описании функции в Delphi мы не обязаны сохранять имена параметров, значение имеют лишь их типы и порядок следования. С учетом всего этого функция, описанная в справке как HMETAFILE CopyMetaFile(HMETAFILE hmfSrc, LPCTSTR lpszFile); в Delphi имеет вид function СоруМеtaFile(hnfSrc: HMETAFILE; lpszFile: LPCTSTR): HMETAFILE; или, что то же самое. function CopyMetaFile(hnfSrc: HMETAFILE; lpszFile: PChar): HMETAFILE; Примечание Несколько особняком стоит тип VOID(или void, что то же самое, но в Windows API этот идентификатор встречается существенно реже). Если функции имеет такой тип, то в Паскале она описывается как процедура. Если вместо параметров у функции в скобках указан void, это означает, что функция не имеет параметров. Например, функция VOID CloseLogFile(VOID); в Delphi описывается как procedure CloseLogFile; Примечание Когда тип параметра является указателем на другой тип (обычно начинается с букв LP), при описании этой функции в Delphi можно пользоваться параметром-переменной, т.к. в этом случае функции передается указатель. Например, функция int GetRgnBox(HRGN hrgn, LPRECT lprc); в модуле Windows описана как function GetRgnBox(RGN: HRGN; var p2: TRec): Integer; Такая замена целесообразна в том случае, если значение параметра не может быть нулевым указателем, потому что при использовании var передать такой указатель будет невозможно. Нулевой указатель в C/C++ обозначается константой NULL. NULLи 0в этих языках взаимозаменяемы, поэтому в справке можно и про целочисленный параметр встретить указание, что он может быть равен NULL. И наконец, если не удается понять, как функция, описанная в справке, должна быть переведена на Паскаль, можно попытаться найти описание этой функции в исходных текстах модулей, поставляемых вместе с Delphi. Эти модули находятся в каталоге $(DELPHI)\Source\RTL\Win (до Delphi 7) или $(BDS)\Source\Win32\RTL\Win (BDS 2006 и выше). Можно также воспользоваться подсказкой, которая всплывает в редакторе Delphi после того, как будет набрано имя функции. Если посмотреть справку, например, по функции GetSystemMetrics, то видно, что эта функция должна иметь один целочисленный параметр. Однако далее в справке предлагается при вызове этой функции подставлять в качестве параметра не числа, a SM_ARRANGE, SM_CLEANBOOTи т.д. Подобная ситуация и со многими другими функциями Windows API. Все эти SM_ARRANGE, SM_CLEANBOOTи т.д. являются именами числовых констант. Эти константы описаны в том же модуле, в котором описана функция, использующая их, поэтому можно не выяснять численные значения этих констант, а указывать при вызове функций их имена, например, GetSystemMetrics(SM_ARRANGE);. Если по каким-то причинам все-таки потребовалось выяснить численные значения, то в справочной системе их искать не стоит — их там нет. Их можно узнать из исходных текстов модулей Delphi, в которых эти константы описаны. Так, например, просматривая Windows.pas, можно узнать, что SM_ARRANGE = 56. В справке, поставляемой вместе с Delphi до 7-й версии включительно, в описании многих функций Windows API вверху можно увидеть три ссылки: QuickInfo, Overview и Group. Первая дает краткую информацию о функции: какой библиотекой реализуется, в каких версиях Windows работает и т.п. (напоминаю, что к информации о версиях в этой справке нужно относиться очень критично). Overview — это обзор какой-то большой темы. Например, для любой функции, работающей с растровыми изображениями, обзор будет объяснять, зачем в принципе нужны эти самые растровые изображения и как они устроены. Страница, на которую ведет ссылка Overview обычно содержит весьма лаконичные сведения, но, нажав кнопку >>, расположенную в верхней части окна, можно получить продолжение обзора. И, наконец, Group. Эта ссылка приводит к списку всех функций, родственных данной. Например, для функции CreateRectRgnгруппу будут составлять все функции, имеющие отношение к регионам. Если теперь нажимать на кнопку <<, то будут появляться страницы с кратким описанием возможных применений объектов, с которыми работают функции (в приведенном примере — описание возможностей регионов). Чтобы читать их в нормальной последовательности, лучше всего нажать на кнопку << столько раз, сколько возможно, а затем пойти в противоположном направлении с помощью кнопки >>. MSDN (а также справка BDS 2006 и выше) предоставляет еще больше полезной информации. В нижней части описания каждой функции есть раздел Requirements, в котором написано, какая библиотека и какая версия Windows требуется для ее использования. В самом низу описания функции расположены ссылки See also. Первая ссылка — обзор соответствующей темы (например, для уже упоминавшейся функции CreateRectRgn— она называется Regions Overview). Вторая список родственных функций (Region Functions в данном случае). Она ведет на страницу, где перечислены все функции, родственные выбранной. После этих двух обязательных ссылок идут ссылки на описание функций и типов, которые обычно используются совместно с данной функцией. Основные типы, константы и функции Windows API объявлены в модулях Windowsи Messages. Но многие функции объявлены в других модулях, которые не подключаются к программе по умолчанию, программист должен сам выяснить, в каком модуле находится требуемый ему идентификатор, и подключить этот модуль. Ни справка, поставляемая с Delphi, ни MSDN, разумеется, не могут дать необходимую информацию. Чтобы выяснить, в каком модуле объявлен нужный идентификатор, можно воспользоваться поиском по всем файлам с расширением pas, находящимся в папке с исходными кодами стандартных модулей. Этим методом можно, например, выяснить, что весьма популярная функция ShellExecuteнаходится в модуле ShellAPI, CoCreateInstance— в модуле ActiveX(а также в модуле Ole2, оставленном для совместимости со старыми версиями Delphi). Еще несколько слов о числовых константах. В справке можно встретить числа вида, например, 0xC56Fили 0x3341. Префикс 0хв C/C++ означает шестнадцатеричное число. В Delphi его следует заменить на $, т.е. эти числа должны быть записаны как $C56Fи $3341соответственно. 1.1.3. Дескрипторы вместо классовПрограммируя в Delphi, мы быстро привыкаем к тому, что каждый объект реализуется экземпляром соответствующего класса. Например, кнопка реализуется экземпляром класса TButton, контекст устройства — классом TCanvas. Но когда создавались первые версии Windows, объектно-ориентированный метод программирования еще не был общепризнанным, поэтому он не был реализован. Современные версии Windows частично унаследовали этот недостаток, поэтому в большинстве случаев приходится работать "по старинке", тем более что DLL могут экспортировать только функции, но не классы. Когда мы будем говорить об объектах, создаваемых через Windows API, будем подразумевать не объекты в терминах ООП, а некоторую сущность, внутренняя структура которой скрыта от нас, поэтому с этой сущностью мы можем оперировать только как с единым и неделимым (атомарным) объектом. Каждому объекту, созданному с помощью Windows API, присваивается уникальный номер (дескриптор). Его конкретное значение не несет для программиста никакой полезной информации и может быть использовано только для того, чтобы при вызове функций из Windows API указывать, с каким объектом требуется выполнить операцию. В большинстве случаен дескрипторы представляют собой 32-значные числа, а значит, их можно передавать везде, где требуются такие числа. В дальнейшем мы увидим, что Windows API несколько вольно обращается с типами, т.е. один и тот же параметр в различных ситуациях может содержать и число, и указатель, и дескриптор, поэтому знание двоичного представления дескриптора все-таки приносит программисту пользу (хотя если бы система Windows была "спроектирована по правилам", тип дескриптора вообще не должен был интересовать программиста). Таким образом, главное различие между методами класса и функциями Windows API заключается в том. что первые связаны с тем экземпляром класса, через который они вызываются, и поэтому не требуют явного указания на объект. Вторым необходимо указание объекта через его дескриптор, т.к. они сами по себе никак не связаны ни с одним объектом. Компоненты VCL нередко являются оболочками над объектами Delphi. В этом случае они имеют свойство (которое обычно называется Handle), содержащее дескриптор соответствующего объекта. Иногда класс Delphi инкапсулирует несколько объектов Windows. Например, класс TBitmapвключает в себя HBITMAPи HPALETTE— картинку и палитру к ней. Соответственно, он хранит два дескриптора: в свойствах Handleи Palettе. Следует учитывать, что внутренние механизмы VCL не могут включиться, если изменение объекта происходит через Windows API. Например, если спрятать окно не с помощью метода Hide, а путем вызова функции Windows API ShowWindow(Handle, SW_HIDE), не возникнет событие OnHide, потому что оно запускается теми самыми внутренними механизмами VCL. Но такие недоразумения случаются обычно только тогда, когда функциями Windows API дублируется то, что можно сделать и с помощью VCL. Все экземпляры классов, созданные в Delphi, должны удаляться. В некоторых случаях это происходит автоматически, а иногда программист должен сам позаботиться о "выносе мусора". Аналогичная ситуация и с объектами, создаваемыми в Windows API. Если посмотреть справку по функции, создающей какой-то объект, то там обязательно будет информация о том. какой функцией можно удалить объект и нужно ли это делать вручную, или система сделает это автоматически. Во многих случаях совершенно разные объекты могут удаляться одной и той же функцией. Так, функция DeleteObjectудаляет косметические перья, геометрические перья, кисти, шрифты, регионы, растровые изображения и палитры. Обращайте внимание на возможные исключения. Например, регионы не удаляются системой автоматически, однако если вызвать для региона функцию SetWindowRgn, то он переходит в собственность операционной системы. Никакие дальнейшие операции с ним, в том числе и удаление, совершать нельзя. Если системный объект используется только одним приложением, то он будет удален при завершении работы приложения. Тем не менее хороший стиль программирования требует, чтобы программа удаляла объекты явно, а не полагалась на систему. 1.1.4. Формы VCL и окна WindowsПод словом "окно" обычно подразумевается некоторая форма наподобие тех, что можно создать с помощью класса TForm. Однако это понятие существенно шире. В общем случае окном называется любой объект, который имеет экранные координаты и может реагировать на мышь и клавиатуру. Например, кнопка, которую можно создать с помощью класса TButton, — это тоже окно. VCL вносит некоторую путаницу в это понятие. Некоторые визуальные компоненты VCL не являются окнами, а только имитируют их, как, например, TImage. Это позволяет экономить ресурсы системы и повысить быстродействие программы. Механизм этой имитации мы рассмотрим позже, а пока следует запомнить, что окнами являются только те визуальные компоненты которые имеют в числе предков класс TWinControl. Разработчики VCL постарались, чтобы разница между оконными и неоконными визуальными компонентами была минимальной. Действительно, на первый взгляд неоконный TLabelи оконный TStaticTextкажутся практически близнецами. Разница становится заметной тогда, когда используется Windows API. С неоконными компонентами можно работать только средствами VCL, они даже не имеют свойства Handle, в то время как оконными компонентами можно управлять с помощью Windows API. Отметим также еще одно различие между оконными и неоконными компонентами: неоконные компоненты рисуются непосредственно на поверхности родительского компонента, в то время как оконные как бы "кладутся" на родителя сверху . В частности, это означает, что неоконный TLabel, размещенный на форме, не может закрывать собой часть кнопки TButton, потому что TLabelрисуется на поверхности формы, а кнопка — это независимый объект, лежащий на форме и имеющий свою поверхность. A TStaticTextможет оказаться над кнопкой, потому что он тоже находится над формой. Примечание Каждое окно принадлежит к какому-то оконному классу. Не следует путать оконный класс с классами Delphi. Это некий шаблон, определяющий базовые свойства окна. Каждому такому шаблону присваивается имя, уникальное в его области видимости. Перед использованием класс необходимо зарегистрировать (функция RegisterClassEx). В качестве параметра эта функция принимает запись типа TWndClassEx, поля которой содержат параметры класса. С каждым окном должна быть связана специальная функция, называющаяся оконной процедурой (подробнее мы рассмотрим ее чуть позже). Она является параметром не отдельного окна, а всего оконного класса, т. е. все окна, принадлежащие данному классу, будут использовать одну и ту же оконную процедуру. Эта процедура может размещаться либо в самом исполняемом модуле, либо в одной из загруженных им DLL. При создании класса указывается дескриптор модуля, в котором находится оконная процедура. Примечание Дескриптор модуля, загруженного в память, можно получить с помощью функции GetModuleHandle. Функция LoadLibraryв случае успешного завершения также возвращает дескриптор загруженной DLL. Кроме того, Delphi предоставляет две переменные: MainInstanceиз модуля Systemи HInstanceиз модуля SysInit(оба этих модуля подключаются к программе автоматически, без явного указания в списке uses). MainInstanceсодержит дескриптор exe-файла, породившего процесс, HInstance— текущего модуля. В исполняемом файле MainInstanceи HInstanceравны между собой, в DLL HInstanceсодержит дескриптор самой библиотеки, а MainIstance— загрузившего ее главного модуля. Каждое окно в Windows привязывается к какому-либо модулю (в Windows 9х/МЕ необходимо явно указать дескриптор этого модуля. NT 2000 ХР определяет модуль, из которого вызвана функция создания окна, автоматически). Соответственно, оконные классы делятся на локальные и глобальные: окна локальных классов может создавать только тот модуль, в котором находится оконная процедура класса, глобальных — любой модуль данного приложения. Будет ли класс локальным или глобальным, зависит от значений полей TWndClassExпри регистрации класса. Оконный класс, к которому принадлежит окно, указывается при его создании. Это может быть зарегистрированный ранее класс или один из системных классов. Системные классы — это 'BUTTON', 'COMBOBOX', 'EDIT', 'LISTBOX', 'MDICLIENT', 'SCROLLBAR'и 'STATIC'. Назначение этих классов понятно из их названий (класс 'STATIC'реализует статические текстовые или графические элементы, т.е. не реагирующие на мышь и клавиатуру, но имеющие дескриптор). Кроме этих классов существуют также классы из библиотеки ComCtl32.dll, они тоже доступны всем приложениям без предварительной регистрации (подробнее об этих классах можно узнать в MSDN в разделе Common Controls Reference). Для окон в обычном понимании этого слова готовых классов не существует, их приходится регистрировать самостоятельно. В частности, VCL для форм регистрирует оконные классы, имена которых совпадают с именами соответствующих классов VCL. Кроме имени, класс включает в себя другие параметры, такие как стиль, кисть и т.д. Они подробно перечислены в справке. Для создания окна служат функции CreateWindowи CreateWindowEx. При создании окна в числе других параметров задается модуль, к которому оно привязано, имя оконного класса, стиль и расширенный стиль. Последние два параметра определяют поведение конкретного окна и не имеют ничего общего со стилем класса. Результат работы этих функций — дескриптор созданного ими окна. Еще один важный параметр этих функций — дескриптор родительского окна. Окно является подчиненным по отношению к своему родителю. Например, если дочернее окно — это кнопка или иной элемент управления, то визуально оно располагается в другом окне, которое является для нею родительским. Если дочернее окно — это MDIChild, то родительским для него будет MDIForm(если быть до конца точным, то не сама форма MDIForm, а специальное окно класса MDICLIENT, которое является дочерним по отношению к MDIForm; дескриптор этого окна хранится в свойстве ClientHandleглавной формы). Другими словами, отношения "родительское — дочернее окно" отражают принадлежность одного окна другому, визуальную связь между ними. Окна, родитель которых не задан (т.е. в качестве дескриптора родителя передан ноль), располагаются непосредственно на рабочем столе. Если при создании окна задан стиль WS_CHILD, то его координаты отсчитываются от левого верхнего угла клиентской области родительского окна, и при перемещении родительского окна все дочерние окна будут перемещаться вместе с ним. Окно, имеющее стиль WS_CHILD, не может располагаться ни рабочем столе, попытка создать такое окно окончится неудачей. Визуальные компоненты VCL имеют два свойства, которые иногда путают: Owner и Parent. Свойство Parent указывает на объект, реализующий окно, являющееся родительским для данного визуального компонента (компоненты, не являющиеся наследником TWinControl, также имеют это свойство — VCL для них имитирует эту взаимосвязь, однако сами они не могут быть родителями других визуальных компонентов). Свойство Owner указывает на владельца компонента. Отношения "владелец-принадлежащий" реализуются полностью внутри VCL. Свойство Ownerесть у любого наследника TComponent, в том числе и у невизуальных компонентов, и владельцем других компонентов также может быть невизуальный компонент (например, TDataModule). При уничтожении компонента он автоматически уничтожает все компоненты, владельцем которых он является (здесь, впрочем, есть некоторое дублирование функций, т.к. оконный компонент также при уничтожении уничтожает все визуальные компоненты, родителем которых он является). Еще владелец отвечает за загрузку всех установленных во время разработки свойств принадлежащих ему компонентов. Свойство Ownerдоступно только для чтения. Владелец компонента задается один раз при вызове конструктора и остается неизменным на протяжении всего жизненного цикла компонента (за исключением достаточно редких случаев явного вызова методов InsertComponentи RemoveComponent). Свойство Parentзадается отдельно и может быть впоследствии изменено (визуально это будет выглядеть как "перепрыгивание" компонента из одного окна в другое). Визуальный компонент может не иметь владельца. Это означает, что ответственность за его удаление лежит на программисте, создавшем его. Но большинство визуальных компонентов не может функционировать, если свойство Parentне задано. Например, невозможно отобразить на экране компонент TButton, у которого не установлено свойство Parent. Это связано с тем, что большинство оконных компонентов имеет стиль WS_CHILD, который, напомним. не позволяет разместить окно на рабочем столе. Окнами без родителя могут быть только наследники TCustomForm. Впрочем, сделать кнопку, не имеющую родителя, можно средствами Windows API. Например, такой командой (листинг 1.2). Листинг 1.2. Создание кнопки, не имеющей родительского окнаCreateWindow('BUTTON', 'Test', WS_VISIBLE or BS_PUSHBUTTON or WS_POPUP, 10, 10, 100, 50, 0, 0, HInstance, nil); Рекомендуем в этом примере убрать стиль WS_POPUPи посмотреть, что получится — эффект достаточно забавный. Отметим, что создавать такие висящие сами по себе кнопки смысла нет, поскольку сообщения о событиях, происходящих со стандартными элементами управления, получает родительское окно, и при его отсутствии программа не может отреагировать, например, на нажатие кнопки. Кроме обычного конструктора Create, у класса TWinControlесть конструктор CreateParented, позволяющий создавать оконные компоненты, родителями которых являются окна, созданные без использования VCL. В качестве параметра этому конструктору передается дескриптор родительского окна. У компонентов, созданных таким образом, не нужно устанавливать свойство Parent. Примечание Создание окон через Windows API требует кропотливой работы. VCL справляется с этой задачей замечательно, поэтому создавать окна самостоятельно приходится только тогда, когда использование VCL нежелательно, например, если необходимо написать как можно более компактное приложение. Во всех остальных случаях приходится только слегка подправлять работу VCL. Например, с помощью Windows API можно изменить форму окна или убрать из нею заголовок, оставив рамку. Подобные действия не требуют от программиста создания нового окна, можно воспользоваться тем, что уже создано VCL. Другой случай, когда могут понадобиться функции Windows API для окон, — если приложение должно что-то делать с чужими окнами. Например, хотя бы просто перечислить все окна, открытые в данный момент, как это делает входящая в состав Delphi утилита WinSight32. Но в этом случае также не приходится самому создавать окна, работа идет с уже имеющимися. 1.1.5. Функции обратного вызоваПрежде чем двигаться дальше, необходимо разобраться с тем, что такое функции обратного вызова (callback functions: этот термин иногда также переводят "функции косвенного вызова"). Эти функции в программе описываются, но обычно не вызываются напрямую, хотя ничто не запрещает сделать это. В этом они похожи на те методы класса, которые связаны с событиями. Ничто не мешает вызывать напрямую, например, метод FormCreate, но делать это приходится крайне редко. С другой стороны, даже если этот метод не вызывается явно, он все равно выполняется, потому что VCL автоматически вызывает его без прямого указания программиста. Еще одно общее свойство — конкретное имя метода при косвенном вызове не важно. Можно изменить его, но если этот метод по-прежнему будет связан с событием OnCreate, он так же будет успешно вызываться. Разница заключается только в том, что такие методы вызываются внутренними механизмами VCL, а функции обратного вызова — самой системой Windows. Соответственно, на эти функции налагаются следующие требования: во-первых, они должны быть именно функциями, а не методами класса; во-вторых, они должны быть написаны в соответствии с моделью вызова stdcall(MSDN предлагает использовать модель callback, которая в имеющихся версиях Windows является синонимом stdcall). Что же касается того, как программист сообщает системе о том, что он написал функцию обратного вызова, то это в каждом случае будет по-своему. В качестве примера рассмотрим перечисление окон с помощью функции EnumWindows. В справке она описана так: BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam); Соответственно, в Windows.pas она имеет вид function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall; Параметр lpEnumFuncдолжен содержать указатель на функцию обратного вызова. Прототип этой функции описан так: BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam); Функции с таким именем в Windows API не существует. Это так называемый прототип функции, согласно которому следует описывать функцию обратного вызова. На самом деле этот прототип предоставляет большую свободу, чем это может показаться на первый взгляд. Во-первых, имя может быть любым. Во-вторых, система не накладывает строгих ограничений на имена и типы параметров — они могут быть любыми, при условии, что новые типы совпадают по размерам с теми, которые указываются (тип TFNWndEnumProc, описанный в модуле Windows— это не процедурный тип, а просто нетипизированный указатель, поэтому компилятор Delphi не будет контролировать соответствие передаваемой функции обратного вызова ее прототипу). Что касается типа функции и типа первого параметра, то они имеют определенный смысл, и изменение их типа вряд ли может быть полезным. Но второй параметр предназначен специально для передачи значения, которое программист волен использовать но своему усмотрению, система просто передает через него в функцию обратного вызова то значение, которое имел параметр lParamпри вызове функции EnumWindows. А программисту может показаться удобнее работать не с типом lParam(т.е. LongInt), а, например, с указателем или же с массивом из четырех байтов. Лишь бы были именно четыре байта, а не восемь, шестнадцать или еще какое-то число. Можно даже превратить этот параметр в параметр-переменную, т.к. при этом функции будут передаваться все те же четыре байта — адрес переменной. Впрочем, тем, кто не очень хорошо разбирается с тем, как используется стек для передачи параметров при различных моделях вызова, лучше не экспериментировать с изменением типа параметра, а строго следовать заявленному прототипу, при необходимости выполняя требуемые преобразования внутри функции обратного вызова. Функция EnumWindowsработает так: после вызова она начинает по очереди перебирать все имеющиеся в данный момент окна верхнего уровня, т.е. те, у которых нет родителя. Для каждого такого окна вызывается заданная функция обратного вызова, в качестве первого параметра ей передается дескриптор данного окна (каждый раз, естественно, новый), в качестве второго — то, что было передано самой функции EnumWindowsв качестве второго параметра (каждый раз одно и то же). Получая по очереди дескрипторы всех окон верхнего уровня, функция обратного вызова может выполнить с каждым из них определенное действие (закрыть, минимизировать и т.п.). Или можно проверять все эти окна на соответствие какому-то условию, пытаясь найти нужное. А значение, возвращаемое функцией обратного вызова, влияет на работу EnumWindows. Если она возвращает False, значит, все, что нужно, уже сделано, можно не перебирать остальные окна. Окончательный код для того случая, когда второй параметр имеет тип Pointer, иллюстрирует листинг 1.3. Листинг 1.3. Вызов функции EnumWindowsс функцией обратного вызова function MyCallbackFunction(Wnd: HWND; Р: Pointer): BOOL; stdcall; begin { что-то делаем} end; ............... var MyPointer: Pointer; ............... EnumWindows(@MyCallbackFunction, LongInt(MyPointer)); Что бы мы ни делали с типом второго параметра функции обратного вызова, тип соответствующего параметра EnumWindowsне меняется. Поэтому необходимо явное приведение передаваемого параметра к типу LongInt. Обратное преобразование типов при вызове MyCallbackFunction, осуществляется автоматически. Использование EnumWindowsи функций обратного вызова демонстрируется примером EnumWnd. Отметим, что функции обратного вызова будут вызываться до того, как завершит работу функция EnumWindows. Однако это не является распараллеливанием работы. Чтобы проиллюстрировать это, рассмотрим ситуацию, когда программа вызывает некоторую функцию А, которая, в свою очередь, вызывает функцию В. Функция В, очевидно, начнет свою работу до того, как завершит работу функция А. То же самое произойдет и с функцией обратного вызова, переданной в EnumWindows: она будет вызываться из кода EnumWindowsтак же, как и функция В из кода функции А. Поэтому код функции обратного вызова получит управление (и не один раз, т.к. EnumWindowsбудет вызывать эту функцию в цикле) до завершения работы EnumWindows. Однако это правило действует не во всех ситуациях. В некоторых случаях система запоминает адрес переданной ей функции обратного вызова, чтобы использовать ее потом. Примером такой функции является оконная процедура: ее адрес передается системе один раз при регистрации класса, и затем система многократно вызывает эту функцию при необходимости. В 16-разрядных версиях Windows вызов функций обратного вызова осложнялся тем, что для них необходим был специальный код. называемый прологом. Пролог создавался с помощью функции MakeProcInstance, удалялся после завершения с помощью FreeProcInstance. Таким образом, вызов EnumWindowsдолжен был бы выглядеть так. как показано в листинге 1.4. Листинг 1.4. Вызов функции EnumWindowsв 16-разрядных версиях Windows var MyProcInstanсe: TFarProc; ............... MyProcInstance := MakeProcInstance(@MyCallBackFunction, HInstance); EnumWindows(MyProcInstance, LongInt(MyPointer)); FreeProcInstance(MyProcInstance); В Delphi этот код будет работоспособным, т.к. для совместимости MakeProcInstanceи FreeProcInstanceоставлены. Но они ничего не делают (в чем легко убедиться, просмотрев исходный файл Windows.pas), поэтому можно обойтись и без них. Тем не менее эти функции иногда до сих пор используются, видимо, просто в силу привычки. Другой способ, с помощью которого и 16-разрядных версиях можно сделать пролог — описать функцию с директивой export. Эта директива сохранена для совместимости и в Delphi, но в 32-разрядных версиях она также ничего не делает (несмотря на то, что справка, например, по Delphi 3 утверждает обратное; в справке по Delphi 4 этой ошибки уже нет). 1.1.6. Сообщения WindowsЧеловеку, знакомому с Delphi, должна быть ясна схема событийного управления. Программист пишет только методы реакции на различные события, а затем этот код получает управление тогда, когда соответствующее событие произойдет. Простые программы в Delphi состоят исключительно из методов реакции на события (например, OnCreate, OnClick, OnCloseQuery). Причем событием называется не только событие в обычном смысле этого слова, т.е. когда происходит что-то внешнее, но и ситуация, когда событие используется просто для передачи управления коду, написанному разработчиком программы, в тех случаях, когда VCL не может сама справиться с какой-то задачей. Пример такого события — TListBox.OnDrawItem. Устанавливая стиль списка в lbOwnerDrawFixedили lbOwnerDrawVariable, программист указывает, что ему требуется нестандартный вид элементов списка, поэтому их рисование он берет на себя. И каждый раз, когда возникает необходимость в рисовании элемента, VCL передает управление специально написанному коду. На самом деле разница между двумя типами событий весьма условна. Можно сказать, что когда пользователь нажимает клавишу, VCL не "знает", что делать, и поэтому передает управление обработчику OnKeyPress. Событийное управление не есть изобретение авторов Delphi. Такой подход заложен в самой системе Windows. Только здесь события называются сообщениями (message), что иногда даже лучше отражает ситуацию. Windows посылает программе сообщения, связанные либо с тем, что произошло внешнее событие (нажатие кнопки мыши, клавиши на клавиатуре и т.п.), либо с тем, что самой системе потребовались от программы какие-то действия. Самое распространенное действие — предоставление информации. Например, при необходимости узнать текст заголовка окна Windows посылает этому окну специальное сообщение, в ответ на которое окно должно сообщить системе свой заголовок. Еще бывают сообщения, которые просто уведомляют программу о начале какого-то действия (например, о начале перетаскивания окна) и предоставляют возможность вмешаться. Но это вмешательство необязательно. В Delphi для реакции на каждое событие обычно создается свой метод. В Windows одна процедура, называемая оконной, обрабатывает все сообщения, адресованные конкретному окну. (В C/C++ нет понятия "процедура", там термин "оконная процедура" не вызывает путаницы, а вот в Delphi четко определено, что такое процедура. И здесь можно запутаться: то, что в системе называется оконной процедурой, с точки зрения Delphi будет не процедурой, а функцией. Тем не менее мы будем употреблять общепринятый термин "оконная процедура".) Каждое сообщение имеет свой уникальный номер, а оконная процедура обычно целиком состоит из оператора case, и каждому сообщению соответствует своя альтернатива этого оператора. Номера сообщений знать не обязательно, потому что можно использовать константы, описанные в модуле Messages. Эти константы начинаются с префикса, указывающего на принадлежность сообщения к какой-то группе. Например, сообщения общего назначения начинаются с WM_: WM_PAINT, WM_GETTEXTLENTH. Сообщения, специфичные, например, для кнопок, начинаются с префикса BM_. Остальные группы сообщений также связаны либо с теми или иными элементами управления, либо со специальными действиями, например, с динамическим обменом данными (Dynamic Data Exchange, DDE). Обычной программе приходится обрабатывать довольно много сообщений, поэтому оконная процедура бывает, как правило, очень длинной и громоздкой. Оконная процедура описывается программистом как функция обратного вызова и указывается при создании оконного класса. Таким образом, все окна данного класса имеют одну и ту же оконную процедуру. Впрочем, существует возможность породить так называемый подкласс, т.е. новый класс, наследующий все свойства существующего, за исключением оконной процедуры. Несколько подробнее об этом будет сказано далее. Кроме номера, каждое сообщение содержит два параметра: wParam и lParam. Префиксы w и l означают "Word" и "Long", т.е. первый параметр 16-разрядный, а второй — 32-разрядный. Однако так было только в старых, 16-разрядных версиях Windows. В 32-разрядных версиях оба параметра 32-разрядные, несмотря на их названия. Конкретный смысл каждого параметра зависит от сообщения. В некоторых сообщениях один или оба параметра могут вообще не использоваться, в других — наоборот, двух параметров даже не хватает. В этом случае один из параметров (обычно lParam) содержит указатель на дополнительные данные. После обработки сообщения оконная процедура должна вернуть какое-то значение. Обычно это значение просто сигнализирует, что сообщение не нуждается в дополнительной обработке, но в некоторых случаях оно более осмысленно, например, WM_SETICONдолжно вернуть дескриптор иконки, которая была установлена ранее. Прототип оконной процедуры выглядит следующим образом: LRESULT CALLBACK WindowProc( HWND hwnd, // дескриптор окна UINT uMsg, // номер сообщения WPARAM wParam, // первый параметр соообщения LPARAM lParam // второй параметр сообщения ); В Delphi оконная процедура объявляется следующим образом: function WindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; Все, что "умеет" окно, определяется тем. как его оконная процедура реагирует на сообщения. Чтобы окно можно было, например, перетаскивать мышью, его оконная процедура должна обрабатывать целый ряд сообщений, связанных с мышью. Чтобы не заставлять программиста каждый раз реализовывать стандартную для всех окон обработку событий, в системе предусмотрена функция DefWindowProc. Разработчик приложения в своей оконной процедуре должен предусмотреть только специфическую для данного окна обработку сообщений, а обработку всех остальных сообщений передать этой функции. Существуют также аналоги функции DefWindowProcдля специализированных окон: DefDlgProcдля диалоговых окон, DefFrameProcдля родительских MDI окон, DefChildMDIProcдля дочерних MDI-окон. Сообщение окну можно либо послать (post), либо отправить (send). Каждая нить, вызвавшая хоть одну функцию из библиотеки user32.dll или gdi32.dll, имеет свою очередь сообщений, в которую помещаются все сообщения, посланные окнам, созданным данной нитью (послать сообщение окну можно например, с помощью функции PostMessage). Соответственно, кто-то должен извлекать эти сообщения из очереди и передавать их окнам-адресатам. Это делается с помощью специального цикла, который называется петлей сообщений (message loop). В этом непрерывном цикле, который должен реализовать разработчик приложения, сообщения извлекаются из очереди с помощью функции GetMessage(реже — PeekMessage) и передаются в функцию DispatchMessage. Эта функция определяет, какому окну предназначено сообщение, и вызывает его оконную процедуру. Таким образом, простейший цикл обработки сообщений выглядит так, как показано в листинге 1.5. Листинг 1.5. Простейшая петля сообщений var Msg: TMsg; ... while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; Блок-схема петли сообщений показана на рис. 1.4. Рис 1.4. Блок-схема петли сообщений Функция GetMessageвозвращает Trueдо тех пор, пока не будет получено сообщение WM_QUIT, указывающее на необходимость завершения программы. Обычная программа для Windows, выполнив предварительные действия (регистрация класса и создание окна), входит в петлю сообщений, которую выполняет до конца своей работы. Все остальные действия выполняются в оконной процедуре при реакции на соответствующие сообщения. Примечание Сообщение, извлеченное из очереди, GetMessageпомещает в первый параметр-переменную типа TMsg. Последние три параметра служат для фильтрации сообщений, позволяя извлекать из очереди только те сообщения, которые соответствуют определенным критериям. Если эти параметры равны нулю, как это обычно бывает, фильтрация при извлечении сообщений не производится. Функция TranslateMessage, которая обычно вызывается в петле сообщений, служит для трансляции клавиатурных сообщении (если петля сообщений реализуется только для обработки сообщении невидимым окнам, которые использует, например, COM/DCOM, или по каким-то другим причинам ввод с клавиатуры не обрабатывается или обрабатывается нестандартным образом, вызов TranslateMessageможно опустить). Когда пользователь нажимает какую-либо клавишу на клавиатуре, система посылает окну, находящему в фокусе, сообщение WM_KEYDOWN. Через параметры этого сообщения передаётся виртуальный код нажатой клавиши — двухбайтное число, которое определяется только положением нажатой клавиши на клавиатуре и не зависит от текущей раскладки, состояния клавиш <CapsLock> и т.п. Функция TranslateMessage. обнаружив такое сообщение, добавляет в очередь (причем не в конец, а в начало) сообщение WM_CHAR, в параметрах которого передается код символа, соответствующего нажатой клавише, с учетом раскладки, состояния клавиш <CapsLock>, <Shift> и т. п. Именно функция TranslateMessageпо виртуальному коду клавиши определяет код символа. При этом нажатие любой клавиши приводит к генерации WM_KEYDOWN, а вот WM_CHARгенерируется не для всех клавиш, а только для тех, которые соответствуют какому-то символу (например, не генерирует WM_CHARнажатие таких клавиш, как <Shift> <Ctrl>, <Insert>, функциональных клавиш). Примечание Если очередь сообщений пуста, функция GetMessageожидает, пока там не появится хотя бы одно сообщение, и только после этого завершает работу. Во время этого ожидания нить не загружает процессор. Петля сообщений может извлечь и отправить на обработку следующее сообщение только тогда, когда оконная процедура закончила обработку предыдущего. Таким образом, сообщение, обработка которого занимает много времени, блокирует обработку следующих сообщений, и все окна, созданные данной нитью, перестают реагировать на действия пользователя. Именно этим объясняется временное зависание программы, которая в одном из своих обработчиков сообщений делает математические расчеты или выполняет длительный запрос к базе данных: сообщения накапливаются в очереди, но не извлекаются из нее и не обрабатываются. Как только обработка текущего сообщения закончится, все остальные сообщения будут извлечены из очереди и обработаны. В некоторых случаях избежать временного зависания программы помогает организация локальной петли сообщений. Если обработчик сообщения, для работы которого требуется много времени, содержит цикл, в него можно вставить вызовы функции PeekMessage, которая позволяет проверить, есть ли в очереди сообщения. Если сообщения обнаружены, нужно вызвать DispatchMessageдля передачи их требуемому окну. В этом случае сообщения будут извлекаться из очереди и обрабатываться до завершения работы обработчика. Блок-схема программы, содержащей локальную петлю сообщений, показана на рис. 1.5 (для краткости в главной петле сообщений показаны только две самых важных функции: GetMessageи DispatchMessage, хотя и в этом случае главная петля целиком выглядит так же, как на рис. 1.4). При использовании локальной петли сообщений существует опасность бесконечной рекурсии. Рассмотрим это на простом примере: предположим, что сложный код, содержащий локальную петлю сообщений, выполняется при нажатии некоторой кнопки на форме приложения. Пока обработчик выполняется, нетерпеливый пользователь может снова нажать кнопку, запустив вторую активацию обработчика нажатия кнопки, и так несколько раз. Конечно, организовать таким образом очень глубокую рекурсию пользователь вряд ли сможет (терпения не хватит), но часто даже то, что несколько активаций обработчика вызваны рекурсивно, может привести к неприятным последствиям. А если программа организует локальную петлю сообщений в обработчике сообщений таймера, то здесь рекурсия действительно может углубляться до переполнения стека. Поэтому при организации петли сообщений следует принимать меры против рекурсии. Например, в случае с кнопкой в обработчике ее нажатие можно запретить ( Enabled := False), и вновь разрешить только после окончания обработки, тогда пользователь не сможет нажать кнопку во время работы локальной петли сообщений. В очередь можно поставить сообщение, не привязанное ни к какому окну. Это делается с помощью функции PostThreadMessage. Такие сообщения необходимо самостоятельно обрабатывать в петле сообщений, потому что функция DispatchMessageих просто игнорирует. Рис. 1.6. Блок-схема программы с локальной петлей сообщений Существуют также широковещательные сообщения, которые посылаются сразу нескольким окнам. Проще всего послать такое сообщение с помощью функции PostMessage, указав в качестве адресата не дескриптор конкретного окна, а константу HWND_BROADCAST. Такое сообщение получат все окна, расположенные непосредственно на рабочем столе и не имеющие при этом владельцев (в терминах системы). Существует также специальная функция BroadcastSystemMessage(начиная с Windows ХР — ее расширенный вариант BroadcastSystemMessageEx), которая позволяет уточнить, каким конкретно окнам будет отправлено широковещательное сообщение. Кроме параметров wParamи lParam, каждому сообщениюприписывается время отправки и координаты курсора в момент возникновения. Соответствующие поля есть в структуре TMsg, которую используют функции GetMessageи DispatchMessage, но у оконной процедуры не предусмотрены параметры для их передачи. Получить время отправки сообщения и координаты курсора при обработке сообщения можно с помощью функций GetMessageTimeи GetMessagePosсоответственно. Существует также ряд функций, которые могут обрабатывать сообщения без участия DispatchMessageи оконной процедуры. Если эти функции распознают сообщение, извлеченное из очереди, как "свое", они сами выполняют все необходимые действия по его обработке, и тогда TranslateMessageи DispatchMessageвызывать не нужно. К этим функциям, в частности, относятся следующие: □ TranslateAccelerator— на основе загруженной из ресурсов таблицы распознает нажатие "горячих" клавиш меню и вызывает оконную процедуру, передавая ей сообщение WM_COMMANDили WM_SYSCOMMAND, аналогичное тому, которое посылается при выборе соответствующего пункта меню пользователем; □ TranslateMDISysAccel— аналог предыдущей функции за исключением того, что распознает "горячие" клавиши системного меню MDI-окон; □ IsDialogMessage— распознает сообщения, имеющие особый смысл для диалоговых окон (например, нажатие клавиши <Tab> для перехода между элементами управления). Используется для немодальных диалоговых окон и окон, не являющихся диалоговыми (т.е. созданными без помощи функций CreateDialogXXXX), но требующими аналогичной функциональности. Перечисленные функции при необходимости вставляются в петлю сообщений. Листинг 1.6 показывает, как будет выглядеть петля сообщений, содержащая вызов TranslateAcceleratorдля родительской MDI-формы и TranslateMDISysAccelдля дочерней. Листинг 1.6. Петля сообщении с обработкой "горячих" клавиш главного меню и системного меню MDI-окон while GetMessage(Msg, 0, 0, 0) do if not TranslateMDISysAccel(ActiveMDIChildHandle, Msg) and not TranslateAccelerator(MDIFormHandle, AccHandle, Msg) then begin TranslateMessage(Msg); DispatchMessage(Msg); end; При отправке сообщения, в отличие от посылки, оно не ставится в очередь, а передается оконной процедуре напрямую. Отправить сообщение можно, например, с помощью функции SendMessage. Если эта функция вызывается из той же нити, которой принадлежит окно-адресат, то фактически это эквивалентно прямому вызову оконной процедуры. Если окно принадлежит другой нити, данное сообщение становится в отдельную очередь, имеющую более высокий приоритет, чем очередь для посланных сообщений. Функции GetMessageи PeekMessageсначала выбирают все сообщения из этой очереди и отправляют их на обработку, и лишь затем приступают к анализу очереди посланных сообщений. Примечание Диалоговые окна обрабатывают сообщения по-особому. Эти окна делятся на модальные (создаются и показываются с помощью функций DialogBoxXXXX) немодальные (создаются с помощью функций CreateDialogXXXXи затем показываются с помощью функции ShowWindow, использующейся и для обычных, не диалоговых, окон). И модальные, и немодальные окна создаются на основ шаблона, который может храниться в ресурсах приложения или в памяти. В шаблоне можно явно указать имя созданного вами оконного класса диалогового окна или (как это обычно бывает) не указывать его вообще, чтобы был выбран класс, предоставляемый системой для диалоговых окон по умолчанию. Оконная процедура диалогового класса должна передавать необработанные сообщения функции DefDlgProc. Все диалоговые окна имеют так называемую диалоговую процедуру — функцию, указатель на которую передается в качестве одного из параметров функциям DialogВохХХХХи CreateDialogXXXX. Прототипы диалоговой и оконной процедур совпадают. Функция DefDlgProcначинаем свою работу с того, что вызывает диалоговую процедуру. Если та не обработала переданное ей сообщение (о чем сигнализирует возвращаемое нулевое значение), функция DefDlgProcобрабатывает его сама. Таким образом, с помощью одного оконного класса и одной оконной процедуры можно реализовывать различные диалоговые окна, используя разные диалоговые процедуры. Функции DialogВохХХХХсоздают диалоговое окно и сразу же показывают его в модальном режиме. Данные функции завершают свое выполнение только тогда, когда модальное окно будет закрыто. Внутри модальных функций организуется собственная петля сообщений. Все прочие окна на время показа модального диалога запрещаются (как если бы для них была вызвана функция EnableWindowс параметром FALSE), т.е. перестают реагировать на сообщения от мыши и клавиатуры. При этом они сохраняют способность реагировать на другие сообщения, благодаря чему могут, например, обновлять свое содержимое по таймеру (в справке написано, что ничто не мешает программисту вставить в диалоговую процедуру вызов функций, разрешающих запрещенные системой окна, но при этом теряется смысл модальных диалогов). Если в очереди нет сообщений, модальная петля посылает родительскому окну диалога сообщение WM_ENTERIDLE, обработка которого позволяет этому окну выполнять фоновые действия. Разумеется, что обработчик WM_ENTERIDLEне должен выполняться слишком долго, иначе модальное окно зависнет. Обычно окно использует оконную процедуру, которая задана при создании соответствующего оконного класса. Однако допускается создание так называемых подклассов — переопределение оконной процедуры после того, как окно создано. Это переопределение касается только заданного окна и не оказывает влияния на остальные окна, принадлежащие данному оконному классу. Осуществляется оно с помощью функции SetWindowLongс параметром GWL_WNDPROC(другие значения этого параметра позволяют менять другие свойства окна, такие как стиль и расширенный сталь). Изменять оконную процедуру можно только у окон, созданных самим процессом. Новая оконная процедура, которая устанавливается при создании подкласса, все необработанные сообщения должна передавать не функции DefWindowProc, а той оконной процедуре, которая была установлена ранее. SetWindowLongпри изменении оконной процедуры возвращает дескриптор старой процедуры (этот же дескриптор можно получить, заранее вызвав функцию GetWindowLongс аргументом GWL_WINDOWPROC). Обычно значение дескриптора численно совпадает с адресом старой оконной процедуры, поэтому в некоторых источниках можно встретить рекомендации использовать этот дескриптор непосредственно как указатель процедурного типа. И это даже будет работать для оконных классов, созданных самой программой. Но безопаснее все же вызов старой оконной процедуры реализовать с помощью системной функции CallWindowProc, предоставив ей "разбираться", является ли дескриптор указателем. В качестве примера рассмотрим создание подкласса для некоторого окна, дескриптор которого содержится в переменной Wnd. Пусть нам потребовалось для этого окна нестандартным образом обрабатывать сообщение WM_KILLFOCUS. Тогда код новой оконной процедуры и код ее установки будет выглядеть так, как показано в листинге 1.7. Листинг 1.7. Создание подкласса для особой обработки сообщенияWM_KILLPFOCUS var OldWndProc: TFNWndProc; function NewWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM) : LRESULT; stdcall; begin if Msg = WM_KILLFOCUS then // Обработка события else Result := CallWindowProc(OldWndProc, hWnd, Msg, wParam, lParam); end; ... // Установка новой оконной процедуры окну Wnd OldWndProc := TFNWndProc(SetWindowLong(Wnd, GWL_WNDPROC, Longint(@NewWindowProc))); ... Примечание Переопределять оконную процедуру с помощью SetWindowLongможно и у тех окон, оконная процедура которых была переопределена ранее. Таким образом создаются цепочки оконных процедур, каждая из которых вызывает предыдущую. 1.1.7. Создание окон средствами VCLТеперь поговорим о том, как в VCL создаются окна. Речь здесь будет идти не о написании кода для создания окна с помощью VCL (предполагается, что читатель это и так знает), а о том, какие функции API и в какой момент вызывает VCL при создании окна. Если смотреть код методов класса TWinControl, которые вызываются при создании и отображении окна, то найти там то место, когда окно создается, удается не сразу. На первый взгляд все выглядит так, будто этот код вообще не имеет отношения к созданию окна, как будто оно создается где-то совсем в другом месте, а TWinControlполучает уже готовый дескриптор. На самом деле окно создает, конечно же, сам TWinControl, а спрятано его создание в свойстве Handle. Метод GetHandle, который возвращает значение свойства Handle, выглядит следующим образом (листинг 1.8). Листинг 1.8. Реализация метода TWinControl.GetHandle procedure TWinControl.HandleNeeded; begin if FHandle = 0 then begin if Parent <> nil then Parent.HandleNeeded; CreateHandle; end; end; function TWinControl.GetHandle: HWnd; begin HandleNeeded; Result := FHandle; end; При каждом обращении к свойству Handleвызывается метод HandleNeeded, который проверяет, создано ли уже окно, и если нет, создает его, попутно создавая, при необходимости, родительское окно. Таким образом, окно создается при первом обращении к свойству Handle. Метод CreateHandle, который вызывается из HandleNeeded, выполняет непосредственно лишь несколько вспомогательных операций, а для создания окна вызывает еще один метод — CreateWnd(листинг 1.9). Листинг 1.9. Реализация метода CreateWnd procedure TWndControl.CreateWnd; var Params: TCreateParams; TempClass: TWndClass; ClassRegistered: Boolean; begin CreateParams(Params); with Params do begin if (WndParent = 0) end (Style and WS_CHILD <> 0) then if (Owner <> nil) end (csReading in Owner.ComponentState) and (Owner is TWinControl) then WndParent TWinControl(Owner).Handle else raise EInvalidOperation.CreateFmt(SParentRequired, [Name]); FDefWndProc := WindowClass.lpfnWndProc; ClassRegistered := GetClassInfo(WindowClass.hInstance, WinClassName, TempClass); if not ClassRegistered or (TempClass.lpfnWndProc <> @InitWndProc) then begin if (ClassRegistered then Windows.UnregisterClass(WinClassName, WindowClass.hInstance); WindowClass.lpfnWndProc := InitWndProc; WindowClass.lpszClassName := WinClassName; if Windows.RegisterClass(WindowClass) = 0 then RaiseLastOSError; end; CreationControl := Self; CreateWindowHandle(Params); if FHandle = 0 then RaiseLastOSError; if (GetWindowLong(FHandle, GWL_STYLE) and WS_CHILD <> 0) and (GetWindowLong(FHandle, GWL_ID) = 0) then SetWindowLong(FHandle, GWL_ID, FHandle); end; StrDispose(FText); FText := nil; UpdateBounds; Perform(WM_SETFONT, FFont.Handle, 1); if AutoSize then AdjustSize; end; Собственно создание окна опять происходит не здесь, а в методе CreateWindowHandle, который очень прост: он состоит из одного только вызова API-функции CreateWindowExс параметрами, значения которых берутся из полей записи Paramsтипа TCreateParams(листинг 1.10) Листинг 1.10. Запись TCreateParams TCreateParams = record Caption: PChar; Style: WORD; ExStyle: DWORD; X, Y: Integer; Width, Height: Integer; WndParent: HWnd; Param: Pointer; WindowClass: TWndClass; WinClassName: array[0..63] of Char; end; В записи Paramsхранятся параметры как окна, передаваемые в функцию WindowCreateEx, так и оконного класса (поля WindowClassи WndClassName). Все поля инициализируются методом CreateParamsна основе значений свойств оконного компонента. Данный метод виртуальный и может быть перекрыт в наследниках, что бывает полезно, когда необходимо изменить стиль создаваемого окна. Например, добавив расширенный стиль WS_EX_CLIENTEDGE(или, как вариант, WS_EX_STATICEDGE), можно получить окно с необычной рамкой (листинг 1.11). Листинг 1.11. Перекрытие метода CreateParams procedure TForm1.CreateParams(var Params: TCreateParams); begin // Вызов унаследованного метода заполнения всех полей // записи Params inherited CreateParams(Params); // Добавляем флаг WS_EX_CLIENTEEDGE к расширенному стилю окна Params.ExStyle := Params.ExStyle or WS_EX_CLIENTEDGE; end; Примечание Обратите внимание, что всем без исключения классам метод CreateWndназначает одну и ту же оконную процедуру — InitWndProc. Это является основой в обработке сообщений с помощью VCL, именно поэтому оконная процедура назначается не в методе CreateParams, а в методе CreateWnd, чтобы в наследниках нельзя было изменить это поведение (метод CreateWndтоже виртуальный, но при его переопределении имеет смысл только добавлять какие-то действия, а не изменять поведение унаследованного метода). Чтобы понять, как работает процедура InitWndProc, обратите внимание на еще одну особенность метода CreateWnd: перед вызовом CreateWindowHandle(т.е. непосредственно перед созданием окна) он записывает ссылку на текущий объект в глобальную переменную СreationСontrol. Эта переменная затем используется процедурой InitWndProc(листинг 1.12). Листинг 1.12. Оконная процедура InitWndProc function InitWndProc(HWindow: HWnd; Message, WParam, LParam: LongInt): LongInt; begin CreationControl.FHandle := HWindow; SetWindowLong (HWindow, GWL_WNDPROC, LongInt(CreationControl.FObjectInstance)); if (GetWindowLong(HWindow, GWL_STYLE) and WS_CHILD <> 0) and (GetWindowLong(HWindow, GWL_ID) = 0) then SetWindowLong(HWindow, GWL_ID, HWindow); SetProp(HWindow, MakeIntAtom(ControlAtom), THandle(CreationControl)); SetProp(HWindow, MakeIntAtom(WindowAtom), THandle(CreationControl)); asm PUSH LParam PUSH WParam PUSH Message PUSH HWindow MOV EAX, CreationControl MOV CreationControl, 0 CALL [EAX].TWinControl.FObjectInstance MOV Result, EAX end; end; Примечание Из листинга 1.12 видно, что оконная процедура InitWndProcне обрабатывает сама никаких сообщений, а просто переназначает оконную процедуру у окна. Таким образом, InitWndProcдля каждого окна вызывается только один раз, чтобы переназначить оконную процедуру. Обработка того сообщения, которое привело к вызову InitWndProc, тоже передается в эту новую процедуру (ассемблерная вставка в конце InitWndProcделает именно это). При просмотре этого кода возникают два вопроса. Первый — зачем писать такую оконную процедуру, почему бы не назначить нужную процедуру обычным образом? Здесь все дело в том. что стандартными средствами оконная процедура назначается одна на весь оконный класс, в то время как по внутренней логике VCL каждый экземпляр компонента должен иметь свою собственную оконную процедуру. Добиться этого можно только порождением подкласса уже после создания окна. Указатель на свою уникальную оконную процедуру (откуда эта процедура берется и почему она должна быть уникальной, мы поговорим в следующем разделе) каждый экземпляр хранит в поле FObjectInstance. Значение глобальной переменной CreationControlприсваивается, как мы помним, непосредственно перед созданием окна, а первое свое сообщение окно получает буквально в момент создания. Так как VCL — принципиально однонитевая библиотека, ситуация, когда другой код вклинивается между присваиванием значения переменной CreationControlи вызовом InitWndProc, невозможна, так что в InitWndProcпопадает правильная ссылка на создаваемый объект. Второй вопрос — зачем так сложно? Почему в методе CreateWndсразу после создания окна нельзя было вызвать SetWindowLongи установить нужную оконную процедуру там, вместо того чтобы поручать это процедуре InitWndProc? Здесь ответ такой: это сделано потому, что свои первые несколько сообщений (например, сообщения WM_CREATEи WM_NCCREATE) окно получает до того, как функция CreateWindowExзавершит свою работу. Чтобы завершить создание окна, CreateWindowExотправляет несколько сообщений окну, и только после того как окно обработает их должным образом, процесс создания окна считается завершенным. Так что назначать уникальную оконную процедуру после завершения CreateWindowEx— это слишком поздно. Именно поэтому уникальная оконная процедура назначается таким неочевидным и несколько неуклюжим способом. 1.1.8. Обработка сообщений с помощью VCLПри использовании VCL в простых случаях самостоятельно работать с оконными сообщениями нет нужды, поскольку практически все можно сделать с помощью свойств, методов и событий компонентов. Тем не менее, некоторые сообщения приходится обрабатывать вручную. Чаще всего это приходится делать при разработке собственных компонентов, но и в обычных приложениях это также может быть полезным. Кроме сообщений, предусмотренных в системе, компоненты VCL обмениваются сообщениями, созданными авторами этой библиотеки. Эти сообщения имеют префиксы CM_и CN_. Они нигде не документированы, разобраться с ними можно только по исходным кодам VCL. При разработке собственных компонентов приходится обрабатывать эти сообщения, которые мы здесь не будем полностью описывать, но некоторые из них будут упоминаться в описании работы VCL с событиями. В Windows API нет понятия главного окна — все окна, не имеющие родителя (или владельца в терминах системы), равноценны, и приложение может продолжать работу после закрытия любых окон. Но в VCL введено понятие главной формы: форма, которая создается первой, становится главной, и ее закрытие означает закрытие всего приложения. Если окно не имеет ни родителя, ни владельца в терминах системы (такие окна называются окнами верхнего уровня), то на панели задач появляется кнопка, связанная с этим окном (окно, имеющее владельца, также может обзавестись такой кнопкой, если оно создано со стилем WS_EX_APPWINDOW). Обычно в приложении одно окно главного уровня, и оно играет роль главного окна этого приложения, хотя система не запрещает приложению создавать несколько окон верхнего уровня (примеры — Internet Explorer, Microsoft Word). Разработчики VCL пошли по другому пути: окно верхнего уровня, ответственное за появление кнопки на панели задач, создается объектом Application. Дескриптор этого окна хранится в свойстве Application.Handle, а само оно невидимо, т.к. имеет нулевые размеры. Как и любое другое, это окно имеет оконную процедуру и может обрабатывать сообщения. Главная форма — это отдельное окно, не имеющее, с формальной точки зрения, никакого отношения к кнопке на панели задач. Видимость связи между этой кнопкой и главной формой обеспечивается взаимодействием объекта Applicationи объекта главной формы внутри VCL. Таким образом, даже простейшее VCL-приложение создает два окна: невидимое окно объекта Application и окно главной формы. Окно, создаваемое объектом Application, мы будем называть невидимым окном приложения. Невидимое окно приложения по умолчанию становится владельцем (в терминах системы) всех форм, у которых явно не установлено свойство Parent, в том числе и главной формы. При обработке сообщений VCL решает две задачи: выборка сообщений из очереди и передача сообщения конкретному компоненту. Рассмотрим сначала первую задачу. Выборкой сообщений из очереди занимается объект Application, непосредственно за извлечение и диспетчеризацию сообщения отвечает его метод ProcessMessage(листинг 1.13). Листинг 1.13. Метод TApplication.ProcessMessage function TApplication.ProcessMessage(var Msg: TMsg): Boolean; var Unicode: Boolean; Handled: Boolean; MsgExists: Boolean; begin Result := False; if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then begin Unicode := (Msg.hwnd <> 0) and IsWindowUnicode(Msg.hwnd); if Unicode then MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE) else MsgExists := PeekMessage(Msg, 0, 0, 0, PM_REMOVE); if not MsgExists then Exit; Result := True; if Msg.Message <> WM_QUIT then begin Handled := False; if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin TranslateMessage(Msg); if Unicode then DispatchMessageW(Msg); else DispatchMessage(Msg); end; end else FTerminate := True; end; end; В этом коде отдельного комментария требует то, как используется функция PeekMessage. Сначала эта функция вызывается с параметром PM_NOREMOVE, — так выполняется проверка условия, что в очереди присутствует сообщение, а также выясняется, для какого окна предназначено первое сообщение в очереди. Само сообщение при этом остается в очереди. С помощью функции IsWindowUnicodeпроизводится проверка, использует ли окно-адресат кодировку ANSI или Unicode, и затем, в зависимости от этого, сообщение извлекается либо функцией PeekMessage, либо ее Unicode-аналогом PeekMessageW(о Unicode-аналогах функций см. разд. 1.1.12). При диспетчеризации сообщения также вызывается либо функция DispatchMessage, либо ее Unicode-аналог DispatchMessageW. Если метод ProcessMessageс помощью PeekMessageизвлекает из очереди сообщение WM_QUIT, то он устанавливает в Trueполе FTerminateи завершает свою работу. Обработка всех остальных сообщений, извлеченных из очереди состоит из следующих основных этапов (см. рис. 1.6): 1. Если назначен обработчик Application.OnMessage, сообщение передается ему. В этом обработчике можно установить параметр-переменную Handleв True, что означает, что сообщение не нуждается в дополнительной обработке. 2. Второй шаг —это предварительная обработка сообщения (вызов метода IsPreProcessMessage). Этот шаг появился только начиная с BDS 2006, в более ранних версиях его не было. Обычно предварительную обработку осуществляет то окно, которому предназначено это сообщение, но если окно-адресат не является VCL-окном, производится поиск VCL-окна по цепочке родителей. Кроме того, если какое-либо окно захватит ввод мыши, предварительную обработку сообщений будет осуществлять именно оно. Если оконный компонент, удовлетворяющий этим требованиям, найден, вызывается его метод PreProcessMessage, который возвращает результат логического типа. Если компонент вернул True, то на этом обработка сообщения заканчивается. Отметим, что ни один из стандартных компонентов VCL не использует эту возможность перехвата сообщений, она реализована для сторонних компонентов. 3. Затем, если на экране присутствует всплывающая подсказка (hint), проверяется, должно ли пришедшее сообщение прятать эту подсказку, и если да, то она убирается с экрана (метод IsHintMessage). Список сообщений, которые должны прятать окно подсказки, зависит от класса этого окна (здесь имеется в виду класс VCL, а не оконный класс) и определяется виртуальным методом THintWindow.IsHintMsg. Стандартная реализации этого метода рассматривает как "прячущие" все сообщения от мыши, клавиатуры, сообщения об активации и деактивации программы и о действиях пользователя с меню или визуальными компонентами. Если метод IsHintMessageвозвращает False, то сообщение дальше не обрабатывается, но стандартная реализация этого метода всегда возвращает True. 4. Далее проверяется значение параметра Handled, установленное в обработчике OnMessage(если он назначен). Если это значение равно True, метод ProcessMessageзавершает свою работу, и обработка сообщения на этом заканчивается. Таким образом, обработка сообщения по событию OnMessageне может отменить предварительную обработку сообщения и исчезновение всплывающей подсказки. Рис. 1.6. Одна итерация петли сообщений VCL (блок-схема метода Application.ProcessMessage) 5. Если главная форма приложения имеет стиль MDIForm, и одно из его дочерних MDI-окон в данный момент активно, сообщение передается функции TranslateMDISysAccel. Если эта функция вернет True, то обработка сообщения на этом завершается (все эти действия выполняются в методе IsMDIMsg). 6. Затем, если получено клавиатурное сообщение, оно отправляется на предварительную обработку тому же окну, что и в пункте 2 (метод IsKeyMsg). Предварительная обработка клавиатурного сообщения начинается с попытки найти полученную комбинацию клавиш среди "горячих" клавиш контекстно-зависимого меню и выполнить соответствующую команду. Если контекстно-зависимое меню не распознало сообщение как свою "горячую" клавишу, то вызывается обработчик события OnShortCutокна, осуществляющего предварительную обработку (если это окно не является формой и не имеет этого события, то вызывается OnShortCutего родительской формы). Если обработчик OnShortCutне установил свой параметрHandled в True, полученная комбинация клавиш ищется среди "горячих" клавиш сначала главного меню, а потом — среди компонентов TActionList. Если и здесь искомая комбинация не находится, возникает событие Application.OnShortCut, которое также имеет параметр Handled, позволяющий указать, что сообщение в дополнительной обработке не нуждается. Если обработчик не установил этот параметр, то сообщение передается главной форме приложения, которое пытается найти нажатую комбинацию среди "горячих" клавиш своего контекстного меню, передает его обработчику OnShortCut, ищет среди "горячих" клавиш главного меню и компонентов TActionList. Если нажатая клавиша не является "горячей", но относится к клавишам, использующимся для управления диалоговыми окнами (<Tab>, стрелки, <Esc> и т.п.), форме передается сообщение об этом, и при необходимости сообщение обрабатывается. Таким образом, на данном этапе средствами VCL эмулируются функции TranslateAcceleratorи IsDialogMessage. 7. Если на экране присутствует один из стандартных диалогов (в VCL они реализуются классами TOpenDialog, TSaveDialogи т.п.), то вызывается функция IsDialogMessage, чтобы эти диалоги могли нормально функционировать (метод IsDlgMsg). 8. Если ни на одном из предыдущих этапов сообщение не было обработано, то вызываются функции TranslateMessageи DispatchMessage, которые завершают обработку сообщения путем направления его соответствующей оконной функции. Примечание Метод ProcessMessageвозвращает True, если сообщение извлечено и обработано, и False, если очередь была пуста. Этим пользуется метод HandleMessage, который вызывает ProcessMessageи, если тот вернет False, вызывает метод Application.Idleдля низкоприоритетных действий, которые должны выполняться только при отсутствии сообщений в очереди. Метод Idle, во-первых, проверяет, над каким компонентом находится курсор мыши, и сохраняет ссылку на него в поле FMouseControl, которое используется при последующей проверке, нужно ли прятать всплывающую подсказку. Затем, при необходимости, прячется старая всплывающая подсказка и показывается новая. После этого вызывается обработчик Application.OnIdle, если он назначен. Этот обработчик имеет параметр Done, по умолчанию равный True. Если в коде обработчика он не меняется на False, метод Idleинициирует события OnUpdateу всех объектов TAction, у которых они назначены (если Doneпосле вызова принял значение False, HandleMessageне тратит время на инициацию событий OnUpdate). Примечание Затем, независимо от значения Done, с помощью процедуры CheckSynchronizeпроверяется, есть ли записи в списке методов, ожидающих синхронизации (эти методы помещаются в указанный список при вызове TThread.Synchronize). Если список не пуст, выполняется первый из этих методов (при этом он, разумеется, удаляется из списка). Затем, если остался равным True, а список методов для синхронизации был пуст (т. е. никаких дополнительных действий выполнять не нужно), HandleMessageвызывает функцию Windows API WaitMessage. Эта функция приостанавливает выполнение нити до тех пор, пока в ее очереди не появятся сообщения. Примечание Главная петля сообщений в VCL реализуется методом Application.Run, вызов которого автоматически вставляется в dpr-файл VCL-проекта. Application.Runвызывает в цикле метод HandleMessage, пока поле FTerminateне окажется равным True(напомним, что значение Trueприсваивается этому полю, когда ProcessMessageизвлекает из очереди сообщение WM_QUIT, а также при обработке сообщения WM_ENDSESSIONи при закрытии главной формы). Для организации локальной петли сообщений существует метод Application.ProcessMessages. Он вызывает ProcessMessageдо тех пор, пока очередь не окажется пустой. Вызов этого метода рекомендуется вставлять в обработчики событий, которые работают долго, чтобы в это время программа не теряла способности реагировать на действия пользователя. Из сказанного может сложиться впечатление, что главная нить проверяет список методов синхронизации только в главной петле сообщений, когда вызывается метод Idle. На самом деле это не так. Модуль Classesсодержит переменную WakeMainThread, хранящую указатель на метод, который вызывается при помещении нового метода в список синхронизации. В конструкторе TApplicationэтой переменной присваивается указатель на метод TApplication.WakeMainThread, который посылает сообщение WM_NULLневидимому окну приложения. Сообщение WM_NULL— это "пустое" сообщение, на которое окно не должно реагировать (оно используется, например, при перехвате сообщений ловушкой: ловушка не может запретить передачу окну сообщения, но может изменить его на WM_NULL, чтобы окно проигнорировало сообщение). Невидимое окно приложения, тем не менее, не игнорирует это сообщение, а вызывает при его получении CheckSynchronize. Таким образом, синхронное выполнение метода не откладывается до вызова Idle, а выполняется достаточно быстро, в том числе и в локальной петле сообщений. Более того, если главная нить перешла в режим ожидания получения сообщения (через вызов WaitMessage), то вызов Synchronizeв другой нити прервет это ожидание, т.к. в очередь будет поставлено сообщение WM_NULL. Процедура CheckSynchronizeи переменная WakeMainThreadпозволяют обеспечить синхронизацию и в тех приложениях, которые не используют VCL в полном объеме. Разработчику приложения необходимо обеспечить периодические вызовы функции CheckSynchronizeиз главной нити, чтобы можно было вызывать TThread.Synchronizeв других нитях. При этом в главной нити можно обойтись без петли сообщений. Присвоение переменной WakeMainThreadсобственного метода позволяет реализовать специфичный для данного приложения способ ускорения вызова метода в главной нити. Примечание При показе VCL-формы в модальном режиме выборка сообщений из очереди осуществляется особым образом. Модальные окна в VCL — это не то же самое, что модальные диалоги с точки зрения API. Диалог может быть создан только на основе шаблона, и его модальность обеспечивается самой операционной системой, a VCL допускает модальность для любой формы, позволяя разработчику не быть ограниченным возможностями предусмотренного системой шаблона. Достигается это следующим образом: при вызове метода ShowModalвсе окна запрещаются средствами VCL, затем окно показывается обычным образом, как немодальное, но из-за того, что все остальные окна запрещены, создается эффект модальности. Внутри ShowModalсоздается своя петля сообщений. В этой петле в цикле вызывается метод Application.HandleMessageдо тех пор, пока не будет установлено свойство ModalResultили не придет сообщение WM_QUIT. После завершения этой петли вновь разрешаются все окна, которые были разрешены до вызова ShowModal, а "модальная" форма закрывается. В отличие от системных модальных диалогов модальная форма VCL во время своей активности не посылает родительскому окну сообщение WM_ENTERIDLE, но благодаря тому, что "модальная" петля сообщений использует HandleMessage, будет вызываться Idle, а значит, будет возникать событие Application.OnIdle, которое позволит выполнять фоновые действия. Теперь рассмотрим, как VCL обрабатывает извлеченные из очереди сообщения. Как уже было сказано ранее, для каждого класса формы VCL регистрирует одноименный оконный класс, а все окна, принадлежащие одному оконному классу, имеют общую оконную процедуру. С другой стороны, логика работы VCL требует, чтобы события обрабатывались тем экземпляром oбъекта, который инкапсулирует окно-адресат. Таким образом, возникает вопрос о том, как передать сообщение заданному экземпляру класса VCL. VCL решает эту задачу следующим образом. Модуль Classesсодержит недокументированную функцию MakeObjectInstance, описанную так: type TWndMethod = procedure(var Message: TMessage) of object; function MakeObjectInstance(Method: TWndMethod): Pointer; Тип TMessageхранит информацию о сообщении. Все методы VCL-компонентов, связанные с обработкой сообщения, используют этот тип (чуть позже мы рассмотрим его более подробно). Функция MakeObjectInstanceдинамически формирует новую оконную процедуру и возвращает указатель на нее (следовательно, любое VCL-приложение содержит самомодифицирующийся код). Задача этой динамически созданной процедуры — передать управление тому методу, который был указан при вызове MakeObjectInstance(таким образом, различные оконные процедуры, сформированные этой функцией, отличаются только тем, метод MainWndProcкакого экземпляра класса они вызывают). Каждый экземпляр оконного компонента создает свою оконную процедуру, которая передает обработку сообщения его методу MainWndProc. Указатель на эту процедуру записывается в поле FObjectInstance. Как мы уже говорили в предыдущем разделе, при регистрации оконного класса в качестве оконной процедуры указывается InitWndProc, которая при получении первого сообщения создает подкласс, и оконной процедурой назначается та, указатель на которую хранится в поле FObjectInstance, т.е. функция, созданная с помощью MakeObjectInstance(см. листинг 1.12). Таким образом, каждый экземпляр получает свою оконную процедуру, а обработку сообщения начинает метод MainWndProc. MainWndProc— это невиртуальный метод, обеспечивающий решение технических вопросов: удаление "мусора", оставшегося при обработке сообщения и обработку исключений. Собственно обработку сообщения он передает методу, на который указывает свойство WindowProc. Это свойство имеет тип TWndMethodи по умолчанию указывает на виртуальный метод WndProc. Таким образом, если разработчик не изменял значения свойства WindowProc, обработкой сообщения занимается WndProc. Метод WndProcобрабатывает только те сообщения, которые должны быть обработаны специальным образом, чтобы поддержать функциональность VCL. Особым образом метод WndProcобрабатывает сообщения от мыши: он следит, в границы какого визуального компонента попадают координаты "мышиных" сообщений, и если этот компонент отличается от того, в чью область попало предыдущее сообщение, компоненту из предыдущего сообщения дается команда обработать сообщение CM_MOUSELEAVE, а новому — сообщение CM_MOUSENTER. Это обеспечивает реакцию визуальных компонентов на приход и уход мыши (в частности, генерирование событий OnMouseEnterи OnMouseExit). Необходимость реализации такого способа отслеживания прихода и ухода мыши вместо использования системных сообщений WM_MOUSEHOVERи WM_MOUSELEAVEсвязана с тем, что системные сообщения пригодны только для работы с окнами, а VCL отслеживает приход и уход мыши и на неоконные визуальные компоненты. Впрочем, WM_MOUSELEAVEв WndProcтоже служит дополнительным средством проверки ухода мыши. Примечание События мыши метод WndProcдиспетчеризует самостоятельно, без помощи функции DispatchMessage. Это связано с тем, что DispatchMessageпередаёт сообщение тому оконному компоненту, которому оно предназначено с точки зрения системы. Однако с точки зрения VCL этот компонент может являться родителем для неоконных визуальных компонентов, и если сообщение от мыши связано с их областью, то оно должно обрабатываться соответствующим неоконным компонентом, а не его оконным родителем. DispatchMessageничего о неоконных компонентах не "знает" и не может передать им сообщения, поэтому разработчикам VCL пришлось реализовывать свой способ. Те сообщения, которые метод WndProcне обрабатывает самостоятельно (а их — подавляющее большинство), он передает в метод Dispatch, который объявлен и реализован в классе TObject. На первый взгляд может показаться странным, что в самом базовом классе реализована функциональность, использующаяся только в визуальных компонентах. Эта странность объясняется тем, что разработчики Delphi встроили поддержку обработки сообщений непосредственно в язык. Методы класса, описанные с директивой message, служат специально для обработки сообщений. Синтаксис описания такого метода следующий: procedure <Name>(var Message: <TMsgType>); message <MsgNumber>; <MsgNumber>— это номер сообщения, для обработки которого предназначен метод. Имя метода может быть любым, но традиционно оно совпадает с именем константы сообщения за исключением того, что в нем выбран более удобный регистр символов и отсутствует символ "_" (например, метод для обработки WM_SIZEбудет называться WMSize). В качестве типа параметра <TMsgType>компилятор разрешает любой тип, но на практике имеет смысл только использование типа TMessageили "совместимого" с ним. Тип TMessageописан в листинге 1.14. Листинг 1.14. Описание типа TMessage TMessage = packed record Msg: Cardinal; case Integer of 0: ( WParam: LongInt; LParam: LongInt; Result: LongInt); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end; Поле Msg содержит номер сообщения, поля WParamи LParam— значение одноименных параметров сообщения. Поле Result— выходное: метод, осуществляющий окончательную обработку сообщения, заносит в него то значение, которое должна вернуть оконная процедура. Поля с суффиксами Loи Hiпозволяют обращаться отдельно к младшему и старшему словам соответствующих полей, что может быть очень полезно, когда эти параметры содержат пару 16-разрядных значений. Например, у сообщения WM_MOUSEREMOVEмладшее слово параметра LParamсодержит X-координату мыши, старшее — Y-координату. В случае обработки этого сообщения поле LParamLoбудет содержать X-координату, LParamHi— Y-координату. "Совместимыми" с TMessageможно назвать структуры, которые имеют такой же размер, а также параметр Msg, задающий сообщение. Эти структуры учитывают специфику конкретного сообщения. Их имена образуются из имени сообщения путем отбрасывания символа и добавления префикса T. Для уже упоминавшегося сообщения WM_MOUSEMOVEсоответствующий тип выглядит, как показано в листинге 1.15. Листинг 1.15. Тип TWMNCMouseMove TWMNCMouseMove = packed record Msg: Cardinal; HitTest: LongInt; XCursor: SmallInt; YCursor: SmallInt; Result: LongInt; end; Параметр WParamпереименован в HitTest, что лучше отражает его смысл в данном случае, а параметр LParamразбит на две 16-разрядных части: XCursorи YCursor. Параметр метода для обработки сообщения имеет тип, соответствующий обрабатываемому сообщению (при необходимости можно описать свой тип), или тип TMessage. Таким образом, обработчик сообщения WM_MOUSEMOVEбудет выглядеть так, как показано в листинге 1.16. Листинг 1.16. Объявление и реализация метода для обработки сообщения WM_MOUSEMOVE type TSomeForm = class(TForm) ............... procedure WMNCMouseMove(var Message: TWMNCMouseMove); message WM_NCMOUSEMOVE; ................ end; procedure TSomeForm.WMNCMouseMove(var Message: TWMNCMouseMove); begin ............... inherited; // Возможно, этот вызов не будет нужен end; Метод для обработки сообщения может выполнить ее полностью самостоятельно, тогда он не должен вызывать унаследованный метод обработки сообщения. Если же реакция предка на сообщение в целом устраивает разработчика, но нуждается только в дополнении, ключевое слово inheritedпозволяет вызвать унаследованный обработчик для данного сообщения. Таким образом, может образовываться целая цепочка вызовов унаследованных обработчиков одного и того же сообщения, каждый из которых выполняет свою часть обработки. Если у предков класса нет обработчика данного сообщения, директива inheritedпередает управление методу TObject.DetaultHandler. Вернемся к методу Dispatch. Он ищет среди обработчиков сообщения класса (собственных или унаследованных) метод для обработки сообщения, заданного полем Msgпараметра Messageи, если находит, передает управление ему. Если ни сам класс, ни его предки не содержат обработчика данного сообщения, то обработка передаётся методу DefaultHandler. Метод DefaultHandlerвиртуальный, в классе TObjectон не выполняет никаких действий, но наследники его переопределяют. Впервые он переопределяется в классе TControlдля обработки сообщений, связанных с получением и установкой заголовка окна — WM_GETTEXT, WM_GETTEXTLENGTHи WM_SETTEXT. Напомним, что класс TControl является предком для всех визуальных компонентов, а не только оконных, и появление обработчика системных сообщений в этом классе — часть той имитации обработки сообщений неоконными компонентами, о которой мы уже говорили. В классе TWinControlметод DefaultHandlerтакже переопределен. Помимо передачи некоторых сообщений дочерним окнам (об этом мы будем подробнее говорить чуть позже) и обработки некоторых внутренних сообщений он вызывает оконную процедуру, адрес которой хранится в свойстве DefWndProc. Это свойство содержит адрес, который был присвоен полю WindowClass.lpfnWndProcструктуры TCreateParamsв методе CreateParams. По умолчанию это поле содержит адрес стандартной оконной процедуры DefWindowProc. Как было сказано ранее, обработка сообщений при использовании API обычно завершается вызовом этой процедуры. В классе TCustomFormметод DefaultHandlerтакже переопределен, если форма является MDI-формой, сообщения, присланные ей, передаются в процедуру DefFrameProc(за исключением WM_SIZE, которое передается в DefWindowProc) независимо от того, какое значение имеет свойство DefWindowProc. Для всех остальных типов форм вызывается унаследованный от TWinControl DefaultHandler. Повторим еще раз всю цепочку обработки сообщений оконными компонентами VCL (рис. 1.7). Для каждого компонента создается уникальная оконная процедура, которая передает управление методу MainWndProc. MainWndProcпередает управление методу, указатель на который хранится в свойстве WindowProc. По умолчанию это метод компонента WndProc. Он осуществляет обработку некоторых сообщений, но в большинстве случаев передает управление методу Dispatch, который ищет среди методов компонента или его предков обработчик данного сообщения. Если обработчик не найден, управление получает метод DefaultHandler(он может также получить управление и в том случае, если обработчик найден, но он вызывает inherited). DefaultHandlerсамостоятельно обрабатывает некоторые сообщения, но большинство из них передаётся оконной процедуре, адрес хранится в свойстве DefWndProc(по умолчанию это стандартная функция Windows API DefWindowProc). Рис. 1.7. Блок-схема оконной процедуры оконных компонентов VCL Класс TControlимеет метод Perform, с помощи которого можно заставить визуальный компонент выполнить обработку конкретного сообщения в обход оконной процедуры и системного механизма передачи сообщений. Performприводит к непосредственному вызову метода, указатель на который хранится в свойстве WindowProc. Дальше цепочка обработки сообщений такая же, как и при получении сообщения через оконную процедуру. Для оконных компонентов вызов Performпо своим последствиям практически эквивалентен передаче сообщения с помощью SendMessageс двумя исключениями. Во-первых, при использовании SendMessageсистема обеспечивает переключение между нитями, и сообщение будет выполнено в той нити, которая создала окно, a Performникакого переключения не производит, и обработка сообщения будет выполнена той нитью, которая вызвала Perform. Поэтому Perform, в отличие от SendMessage, можно использовать только в главной нити (напомним, что VCL — принципиально однонитевая библиотека, и создание форм вне главной нити с ее помощью недопустимо). Во-вторых, Performвыполняется чуть быстрее, т.к. оконная процедура и метод MainWndProcисключаются из цепочки обработки сообщения. Но основное преимущество Performперед SendMessageзаключается в том, что Performпригоден для работы со всеми визуальными компонентами, а не только с оконными. Неоконные визуальные компоненты не могут иметь оконной процедуры, но цепочка обработки сообщений у них есть. В ней отсутствует оконная процедура и метол MainWndProc, a DefaultHandlerне вызывает никаких стандартных оконных процедур, но во всем остальном эта цепочка полностью эквивалентна цепочке оконных компонентов. Таким образом, цепочка обработки сообщений оконных компонентов имеет две точки входа: оконную процедуру и метод Perform, а цепочка неоконных компонентов — только метод Perform. Следовательно, метод Performуниверсален: он одинаково хорошо подходит как для оконных, так и для неоконных компонентов. Он широко применяется в VCL, т.к. позволяет единообразно работать с любыми визуальными компонентами. Неоконным визуальным компонентам сообщения посылает их родительское окно. Например, как мы уже говорили, обработка сообщений, связанных с мышью, в классе TWinControlвключает в себя, не попадают ли координаты курсора в область какого-либо из дочерних неоконных компонентов. И если попадает, оконный компонент не обрабатывает это сообщение самостоятельно, а транслирует его соответствующему неоконному компоненту с помощью Perform. Эта трансляция и обеспечивает получение сообщений неоконными компонентами. Сообщения в VCL транслируются не только неоконным, но и оконным компонентам. В Windows все сообщения, информирующие об изменении состояния стандартных элементов управления, получает их родительское окно, а не сам элемент. Например, при нажатии на кнопку уведомительное сообщение об этом получает не сама кнопка, а окно, ее содержащее. Сама кнопка получает и обрабатывает только те сообщения, которые обычно разработчику неинтересны. Это упрощает работу программиста, т.к. не требуется для каждого элемента управления писать свою оконную процедуру, все значимые сообщения получает оконная процедура родительского окна. Рассмотрим, что происходит при нажатии кнопки на форме. Окно, содержащее эту кнопку, получает сообщение WM_COMMAND, уведомляющее о возникновении события среди оконных компонентов. Параметры сообщения позволяют определить, какое именно событие и с каким элементом управления произошло (в данном случае событие будет BN_CLICKED). Обработчик WM_COMMANDкласса TWinControlнаходит компонент, вызвавший сообщение, и посылает ему сообщение CN_COMMAND(как видно из префикса, это внутреннее сообщение VCL) с теми же параметрами. В нашем примере это будет экземпляр класса TButton, реализующий кнопку, которую нажал пользователь. Получив CN_COMMAND, компонент начинает обработку произошедшего с ним события (в частности, TButtonинициирует событие OnСlick). Примечание Рассмотрим все методы, с помощью которых можно встроить свой код в цепочку обработки сообщений оконным компонентом и перехватить сообщения. Всего существует шесть способов сделать это. 1. Как и у всякого окна, у оконного компонента VCL можно изменить оконную процедуру с помощью функции SetWindowLong. Этот способ лучше не применять, поскольку код VCL не будет ничего "знать" об этом переопределении, и сообщения, получаемые компонентом не через оконную процедуру, а с помощью Perform, не будут перехвачены. Другой недостаток данного способа — то, что изменение некоторых свойств компонента (например, FormStyleи BorderStyleу формы) невозможно без уничтожения окна и создания нового. Для программиста это пересоздание окна выглядит прозрачно, но новое окно получит новую оконную процедуру, и нужно будет выполнять перехват заново. Отследить момент пересоздания окна можно с помощью сообщения CM_RECREATEWND, обработчик которого уничтожает старое окно, а создание нового окна откладывается до момента первого обращения к свойству Handle. Если перехватить по сообщение, то, в принципе, после выполнения стандартного обработчика можно зaново установить перехват с помощью SetWindowLong, но т.к. этот способ не дает никаких преимуществ перед другими, более простыми, им все равно лучше не пользоваться. 2. Можно создать собственный метод обработки сообщения и поместить указатель на него в свойство WindowProc. При этом старый указатель обычно запоминается, т.к. новый обработчик обрабатывает лишь некоторые сообщения, а остальные передает старому. Достоинство этого способа — то, что метод, указатель на который помещается в WindowProc, не обязан принадлежать тому компоненту, сообщения которого перехватываются. Это позволяет, во-первых, создавать компоненты, которые влияют на обработку сообщений родительскими формами, а во-вторых, реализовывать нестандартную обработку сообщений стандартными компонентами, не порождая от них наследника. 3. При написании нового компонента можно перекрыть виртуальный метод WndProcи реализовать обработку нужных сообщений в нем. Это позволяет компоненту перехватывать сообщения в самом начале цепочки (за исключением внешних обработчиков, установленных с помощью свойства WindowProc— здесь разработчик компонента не властен). 4. Наиболее удобный способ самостоятельной обработки событий — написание их методов-обработчиков. Этот способ встречается чаще всего. Его недостатком является то, что номера обрабатываемых сообщений должны быть известны на этапе компиляции. Для системных сообщений и внутренних сообщений VCL это условие выполняется, но далее мы будем говорить об определяемых пользователем сообщениях, номера которых в некоторых случаях на этапе компиляции неизвестны. Обрабатывать такие сообщения с помощью методов с директивой невозможно. 5. Для перехвата сообщений, которые не были обработаны с помощью методов-обработчиков, можно перекрыть виртуальный метод. 6. И наконец, можно написать оконную процедуру и поместить указатель на нее в свойство DefWndProc. Этот способ по своим возможностям практически эквивалентен предыдущему, но менее удобен. Однако предыдущий способ пригоден только для создания собственного компонента, в то время как DefWndProcможно изменять у экземпляров существующих классов. Напомним, что этот способ не подходит для форм, у которых FormStyle = fsMDIForm, т.к. такие формы игнорируют значение свойства DefWndProc. Для перехвата сообщений неоконных визуальных компонентов допустимы все перечисленные способы, за исключением первого и последнего. Метод WndProcоконного компонента транслирует сообщения от мыши неоконным визуальным компонентам, родителем которых он является. Например. если положить на форму компонент TImageи переопределить у этой формы метод для обработки сообщения WM_LBUTTONDOWN, то нажатие кнопки мыши над TImageне приведет к вызову этого метода, т.к. WndProcпередаст это сообщение в TImage, и Dispatchне будет вызван. Но если переопределить WndProcили изменить значение свойства WindowProc(т.е. использовать второй или третий метод перехвата), то можно получать и обрабатывать и те "мышиные" сообщения, которые должны транслироваться неоконным дочерним компонентам. Это общее правило: чем раньше встраивается собственный код в цепочку обработки сообщений, тем больше у него возможностей. Как мы уже говорили, начиная с BDS 2006 появился еще один способ перехвата сообщений — перекрытие метода PreProcessMessage. Этот способ нельзя ставить в один ряд с перечисленными ранее шестью способами, т.к. он имеет два существенных отличия от них. Во-первых, с помощью этого способа перехватываются все сообщения, попавшие в петлю сообщений, а не только те, которые посланы конкретному компоненту, из-за чего может понадобиться дополнительная фильтрация сообщений. Во-вторых, метод PreProcessMessageперехватывает сообщения, попавшие в петлю сообщений, а не в оконную процедуру компонента. С одной стороны, это даёт возможность перехватывать те сообщения, которые метод Аррlication.ProcessMessageне считает нужным передавать в оконную процедуру, но с другой стороны, не позволяет перехватывать те сообщения, которые окно получает, минуя петлю сообщений (например, те, которые отправлены с помощью SendMessageили Perform). По этим причинам область применения данного способа совсем другая, чем у способов, связанных с внедрением кода в оконную процедур. Перекрытие PreProcessMessageсопоставимо, скорее, с использованием события Application.OnMessage. Различные способы перехвата сообщений иллюстрируются рядом примеров на прилагающемся к книге компакт-диске: использование свойства WindowProcпоказано в примерах Line, CoordLabelи PanelMsg, перекрытие метода WndProc— в примере NumBroadcast, создание метода для обработки сообщения — в примере ButtonDel. 1.1.9. Сообщения, определяемые пользователемСообщения очень удобны в тех случаях, когда нужно заставить окно выполнить какое-то действие. Поэтому Windows предоставляет возможность программисту создавать свои сообщения. Существуют три типа пользовательских сообщений: □ сообщения оконного класса; □ сообщения приложения; □ глобальные (строковые) сообщения. Для каждого из них выделен отдельный диапазон номеров. Номера стандартных сообщений лежат в диапазоне от 0 до WM_USER-1( WM_USER— константа, для 32-разрядных версий Windows равная 1024). Сообщения оконного класса имеют номера в диапазоне от WM_USERдо WM_APP-1( WM_APPимеет значение 32 768). Программист может выбирать произвольные номера для своих сообщений в этом диапазоне. Каждое сообщение должно иметь смысл только для конкретного оконного класса. Для различных оконных классов можно определять сообщения, имеющие одинаковые номера. Система никак не следит за тем, чтобы сообщения, определенные для какого-либо оконного класса, посылались только окнам этого класса — программист должен сам об этом заботиться. В этом же диапазоне лежат сообщения, специфические для стандартных оконных классов 'BUTTON', 'EDIT', 'LISTBOX', 'COMBOBOX'и т.п. Использование сообщений из этого диапазона иллюстрируется примером ButtonDel. Диапазон от WM_APPдо 49 151 (для этого значения константа не предусмотрена) предназначен для сообщений приложения. Номера этих сообщений также выбираются программистом произвольно. Система гарантирует, что ни один из стандартных оконных классов не задействует сообщения из этого диапазона. Это позволяет выполнять их широковещательную в пределах приложения рассылку. Ни один из стандартных классов не откликнется на такое сообщение и не выполнит нежелательных действий. Упоминавшиеся ранее внутренние сообщения VCL с префиксами CM_и CN_имеют номера в диапазоне от 45 056 до 49 151, т.е. используют часть диапазона сообщений приложения. Таким образом, при использовании VCL диапазон сообщений приложения сокращается до WM_APP..45055. Сообщения оконного класса и приложения пригодны и для взаимодействия с другими приложениями, но при этом отправитель должен быть уверен, что адресат правильно его поймет. Широковещательная рассылка при этом исключена — реакция других приложений, которые также получат это сообщение, может быть непредсказуемой. Если все же необходимо рассылать широковещательные сообщения между приложениями, то следует воспользоваться глобальными сообщениями, для которых зарезервирован диапазон номеров от 49 152 до 65 535. Глобальное сообщение обязано иметь имя (именно поэтому такие сообщения называются также строковыми), под которым оно регистрируется в системе с помощью функции в RegisterWindowMessage. Эта функция возвращает уникальный номер регистрируемого сообщения. Если сообщение с таким именем регистрируется впервые, номер выбирается из числа ещё не занятых. Если же сообщение с таким именем уже было зарегистрировано, то возвращается тот же самый номер, который был присвоен ему при первой регистрации. Таким образом, разные программы, регистрирующие сообщения с одинаковыми именами, получат одинаковые номера и смогут понимать друг друга. Для прочих же окон это сообщение не будет иметь никакого смысла. Создание и использование оконных сообщений демонстрируется примером NumBroadcast, содержащимся на прилагаемом компакт-диске. Разумеется, существует вероятность, что два разных приложения выберут для своих глобальных сообщений одинаковые имена, и это приведет к проблемам при широковещательной рассылке этих сообщений. Но, если давать своим сообщениям осмысленные имена, а не что-то вроде WM_MYMESSAGE1, вероятность такого совпадения будет очень мала. В особо критических ситуациях можно в качестве имени сообщения использовать GUID, уникальность которого гарантируется. Номера глобальных сообщений становятся известными только на этапе выполнения программы. Это означает, что для их обработки нельзя использовать методы с директивой message, вместо этого следует перекрывать методы WndProcили DefaultHandler. 1.1.10. Особые сообщенияОтправка и обработка некоторых сообщений производится не по общим правилам, а с различными исключениями. Приведенный далее список таких сообщений не претендует на полноту, но все-таки может дать представление о таких исключениях. Сообщение WM_COPYDATAслужит для передачи блока данных от одного процесса к другому. В 32-разрядных версиях Windows память, выделенная некоторому процессу, недоступна для всех остальных процессов. Поэтому просто передать указатель другому процессу нельзя, поскольку он не сможет получить доступ к этой области памяти. При передаче сообщения WM_COPYDATAсистема копирует указанный блок из адресного пространства отправителя в адресное пространство получателя, передает получателю указатель на этот блок, и при завершении обработки сообщения освобождает блок. Все это требует определенной синхронности действий, которой невозможно достичь при посылке сообщения, поэтому с помощью WM_COPYDATAможно только отправлять, но не посылать (т.е. можно использовать SendMessage, но не PostMessage). Сообщение WM_PAINTпредназначено для перерисовки клиентской облаcти окна. Если изображение сложное, этот процесс занимает много времени, поэтому в Windows предусмотрены механизмы, минимизирующие количество перерисовок. Перерисовывать свое содержимое окно должно при получении сообщения WM_PAINT. С каждым таким сообщением связан регион, нуждающийся в обновлении. Этот регион может совпадать с клиентской областью окна или быть ее частью. В последнем случае программа может ускорить перерисовку, рисуя не все окно, а только нуждающуюся в этом часть (VCL игнорирует возможность перерисовки только части окна, поэтому при работе с этой библиотекой окно всегда перерисовывается полностью). Послать сообщение WM_PAINTс помощью PostMessageокну нельзя, т.к. оно не ставится в очередь. Вместо этого можно пометить регион как нуждающийся в обновлении с помощью функций InvalidateRectи InvalidateRgn. Если на момент вызова этих функций регион, который необходимо обновить, не был пуст, новый регион объединяется со старым. Функции GetMessageи PeekMessage, если очередь сообщений пуста, а регион, требующий обновления, не пуст, возвращают сообщение WM_PAINT. Таким образом, перерисовка окна откладывается до того момента, когда все остальные сообщения будут обработаны. Отправить WM_PAINTс помощью SendMessageтоже нельзя. Если требуется немедленная перерисовка окна, следует вызвать функции UpdateWindowили RedrawWindow, которые не только отправляют сообщение окну, но и выполняют сопутствующие действия, связанные с регионом обновления. Обработка сообщения WM_PAINTтакже имеет некоторые особенности. Обработчик должен получить контекст устройства окна (см. разд. 1.1.11 данной главы) с помощью функции BeginPaintи по окончании работы освободить его с помощью EndPaint. Эти функции должны вызываться только один раз при обработке сообщения. Соответственно, если сообщение обрабатывается поэтапно несколькими обработчиками, как это бывает при перехвате сообщений, получать и освобождать контекст устройства должен только первый из них, а остальные должны пользоваться тем контекстом, который он получил. Система не накладывает обязательных требований, которые могли бы решить проблему, но предлагает решение, которое используют все предопределенные системные классы. Когда сообщение WM_PAINTизвлекается из очереди, его параметр wParamравен нулю. Если же обработчик получает сообщение с wParam <> 0, то он рассматривает значение этого параметра как дескриптор контекста устройства и использует его, вместо того чтобы получать дескриптор через BeginPaint. Первый в цепочке обработчиков должен передать вниз по цепочке сообщение с измененным параметром wParam. Компоненты VCL также пользуются этим решением. При перехвате сообщения WM_PAINTэто нужно учитывать. Примеры PanelMsg и Line, имеющиеся на прилагаемом компакт-диске, демонстрируют, как правильно перехватывать сообщение WM_PAINT. Простые таймеры, создаваемые системой с помощью функции SetTimer, сообщают об истечении интервала посредством сообщения WM_TIMER. Проверка того, истек ли интервал, осуществляется внутри функций GetMessage, PeekMessage. Таким образом, если эти функции долго не вызываются, сообщение WM_TIMERне ставится в очередь, даже если положенный срок истек. Если за время обработки других сообщений срок истек несколько раз, в очередь ставится только одно сообщение WM_TIMER. Если в очереди уже есть сообщение WM_TIMER, новое в очередь не добавляется, даже если срок истек. Таким образом, часть сообщений WM_TIMERтеряется, т.е., например, если интервал таймера установить равным одной секунде, то за час будет получено не 3600 сообщений WM_TIMER, а меньшее число, и разница будет тем больше, чем интенсивнее программа загружает процессор. Примечание Некоторую специфику имеют и сообщения от клавиатуры. При обработке таких сообщений можно использовать функцию GetKeуState, которая возвращает состояние любой клавиши (нажата-отпущена) в момент возникновения данного события. Именно в момент возникновения, а не в момент вызова функции. Если функцию GetKeyStateиспользовать при обработке не клавиатурного сообщения, оно вернет состояние клавиши на момент последнего извлеченного из очереди клавиатурного сообщения. 1.1.11. Графика в Windows APIТа часть Windows API, которая служит для работы с графикой, обычно называется GDI (Graphic Device Interface). Ключевое понятие в GDI — контекст устройства (Device Context, DC). Контекст устройства — это специфический объект, хранящий информацию о возможностях устройства, о способе работы с ним и о разрешенной для изменения области. В Delphi контекст устройства представлен классом TCanvas, свойство Handleкоторого содержит дескриптор контекста устройства. TCanvasуниверсален в том смысле, что с его помощью рисование в окне, на принтере или в метафайле выглядит одинаково. То же самое справедливо и для контекста устройства. Разница заключается только в том, как получить в разных случаях дескриптор контекста. Большинство методов класса TCanvasявляются "калькой" с соответствующих (в большинстве случаев одноименных) функций GDI. Но в некоторых случаях (прежде всего в методах вывода текста и рисования многоугольников) параметры методов TCanvasимеют более удобный тип, чем функции GDI. Например, метод TCanvas.Polygonтребует в качестве параметра открытый массив элементов типа TPoint, а соответствующая функция GDI — указатель на область памяти, содержащую координаты точек, и число точек. Это означает, что до вызова функции следует выделить память, а потом — освободить ее. Еще нужен код, который заполнит эту область памяти требуемыми значениями. И ни в коем случае нельзя ошибаться в количестве элементов массива. Если зарезервировать память для одного числа точек, а при вызове функции указать другое, программа будет работать неправильно. Но для простых функций работа через GDI ничуть не сложнее, чем через TCanvas. Для получения дескриптора контекста устройства существует много функций. Только для того, чтобы получить дескриптор контекста обычного окна, существуют четыре функции: BeginPaint, GetDC, GetWindowDCи GetDCEx. Первая из них возвращает контекст клиентской области окна при обработке сообщения WM_PAINT. Вторая дает контекст клиентской области окна, который можно использовать в любой момент времени, а не только при обработке WM_PAINT. Третья позволяет получить контекст всего окна, вместе с неклиентской частью. Последняя же дает возможность получить контекст определенной области клиентской части окна. После того как дескриптор контекста получен, можно воспользоваться преимуществами класса TCanvas. Для этого необходимо создать экземпляр такого класса и присвоить его свойству Handleполученный дескриптор. Освобождение ресурсов нужно проводить в следующем порядке сначала свойству Handle присваивается нулевое значение, затем уничтожается экземпляр класса TCanvas, после этого с помощью подходящей функции GDI освобождается контекст устройства. Пример такого использования класса TCanvasдемонстрируется листингом 1.17. Листинг 1.17. Использование класса TCanvasдля работы с произвольным контекстом устройства var DC: HDC; Canvas: TCanvas; begin DC := GetDC(...); // Здесь возможны другие способы получения DC Canvas := TCanvas.Create; try Canvas.Handle := DC; // Здесь рисуем, используя Canvas finally Canvas.Free; end; // Освобождение объекта Canvas не означает освобождения контекста DC // DC необходимо удалить вручную ReleaseDC(DC); end; Использование класса TCanvasдля рисования на контексте устройства, для которого имеется дескриптор, показано в примере PanelMsg на прилагающемся компакт-диске. Разумеется, можно вызывать функции GDI при работе через TCanvas. Для этого им просто нужно передать в качестве дескриптора контекста значение свойства Canvas.Handle. Коротко перечислим те возможности GDI, которые разработчики VCL почему-то не сочли нужным включать в TCanvas: работа с регионами и траекториями; выравнивание текста по любому углу или по центру; установка собственной координатной системы; получение детальной информации об устройстве; использование геометрических перьев; вывод текста под углом к горизонтали; расширенные возможности вывода текста; ряд возможностей по рисованию нескольких кривых и многоугольников одной функцией; поддержка режимов заливки. Доступ ко всем этим возможностям может быть осуществлен только через API. Отметим также, что Windows NT/2000/XP поддерживает больше графических функций, чем 9x/МЕ. Функции, которые не поддерживаются в 9x/ME, также не имеют аналогов среди методов TCanvas, иначе программы, написанные с использованием данного класса, нельзя было бы запустить в этих версиях Windows. GDI предоставляет очень интересные возможности по преобразованию координат, но только в Windows NT/2000/XP; в Windows 9x/ME они не поддерживаются. С помощью функции SetWorldTransformможно задать произвольную матрицу преобразования координат, и все дальнейшие графические операции будут работать в новой координатной системе. Матрица позволяет описать такие преобразования координат, как поворот, сдвиг начала координат и масштабирование, т.е. возможности открываются очень широкие. Также существует менее гибкая, но тоже полезная функция преобразования координат — SetMapMode, которая поддерживается во всех версиях Windows. С ее помощью можно установить такую систему координат, чтобы во всех функциях задавать координаты, например, в миллиметрах, а не пикселах. Это позволяет использовать один и тот же код для вывода на устройства с разными разрешениями. Некоторые возможности GDI, которым нет аналогов в TCanvas, демонстрируются примером GDI Draw. Для задания цвета в GDI предусмотрен тип COLORREF(в модуле Windows определен также его синоним для Delphi — TColorRef). Это 4-байтное беззнаковое целое, старший байт которого определяет формат представления цвета. Если этот байт равен нулю (будем называть этот формат нулевым), первый, второй и третий байты представляют собой интенсивности красного, зеленого и синего цветов соответственно. Если старший байт равен 1, два младших байта хранят индекс цвета в текущей палитре устройства, третий байт не используется и должен быть равен нулю. Если старший байт равен 2, остальные байты, как и в нулевом формате, показывают интенсивность цветовых составляющих. Тип TColorRefпозволяет менять глубину каждого цветового канала от 0 до 255, обеспечивая кодирование 16 777 216 различных оттенков (это соответствует режиму TrueColor). Если цветовое разрешение устройства невелико, GDI подбирает ближайший возможный цвет из палитры. Если старший байт TColorRefравен нулю, цвет выбирается из текущей системной палитры (по умолчанию эта палитра содержит всего 20 цветов, поэтому результаты получаются далекими от совершенства). Если же старший байт равен 2, то GDI выбирает ближайший цвет из палитры устройства. В этом случае результаты получаются более приемлемыми. Если устройство имеет большую цветовую глубину и не задействует палитру, разницы между нулевым и вторым форматом COLORREFнет. Примечание В Windows API определены макросы (а в модуле Windows, соответственно, одноименные функции) RGB, PaletteIndexи PaletteRGB. RGBпринимает три параметра — интенсивности красного, зеленого и синего компонентов и строит из них значение типа TColorRefнулевого формата. PaletteIndexпринимает в качестве параметра номер цвета в палитре и на его основе конструирует значение первого формата. Макрос PaletteRGBэквивалентен RGB, за исключением того, что устанавливает старший байт возвращаемого значения равным двум. Для извлечения интенсивностей отдельных цветовых компонентов из значения типа TColorRefможно воспользоваться функциями GetRValue, GetGValueи GetBValue. В системе определены два специальных значения цвета: CLR_NONE( $1FFFFFFF) и CLR_DEFAULT( $20000000). Они используются только в списках рисунков (image lists) для задания фонового и накладываемого цветов при выводе рисунка. CLR_NONEзадаёт отсутствие фонового или накладываемого цвета (в этом случае соответствующий визуальный эффект не применяется). CLR_DEFAULT— установка цвета, заданного для всего списка. В VCL для передачи цвета предусмотрен тип TColor, определенный в модуле Graphics. Это 4-байтное число, множество значений которого является множеством значений типа TColorRef. К системным форматам 0, 1 и 2 добавлен формат 255. Если старший байт значения типа TColorравен 255, то младший байт интерпретируется как индекс системного цвета (второй и третий байт при этом не учитываются). Системные цвета — это цвета, используемые системой для рисования различных элементов интерфейса пользователя. Конкретные RGB-значения этих цветов зависят от версии Windows и от текущей цветовой схемы. RGB-значение системного цвета можно получить с помощью функции GetSysColor. 255-й формат TColor освобождает от необходимости явного вызова данной функции. Для типа TColor определен ряд констант, облегчающих его использование. Некоторые из них соответствуют определенному RGB-цвету ( clWhite, clBlack, clRedи т.п.), другие — определенному системному цвету ( clWindow, clHighlight, clBtnFaceи т.п.). Значения RGB-цветов определены в нулевом формате. Это не приведет к потере точности цветопередачи в режимах с палитрой, т.к. константы определены только для 16-ти основных цветов, которые обязательно присутствуют в системной палитре. Значениям CLR_NONEи CLR_DEFAULTсоответствуют константы clNoneи clDefault. Они служат (помимо списков рисунков) для задания прозрачного цвета в растровом изображении. Если этот цвет равен clNone, изображение считается непрозрачным, если clDefault, в качестве прозрачного цвета берется цвет левого нижнего пиксела. Везде, где требуется значение типа TColor, можно подставлять TColorRef, т.е. всем свойствам и параметрам методов класса TCanvas, имеющим тип TColorможно присваивать те значения TColorRef, которые сформированы функциями API. Обратное неверно: API-функции не умеют обращаться с 255-м форматом TColor. Преобразование из TColorв TColorRefосуществляется с помощью функции ColorToRGB. Значения нулевого, первого и второго формата, а также clNoneи clDefaultона оставляет без изменения, а значения 255-го формата приводит к нулевому с помощью функции GetSysColor. Эту функцию следует использовать при передаче значении типа TColorв функции GDI. Применение кистей, перьев и шрифтов в GDI принципиально отличается от того, как это делается в VCL. Класс TCanvasимеет свойства Pen, Brush, и Font, изменение свойств которых приводит к выбору того или иного пера, кисти, шрифта. В GDI эти объекты самостоятельны, должны создаваться, получать свой дескриптор, "выбираться" в нужный контекст устройства с помощью функции SelectObjectи уничтожаться после использования. Причем удалять можно только те объекты, которые не выбраны ни в одном контексте. Есть также несколько стандартных объектов, которые не нужно ни создавать, ни удалять. Их дескрипторы можно получить с помощью функции GetStockObject. Для примера рассмотрим фрагмент программы, рисующей на контексте с дескриптором DC две линии: синюю и красную (листинг 1.18). В этом фрагменте используется то, что функция SelectObjectвозвращает дескриптор объекта, родственного выбираемому, который был выбран ранее. Так, при выборе нового пера она вернет дескриптор того пера, которое было выбрано до этого. Листинг 1.18. Рисование разными перьями с использованием GDI SelectObject(DC, CreatePen(PS_SOLID, 1, RGB(255, 0, 0))); MoveToEx(DC, 100, 100, nil); LineTo(DC, 200, 200); DeleteObject(SelectObject(DC, CreatePen(PS_SOLID, 1, RGB(0, 0, 255)))); MoveToEx(DC, 200, 100, nil); LineTo(DC, 100, 200); DeleteObject(SelectObject(DC, SetStockObject(BLACK_PEN))); Дескрипторы объектов GDI имеют смысл только в пределах того процесса, который их создал, передавать их между процессами нельзя. Тем не менее изредка можно встретить утверждения, что такая передача возможна. Источник этой ошибки в том. что дескрипторы объектов GDI можно было передавать между процессами в старых, 16-разрядных версиях Windows, так что все утверждения о возможности такой передачи просто основываются на устаревших сведениях. Для хранения растровых изображений в Windows существуют три формата: DDB, DIB и DIB-секция. DDB — это Device Dependent Format, формат, определяемый графическим устройством, на которое идет вывод. DIB — это Device Independent Bitmap, формат, единый для всех устройств. Формат DIB — это устаревший формат, который не позволяет использовать графические функции GDI для модификации картинки, модифицировать изображение можно, только одним способом: вручную изменяя цвета отдельных пикселов. В 32-разрядных версиях появился еще один формат — DIB-секция. По сути дела это тот же самый DIB, но дополненный возможностями рисовать на нем с помощью GDI-функций. Все различия между этими тремя форматами можно прочитать в замечательной книге [1]; мы же здесь ограничимся только кратким их обзором. Формат DDB поддерживается самой видеокартой (или другим устройством вывода), поэтому при операциях с таким изображением задействуется аппаратный ускоритель графики. DDB-изображение хранится в выгружаемом системном пуле памяти (Windows NT/2000/XP) или в куче GDI (Windows 9x/ME). При этом размер DDB-растра не может превышать 16 Мбайт в Windows 9x/ME и 48 Мбайт в Windows NT/2000/XP. Формат DDB не переносим с одного устройства на другое, он должен использоваться только в рамках одного устройства. Прямой доступ к изображению и его модификация вручную невозможны, т.к. формат хранения изображения конкретным устройством непредсказуем. Модифицировать DDB можно только с помощью функций GDI. Цветовая глубина DDB-изображений определяется устройством. DIB-секция может храниться в любой области памяти, ее размер ограничивается только размером доступной приложению памяти, функции GDI для рисования на таком изображении используют чисто программные алгоритмы, никак не задействуя аппаратный ускоритель. DIB-секция поддерживает различную цветовую глубину и прямой доступ к области памяти, в которой хранится изображение. DIB-секция переносима с одного устройства на другое. BMP-файлы хранят изображение как DIB. Скорость работы с изображением в формате DIB-секции зависит только от производительности процессора, памяти и качества реализации графических алгоритмов системой (а они, надо сказать, реализованы в Windows очень неплохо). Скорость работы с изображением в формате DDB зависит еще и от драйвера и аппаратного ускорителя видеокарты. Во-первых, аппаратный ускоритель и драйвер могут поддерживать или не поддерживать рисование графических примитивов (в последнем случае эти примитивы рисует система: то, какие операции поддерживает драйвер, можно узнать с помощью функции GetDeviceCaps). До недавнего времени была характерной ситуация, когда рисование картинки на DDB-растре и вывод такого растра на экран были заметно (иногда — в два-три раза) быстрее, чем аналогичные операции с DIB-секцией. Однако сейчас разница стала существенно меньше, производительность системы в целом выросла сильнее, чем производительность двумерных аппаратных ускорителей (видимо, разработчики видеокарт больше не считают двумерную графику узким местом и поэтому сосредоточили свои усилия на развитии аппаратных ускорителей 3D-графики). На некоторых мощных компьютерах можно даже столкнуться с ситуацией, когда DDB-изображение отстает по скорости от DIB. Класс TBitmapможет хранить изображение как в виде DDB, так и в виде DIB- секции — это определяется значением свойства PixelFormat. Значение pfDeviceозначает использование DDB, остальные значения — DIB-секции с различной цветовой глубиной. По умолчанию TBitmapсоздает изображение с форматом pfDevice, но программист может изменить формат в любой момент. При этом создается новое изображение требуемого формата, старое копируется в него и уничтожается. Со свойством PixelFormatтесно связано свойство HandleType, которое может принимать значения bmDIBи bmDDB. Изменение свойства PixelFormatприводит к изменению свойства HandleType, и наоборот. Примечание При загрузке изображения из файла, ресурса или потока класс TBitmapобычно создает изображение в формате DIB-секции, соответствующее источнику по цветовой глубине. Исключение составляют сжатые файлы (формат BMP поддерживает сжатие только для 16- и 256-цветных изображений) — в этом случае создается DDB. В файле Graphicsопределена глобальная переменная DDBsOnly, которая по умолчанию равна False. Если изменить ее значение на True, загружаемое изображение всегда будет иметь формат DDB. Примечание Класс TBitmapимеет свойство ScanLine, через которое можно получить прямой доступ к массиву пикселов, составляющих изображение. В справке написано, что это свойство можно использовать только с DIB-изображениями. Но на самом деле DDB-изображения тоже позволяют использовать это свойство, хотя и с существенными ограничениями. Если изображение хранится в DDB- формате, при обращении к ScanLineсоздастся его DIB-копия, и ScanLineвозвращает указатель на массив этой копии. Поэтому, во-первых, ScanLineработает с DDB-изображениями очень медленно, а во-вторых, работает не с изображением, а с его копией, откуда вытекают следующие ограничения: 1. Копия создаётся на момент обращения к ScanLine, поэтому изменения, сделанные на изображении с помощью GDI-функций после этого, будут недоступными. 2. Каждое обращение к ScanLineсоздает новую копию изображения, а старая при этом уничтожается. Гарантии, что новая копия будет располагаться в той же области памяти, нет, поэтому указатель, полученный при предыдущем обращении к ScanLine, больше нельзя использовать. 3. Изменения, сделанные в массиве пикселов, затрагивают только копию изображения, но само изображение при этом не меняется. Поэтому в случае DDB свойство ScanLineдает возможность прочитать, но не изменить изображение. Следует отметить, что TBitmapиногда создает DIB-секции, даже если свойства HandleTypeи PixelFormatявно указывают на использование DDB. Особенно часто это наблюдается для изображений большого размера. По всей видимости, это происходит, когда в системном пуле нет места для хранения DDB-изображения такого размера, и разработчики TBitmap решили, что в таком случае лучше создать DIB-изображение, чем не создавать никакого. Пример BitmapSpeedна прилагаемом компакт-диске позволяет сравнить скорость выполнения различных операций с DDB- и DIB-изображениями. 1.1.12. ANSI и UnicodeWindows поддерживает две кодировки: ANSI и Unicode. В кодировке ANSI (American National Standard Institute) каждый символ кодируется однобайтным кодом. Коды от 0 до 127 совпадают с кодами ASCII, коды от 128 до 255 могут означать разные символы различных языков в зависимости от выбранной кодовой страницы. Кодовые страницы позволяют уместить многочисленные символы различных языков в однобайтный код, но при этом можно работать только с одной кодовой страницей, т.е. с одним языком. Неверно выбранная кодовая страница приводит к появлению непонятных символов (в Интернете их обычно называют "кракозябрами") вместо осмысленного текста. В кодировке Unicode используется 2 байта на символ, что даёт возможность закодировать 65 536 символов. Этого хватает для символов латиницы и кириллицы, греческого алфавита, китайских иероглифов, арабских и еврейских букв, а также многочисленных дополнительных (финансовых, математических и т. п.) символов. Кодовых страниц в Unicode нет. Примечание Windows NT/2000/XP поддерживает ANSI и Unicode в полном объеме. Это значит, что любая функция, работающая со строками, представлена в двух вариантах: для ANSI и для Unicode. Windows 9x/МЕ в полном объеме поддерживает только ANSI. Unicode-варианты в этих системах есть у относительно небольшого числа функций. Каждая страница MSDN, посвященная функции, работающей со строками (или со структурами, содержащими строки), в нижней части содержит надпись, показывающую, реализован ли Unicode-вариант этой функции только для NT/2000/XP или для всех платформ. Примечание Рассмотрим, как сосуществуют два варианта на примере функции RegisterWindowMessage. Согласно справке, она экспортируется библиотекой user32.dll. Однако если посмотреть список функций, экспортируемых этой библиотекой (это можно сделать, например, при помощи утилиты TDump.exe, входящей в состав Delphi), то там этой функции не будет, зато будут функции RegisterWindowMessageAи RegisterWindowMessageW. Первая из них — это ANSI-вариант функции, вторая — Unicode-вариант (буква W означает Wide — широкий; символы кодировки Unicode часто называются широкими из-за того, что на один символ приходится не один, а два байта). Сначала разберемся с тем, как используются два варианта одной функции в Microsoft Visual C++. В стандартных заголовочных файлах учитывается наличие макроопределения UNICODE. Есть два символьных типа — CHARдля ANSI и WCHARдля Unicode. Если макрос UNICODEопределен, тип TCHARсоответствует типу WCHAR, если не определен — типу CHAR(после этого производные от TCHARтипы, такие как LPCTSTRавтоматически начинают соответствовать кодировке, определяемой наличием или отсутствием определения UNICODE). В заголовочных файлах импортируются оба варианта функции, а также определяется макрос RegisterWindowMessage. Его смысл также зависит от макроса UNICODE: если он определен, RegisterWindowMessageэквивалентен RegisterWindowMessageW, если не определен — RegisterWindowMessageA. Все функции, поддерживающие два варианта кодировки, импортируются точно так же. Таким образом, вставляя или убирая макроопределение UNICODE, можно, не меняя ни единого символа в программе, компилировать ее ANSI- или Unicode-версию. Разработчики Delphi не стали полностью копировать этот механизм, видимо, этому помешала существующая в Delphi раздельная компиляция модулей, из-за которой невозможно определением одного символа заставить все модули перекомпилироваться (тем более что часть из них может не иметь исходных кодов). Поэтому в Delphi нет типа, аналогичного TCHAR. Рассмотрим, как та же функция RegisterWindowMessageимпортируется модулем Windows (листинг 1.19). Листинг 1.19. Импорт функции RegisterWindowMessage interface ... function RegisterWindowMessage(lpString: PChar): UINT; stdcall; function RegisterWindowMessageA(lpString: PAnsiChar): UINT; stdcall; function RegisterWindowMessageW(lpString: PWideChar): UINT; stdcall; ... implementation ... function RegisterWindowMessage; external user32 name 'RegisterWindowMessageA'; function RegisterWindowMessageA; external user32 name 'RegisterWindowMessageA'; function RegisterWindowMessageW; external user32 name 'RegisterWindowMessageW'; Видно, что функция RegisterWindowMessageAимпортируется дважды: один раз под своим настоящим именем, а второй раз — под именем RegisterWindowMessage. Любое из этих имен подходит для вызова ANSI-варианта этой функции (напоминаю, что типы PCharи PAnsiCharэквивалентны). Чтобы вызвать Unicode-вариант функции, потребуется функция RegisterWindowMessageW. Структуры, содержащие строковые данные, также имеют ANSI- и Unicode-вариант. Например, структура WNDCLASSв модуле Windows представлена типами TWndClassA(с синонимами WNDCLASSAи tagWNDCLASSA) и TWndClassW(с синонимами WNDCLASSWи tagWHDCLASSW). Тип TWndClass(и его синонимы WNDCLASSи tagWNDCLASS) эквивалентен типу TWndClassA. Соответственно, при вызове функций RegisterClassAи RegisterClassExAиспользуется тип TWndClassA, при вызове RegisterClassWи RegisterClassExW— тип TWndClassW. 1.1.13. Строки в Windows APIUnicode в Delphi встречается редко, т.к. программы, использующие эту кодировку, не работают в Windows 9x/МЕ. Библиотека VCL также игнорирует возможность работы с Unicode, ограничиваясь ANSI. Поэтому далее мы будем говорить только об ANSI. С кодировкой Unicode можно работать аналогично, заменив PCharна PWideCharи stringна WideString. Для работы со строками в Delphi наиболее распространен тип AnsiString, обычно называемый просто string(более детально отношения между этими типами рассмотрены в главе 3). Переменная типа stringявляется указателем на строку, хранящуюся в динамической памяти. Этот указатель указывает на первый символ строки. По отрицательному смещению хранится число символов в строке и счетчик ссылок, который позволяет избежать ненужных копирований строки, реализуя так называемое "копирование при необходимости". Если присвоить одной строковой переменной значение другой строковой переменной, то строка не копируется, а просто обе переменные начинают указывать на одну и ту же строку. Счетчик ссылок при этом увеличивается на единицу. Когда строка модифицируется, проверяется счетчик ссылок: если он не равен единице, то строка копируется, счетчик ссылок старой копии уменьшается на единицу, у новой копии счетчик ссылок будет равен единице, и переменная, которая меняется, будет указывать на новую копию. Таким образом, строка копируется только тогда, когда одна из ссылающихся на нее переменных начинает изменять эту строку, чтобы изменения не коснулись остальных переменных. При любых модификациях строки в ее конец автоматически добавляется нулевой символ (при подсчете длины строки с помощью функции Lengthон игнорируется). Но если присвоить строковой переменной пустую строку, то эта переменная станет нулевым указателем ( nil), память для хранения одного символа #0 выделена не будет. При выходе строковой переменной из области видимости (т. е., например, при завершении процедуры, в которой она является локальной переменной, или при уничтожении объекта, полем которого она является) она автоматически финализируется, т.е. счетчик ссылок уменьшается на единицу и, если он оказывается равен нулю, память, выделенная для строки, освобождается. (О внутреннем устройстве AnsiStringсм. также разд. 3.3.) Механизм выделения и освобождения памяти и подсчета ссылок прозрачен для программы. От разработчика требуется только не вмешиваться в его работу с помощью низкоуровневых операций с указателями, чтобы менеджер памяти не запутался. Примечание Функции Windows API не поддерживают тип string. Они работают со строками, оканчивающимися на #0(нуль-терминированные строки, null-terminated strings). Это означает, что строкой называется указатель на цепочку символов. Признаком конца такой цепочки является символ с кодом 0. Раньше для таких строк существовал термин ASCIIZ. ASCII — название кодировки, Z — zero. Сейчас кодировка ASCII в чистом виде не встречается, поэтому этот термин больше не применяется, хотя это те же самые по своей сути строки. Как уже говорилось, в Delphi ко всем строкам типа stringнеявно добавляется нулевой символ, не учитывающийся при подсчете числа символов. Это сделано для совместимости с нуль-терминированными строками. Однако эта совместимость ограничена. Для работы с нуль-терминированными строками в Delphi обычно служит тип PChar. Формально это указатель на один символ типа Char, но подразумевается, что это только первый символ строки, и за ним следуют остальные символы. Где будут эти символы размещены и какими средствами для них будет выделена память, программист должен решить самостоятельно. Он же должен позаботиться о том, чтобы в конце цепочки символов стоял #0. Строку, на которую указывает PChar, можно использовать везде, где требуется string — компилятор сам выполнит необходимые преобразования. Обратное неверно. Фактически, string— это указатель на начало строки, завершающейся нулем, т.е. тот самый указатель, который требуется при работе с PChar. Однако, как уже отмечалось, некорректные манипуляции с этим указателем могут привести к нежелательным эффектам, поэтому компилятор требует явного приведения переменных и выражений типа stringк PChar. В свою очередь, программист должен ясно представлять, к каким последствиям это может привести. Если посмотреть описание функций API, имеющих строковые параметры, в справке, можно заметить, что в некоторых случаях строковые параметры имеют тип LPCTSTR(как, например, у функции SetWindowText), а в некоторых — LPTSTR( GetWindowText). Ранее мы говорили, что появление префикса Cпосле LPуказывает на то, что это указатель на константу, т.е. то, на что указывает такой указатель, не может быть изменено. Тип LPCTSTRимеют те строковые параметры, содержимое которых функция только читает, но не модифицирует. С такими параметрами работать проще всего. Рассмотрим на примере функции SetWindowText, как можно работать с такими параметрами (листинг 1.20). Листинг 1.20. Вызов функции с параметром типа LPCTSTR { Вариант 1 } SetWindowText(Handle, 'Строка'); { Вариант 2 S -переменная типа string } SetWindowText(PChar(S)); { Вариант 3 X - переменная типа Integer } SetWindowText(PChar('Выполнено ' + IntToStr(X) + '%')); В первом варианте компилятор размещает строковый литерал в сегменте кода, а в функцию передает указатель на эту область памяти. Поскольку функция не модифицирует строку, а только читает, передача такого указателя не вызывает проблем. Во втором варианте функции передается указатель, хранящийся в переменной S. Такое приведение stringк PCharбезопасно, т.к. строка, на которую ссылается переменная S, не будет модифицироваться. Но здесь существует одна тонкость: конструкция PChar(S)— это не просто приведение типов, при ее использовании неявно вызывается функция _LStrToPChar. Как мы уже говорили, когда stringхранит пустую строку, указатель просто имеет значениеnil. Функция _LStrToPCharпроверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещен в сегменте кода. Поэтому даже если содержит пустую строку, в функцию будет передан ненулевой указатель. Вычисление строковых выражений требует перераспределения памяти, а это компилятор делает только с выражениями типа string. Поэтому результат выражения, приведенного в третьем варианте, также имеет тип string. Но его можно привести к PChar. Память для хранения результата выражения выделяется динамически, как и для обычных переменных типа string. Чтобы передать указатель на но выражение в функцию, следует привести его к PChar. В эпилог процедуры, вызывающей функцию SetWindowTextили иную функцию с подобным аргументом, добавляется код, который освобождает динамически сформированную строку, поэтому утечек памяти не происходит. Разумеется, существуют и другие способы формирования параметра типа LPCTSTR, кроме предложенных здесь. Можно, например, выделить память для нуль-терминированной строки с помощью StrNewили родственной ей функции из модуля SysUtils. Можно использовать массив типа Char. Можно выделять память какими-либо другими способами. Но предложенные здесь три варианта в большинстве случаев наиболее удобны. Параметры типа LPTSTRприменяются в тех случаях, когда функция может не только читать, но и модифицировать передаваемое ей значение. В большинстве случаев такие параметры чисто выходные, т.е. функция не интересуется, какое значение имел параметр при вызове, используя его только для возврата значения. При возврате строкового значения всегда возникает проблема: где, кем и как будет выделена память, в которую будет записана строка? Функции Windows API, за очень редким исключением, решают эту проблему следующим образом: память должна выделить вызывающая программа, а в функцию передается указатель на этот заранее выделенный блок. Сама функция только копирует строку в этот блок. Таким образом, перед программой встает задача узнать, какой объем памяти следует выделить под возвращаемую строку. Здесь API не предлагает универсального решения, разные функции по-разному решают эту проблему. Например, при получении заголовка окна с помощью GetWindowTextразмер этого заголовка можно узнать, вызвав предварительно GetWindowTextLength. Функции типа GetCurrentDirectoryвозвращают длину строки. Если при первом вызове этой функции памяти выделено недостаточно, можно увеличить буфер и вызвать функцию еще раз. И наконец, есть функции типа SHGetSpecialFolderPath, в описании которых написано, каков минимальный размер буфера, необходимый для гарантированной передачи полной строки этой функцией (это, разумеется, возможно только в том случае, когда размер возвращаемой строки имеет какое-то естественное ограничение). Следует также отметить, что большинство API-функций, возвращающих строки, в качестве одного из параметров принимают размер буфера, чтобы не скопировать больше байтов, чем буфер может принять. Выделять буфер для получения строки можно многими способами. На практике удобнее всего статические массивы, тип stringили динамическое выделение памяти для нуль-терминированных строк. Статические массивы могут использоваться, если размер буфера известен на этапе компиляции. Массивы типа Charс начальным индексом 0 рассматриваются компилятором как нуль-терминированные строки, поэтому с ними удобно выполнять дальнейшие операции. Этот способ удобен тем, что не нужно заботиться о выделении и освобождении памяти, поэтому он часто применяется там, где формально длина строки на этапе неизвестна, но "исходя из здравого смысла" можно сделать вывод, что в подавляющем большинстве случаев эта длина не превысит некоторой величины, которая и берется в качестве размера массива. Строки типа stringтакже могут служить буфером для получения строковых значений от системы. Для этого нужно предварительно установить требуемую длину строки с помощью SetLength, а затем передать указатель на начало строки в функцию API. Здесь следует соблюдать осторожность: если длина строки окажется равной нулю, переменная типа stringбудет иметь значение nil, а система попытается записать по этому указателю пустую строку, состоящую из единственного символа #0. Это приведет к ошибке Access violation. Третий способ — выделение памяти для буфера с помощью StrAllocили аналогичной ей функции. Память, выделенную таким образом, следует обязательно освобождать с помощью StrDispose. При этом крайне желательно использовать конструкцию try/finally, чтобы возникновение исключений не привело к утечкам памяти. Все три способа получения строковых данных от функций Windows API показаны в примере EnumWnd, находящемся на прилагаемом компакт-диске. 1.2. Примеры использования Windows APIВ этом разделе разобраны простые примеры, находящиеся на компакт-диске. Все эти примеры уже упоминались ранее, и каждый из них иллюстрирует какую-то отдельно взятую возможность API. Более сложным обобщающим примерам, которые задействуют сразу несколько возможностей API и VCL, посвящен следующий, третий раздел данной главы. 1.2.1. Пример EnumWndПрограмма EnumWnd представляет собой простой пример использования функций EnumWindowsи EnumChildWindows, а также функций обратного вызова, которые необходимы для работы этих двух функций. Программа ищет все окна, созданные на данный момент в системе, и отображает их в виде дерева: каждый узел дерева соответствует одному окну, дочерние узлы соответствуют дочерним окнам данного окна (рис. 1.8). Программа EnumWnd является также примером того, как можно работать с параметрами типа LPTSTR, через которые функции Windows API возвращают программе строковые значения. В разд. 1.1.13 были перечислены три способа создания буфера для работы с такими параметрами: выделение памяти в стеке в виде массива элементов типа Char, использование строк типа stringи строк типа PChar. Все три способа реализованы в примере EnumWnd. На главной и единственной форме программы EnumWndразмещены два компонента: TreeWindowтипа TTreeViewи кнопка BtnBuild. Обработчик нажатия кнопки выглядит очень лаконично (листинг 1.21). Листинг 1.21. Обработчик нажатия кнопки BtnBuild procedure TFomWindows.BtnBuildClick(Sender: TObject); begin Screen.Cursor := crHourGlass; try TreeWindows.Items.Clear; EnumWindows(@EnumWindowsProc, 0); finally Screen.Cursor := crDefault; end; end; Рис. 1.8. Окно программы EnumWnd Все, что делает этот обработчик, — это очищает компонент TreeWindowsи вызывает EnumWindows, передавая ей функцию обратного вызова EnumWindowsProc, в которой и выполняется основная работа. Сразу отметим, что в этом примере мы будем использовать одну и ту же функцию обратного вызова как для EnumWindows, так и для EnumWindowsProc. Сама функция обратного вызова выглядит следующим образом (листинг 1.22). Листинг 1.22. Функция обратного вызова EnumWindowsProc(первый вариант) // Это функция обратного вызова, которая будет // использоваться при вызове EnumWindows и EnumChildWindows. // Тип второго параметра не совпадает с типом, который // указан MSDN. Однако TTreeNode, как и любой класс, // является указателем, поэтому может использоваться везде, // где требуется нетипизированный указатель - на двоичном // уровне между ними нет разницы. Указатель на функцию // обратного вызова в EnumWindows и EnumChildWindows в // модуле Windows.dcu объявлен как нетипизированный // указатель, поэтому компилятор не контролирует // соответствие реального прототипа заявленному. function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall; // Система не предусматривает возможности узнать, какова // длина имени класса, поэтому при получении этого имени // приходится выделять буфер большой длины в надежде, что // имя класса не окажется еще длиннее. В данном примере // размер этого буфера определяется константой ClassNameLen. // Крайне маловероятно, что имя класса скажется длиннее, // чем 511 символов (512-й зарезервирован для завершающего // нулевого символа). const ClassNameLen = 512; var // Здесь будет храниться заголовок окна Text: string; TextLen: Integer; // Это - буфер для имени класса ClassName: array[0..ClassNameLen - 1] of Char; Node: TTreeNode; NodeName: string; begin Result := True; // Функция EnumChildWindows перечисляет не только // непосредственно дочерние окна данного окна, но и // дочерние окна его дочерних окон и т.п. Но при // построении дерева на каждом шаге нам нужны только // прямые потомки, поэтому все окна, не являющиеся прямыми // потомками, мы здесь игнорируем. if Assigned(ParentNode) and (GetParent(Wnd) <> HWND(ParentNode.Data)) then Exit; // Получаем длину заголовка окна. Вместо функций // GetWindowText и GetWindowTextLength мы здесь // используем сообщения WM_GETTEXT и WM_GETTEXTLENGTH, // потому что функции, в отличие от сообщений, не // умеют работать с элементами управления, // принадлежащими окнам чужих процессов. TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0); // Устанавливаем длину строковой переменной, которая // будет служить буфером для заголовка окна. // Использование SetLength гарантирует, что будет // выделена специальная область памяти, на которую не // будет других ссылок. SetLength(Text, TextLen); // Если заголовок окна - пустая строка, TextLen будет // иметь значение 0, и указатель Text при выполнении // Set Length получит значение nil. Но при обработке // сообщения WM_GETTEXT оконная процедура в любом случае // попытается записать строку по переданному адресу, // даже если заголовок окна пустой - в этом случае в // переданный буфер будет записан один символ - // завершающий ноль. Но если будет передан nil, то // попытка записать что-то в такой буфер приведет к // Access violation, поэтому отправлять окну WM_GETTEXT // можно только в том случае, если TextLen > 0. if TextLen > 0 then SendMessage(Wnd, WM_GETTEXT, TextLen + 1, LParam (Text)); // Заголовок окна может быть очень длинным - например, в // Memo заголовком считается весь текст, который там // есть. Практика показывает, что существуют проблемы // при добавлении в TTreeView узлов с очень длинным // названиями: при попытке открыть такой узел программа, // запущенная из Delphi, вылетает в отладчик (при // запуске вне среды Delphi проблем не замечено). Чтобы // этого не происходило, слишком длинные строки // обрезаются. if TextLen > 100 then Text := Copy(Text, 1, 100) + '...'; GetClassName(Wnd, ClassName, ClassNameLen); ClassName[ClassNameLen - 1] := #0; if Text = '' then NodeName := 'Без названия (' + ClassName + ') ' else NodeName := Text + ' (' + ClassName + ')'; Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName); // Записываем в данные узла дескриптор соответствующего // ему окна, чтобы иметь возможность отбросить непрямые // потомки. Node.Data := Pointer(Wnd); // Вызываем EnumChildWindows, передавая функцию // EnumWindowsProc в качестве параметра, а указатель на // созданный узел - в качестве параметра этой функции. // При этом EnumWindowsProc будет вызываться из // EnumChildWindows, т.е. получается рекурсия. EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Mode)); end; Как мы помним, первый параметр функции обратного вызова для EnumWindowsсодержит дескриптор найденного окна, а второй параметр может быть произвольным 4-байтным значением, которое система игнорирует, просто копируя сюда то значение, которое было передано при вызове EnumWindowsили EnumChildWindows. Мы задействуем этот параметр для передачи ссылки на узел дерева, соответствующий родительскому окну. Также договоримся, что в свойство Data каждого узла будем записывать дескриптор связанного с ним окна. Для окон верхнего уровня ссылка будет иметь значение nil— это обеспечивается тем, что при вызове EnumWindows второй параметр равен нулю (см. листинг 1.21). Работа функции начинается с проверки того, что родительским окном для данного окна действительно является то окно, чей дескриптор связан с узлом родительского окна. Эта проверка нужна потому, что функция EnumChildWindowsперечисляет не только дочерние, но и "внучатые", "правнучатые" и т.д. окна. Нам здесь это не нужно, на каждом шаге нас интересуют только непосредственные "дети" окна, а до "внуков" мы доберемся, когда вызовем EnumChildWindowsдля дочерних окон, поэтому и отсеиваем лишнее. Следующий шаг — получение заготовка окна. Для этого мы используем сообщение WM_GETTEXT(разница между этим сообщением и функцией GetWindowTextобсуждается в разд. 1.3.1). Буфером является переменная Textтипа string. Сначала с помощью сообщения WM_GETTEXTLENGTHмы узнаем длину заголовка окна, а затем выделяем под строку Textтребуемое количество памяти с помощью SetLength. После этого можно получить строку с помощью сообщения WM_GETTEXT. Второй параметр этого сообщения — адрес буфера, в который будет помещена строка. Так как переменная типа stringи есть указатель на буфер строки (это детально обсуждается в разд. 3.3), достаточно просто привести переменную Textк типу LParamи передать получившееся значение. Примечание Далее получаем название класса окна. Для этого мы используем статический массив ClassName, т.е. размер буфера определяется на этапе компиляции. С одной стороны, это неправильно, потому что ограничений на длину имени класса не существует (по крайней мере, в документации они не упомянуты), а мы уже говорили, что такой метод следует применять только тогда, когда существует известное на этапе компиляции ограничение длины. По с другой стороны, когда речь идет об имени класса, не существует ничего подобного сообщению WM_SETTEXTLENGTH, т.е. API не дает возможности получить длину имени класса, что делает бессмысленными все манипуляции с размером буфера во время работы программы. Поэтому мы определяем размер буфера еще на этапе компиляции, исходя из того, что слишком уж длинные имена классов встречаются редко. При вызове функции с параметром типа LPTSTRможно просто передавать массив без приведения типа, т.к. LPTSTR— это PChar, а массивы символов Char, индексирующиеся с нуля, компилятор полагает совместимыми с этим типом и все необходимые преобразования делает неявно. И, хотя мы и взяли размер буфера с хорошим запасом, нельзя исключать ситуации, когда имя класса окажется длиннее, чем буфер. Ничего страшного при этом не произойдет, т.к. мы передаем в функцию размер буфера специально для того, чтобы она не пыталась что-то записать за пределами буфера. Но в этом случае завершающий строку символ #0не попадет в буфер, и при попытке дальше работать с этой строкой какая-нибудь другая функция может, не найдя конца строки в пределах буфера, попытаться поискать этот конец за его пределами, что приведет к непредсказуемым результатам. Поэтому на всякий случай записываем #0в последний символ буфера. Если имя класса оказалось длиннее буфера, это обрежет строку по границе буфера, а если короче, то это ничему не повредит, т.к. признак конца строки будет в буфере где-то раньше, а все символы после него все равно игнорируются. После этого остается только создать новый элемент в дереве, а чтобы заполнить его дочерние элементы — вызвать EnumChildWindowsдля получения списка дочерних окон. Так как в EnumChildWindowsпередается та же функция обратного вызова, получается рекурсия, которая останавливается тогда, когда функция доходит до окна, не имеющего дочерних окон. Ранее мы говорили, что программа EnumWndдемонстрирует три метода получения строки через параметр типа LPTSTR, но пока мы увидели только два (действительно, трудно показать три различных метода на примере получения двух строк). Чтобы показать третий вариант — организацию буфера через строки типа PChar— перепишем функцию EnumWindowsProc(листинг 1.23). В исходном коде программы EnumWndэтот вариант присутствует в виде комментария. Можно убрать этот комментарий, а закомментировать, наоборот, первый вариант, чтобы попробовать, как работает получение строки с помощью PChar. Листинг 1.23. Функция обратного вызова EnumWindowsProc(второй вариант) // Ниже приведен другой вариант функции // EnumWindowsРrос, который отличается от предыдущего тем, // что буфер для получения заголовка окна организуется // вручную с помощью переменной типа PChar, а не string. По // своим функциональным возможностям оба варианта равноценны. function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall; const ClassNameLen = 512; var TextLen: Integer; Text: PChar; ClassName: array[0..ClassNameLen - 1] of Char; Node: TTreeNode; NodeName: string; begin Result := True; if Assigned(ParentNode) and (GetParent(Wnd) <> HWND(ParentNode.Data)) then Exit; // Здесь, в отличие от предыдущего варианта к длине, // получаемой через WM_GETTEXTLENGTH, добавляется // единица, потому что нужно вручную учесть добавочный // байт для завершающего нуля. TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0) + 1; // Выделяем требуемое количество памяти. Так как // компилятор не освободит эту памяти автоматически, // необходимо использовать блок try/finally, иначе будут // утечки памяти при исключениях. Text := StrAlloc(TextLen); try // Так как для буфера даже при пустом заголовке будет // выделен хотя бы один байт, здесь можно отправлять // WM_GETTEXT, не проверяя длину строки, как это было // в предыдущем варианте - буфер всегда будет // корректным. SendMessage(Wnd, WM_GETTEXT, TextLen, LParam(Text)); // Обрезаем слишком длинною строку. Модифицировать // PChar сложнее, чем string. Вставка нуля в середину // строки приводит к тому, что все API-функции будут // игнорировать "хвост", но на работу StrDispose это не // повлияет, т.к. функция StrAlloc (а также прочие // функции выделения памяти для нуль-терминированных // строк модуля SysUtils) сохраняет размер выделенной // памяти рядом с самой строкой, и StrDispose // ориентируется именно на этот размер, а не на // завершающий ноль. if TextLen > 104 then begin (Text + 104)^ := #0; (Text + 103)^ := '.'; (Text + 102)^ := '.'; (Text + 101)^ := '.'; (Text + 100)^ := ' '; end; GetClassName(Wnd, ClassName, ClassNameLen); if Text^ = #0 then NodeName := 'Без названия (' + ClassName + ') ' else NodeName := Text + ' (' + ClassName + '); Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName); Node.Data := Pointer(Wnd); EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node)); finally // Вручную освобождаем память, выделенную для буфера StrDispose(Text); end; end; Второй вариант функции EnumWindowsProcотличается от первого только тем что для организации буфера для получения имени окна вместо переменной типа stringиспользуется переменная типа PChar. Соответственно, все манипуляции с динамической памятью теперь выполняются вручную, а просто отсечь конец слишком длинной строки и прибавить к результату другую строку (многоточие) мы не можем, приходится модифицировать строку посимвольно. Тем не менее видно, что и с помощью типа PCharзадача создания буфера для строки, возвращаемой API-функцией, достаточно легко решается. 1.2.2. Пример LineПример Line представляет собой невизуальный компонент TLine, который перехватывает оконные сообщения своего владельца (владельца в терминах VCL, разумеется, раз речь идет о неоконном компоненте). Компонент TLineрисует на своем владельце линию из точки ( StartX, StartY) в точку ( EndX, EndY) цветом Color. Пользователь может перемещать концы линии мышью. Достаточно разместить компонент TLineна форме, и на ней появится линия, которую пользователь может перемещать как во время проектирования формы, так и во время выполнения программы. Можно также разместить на форме, например, панель, и сделать ее владельцем компонента TLine— тогда линия будет рисоваться на панели. Но это можно сделать только во время исполнения программы, потому что владельцем всех компонентов, созданных во время проектирования формы, становится сама форма. Чтобы установить компонент, нужно выполнить следующие действия: 1. Переписать с компакт-диска файлы Line.pas и Line.dcr в папку, где вы храните компоненты. Если такой папки еще нет, самое время создать ее. Где именно она будет расположена, значения не имеет, выбирайте любое удобное для вас место. Главное — это прописать эту папку в путях, где Delphi ищет компоненты. Чтобы сделать это в Delphi 7 и более ранних версиях, откройте меню Tools\Environment Options, в появившемся диалоговом окне выберите закладку Library и добавьте свою папку в поле Library path. В BDS 2006 и выше откройте меню Tools\Options, в появившемся диалоговом окне в дереве в левой части выберите пункт Environment Options\Delphi Options\Library — Win32 и добавьте папку в поле Library path. 2. Создайте новый пакет (меню File\New\Other, в открывшемся окне выбрать Package). После этого в Delphi 7 и более ранних версиях откроется небольшое окно пакета. В BDS 2006 и более поздних версиях окно не откроется, но пакет появится в группе проектов (по умолчанию это окно Project Manager в правом верхнем углу главного окна). Сохраните пакет в ту же папку, где находится Line.pas, под любым именем, кроме Line (иначе будет конфликт имен). 3. Добавьте в пакет файл Line.pas. В BDS 2006 для этого необходимо с помощью правой кнопки мыши вызвать контекстное меню пакета в окне Project Manager и выбрать там пункт Add. В Delphi 7 и более ранних версиях в окне пакета нужно нажать кнопку Add. 4. Установите компонент. В BDS 2006 и выше для этого следует выбрать пункт Install в контекстном меню проекта, а в Delphi 7 и более ранних версиях — нажать кнопку Install в окне пакета. После этого в палитре компонентов у вас появится вкладка Delphi Kingdom Samples, a в ней — компонент TLine. Если вы не хотите помещать компонент TLineв палитру компонентов (или у вас Turbo Delphi Explorer, и вы просто не имеете такой возможности), можно воспользоваться проектом LineSample, который во время выполнения создаёт два экземпляра TLine, владельцем одного из которых является форма, другого — панель. Перехват сообщения владельца осуществляется путем изменения его свойства WindowProc— записи в него указателя на свой обработчик сообщений. Здесь можно применить один хитрый прием. Компонент TLineне имеет своей оконной процедуры, т.к., будучи прямым наследником класса TComponent, окном не является. Но метод Dispatchу него есть, поскольку он объявлен в классе TObject. В классе TComponentи в его предках метод Dispatchникогда не вызывается. Если мы напишем обработчик сообщений таким образом, что он будет передавать сообщения методу Dispatch, то сможем в нашем компоненте создавать свои методы для обработки сообщений, в которые метод Dispatchпри необходимости будет передавать сообщения для обработки. Необработанные сообщения при этом будут передаваться в метод DefaultHandler, который у класса TComponentничего не делает. Если мы перекроем DefaultHandlerтак, чтобы он вызывал оригинальный обработчик сообщений родителя, то все необработанные сообщения пойдут туда. Более того, вызов inheritedиз методов-обработчиков сообщений тоже будет приводить к вызову оригинального обработчика родителя, т.к. в данном случае inheritedпри отсутствии унаследованного обработчика приводит к вызову DefaultHandler. В листинге 1.24 показано объявление класса TLineи код его методов, относящихся к перехвату сообщений. Листинг 1.24. Базовая часть класса TLine type TLine = class(TComponent) private // FCoords хранит координаты линии. Начало линии // находится в точке (FCoords[0], FCoords[1]), // конец - в (FCoords[2], FCoords[3]). FCoords:array[0..3] of Integer; // Конструктор класса написан так, что владельцем TLine // может стать только TWinControl или его наследник. // Но свойство Owner имеет тип TComponent, поэтому при // использовании свойств и методов класса TWinControl // Owner придется каждый раз приводить к типу // TWinControl. Чтобы избежать приведений типа, // используется поле FWinOwner. Оно указывает на тот же // объект, что и Owner, но имеет тип TWinControl. FWinOwner: TWinControl; // Здесь хранится адрес обработчика сообщений, бывший до // перехвата. FOldProc: TWndMethod; // Цвет линии FColor: TColor; // Состояние линии. Если FStartMoving = True, в данный // момент пользователь перемещает начало линии, если // FEndMoving = True - ее конец. FStartMoving, FEndMoving: Boolean; // Если FDrawLine = False, линия не рисуется. Это // используется, когда нужно стереть линию. FDrawLine: Boolean; procedure WMPaint(var Msg: TWMPaint); message WM_PAINT; procedure WMLButtonDown(var Msg: TWMLButtonDown); message WM_LBUTTONDOWN; procedure WMLButtonUp(var Msg: TWMButtonUp); message WM_LBUTTONUP; procedure WMMouseMove(var Msg: TWMMouseMove); message WM_MOUSEMOVE; procedure SetColor(Value: TColor); procedure SetCoord(Index, Value: Integer); protected // Этот метод будет новым обработчиком сообщений // владельца procedure HookOwnerMessage(var Msg: Message); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure DefaultHandler(var Msg); override; published property Color: TColor read FColor write SetColor default clWindowText; property StartX: Integer index 0 read FCoords[0] write SetCoord default 10; property StartY: Integer index 1 read FCoords[1] write SetCoord default 10; property EndX: Integer index 2 reed FCoords[2] write SetCoord default 50; property EndY: Integer index 3 read FCoords[3] write SetCoord default 50; end; ... constructor TLine.Create(AOwner: TComponent); begin if not Assigned(AOwner) then raise EWrongOwner.Create( 'Должен быть назначен владелец компонента TLine'); if not (AOwner is TWinControl) then raise EWrongOwner.Create( 'Владелец компонента TLine должен быть наследником TWinControl'); FWinOwner := AOwner as TWinControl; inherited; FCoords[0] := 10; FCoords[1] := 10; FCoords[2] := 50; FCoords[3] := 50; FColor := clWindowText; FStartMoving := False; FEndMoving := False; FDrawLine := True; // Запоминаем старый обработчик сообщений владельца и // назначаем новый. FOldProc := FWinOwner.WindowProc; FWinOwner.WindowProc := HookOwnerMessage; FWinOwner.Refresh; end; destructor TLine.Destroy; begin // Восстанавливаем старый обработчик сообщений владельца. FWinOwner.WindowProc := FOldProc; FWinOwner.Refresh; inherited; end; procedure TLine.HookOwnerMessage(var Msg: TMessage); begin // Единственное, что делает перехватчик сообщений - // передает их методу Dispatch. Было бы оптимальнее // назначить обработчиком сообщений сам метод Dispatch, // но формально он имеет прототип, несовместимый с // типом TWndMethod, поэтому компилятор не разрешает // подобное присваивание. Фактически же Dispatch // совместим с TWndMethod, поэтому, используя хакерские // методы, можно было бы назначить обработчиком его и // обойтись без метода HookOwnerMessage. Но хакерские // методы - вещь небезопасная, они допустимы только // тогда, когда других средств решения задачи нет. Dispatch(Msg); end; procedure TLine.DefaultHandler(var Msg); begin FOldProc(TMessage(Msg)); end; Собственно рисование линии на поверхности владельца обеспечивает метод WMPaint(листинг 1.25). Листинг 1.25. Метод WMPaint procedure TLine.WMPaint(var Msg: TWMPaint); var NeedDC: Boolean; PS: TPaintStruct; Pen: HPEN; begin if FDrawLine then begin // Проверка, был ли DC получен предыдущим обработчиком NeedDC := Msg.DC = 0; if NeedDC then Msg.DC := BeginPaint(FWinOwner.Handle, PS); inherited; Pen := CreatePen(PS_SOLID, 1, ColorToRGB(FColor)); SelectObject(Msg.DC, Pen); MoveToEx(Msg.DC, FCoords[0], FCoords[1], nil); LineTo(Msg.DC, FCoords[2], FCoords[3]); SelectObject(Msg.DC, GetStockObject(BLACK_PEN)); DeleteObject(Pen); if NeedDC then EndPaint(FWinOwner.Handle, PS); end else inherited; end; Поскольку рисуется простая линия, мы не будем здесь создавать экземпляр TCanvasи привязывать его к контексту устройства, обойдемся вызовом функций GDI. Особенности работы с контекстом устройства при перехвате сообщения WM_PAINTописаны в разд. 1.2.4. Чтобы пользователь мог перемещать концы линии, нужно перехватывать и обрабатывать сообщения, связанные с перемещением мыши и нажатием и отпусканием ее левой кнопки (листинг 1.26). Листинг 1.26. Обработка сообщений мышиprocedure TLine.WMLButtonDown(var Msg: TWMLButtonDown); var DC: HDC; OldMode: Integer; begin if PTInRect(Rect(FCoords[0] - 3, FCoords[1] - 3, FCoords[0] + 4, FCoords[1] + 4), Point(Msg.XPos, Msg.YPos)) then begin FStartMoving := True; FDrawLine := False; FWinOwner.Refresh; FDrawLine := True; DC := GetDC(FWinOwner.Handle); OldMode := SetROP2(DC, R2_NOT); SelectObject(DC, GetStockObject(BLACK_PEN)); MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3]); SetROP2(DC, OldMode); ReleaseDC(FWinOwner.Handle, DC); SetCapture(FWinOwner.Handle); Msg.Result := 0; end else if PTInRect(Rect(FCoords[2] - 3, FCoords[3] - 3, FCoords[2] + 4, FCoords[3] + 4), Point(Msg.XPos, Msg.YPos)) then begin FEndMoving := True; FDrawLine := False; FWinOwner.Refresh; FDrawLine := True; DC := GetDC(FWinOwner.Handle); OldMode := SetROP2(DC, R2_NOT); SelectObject(DC, GetStockObject(BLACK_PEN)); MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3]); SetROP2(DC, OldMode); ReleaseDC(FWinOwner.Handle, DC); SetCapture(FWinOwner.Handle); Msg.Result := 0; end else inherited; end; procedure TLine.WMLButtonUp(var Msg: TWMLButtonUp); begin if FStartMoving then begin FStartMoving := False; ReleaseCapture; FWinOwner.Refresh; Msg.Result := 0; end else if FEndMoving then begin FEndMoving := False; ReleaseCapture; FWinOwner.Refresh; Msg.Result := 0; end else inherited; end; procedure TLine.WMMouseMove(var Мsg: TWMMouseMove); var DC: HDC; OldMode: Integer; begin if FStartMoving then begin DC := GetDC(FWinOwner.Handle); OldMode := SetROP2(DC, R2_NOT); SelectObject(DC, GetStockObject(BLACK_PEN)); MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3]); FCoords[0] := Msg.XPos; FCoords[1] := Msg.YPos; MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3])); SetROP2(DC, OldMode); ReleaseDC(FWinOwner.Handle, DC); Msg.Result := 0; end else if FEndMoving then begin DC := GetDC(FWinOwner.Handle); OldMode := SetROP2(DC, R2_NOT); SelectObject(DC, GetStockObject(BLACK_PEN)); MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3]); FCoords[2] := Msg.XPos; FCoords[3] := Msg.YPos; MoveToEx(DC, FCoords[0], FCoords[1], nil); LineTo(DC, FCoords[2], FCoords[3]); SetROP2(DC, OldMode); ReleaseDC(FWinOwner.Handle, DC); Msg.Result := 0; end else inherited; end; Здесь реализован инверсный способ создания "резиновой" линии, когда при рисовании линии все составляющие ее пикселы инвертируются, а при стирании инвертируются еще раз. Этот способ подробно описан в разд. 1.3.4.2. Перехват сообщений родителя — дело относительно простое, гораздо хуже обстоят дела с удалением компонента, перехватившего сообщения родителя. Пока такой компонент один, проблем не возникает, но когда их несколько приходится обращаться с ними очень аккуратно. Рассмотрим, например, такой код (листинг 1.27). Листинг 1.27. Пример кода, вызывающего ошибкуLine1 := TLine.Create(Form1); Line2 := TLine.Create(Form2); ... Line1.Free; ... Line2.Free; Проанализируем, что происходит при выполнении этого кода. Для простоты предположим, что других компонентов, перехватывающих сообщения, здесь нет, и перед выполнением этого кода Form1.WindowProcссылается на Form1.WndProc, т.е. на собственный обработчик сообщений формы. При создании объекта Line1он перехватывает обработчик, и Form1.WindowProcначинает ссылаться на Line1.HookOwnerMessage, а ссылка на Form1.WndProcсохраняется в Line1.FOldProc. Объект Line2также перехватывает обработчик сообщений, и после его создания Form1.WindowProcбудет ссылаться на Line2.HookOwnerMessage, a Line2.FOldProc— на Line1.HookOwnerMessage. Теперь удалим Line1. При удалении объект восстановит ссылку на тот обработчик сообщений, который был установлен на момент его создания, т.е. Form1.WindowProcвновь станет указывать на Form1.WndProc. Соответственно, компонент Line2потеряет способность реагировать на сообщения владельца. Поле Line2.FOldProcпри этом останется без изменений. Но самое неприятное начнется при удалении объекта Line2. Он тоже восстановит ссылку на обработчик, который был назначен на момент его создания, т.е. запишет в свойство Form1.WindowProcссылку на Line1.HookOwnerMessage. Но поскольку объекта Line1уже не существует, это будет ссылка в никуда, и обработка первого же сообщения, пришедшего форме, даст ошибку Access violation. Примечание Проблема не возникает, если удалять объекты в порядке, обратном порядку их создания. Но в общем случае это не может быть решением проблемы, т.к. объекты должны создаваться и удаляться в том порядке, который требуется логикой работы программы. Соответственно, единственное решение — все перехватывающие сообщения владельца компоненты должны знать друг о друге и уведомлять друг друга о своем создании и удалении. Но и этот способ не дает полной гарантии. Пока один разработчик пишет компонент или библиотеку компонентов, он может обеспечить взаимодействие всех экземпляров компонентов в программе. Но если в одной программе будут использованы две такие библиотеки от разных разработчиков, они так же будут конфликтовать друг с другом, и универсального решения проблемы, судя по всему, не существует. Пользователю библиотек остается только соблюдать порядок удаления компонентов. Но, с другой стороны, есть ряд задач, в которых без перехвата сообщений владельца не обойтись, поэтому иногда приходится идти на это. 1.2.3. Пример CoordLabelCoordLabel — это пример визуального компонента, перехватывающего сообщения своего родителя. Компонент TCoordLabelотслеживает нажатие левой кнопки мыши на своем родителе и отображает координаты точки, в которой произошло нажатие. Для перехвата сообщений родителя используется тот же способ через свойство WindowProc, что и в предыдущем примере, но т.к. теперь перехватываются сообщения родителя, а не владельца, появляются некоторые нюансы. Установка компонента TCoordLabelполностью аналогична установке компонента TLineиз предыдущего раздела. На прилагаемом компакт-диске находится также проект LineCoordSample для того, чтобы работу компонента можно было увидеть без установки в палитру компонентов. На форме проекта LineCoordSample находится панель, кнопка Переместить и компонент TLineCoordSample, который по нажатию кнопки меняет родителя с формы на панель и обратно. Код компонента TCoordLabelприведен в листинге 1.28. Листинг 1.28. Компонент TCoordLabel type TCoordLabel = class(TLabel) private // Здесь хранится адрес обработчика // сообщений, бывший до перехвата. FOldProc: TWndMethod; protected procedure SetParent(AParent: TWinControl); override; // Этот метод будет новым обработчиком // сообщений владельца procedure HookParentMessage(var Msg: TMessage); end; ... procedure TCoordLabel.SetParent(AParent: TWinControl); begin if Assigned(Parent) and Assigned(FOldProc) then Parent.WindowProc := FOldProc; inherited; if Assigned(Parent) then begin FOldProc := Parent.WindowProc; Parent.WindowProc := HookParentMessage; end; end; procedure TCoordLabel.HookParentMessage(var Msg: TMessage); begin if Msg.Msg = WM_LBUTTONDOWN then Caption := '(' + IntToStr(Msg.LParamLo) + ', ' + IntToStr(Msg.LParamHi) + ')'; FOldProc(Msg); end; Класс TLabel, предок TCoordLabel, является визуальным компонентом и сам может получать и обрабатывать сообщения, поэтому метод Dispatchу него уже "занят". Соответственно, мы не можем диспетчеризовать с его помощью перехваченные сообщения и должны обрабатывать их внутри метода HookParentMessage. Сам перехват осуществляется не в конструкторе, т.к. на момент вызова конструктора родитель компонента еще неизвестен. Он устанавливается позже, через свойство Parent, которое приводит к вызову виртуального метода SetParent. Мы перекрываем этот метод и выполняем в нем как восстановление обработчика старого родителя, так и перехват сообщений нового. Это позволяет компоненту менять родителя во время работы программы. Писать отдельно деструктор для восстановления оригинального обработчика родителя в данном случае нужды нет, поскольку деструктор, унаследованный от TControl, содержит вызов метода SetParentс параметром nil. Так как мы уже перекрыли SetParent, это приведет к восстановлению оригинального обработчика, т.е. к тому, что нам нужно. Если на форму, содержащую TCoordLabel, поместить другие компоненты можно заметить, что TCoordLabelотлавливает нажатия мыши, сделанные на неоконных компонентах, но игнорирует те, которые сделаны на оконных. Это происходит потому, что неоконные компоненты получают сообщения через оконную процедуру родителя (которая перехвачена), а оконные имеют свою оконную процедуру, никак не связанную с оконной процедурой родителя. И, разумеется, компонент TCoordLabelимеет те же проблемы с восстановлением оригинального обработчика, что и TLine, если на одном родителе расположены несколько компонентов. Соответственно, применять TCoordLabelнеобходимо аккуратно, с учетом возможных последствий. 1.2.4. Пример PanelMsgПрограмма PanelMsg показывает, как можно перехватить оконные сообщения, поступающие компоненту, лежащему на форме. В данном случае этим компонентом будет TPanel. Для перехвата сообщений используется свойство WindowProcпанели. Мы будем обрабатывать два сообщения, приходящих с панели: WM_RBUTTONDBLCLKи WM_PAINT. Таким образом, наша панель получит возможность реагировать на двойной щелчок правой кнопки мыши, а также рисовать что-то на своей поверхности. С помощью одной только библиотеки VCL это сделать нельзя. Примечание При перехвате сообщения WM_PAINTлюбого компонента, на котором расположены неоконные визуальные компоненты, может возникнуть проблема с перерисовкой этих компонентов. Чтобы продемонстрировать способ решения этих проблем, разместим на панели компонент TLabel, который заодно будет показывать пользователю реакцию на двойной щелчок правой кнопкой мыши. В результате получается окно, показанное на рис. 1.9. При двойном щелчке правой кнопкой мыши на панели надпись Сделайте двойной щелчок правой кнопкой перемещается в то место, где находится курсор. Чтобы перехватить оконную процедуру панели, следует написать метод, который ее подменит, а адрес старого метода сохранить в предназначенном для этого поле. Сам перехват будем осуществлять в обработчике события OnCreateформы (листинг 1.29). Рис. 1.9.Окно программы PanelMsg Листинг 1.29. Перехват обработчика сообщений панели type TForm1 = class(TForm) Panel: TPanel; Label1: TLabel; procedure FormCreate(Sender: TObject); private // Здесь будет храниться исходный обработчик сообщений // панели FOldPanelWndProc: TWndMethod; // Этот метод будет перехватывать сообщения, // предназначенные панели procedure NewPanelWndProc(var Msg: TMessage); end; ... procedure TForm1.FontCreate(Sender: TObject); begin FOldPanelWndProc := Panel.WindowProc; Panel.WindowProc := NewPanelWndProc; end; Сам перехватчик выглядит так, как показано в листинге 1.30. Листинг 1.30. Метод-перехватчик сообщений панелиprocedure TForm1.NewPanelWndProc(var Msg: TMessage); var NeedDC: Boolean; PS: TPaintStruct; PanelCanvas: TCanvas; begin if Msg.Msg = WM_RBUTTONDBLCLK then begin Label1.Left := Msg.LParamLo; Label1.Top := Msg.LParamHi; Msg.Result := 0; end else if Msg.Msg = WM_PAINT then begin // Проверяем, был ли запрошен контекст устройства // обработчиком, стоящим раньше по цепочке, и если не // был, то запрашиваем его. NeedDC := Msg.WParam = 0; if NeedDC then Msg.WParam := BeginPaint(Panel.Handle, PS); // Вызываем старый обработчик WM_PAINT. Его нужно // вызывать обязательно до того, как мы начнем рисовать // на поверхности что-то свое, т.к. в противном случае // это что-то будет закрашено стандартным обработчиком. POldPanelWndProc(Msg); // При использовании графических функций API самое // неудобное - это вручную создавать и уничтожать кисти, // карандаш и т.п. Поэтому здесь создается экземпляр // класса TCanvas для рисования на контексте устройства // с дескриптором, полученным при вызове BeginPaint. PanelCanvas := TCanvas.Create; try PanelCanvas.Handle := Msg.WParam; FanelCanvas.Pen.Style := psClear; PanelCanvas.Brush.Style := bsSolid; PanelCanvas.Brush.Color := clWhite; PanelCanvas.Ellipse(10, 10, Panel.Width - 10, Panel.Height - 10); PanelCanvas.Brush.Color := clYellow; PanelCanvas.Rectangle(100, 100, Panel.Width - 100, Panel.Height - 100); finally PanelCanvas.Free; end; // В данном случае панель содержит визуальный неоконный // компонент TLabel. Отрисовка неоконных компонентов // происходит при обработке WM_PAINT родительского // компонента, т.е. здесь она была выполнена при вызове // стандартного обработчика. Таким образом, сделанный // рисунок закрасил не только фон панели, но и // неоконные компоненты. Чтобы компоненты были поверх // рисунка, их приходится перерисовывать еще раз, // вызывая protected-метод PaintControls. Это не очень // эффективно, т.к. получается, что компоненты рисуются // дважды: в стандартном обработчике и здесь. Но // другого способа решить проблему, видимо, нет. Если // бы на панели лежали только оконные компоненты, // вызывать PaintControls не понадобилось, поскольку то, что // мы рисуем на панели, не может затереть поверхность // лежащих на этой панели других окон. TFakePanel(Panel).PaintControls(Msg.WParam, nil); // Если мы получали контекст устройства, мы же должны // освободить его. if NeedDC then begin EndPaint(Panel.Handle, PS); Msg.WParam := 0; end; end else FOldPanelWndProc(Msg); end; Так как в наш обработчик поступают все сообщения, передающиеся в оконную процедуру панели, начинается он с проверки того, какое сообщение пришло. Сначала реализуем реакцию на WM_RBUTTONDBLCLKпросто перемещаем метку Label1на то место, где пользователь щелкнул мышью. Затем обнуляем результат, давая понять системе, что сообщение полностью обработано. Вызов унаследованного обработчика в данном случае не выполняем, потому что никакая унаследованная реакция на данное событие нам не нужна. Обработка сообщения WM_PAINTсложнее. Сначала необходимо разобраться с контекстом устройства, на котором будет производиться рисование. Вообще говоря, обработчик WM_PAINTдолжен получать этот контекст с помощью функции BeginPaint. Но если до написанного нами кода сообщение WM_PAINTуже начало обрабатываться, то контекст устройства уже получен, а вызывать BeginPaintдва раза нельзя. В этом случае контекст устройства передаётся через параметр сообщения WParam. Соответственно, обработка сообщения WM_PAINTначинается с того, что мы проверяем, равен ли нулю параметр wParam, и если равен, то получаем контекст устройства, а если не равен, используем то, что передано. Унаследованный обработчик закрашивает всю панель целиком, поэтому его нужно вызывать до того, как мы нарисуем что-то свое, иначе он просто закрасит то, что мы нарисовали. Так что следующий шаг — это вызов стандартного обработчика сообщений панели, указатель на который мы сохранили в поле FOldPanelWndProc. Только после этого можно что-то рисовать. Примечание Теперь можно нарисовать что-то свое. Здесь мы рисуем большой белый круг, а на его фоне — желтый прямоугольник. Для этого используем класс TCanvasспособом, который был продемонстрирован в листинге 1.17 (см. разд. 1.1.11). Если бы мы остановились на этом, то увидели бы интересную картину: нарисованные фигуры лежат поверх текста метки Label1. Объяснение этому очень простое: метка является неоконным визуальным компонентом и рисуется на поверхности своего родительского компонента при обработке его сообщения WM_PAINT. А поскольку стандартный обработчик у нас вызывается до того, как рисуются круг и прямоугольник, любой неоконный компонент будет перекрыт ими. К оконным компонентам это, разумеется, не относится, они лежат над родительской панелью, и то, что мы рисуем на этой панели, не может оказаться над ними. Мы не можем вставить свой код между рисованием непосредственно поверхности панели и рисованием компонентов на ней. Поэтому после отработки нашего кода приходится рисовать неоконные компоненты еще раз. Проще всего это сделать, вызвав метод PaintControls, который и используется стандартным обработчиком. Конечно, получится, что неоконные компоненты рисуются дважды: в стандартном обработчике и в нашем, и это не очень хорошо. Но повторим еще раз, что программа PanelMsg— не образец для подражания, а что-то вроде зонда для исследования особенностей работы VCL. Вызов метода PaintControlsзатруднен тем, что он объявлен в разделе protected, а потому не может быть вызван из метода NewPanelWndProc, который относится к классу формы. Чтобы обойти это ограничение, нужно породить наследника от TPanel— TFakePanel. Этот наследник ничего не добавляет к классу TPanelи ничего не переопределяет в нем. Но раз он объявлен в нашем модуле, все его protected-члены, в том числе и унаследованный метод PaintControls, становятся доступными в нем. После этого мы можем привести поле, содержащее ссылку на панель, к этому типу и вызвать PaintControls. Так как структуры типов TPanelи TFakePanelидентичны, это приведет к вызову нужного метода. Для завершения обработки сообщения WM_PAINTосталось только вызвать EndPaint, разумеется, только в том случае, если BeginPaintвызывали мы сами. И последнее, что мы должны сделать, — это передать все остальные сообщения стандартному обработчику. После этого программа PanelMsg готова. 1.2.5. Пример NumBroadcastПрограмма NumBroadcast демонстрирует широковещательную рассылку глобальных сообщений. Окно программы показано на рис. 1.10. Рис 1.10. Окно программы NumBroadcast Для того чтобы увидеть, как работает программа, нужно запустить несколько ее экземпляров. После ввода числа и нажатия кнопки Разослать любому из экземпляров программы число под кнопкой меняется во всех экземплярах. Чтобы добиться такого эффекта, программа NumBroadcast регистрирует глобальное сообщение с помощью функции RegisterWindowMessage, а в оконной процедуре предусмотрена реакция на это сообщение (число передастся через параметр WParam). Код программы приведен в листинге 1.31. Листинг 1.31. Модуль главного окна программы NumBroadcast unit NBMain; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) EditNumber: TEdit; BtnBroadcast: TButton; LabelNumber: TLabel; procedure BtnBroadcastClick(Sender: TObject); private // Здесь будет храниться номер, присвоенный системой // глобальному сообщению FSendNumberMessage: Cardinal; protected // Так как номер сообщения станет известным только при // выполнении программы, объявить обработчик сообщения // с помощью директивы message нельзя. Приходится // перекрывать метод WndProc и обрабатывать сообщение в // нем. Можно было бы вместо WndProc перекрыть метод // DefaultHandler, но при этом зарегистрированное // сообщение обрабатывалось бы медленнее, потому что // сначала выполнялся бы метод WndProc, затем Dispatch // пытался бы найти подходящий обработчик среди методов // объекта, и лишь затем дело доходило бы до перекрытого // DefaultHandler. Но, с другой стороны, при перекрытии // WndProc обработка всех сообщений начинается со // сравнения их номера с FSendNumberMessage и вызова // унаследованного WndProc, если это другое сообщение. // А до DefaultHandler многие сообщения не дойдут, т.к. // будут обработаны ранее, и накладные расходы на // сравнение и вызов унаследованного метода будут меньше. procedure WndProc(var Msg: TMessage); override; public constructor Create(AOwner: TComponent); override; end; var Form1: TForm1; implementation {$R *.dfm} constructor TForm1.Create(AOwner: TComponent); begin // Регистрируем глобальное сообщение с именем // WM_DelphiKingdom_APISample_SendNumber. Имя достаточно // длинное и осмысленное, поэтому можно надеяться, что // никакое другое приложение не зарегистрирует сообщение с // таким же именем. Регистрация сообщения выполняется до // вызова унаследованного конструктора, т.к. при // выполнении этого конструктора окно получит ряд // сообщений, и метод WndProc будет несколько раз вызван. // Если вызвать унаследованный конструктор до вызова // RegisterWindowMessage, то поле FSendNumberMessage // будет иметь присвоенное ему по умолчанию значение 0, // а это - код сообщения WM_NULL. Таким образом, если в // это время окно получит сообщение WM_NULL, оно будет // неправильно обработано. Конечно, вероятность получения // WM_NULL во время выполнения унаследованного // конструктора крайне мала, но лучше подстраховаться и // сделать так, чтобы поле FSendNumberMessage на момент // первого вызова WndProc уже имело правильное значение. FSendNumberMessage := RegisterWindowMessage('WM_DelphiKingdom_APISample_SendNumber'); inherited; // Здесь мы меняем стиль окна поля ввода, добавляя в него // ES_NUMBER. Стиль ES_NUMBER запрещает полю ввода // вводить какие-либо символы, кроме цифр. Это уменьшает // риск ошибки ввода в тех случаях, когда требуется целое // неотрицательное число. SetWindowLong(EditNumber.Handle, GWL_STYLE, GetWindowLong(EditNumber.Handle, GWL_STYLE) or ES_NUMBER); end; procedure TForm1.BtnBroadcastClick(Sender: TObject); var Num: Integer; Recipients: DWORD; begin try Num := StrToInt(EditNumber.Text); // Для широковещательной рассылки сообщения служит // функция BroadcastSystemMessage. В литературе обычно // советуют использовать более простую функцию // PostMessage, указывая в качестве адресата // HWND_BROADCAST. Однако PostMessage рассылает // сообщения только окнам верхнего уровня, не имеющим // владельца (в терминах системы). Но главная форма // приложения имеет владельца - это невидимое окно // приложения, реализуемое объектом TApplication. // Поэтому такое широковещательное сообщение главная // форма приложения не получит — его получит только // невидимое окно приложения (это сообщение можно // будет перехватить, назначив обработчик // Application.OnMessage - вручную или с помощью // компонента TApplicationEvents). Чтобы главная форма // тоже попала в список окон, получающих // широковещательное сообщение, используется функция // BroadcastSystemMessage. Recipients := BSM_APPLICATIONS; BroadcastSystemMessage(BSF_POSTMESSAGE, @Recipients, FSendNumberMessage, Num, 0); except on EConvertError do begin Application.MessageBox( 'Введенное значение не является числом', 'Ошибка', MB_OK or MB_ICONSTOP); end; end; end; procedure TForm1.WndProc(var Msg: TMessage); begin if Msg.Msg = FSendNumberMessage then LabelNumber.Caption := IntToStr(Msg.WParam) else inherited; end; end. Как уже отмечалось ранее, для обработки глобального сообщения нельзя использовать методы с директивой message, т.к. номер сообщения на этапе компиляции еще не известен. Здесь для обработки глобального сообщения мы перекрываем метод WndProc. Соответственно, все оконные сообщения, в том числе и те, которые окно получает при создании, будет обрабатывать перекрытый метод WndProc. Это значит, что поле FSendNumberMessage, которое задействовано в этом методе, должно быть правильно инициализировано раньше, чем окно получит первое сообщение. Поэтому вызов функции RegisterWindowMessageвыполнять, например, в обработчике события OnCreateформы уже поздно. Его необходимо выполнить в конструкторе формы, причем до того, как будет вызван унаследованный конструктор. Примечание Нажатие на кнопку BtnBroadcastприводит к широковещательной отправке сообщения. Отправить широковещательное сообщение можно двумя способами: функцией PostMessageс адресатом HWND_BROADCASTвместо дескриптора окна и с помощью функции BroadcastSystemMessage. Первый вариант позволяет отправить сообщения только окнам верхнего уровня, не имеющим владельца в терминах системы. Таким окном в VCL-приложении является только невидимое окно приложения, создаваемое объектом Application. Главная форма имеет владельца в терминах системы — то самое невидимое окно приложения. Поэтому широковещательное сообщение, посланное с помощью PostMessage, главная форма не получит, это сообщение пришлось бы ловить с помощью события Application.OnMessage. Мы здесь применяем другой способ — отправляем сообщение с помощью функции BroadcastSystemMessage, которая позволяет указывать тип окон, которым мы хотим отправить сообщения. В частности, здесь мы выбираем тип BSM_APPLICATION, чтобы сообщение посылалось всем окнам верхнего уровня, в том числе и тем, которые имеют владельца. При таком способе отправки главная форма получит это широковещательное сообщение, поэтому его обработку можно реализовать в главной форме. 1.2.6. Пример ButtonDelПрограмма ButtonDel демонстрирует, как можно удалить кнопку в обработчике нажатия этой кнопки. Очень распространенная ошибка — попытка написать код, один из примеров которого приведен в листинге 1.32. Листинг 1.32. Неправильный вариант удаления кнопки в обработчике ее нажатияprocedure TForm1.Button1Click(Sender: TObject); begin Button1.Free; end; Рассмотрим, что произойдет в случае выполнения этого кода. Когда пользователь нажимает на кнопку, форма получает сообщение WM_COMMAND. При обработке форма выясняет, что источником сообщения является объект Button1и передает этому объекту сообщение CN_COMMAND. Button1, получив его, вызывает метод Click, который проверяет, назначен ли обработчик OnClick, и, если назначен, вызывает его. Таким образом, после завершения Button1Clickуправление снова вернется в метод Clickобъекта Button1, из него — в метод CNCommand, из него — в Dispatch, оттуда — в WndProc, а оттуда — в MainWndProc. А из MainWndProcуправление будет передано в оконную процедуру, сформированную компонентом с помощью MakeObjectInstance. В деструкторе Button1эта оконная процедура будет уже удалена. Таким образом, управление получат последовательно пять методов уже не существующего объекта и одна несуществующая процедура. Это может привести к самым разным неприятным эффектам, но, скорее всего, — к ошибке Access violation (обращение к памяти, которую программа не имеет права использовать). Поэтому приведенный в листинге 1.32 код будет неработоспособным. В классе TCustomFormдля безопасного удаления формы существует метод Release, который откладывает уничтожение объекта до того момента, когда это будет безопасно, но остальные компоненты подобного метода не имеют. Примечание Очевидно, что для безопасного удаления кнопки эту операцию следует отложить до того момента, когда все методы удаляемой кнопки уже закончат свою работу. Вставить требуемый код в обработчик WM_COMMANDформы достаточно сложно, поэтому мы будем использовать другой способ: пусть обработчик кнопки посылает форме сообщение, в обработчике которого она будет удалять кнопку. Здесь важно, что сообщение посылается, а не отправляется, т.е. ставится в очередь, из которой извлекается уже после того, как будет полностью обработано сообщение WM_COMMAND. В этом случае методы удаляемой кнопки не будут вызваны, и удаление пройдет без неприятных последствий. Как раз для подобных случаев и предусмотрена возможность определять свои сообщения, т.к. ни одно из стандартных для наших целей не подходит. Свое сообщение мы будем посылать только одному окну, без широковещания, поэтому для него вполне подходит диапазон сообщений класса. Номер сообщения становится известным на этапе компиляции, поэтому для обработки этого сообщения мы можем применить самый удобный способ написать метод-обработчик с помощью директивы message. С учётом всего этого код выглядит следующим образом (листинг 1.33). Листинг 1.33. Модуль главной формы программы ButtonDelunit BDMain; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; // Определяем свое сообщение. Константа, добавляемая к // WM_USER, может иметь произвольное значение в диапазоне // от 0 до 31743. const WM_DELETEBUTTON = WM_USER + 1; type TForm1 = class(TForm) BtnDeleteSelf: TButton; procedure BtnDeleteSelfClick(Sender: TObject); private // Определяем метод - обработчик событий WM_DELETEBUTTON. // Ему будет передано управление через Dispatch. procedure WMDeleteButton(var Msg: TMessage); message WM_DELETEBUTTON; public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.BtnDeleteSelfClick(Sender: TObject); begin // Помещаем сообщение WM_DELETEBUTTON в очередь формы. // Указатель на объект, который нужно удалить, помещаем // в LParam. В 32-разрядных версиях Windows указатель // можно помещать как в wParam, так и в lParam, но по // традиции, берущей начало в 16-разрядных версиях, // указатель обычно передают через lParam. PostMessage(Handle, WM_DELETEBUTTON, 0, LParam(BtnDeleteSelf)); // Здесь принципиально использование PostMessage, а не // SendMessage. SendMessage в данном случае привел бы к // немедленному вызову оконной процедуры, и метод // WMDeleteButton был бы вызван до завершения работы // BtnDeleteSelfClick. Это привело бы к тому же // результату, что и прямой вызов BtnDeleteSelf.Free. end; procedure TForm1.WMDeleteButton(var Msg: TMessage); begin // Просто удаляем объект, указатель на который передан // через lParam. TObject(Msg.LParam).Free; end; end. Приведенный здесь способ хорошо работает в такой простой ситуации, но в более сложных случаях может не дать результата. Рассмотрим, например, ситуацию, когда на форме лежат две кнопки: Button1и Button2. Обработчик нажатия Button1содержит длительную операцию, и поэтому в нем вызывается Application.ProcessMessages. Обработчик нажатия Button2содержит строку Button1.Free. Если после запуска программы сразу нажать Button2, проблем не возникнет и объект Button1будет благополучно удален. Но если сначала нажать Button1, а затем — Button2, возникнет ошибка. Это произойдёт потому, что нажатие Button2будет в данном случае обработано локальной петлей сообщения, и после обработки управление вернется Button1Click, а оттуда — в методы уже не существующего объекта Button1. Посылка в Button2Clickсообщения форме здесь не поможет, потому что это сообщение также будет извлечено и обработано локальной петлей. Общего решения таких проблем, видимо, не существует. В сложных случаях можно посоветовать не удалять объект, а просто прятать его ( Visible := False) — видимый результат для пользователя будет тот же самый. 1.2.7. Пример GDIDrawПрограмма GDIDraw демонстрирует некоторые возможности GDI, которые не поддерживаются классом TCanvas. Выбраны только те возможности, которые поддерживаются не только в Windows NT/2000/XP, но и в 9x/ME. Окно программы показано на рис. 1.11. В своей работе программа использует рисунок из стандартных картинок Delphi, предполагая, что эти картинки установлены в папку "С:\Program Files\Common Files\Borland Shared\Images". Если у вас эти картинки установлены в другую папку, или по каким-то причинам вы хотите выбрать другой рисунок, измените обработчик события OnCreateформы так, чтобы он загружал рисунок из нужного вам файла. Загруженный рисунок сохраняется в поле FBitmapформы. Рис. 1.11. Окно программы GDIDraw Основная работа выполняется в обработчике события OnPaintформы. Мы здесь будем разбирать этот обработчик не целиком, а по частям в соответствии с тем, что каждая часть рисует. Начнем с надписи Delphi Kingdom в левом верхнем углу окна (листинг 1.34). Листинг 1.34. Вывод надписи Delphi Kingdom var R: TRect; ... // Формируем регион, использующийся для отсечения. // Формируем его только при первом вызове метода, а при // дальнейших используем созданный ранее. Поле FRgn // содержит дескриптор этого региона if FRgn = 0 then begin Canvas.Font.Name := 'Times New Roman'; Canvas.Font.Style := [fsBold]; Canvas.Font.Height := 69; // Начинаем рисование траектории. Все вызовы // графических функций, находящиеся между BeginPath // и EndPath, не будут приводить к выводу на экран. // Вместо этого информация о том, что рисуется, будет // сохраняться а специальном объекте GDI - траектории. BeginPath(Canvas.Handle); R := Rect(10, 10, 10 + FBitmap.Width, 10 + FBitmap.Height); // Если не установить с помощью SetBkMode прозрачный // фон, в траекторию попадут не только контуры букв, // но и контуры содержащих их прямоугольных знакомест. SetBkMode(Canvas.Handle, TRANSPARENT); // Выводим текст "Delphi Kingdom", выравнивая его по // центру по вертикали и горизонтали. DrawText(Canvas.Handle, 'Delphi'#13#10'Kingdom', -1, R, DT_CENTER or DT_VCENTER); EndPath(Canvas.Handle); // Превращаем траекторию в регион. В результате вызова // этой функции получится регион, контуры которого // совпадают с контурами надписи "Delphi Kingdom", // сделанной в указанных координатах выбранным шрифтом. FRgn := PathToRegion(Canvas.Handle); end; // Устанавливаем регион отсечения. Все, что не будет // попадать в выбранный регион, при выводе будет // игнорироваться. SelectClipRgn(Canvas.Handle, FRgn); // Выводим изображение. Все, что не попадает в область // региона, отсекается. Таким образом, получаем надпись // "Delphi Kingdom", подсвеченную выбранным изображением. Canvas.Draw(10, 10, FBitmap); // Отменяем отсечение по региону SelectClipRgn(Canvas.Handle, 0); Если присмотреться к надписи, видно, что внутренняя часть контуров букв содержит тот самый рисунок, который был загружен в обработчик OnCreate(как будто мы нарисовали этот рисунок через трафарет, имеющий форму надписи). По сути, так оно и есть, только называется это не трафарет, а регион отсечения. Регион — это специальный объект, который хранит область произвольной формы. Способы применения регионов различны (см. разд. 1.3.3), и один из них — это использование региона для отсечения графического вывода. Если установить регион отсечения для контекста устройства, то, что бы мы ни выводили потом в данный контекст, все, что лежит за пределами региона отсечения, игнорируется. Соответственно, чтобы сделать такую надпись, нужно создать регион, совпадающий по форме с этой надписью. В GDI есть целый ряд функций для создания регионов различной формы, но вот для создания региона в форме букв функции нет. Зато GDI поддерживает другие объекты — траектории. Строго говоря, это не совсем объекты, траектория не имеет дескриптора (по крайней мере, API не предоставляет этот дескриптор программам), и в каждом контексте устройства может быть только одна траектория. Создание траектории начинается с вызова функции BeginPath, заканчивается вызовом функции EndPath. Графические функции, вызванные между BeginPathи EndPath, не выводят ничего в контекст устройства, а то, что должно быть выведено, вместо этого запоминается в траектории (которая представляет собой совокупность замкнутых кривых). С траекторией можно выполнить много полезных операций (см., например, разд. 1.3.4). В нашем случае между вызовами BeginPathи EndPathмы вызываем DrawText. формируя таким образом траекторию, состоящую из контуров букв. Затем с помощью функции PathToRegionмы создаем регион, границы которого совпадают с контурами траектории, т.е., в данном случае, регион, совпадающий по форме с надписью. Примечание В ходе работы программы регион не меняется, так что нет нужды создавать его каждый раз при обработке события OnPaint. Он создается только один раз, и его дескриптор сохраняется в поле FRgnформы для дальнейшего использования. Все, что осталось сделать, — это установить регион отсечения с помощью функции SelectClipRgn, отобразить рисунок и убрать регион отсечения, чтобы не мешал в дальнейшем. Теперь рассмотрим, как рисуются звезды в правом верхнем углу окна (листинг 1.35). Листинг 1.35. Рисование звездvar I: Integer; Star: array[0..4] of TPoint; ... // Следующая группа команд рисует две звезды справа от // надписи. Эти звезды демонстрируют использование двух // режимов заливки: WINDING и ALTERNATE. Для простых // фигур эти режимы дают одинаковые результаты, разница // возникает только при закрашивании сложных фигур, // имеющих самопересечения. Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Pen.Color := clRed; Canvas.Brush.Style := bsSolid; Canvas.Brush.Color := clRed; // Вычисляем координаты вершин звезды. Они помещаются // в массив Star в следующем порядке (если первой // считать верхнюю вершину и нумеровать остальные по // часовой стрелке от нее): 1-3-5-2-4 for I := 0 to 4 do begin Star[I].X := Round(380 + 90 * Sin(0.8 * I * Pi)); Star[I].Y := Round(100 - 90 * Cos(0.8 * I * Pi)); end; // Устанавливаем режим заливки WINDING. При // использовании этого режима закрашивается все // содержимое многоугольника независимо от того, // как именно он нарисован. SetPolyFillMode(Canvas.Handle, WINDING); Canvas.Polygon(Star); // Сдвигаем координаты звезды, чтобы нарисовать ее // правее с другим режимом заливки. for I := 0 to 4 do Inc(Star([I].X, 200); // Устанавливаем режим заливки ALTERNATE. При // использовании этого режима заполняются горизонтальные // линии, лежащие между нечетной и четной сторонами // многоугольника. В результате пятиугольник в центре // звезды оказывается незаполненным. SetPolyFillMode(Canvas.Handle, ALTERNATE); Canvas.Polygon(Star); Самое интересное здесь то, что обе звезды рисуются практически одинаково, меняется только режим заливки. Сначала с помощью простейшей тригонометрии вычисляются координаты вершин звезды, помещаются в массив Starи эта звезда рисуется с режимом заливки WINDING. При этом закрашиваются все точки, для которых выполняется условие, что луч, выпущенный из этой точки, пересекает контур многоугольника нечетное число раз, т.е. всю внутренность контура. Затем координаты вершин звезды смещаются вправо, и такая же звезда рисуется точно так же, но уже с режимом заливки ALTERNATE. В этом режиме закрашиваются только те точки, которые оказались между четной и нечетной сторонами многоугольника, и пятиугольник внутри звезды остается незакрашенным. Обратите внимание, что звезду мы здесь рисуем с помощью класса TCanvas, и только режимы заливки переключаем API-функциями. Следующий шаг — это рисование черной прямоугольной рамки на фоне пересекающихся зеленых линий. Линии рисуются до рамки для того, чтобы показать, что центр рамки действительно остается прозрачным, а не заливается цветом фона. Сама рамка рисуется вызовом одной функции PolyPolygon, позволяющей за один раз нарисовать фигуру, ограниченную несколькими замкнутыми многоугольными контурами (листинг 1.36). Листинг 1.36. Рисование рамки с использованием PolyPolygon const Pts: array[0..7] of TPoint = ( (X: 40; Y: 230), (X: 130; Y: 230), (X: 130; Y: 320), (X: 40; Y: 320), (X: 60; Y: 250), (X: 60; Y: 300), (X: 110; Y: 300), (X: 110; Y: 250)); Cnt: array[0..1] of Integer = (4, 4); ... // Следующая группа команд рисует прямоугольную рамку Canvas.Pen.Color := clLime; Canvas.Pen.Width := 3; // Эти линии рисуются для того, чтобы показать, что // центр рамки остается прозрачным. Canvas.MoveTo(30, 220); Canvas.LineTo(140, 330); Canvas.MoveTo(140, 220); Canvas.LineTo(30, 330); Canvas.Pen.Color := clBlack; Canvas.Brush.Color := clBlack; // Функция PolyPolygon позволяет нарисовать несколько // многоугольников одной командой. Второй параметр // задает координат всех многоугольников, третий // параметр задает массив, содержащий число вершин // каждого из многоугольников. В нашем случае массив // Cnt имеет значение (4, 4). Это значит, что первые // четыре элемента массива PCs задают координаты первого // многоугольника, следующие четыре - второго. Отметим, // что указатели на массивы приходится передавать не // очень простым способом: сначала нужно получить // указатель на массив с помощью оператора @, а потом // этот указатель разыменовать. Формальные параметры, // определяющие указатели на массив, при импорте функции // PolyPolygon в модуле Windows.dcu объявлены как // нетипизированные параметры-переменные, поэтому // компилятор не разрешает просто передать Pts и Cnt в // качестве фактических параметров - он запрещает // использовать константы там, где требуются переменные. // Это не совсем корректно, т.к. локальная // типизированная константа - это на самом деле не // константа, а глобальная переменная с локальной // областью видимости. Тем не менее компилятор имеет // такую особенность, которую приходится учитывать. // В данном примере функция PolyPolygon используется для // рисования двух квадратов, один из которых целиком // лежит внутри другого. При этом содержимое внутреннего // квадрата остается незаполненным. Обратите внимание, // что квадраты рисуются в разных направлениях: внешний // по часовой стрелке, внутренний - против. Если // установлен режим заполнения ALTERNATE, это никак не // влияет на результат, но если установить режим WINDING, // внутренний квадрат не будет закрашен только в том // случае, если квадраты рисуются в противоположных // направлениях. PolyPolygon(Canvas.Handle, (@Pts)^, (@Cnt)^, 2); Вся хитрость в этом коде — как передать параметры в функцию PolyPolygon. Ее второй параметр — это указатель на массив элементов TPoint, содержащий координаты вершин всех контуров в массиве: сначала все вершины первого контура в нужном порядке, затем — все вершины второго контура и т.д. Третий параметр — это указатель на массив, содержащий число точек в каждом контуре: первый элемент массива содержит число точек в первом контуре, второй — во втором и т.д. Общее число контуров определяется четвёртым, последним параметром функции PolyPolygon. Число элементов во втором массиве должно быть равно значению четвертого параметра, a число элементов в первом массиве — сумме значений элементов второго массива. За выполнением этих требований должен следить сам программист, если он ошибется, функция может обратиться к памяти, лежащей за пределами массивов, и последствия будут непредсказуемыми. В оригинале параметры-массивы функции PolyPolygonобъявлены как указатели на типы элементов массива. В модуле Windowsпри импорте этой функции, как это часто бывает в подобных случаях, эти параметры стали нетипизированными параметрами-переменными. В нашем случае массивы объявлены как локальные типизированные константы. По сути, в этом случае они являются глобальными переменными с локальной областью видимости, т.е., как обычные глобальные переменные, хранятся в сегменте данных и существуют на протяжении всего времени работы программы, но компилятор разрешает использовать их только внутри той процедуры, в которой они объявлены. И, несмотря на то, что по сути такие "константы" являются переменными, компилятор их рассматривает как константы и запрещает подставлять там, где требуются параметры-переменные. Поэтому приходится "обманывать" компилятор, получая указатель на эти константы, а затем разыменовывая его. Если бы наши массивы хранились в обычных переменных, нужды прибегать к такому приему не было бы. Нетрудно убедиться, что первые четыре элемента массива Ptsсодержат координаты вершин внешнего квадрата рамки, последние четыре — внутреннего квадрата. МассивCnt, соответственно, содержит два элемента, оба из которых имеют значение 4. Это означает, что в нашей фигуре два замкнутых контура, и оба содержат по четыре вершины. Порядок следования вершин во внешнем квадрате — по часовой стрелке, во внутреннем — против. Это имеет значение, если выбран режим заливки WINDING, тогда направления обхода контуров должны быть противоположными, иначе отверстие тоже окажется закрашенным. Для режима заливки ALTERNATEнаправление обхода контуров не имеет значения. Далее программа GDIDrawдемонстрирует работу функции InvertRect, которая инвертирует цвета в заданной прямоугольной области контекста устройства. Для того чтобы это было нагляднее, мы сначала выведем на форму загруженный в OnCreateрисунок (только на этот раз без региона отсечения) и инвертируем область, частично пересекающуюся с областью рисунка (листинг 1.37). Листинг 1.37. Пример использования функции InvertRect // Следующая группа команд выводит рисунок и конвертирует // его часть Canvas.Draw(300, 220, FBitmap); // Функция InvertRect делает указанный прямоугольник // "негативом". InvertRect(Canvas.Handle, Rect(320, 240, 620, 340)); Ещё одна забавная функция GDI, которая почему-то не нашла отражения в классе TCanvas— это GrayString. Она предназначена для вывода "серого" текста, т.е. текста, который по яркости находится посредине между черным и белым. Обычно для этого просто устанавливается цвет RGB(128, 128, 128), но некоторые черно-белые устройства не поддерживают полутона (это касается, прежде всего, старых моделей принтеров) — именно на них и ориентирована функция GrayString. Она позволяет рисовать серый текст произвольным образом с помощью функции обратного вызова, но эту функцию можно не указывать, и тогда рисование осуществляется функцией TextOut. Но при этом текст выводится через промежуточную растровую картинку в памяти, что обеспечивает полупрозрачность текста, т.к. закрашиваются не все пикселы, а только половина в шахматном порядке. На черно-белых принтерах с большим разрешением это действительно выглядит как серый текст, на экране же можно получать "полупрозрачные" надписи. Пример использования функции GrayStringприведен в листинге 1.38. Листинг 1.38. Пример использования функции GrayString // Следующая группа команд выводит "полупрозрачную" // надпись "Windows API" Canvas.Brush.Color := clBlue; // Функция GrayString при выводе текста закрашивает // заданной кистью не все пикселы подряд, а в шахматном // порядке, оставляя остальные нетронутыми. Это создает // эффект полупрозрачности. Canvas.Font.Name := 'Times New Roman'; Canvas.Font.Style := [fsBold]; Canvas.Font.Height := 69; GrayString(Canvas.Handle, Canvas.Brush.Handle, nil, LPARAM(PChar('Windows API')), 0, 20, 350, 0, 0); Обратите внимание на второй параметр — через него передается дескриптор кисти, с помощью которой будет осуществляться закраска пикселов в выводимой строке. Функция GrayStringигнорирует ту кисть, которая выбрана в контексте устройства и использует свою. Здесь для простоты мы передаем ей кисть контекста устройства, но, в принципе, это могла бы быть любая другая кисть. Третий параметр — это указатель на функцию обратного вызова. В нашем случае он равен nil, что указывает на использование функции TextOut. Четвертый параметр имеет тип LPARAMи содержит произвольные данные, которые передаются функции обратного вызова. В случае использования TextOutэто интерпретируется как указатель на строку, которую нужно вывести, поэтому здесь приходится возиться с приведением типов. Пятый параметр содержит длину выводимой строки. Это очень характерно для функций GDI, предназначенных для вывода текста, конец строки в них определяется не обязательно по завершающему символу #0, можно вывести только часть строки, явно задав нужное число символов. Но этот же параметр можно сделать равным нулю (как в нашем случае), и тогда длина строки определяется обычным образом — по символу #0. Прочие параметры функции определяют координаты выводимой строки. Последняя часть примера посвящена вопросу, который долгое время был очень популярен в форумах: как вывести текст, расположенный наклонно (в программе примером такого текста является надпись Sample, выведенная под углом 60 градусов). Это связано с тем, что только в BDS 2006 у класса TFont появилось свойство Orientation, позволяющее задавать направление текста (в справке BDS 2006 информация об этом свойстве отсутствует, она появляется только в справке Delphi 2007, но это свойство, тем не менее, есть и в BDS 2006, а также в Turbo Delphi). В более ранних версиях текст под углом можно было вывести только с помощью функций GDI, вручную создавая шрифт (листинг 1.9). Листинг 1.39. Вывод текста под углом средствами GDI // Следующая группа функций выводит надпись "Sample". // повернутую на угол 60 градусов. Canvas.Brush.Style := bsClear; // При создании логического шрифта для контекста // устройства следует в обязательном порядке указать // угол поворота. Однако класс TFont игнорирует такую // возможность, поэтому шрифт нужно создавать вручную. // Чтобы выбрать шрифт в контексте устройства, легче // всего присвоить его дескриптор свойству // Canvas.Font.Handle. Параметры fdwItalic, fdwUnderline // и fdwStrikeOut, согласно справке, могут принимать // значения True или False, но имеют тип DWORD. Для // С/C++ это не имеет значения - целый и логический типы // в этих языках совместимы. Но в Delphi приходится // использовать 0 и 1 вместо True и False. Угол поворота // шрифта задается в десятых долях градуса, т.е. // значение 600 означает 60 градусов. Canvas.Font.Handle := CreateFont(60, 0, 600, 600, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_РIТСН, 'Times New Roman'); Canvas.TextOut(140, 320, 'Sample'); // Эта строка нужна для того, чтобы пример работал // корректно в BDS2006 и выше. В этой версии у класса // TFont появилось свойство Orientation, задающее // направление текста, и этот класс научился определять // и сохранять это направление даже в том случае, если // оно было задано функцией GDI, а не через свойство // Orientation. Чтобы этого не происходило, нужно снова // придать шрифту горизонтальное направление. В версиях // Delphi, более ранних, чем BDS 2006, эта строка // не нужна: при изменении шрифта через класс TFont // направление текста и так станет горизонтальным. Canvas.Font.Handle := Create Font(60, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, 'Times New Roman'); Новый шрифт создается функцией CreateFont. Если бы мы программировали без VCL, то полученный в результате вызова этой функции дескриптор шрифта необходимо было бы выбрать в контексте устройства (функция SelectObject) и вывести надпись. Затем в устройстве следовало бы выбрать другой шрифт, а созданный ранее удалить. Но т.к. VCL мы все же используем, можно поступить проще: присвоить созданный дескриптор свойств Canvas.Font.Handle, а все остальное сделают классы TCanvasи TFont. Примечание Функция CreateFontимеет 14 параметров, определяющих свойства создаваемого шрифта. Мы не будем перечислять их все, отметим только, что мы здесь создаем шрифт на основе гарнитуры Times New Roman, имеющий размер 60 обычный (т.е. не жирный и не наклонный). О точных значениях всех параметров рекомендуем посмотреть в MSDN. Самые интересные для нас параметры — это третий ( nEscapement) и четвертый ( nOrientation), которые и определяют угол наклона шрифта. Они задаются в десятых долях градуса, т.е., чтобы получить нужное значение параметра, следует требуемое число градусов умножить на 10) (в нашем примере оба эти параметра равны 600, что означает 60 градусов). Параметр nEscapementзадает угол поворота базовой линии текста относительно горизонтальной оси. Параметр nOrientationзадаст угол поворота отдельных букв относительно своего нормального положения. По умолчанию в контекст устройства включен режим GM_COMPATIBLEпри котором эти два значения должны совпадать, т.е. угол поворота надписи в целом и угол поворота отдельной буквы всегда совпадают. В Windows NT/2000/ХР с помощью функции SetGraphicsModeможно установить для контекста устройства режим GM_ADVANCED, при котором, в частности, параметры ( nOrientationи nEscapementмогут принимать различные значения (в Windows 9х/МЕ тоже есть функция SetGraphicsMode, но установить режим GM_ADVANCEDона не позволяет). Когда мы присваиваем значение свойству TFont.Handle, все прочие свойства объекта TFont меняют свои значения в соответствии с тем, какой шрифт установлен. Так как в Delphi до 7-й версии свойство TFont.Orientationотсутствует, направление шрифта, установленное нами, в этом классе не запоминается, и поэтому при дальнейшем изменении шрифта с помощью свойств Canvas.Font.Name, Canvas.Font.Sizeи т.п. мы снова получим горизонтальный шрифт. Другое дело — BDS 2006 и выше. В этих версиях направление шрифта тоже запоминается, и поэтому дальнейшие манипуляции со свойствами Canvas.Fontбудут снова давать наклонный шрифт, пока мы явно не присвоим значение 0 свойству Canvas.Font.Orientation. В нашем случае это означает, что при повторном вызове события OnPaintпри вызове функции GrayStringбудет выведен наклонный текст, если не принять дополнительные меры. Как мы уже сказали, проблема легко решается присваиванием нуля свойству Canvas.Font.Orientation, но, т.к. наши примеры должны работать во всех версиях Delphi, начиная с пятой, этот вариант нам не подходит. Поэтому мы здесь вновь вручную создаем шрифт, на этот раз не важно, какой именно, главное, чтобы его параметры nOrientationи nEscapementбыли равны нулю. В Delphi до 7-й версии программа GDIDraw будет корректно работать и без второго вызова функции CreateFont. Отметим, что во всех версиях до Delphi 2007 как минимум, класс TFontимеет свойство Orientation, но не имеет свойства Escapement. Это означает, что если вы хотите вывести надпись, у которой угол наклона букв и угол наклона базовой линии будут разными, вам все-таки придется самостоятельно вызывать функцию CreateFont. 1.2.8. Пример BitmapSpeedПрограмма BitmapSpeed предназначена для сравнения скорости работы с растровыми изображениями в формате DDB и DIB через класс TBitmap. Тестируются три операции: рисование прямых линий, вывод растра на экран и работа со свойством ScanLine. Окно программы показано на рис 1.12. Рис. 1.12. Окно программы BitmapSpeed после завершения теста Одна отдельно взятая операция выполняется настолько быстро, что измерить время ее выполнения можно только с большой погрешностью. Чтобы уменьшить погрешность, нужно повторить операцию много раз и измерить общее время. Все три теста выполняются методом DoTest, показанном в листинге 1.40. Листинг 1.40. Метод DoTest, выполняющий тесты скорости procedure TForm1.DoTest(Cnt, XOfs, ColNum: Integer; PixelFormat: TPixelFormat); { Cnt - число повторов операции при тестах XOfs - X-координата области, в которой будет выполняться вывод изображения во втором тесте ColNum - номер колонки в GridResults, в которую будут выводиться результаты Pixel Format - формат изображения } var Pict: TBitmap; I: Integer; P: Pointer; Freq, StartTime, EndTime: Int64; begin // Узнаем частоту условного счетчика тактов QueryPerformanceFrequency(Freq); // Создаем изображение Pict := TBitmap.Create; try Pict.PixelFormat := PixelFormat; Pict.Width := PictSize; Pict.Height := PictSize; Pict.Canvas.Pen.Width := 0; // Вывод линий на картинку // Выводится Cnt линий со случайными координатами QueryPerformanceCounter(StartTime); for I := 1 to Cnt do begin Pict.Canvas.Pen.Color := RGB(Random(256), Random(256), Random(256)); Pict.Canvas.MoveTo(Random(PictSize), Random(PictSize)); Pict.Canvas.LineTo(Random(PictSize), Random(PictSize)); end; QueryPerformanceCounter(EndTime); GridResults.Cells[ColNum, 1] := FloatToStrF((EndTime - StartTime) / Freq * 1000, ffFixed, 10, 2); // Вызываем Application.ProcessMessages, чтобы GridResults // перерисовался в соответствии с новым значением ячейки Application.ProcessMessages; // Второй тест - вывод рисунка на экран QueryPerformanceCounter(StartTime); // Повторяем вывод рисунка на экран Cnt раз // Чтобы пользователь мог видеть, когда вывод // заканчивается, каждый раз добавляем к координатам // случайную величину for I := 1 to Cnt do Canvas.Draw(XOfs + Random(50), 10 + Random(50), Pict); QueryPerformanceCounter(EndTime); GridResults.Cells[ColNum, 2] := FloatToStrF((EndTime - StartTime) / Freq + 1000, ffFixed, 10, 2); Application.ProcessMessages; // Третий тест - доступ к свойству ScanLine QueryPerformanceCounter(StartTime); // Обращаемся к случайной строке свойства ScanLine // Cnt раз for I := 1 to Cnt do P := Pict.ScanLine(Random(PictSize)); QueryPerformanceCounter(EndTime); GridResults.Cells[ColNum, 3] := FloatToStrF((EndTime - StartTime) / Freq * 1000, ffFixed, 10, 2); Application.ProcessMessages; finally Pict.Free; end; end; Для измерения скорости работы будем использовать счетчик производительности — это высокопроизводительный счетчик, поддерживаемый системой для измерения производительности. Текущее значение счетчика можно узнать с помощью функции QueryPerformanceCounter, число тактов счетчика в секунду — с помощью функции QueryPerformanceFrequency. Этот счетчик позволяет получить более точные результаты, чем традиционно применяющаяся для таких целей функция GetTickCount. Теоретически, счетчик производительности может не поддерживаться аппаратной частью (в этом случае функция QueryPerformanceFrequencyвернет нулевую частоту), однако все современные компьютеры такой счетчик поддерживают, поэтому его можно применять без опасений. В зависимости от параметра PixelFormatметод DoTestсоздает DDB- или DIB-изображение и тестирует скорость исполнения операций с ним. В первом тесте Cntраз рисуется линия случайного цвета со случайными координатами — так проверяется скорость рисования на картинке. Разумеется, это весьма односторонний тест, т.к. при рисовании других примитивов будет, скорее всего, иное соотношение скоростей для DIB и DDB. Но общее представление о соотношении скоростей он все же дает. Во втором тесте полученное изображение Cntраз выводится на экран. Если бы оно выводилось всегда в одном и том же месте, пользователь не видел бы процесс вывода на экран, т.к. каждый следующий раз картинка рисовалась бы точно в том же месте, что и в предыдущий, и общее изображение не менялось бы. Чтобы этого не происходило, изображение выводится со случайным смещением относительно базовых координат, и пользователь может наблюдать за процессом. Кроме того, координаты определяются также параметром XOfs — это сделано для того, чтобы при тестировании DDB- и DIB-изображений рисунки выводились в разных частях окна и не накладывались друг на друга. На некоторых компьютерах в этом тесте с DDB-изображением наблюдается интересный эффект: время, измеренное программой, заметно меньше, чем время, когда картинка меняется на экране (например, пользователь ясно видит, что тест выполняется в течение примерно трех секунд, в то время как программа дает значение около одной секунды). Это связано со способностью некоторых видеокарт буферизовать переданные им команды и выполнять их асинхронно, т.е. вызов функции завершается очень быстро, программа продолжает работать дальше, а видеокарта параллельно ей выполняет команду. Если вы столкнетесь с такой ситуацией, можете провести небольшой эксперимент: вставить вызов функции Beep сразу после окончания второго теста. Вы услышите звуковой сигнал раньше, чем изображение закончит меняться. Третий тест самый простой: Cntраз значение свойства ScanLine присваивается переменной P. Так как значение Pпотом нигде не используется, компилятор выдает соответствующую подсказку, но в данном случае ее можно игнорировать. Таким образом, метод DoTestнужно вызвать два раза: для DDB-изображения и для DIB это делает обработчик нажатия кнопки BtnStart(листинг 1.41). Листинг 1.41. Обработчик нажатия кнопки BtnStart procedure TForm1.BtnStartClick(Sender: TObject); var IterCnt, RandomStart: Integer; begin IterCnt := StrToInt(EditIter.Text); GridResults.Cells[1, 1] := ''; GridResults.Cells[1, 2] := ''; GridResults.Cells[1, 3] := ''; GridResults.Cells[2, 1] := ''; GridResults.Cells[2, 2] := ''; GridResults.Cells[2, 3] := ''; // Чтобы новый текст ячеек отобразился в GridResults, // нужно, чтобы было извлечено их очереди и обработано // сообщение WM_PAINT. Чтобы сделать это немедленно, // вызываем Application.ProcessMessages. Application.ProcessMessages; Random.Start := Random(MaxInt); Screen.Cursor := crHourGlass; // Точное измерение времени выполнения кода в Windows // невозможно, потому что это многозадачная система, и // часть измеренного времени может быть потрачена на // выполнение кода других процессов. Чтобы максимально // уменьшить погрешность измерения, нужно установить // наивысший приоритет процессу и его главной нити - // тогда вероятность переключения процессора на // выполнение другой задачи будет минимальным. Столь // высокий приоритет приводит к тому, что во время // выполнения теста система перестаёт реагировать на // перемещение мыши. Поэтому необходимо использовать блок // try/finally, чтобы даже при возникновении исключения // приоритет процесса и нити был снижен до нормального // уровня. SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL); SetPriorityClass(GetCurrentProcess, REALTIME_PRIORITY_CLASS); try // В тестах активно используются псевдослучайные числа. // Чтобы сравнение было корректно, нужно, чтобы // последовательности чисел в экспериментах с DIB и DDB // были одинаковыми. Каждое следующее псевдослучайное // число генерируется на основе значения глобальной // переменной модуля System RandSeed. Значение RandSeed // при этом обновляется по определенному закону. Таким // образом, если установить определенное значение // RandSeed, то последовательность псевдослучайных чисел // будет строго детерминирована. Это свойство генератора // случайных чисел используется, чтобы в обоих // экспериментах были одинаковые последовательности. RandSeed := RandomStart; DoTest(IterCnt, 200, 1, pfDevice); RandSeed := RandomStart; DoTest(IterCnt, 450, 2, pf24bit); finally Screen.Cursor := crDefault; SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_NORMAL); SetPriorityClass(GetCurrentProcess, NORMAL_PRIORITY_CLASS); end; end; Все три теста используют случайные числа. Чтобы условия были одинаковыми, нужно обеспечить идентичность последовательностей случайных чисел при тестировании DDB- и DIB-изображений. К счастью, этою легко добиться, установив перед тестированием одинаковые значения переменной RandSeedмодуля System, которая и определяет последующее случайное число. Начальное значение RandSeedтакже выбирается случайным образом, а т.к. в обработчике события OnCreateформы есть вызов Randomize, при каждом запуске будет сгенерирована новая последовательность случайных чисел. Это одна из причин того, что результаты тестов будут меняться от запуска к запуску. Вторая причина заключается в том, что Windows — это система с вытесняющей многозадачностью, и ни одна программа не может даже на короткое время захватить процессор для монопольного использования. Пока выполняются тесты, Windows может время от времени переключаться на выполнение других операций, внося тем самым погрешность в результаты измерений времени выполнения тестов. Чтобы уменьшить эту погрешность до минимума, перед выполнением тестов мы назначаем своему процессу и его главной нити максимальный приоритет, чтобы минимизировать число ситуаций, когда система может отобрать квант времени у теста. Тем не менее полностью исключить такую возможность нельзя, поэтому результаты имеют некоторую степень условности. Что касается самих результатов, то они, конечно, сильно зависят от конфигурации компьютера. По первым двум тестам время выполнения для DDB-растра может быть как в два-три раза меньше, чем для DIB, так и несколько превышать его. В третьем тесте DIB-растр, разумеется, существенно опережает по скорости DDB, хотя отношение и здесь зависит от компьютера. Также наблюдается некоторая зависимость от версии Delphi, под которой откомпилирован проект. Например, первый тест и для DIB, и для DDB выполняется несколько быстрее под Delphi 2007, чем под Delphi 5, а вот третий тест под Delphi 2007 выполняется несколько медленнее. 1.3. Обобщающие примерыРассмотрев основы работы с функциями API. мы переходим к обобщающим примерам — программам, использующим разные средства API для создания простого законченного примера. 1.3.1. Обобщающий пример 1 — Информация о процессахПервым обобщающим примером станет программа для получения информации о процессах системы и об окнах, которые они открывают. На компакт-диске, прилагаемом к книге, эта программа называется ProcInfo. Окно программы ProcInfo показано на рис 1.13. Рис. 1.13. Окно программы ProcInfo 1.3.1.1. Получение списка процессовИсторически сложилось так, что существует два способа получить список процессов: с помощью функций Tool Help и посредством функций PSAPI. Эти две группы функций использовались в разных линиях Windows: функции Tool Help появились в Windows 95, функции PSAPI — в Windows NT 4. Windows 2000 XP также поддерживают функции Tool Help, в то время как Windows 98/ME не поддерживают PSAPI. Поэтому мы выберем функции Tool Help, что даст нашему примеру возможность работать во всех версиях Windows, кроме NT 4 (впрочем, в Windows 95 пример тоже не будет работать, но по другой причине: из-за функций GetWindowInfoи RealGetWindowClass, отсутствующих в этой версии). Функции Tool Help объявлены в модуле TlHelp32. Для получения списка процессов необходимо сделать "снимок" состояния системы с помощью функции CreateToolhelp32Snapshot. Эта функция создает специальный объект, который может хранить информацию о процессах, модулях, нитях и кучах, созданных в системе. Этот объект называется снимком потому, что информация, хранящаяся в нем, актуальна на момент вызова функции CreateToolhelp32Snapshot; дальнейшие изменения списка процессов, модулей и т.п. не приводят к изменению снимка. Доступ к снимку, как и к большинству объектов системы, осуществляется через его дескриптор. В данном случае функция CreateToolhelp32Snapshotвызывается с параметром TH32CS_SNAPPROCESSдля получения списка процессов. Навигация по списку процессов, сохраненных в снимке, осуществляется с помощью функций Process32Firstи Process32Next. Они позволяют получить ряд параметров процесса, главный среди которых — идентификатор процесса (Process Identifier, PID). Это уникальный идентификатор процесса, с помощью которого можно отличать один процесс от другого. Примечание Код для получения списка процессов показан в листинге 1.42. Листинг 1.42. Получение списка процессов с помощью Tool Helpprocedure TProcessesInfoForm.FillProcessList; var SnapProc: THandle; ProcEntry: TProcessEntry32; Item: TListItem; begin ClearAll; // Создаем снимок, в котором сохраняем все процессы, а // затем в цикле получаем информацию о каждом из этих // процессов, перенося ее в ListProcesses SnapProc := CreateToolhelp32Snapshot(TH32CS_SNAPROCESSES, 0); if SnapProc <> INVALID_HANDLE_VALUE then try ProcEntry.dwSize := SizeOf(TProcessEntry32); if Process32First(SnapProc, ProcEntry) then repeat Item := ListProcesses.Items.Add; Item.Caption := ProcEntry.szExeFile; Item.SubItems.Add(IntToStr(ProcEntry.tb32ProcessID); Item.SubItems.Add(IntToStr(ProcEntry.th32ParentProcessID)); Item.SubItems.Add(IntToStr(ProcEntry.cntThreads)); // Сохраняем PID в поле Data соответствующего // элемента списка. Вообще, поле Data имеет тип // Pointer, а PID - это целое число, но т.к. оба этих // типа 32-битные, их можно приводить друг к другу Item.Data := Pointer(ProcEntry.th32ProcessID); until not Process32Next(SnapProc, ProcEntry); finally CloseHandle(SnapProc); end else begin ListProcesses.Visible := False; LabelProcessError.Caption := 'Невозможно получить список процессов:'#13#10'Ошибка №' + IntToStr(GetLastError); end; end; Для получения списка модулей данного процесса также используется снимок. Функция CreateToolhelp32Snapshotвызывается с параметром TH32CS_SNAPMODULE, в качестве второго параметра ей передается PID процесса, модули которого требуется получить. Навигация по снимку модулей осуществляется с помощью функций Module32Firstи Module32Next. В остальном код получения списка модулей совпадает с кодом, приведенным в листинге 1.42. 1.3.1.2. Получение списка и свойств оконСписок окон, созданных процессом, формируется с помощью функции EnumWindows, которая позволяет получить список всех окон верхнего уровня (т.е. расположенных непосредственно на рабочем столе). Для каждого из этих окон с помощью функции GetWindowThreadProcessIDопределяется идентификатор процесса. Окна, не принадлежащие выбранному процессу, отсеиваются. Для каждого из окон верхнего уровня, принадлежащих процессу, с помощью функции EnumChildWindowsищутся дочерние окна, а для каждого из найденных таким образом дочерних окон — его дочерние окна. Здесь следует учесть, что EnumChildWindowsвозвращает не только дочерние окна заданного окна, но и все окна, которыми владеют эти дочерние окна. Чтобы в дереве окон не было дублирования, при построении очередного уровня дерева окон отбрасываются все окна, непосредственным родителем которых не является данное окно. Код, выполняющий построение дерева, приведен в листинге 1.43. Листинг 1.43. Получение всех окон, относящихся к данному процессу function EnumWindowsProc(Wnd: HWnd; ParentNode: TTreeNode): BOOL; stdcall; var Text: string, TextLen: Integer; ClassName: array [0..ClassNameLen - 1] of Char; Node: TTreeNode; NodeName: string; begin Result := True; // функция EnumChildWindows возвращает список // не только прямых потомков окна, но и потомков его // потомков, поэтому необходимо отсеять все те окна, // которые не являются прямыми потомками данного if Assigned(ParentNode) and (THandle(ParentNode.Data) <> GetAncestor(Wnd, GA_PARENT)) then Exit; TextLen := GetWindowTextLength(Wnd); SetLength(Text, TextLen); if TextLen > 0 then GetWindowText(Wnd, PChar(Text), TextLen + 1); if TextLen > 100 then Text := Copy(Text, 1, 100) + '...'; GetClassName(Wnd, ClassName, ClassNameLen); ClassName[ClassNameLen - 1] := #0; if Text = '' then NodeName := 'Без названия (' + ClassName + ')'; else NodeName := Text + ' (' + ClassName + ')'; NodeName := '$' + IntToHex(Wnd, 8) + ' ' + NodeName; Node := ProcessesInfoForm.TreeWindows.Items.AddChild(ParentNode, NodeName); Node.Data := Pointer(Wnd); EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node)); end; function EnumTopWindowsProc(Wnd: HWnd; PIDNeeded: Cardinal): BOOL; stdcall; var Text: string; TextLen: Integer; ClassName: array[0..ClassNameLen - 1] of Chars; Node: TTreeNode; NodeName: string; WndPID: Cardinal; begin Result := True; // Здесь отсеиваются окна, которые не принадлежат // выбранному процессу GetWindowThreadProcessID(Wnd, @WndPID); if WndPID = PIDNeeded then begin TextLen := GetWindowTextLength(Wnd); SetLength(Text, TextLen); if TextLen > 0 then GetWindowText(Wnd, PChar(Text), TextLen + 1); if TextLen > 100 then Text := Copy(Text, 1, 100) + '...'; GetClassName(Wnd, ClassName, ClassNameLen); ClassName[ClassNameLen - 1] := #0; if Text = '' then NodeName := 'Без названия (' + ClassName + ')' else NodeName := Text + ' (' + ClassName + ')'; NodeName := '$' + IntToHex(Wnd, 8) + ' ' + NodeName; Node := ProcessesInfoForm.TreeWindows.Items.AddChild(nil, NodeName); Node.Data := Pointer(Wnd); EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node)); end; end; procedure TProcessesInfoForm.FillWindowList(PID: Cardinal); begin if PID = 0 then Exit; EnumWindows(@EnumTopWindowsProc, PID); end; В отличие от примера EnumWndиз разд. 1.2.1 здесь для функций EnumWindowsи EnumChildWindowsпредусмотрены разные процедуры обратного вызова. Связано это с тем, что окна верхнего уровня необходимо фильтровать по принадлежности к выбранному процессу, и параметр функции обратного вызова служит для передачи PID этого процесса. А их дочерние окна фильтровать по процессам не обязательно (мы предполагаем, что если некоторое окно верхнего уровня принадлежит данному процессу, то и все его дочерние окна также принадлежат этому процессу), зато нужно передавать указатель на элемент дерева, соответствующий родительскому окну, чтобы процедура знала, где размещать новый элемент. Таким образом, смысл параметра меняется, поэтому требуется новая процедура обратного вызова. (В принципе, можно было бы проверять, есть ли у найденного окна родитель, и в зависимости от этого трактовать параметр так или иначе, используя приведение типов. Но сильно усложняет код, поэтому в учебном примере мы не будем использовать такой способ.) Примечание Для получения названия окна в приведенном коде используется функция GetWindowText. Эта функция безопасна при работе с зависшими приложениями, поскольку она гарантирует, что вызвавшее ее приложение не зависнет само, пытаясь получить ответ от зависшего приложения. Но GetWindowTextне всегда может получить названия элементов управления, расположенных в окнах чужих приложений (точнее, в MSDN написано, что она вообще не может получать названия элементов управления в чужих окнах, но практика показывает, что нередко GetWindowTextвсе же делает это). Существует альтернативный способ получения названия окна — отправка ему сообщения WM_GETTEXT. При этом ограничений на работу с чужими элементами управления нет, но и гарантии, что из-за отсутствия ответа от чужого приложения программа не зависнет, тоже нет. Использование WM_GETTEXTпоказано в другой части программы — при заполнении списка параметров окна, отображающегося в правом нижнем углу формы. Чтобы программа не зависала, вместо SendMessageдля отправки WM_GETTEXTприменяется SendMessageTimeout. Код получения имени показан в листинге 1.44. Листинг 1.44. Получение заголовков "чужих" окон if SendMessageTimeout(Wnd, WM_GETTEXTLENGTH, 0, 0, SMTO_NORMAL or SMTO_ABORTIFHUNG, 5000, TextLen) = 0 then begin LastError := GetLastError; if LastError = 0 then Text := 'Приложение не отвечает' else Text:= 'Ошибка при получении длины заголовка: ' + IntToStr(LastError); end else begin SetLength(Text, TextLen); if TextLen > 0 then if SendMessageTimeout(Wnd, WM_GETTEXT, TextLen + 1, LParam(Text), SMTO_NORMAL or SMTO_ABORTIFHUNG, 5000, TextLen) = 0 then begin LastError := GetLastError; if LastError = 0 then Text := 'Приложение не отвечает' else Text := 'Ошибка при получении заголовка:' + IntToStr(LastError); end; end; Для каждого окна программа выводит имя оконного класса и реальный класс окна. В большинстве случаев эти два класса совпадают. Они различаются только для тех окон, чьи классы "унаследованы" от стандартных классов, таких как EDIT, COMBOBOXи т.п. Вообще, наследования оконных классов в Windows нет. Но существует один нехитрый прием, который позволяет имитировать наследование. Оконная процедура обычно не обрабатывает все сообщения, а передает часть их в одну из стандартных оконных процедур ( DefWindowProc, DefFrameProcи т.п.). Программа может с помощью функции GetClassInfoузнать адрес оконной процедуры, назначенной стандартному классу, и использовать ее вместо стандартной оконной процедуры. Так как большая часть свойств окна определяется тем, как и какие сообщения оно обрабатывает, использование оконной процедуры другого класса позволяет почти полностью унаследовать свойства этого класса. (В VCL для наследования оконных классов существует метод TWinControl.CreateSubClass.) Функция RealGetWindowClassпозволяет узнать имя класса-предка, если такой имеется. Соответствующая часть кода примера приведена в листинге 1.45. Листинг 1.45. Получение реального класса окна GetClassName(Wnd, ClassName, ClassNameLen); ClassName[ClassNameLen - 1] := #0; ListParams.Items[2].SubItems[0] := ClassName; RealGetWindowClass(Wnd, ClassName, ClassNameLen); ClassName[ClassNameLen - 1] := #0; ListParams.Items[3].SubItems[0] := ClassName; У окна, если оно имеет стиль WS_CHILD, должно быть родительское окно. Если такого стиля нет, то окно располагается непосредственно на рабочем столе. Кроме того, такое окно может (но не обязано) иметь владельца. Получить дескриптор родительского окна можно с помощью функции GetParent. Владельца — с помощью функции GetWindowс параметром GW_OWNER. Примечание Значительную часть кода программы составляет анализ того, какие флаги присутствуют в стиле окна. В этом нет ничего сложного, но он громоздкий из-за большого числа флагов. Следует также учесть, что для стандартных классов одни и те же числовые значения могут иметь разный смысл. Так, например, константы ES_NOHIDESELи BS_LEFTимеют одинаковые значения. Поэтому при расшифровке стиля следует также учитывать класс окна. Приводить здесь этот код мы не будем по причине его тривиальности. Его можно посмотреть в примере на компакт-диске. 1.3.2. Обобщающий пример 2 — Ассоциированные файлы и предотвращение запуска второй копии приложенияРасширения файлов могут быть связаны (ассоциированы) с определенной программой. Такие ассоциации помогают системе выбрать программу для выполнения различных действий с файлом из Проводника. Так, например, если на компьютере установлен Microsoft Office, двойной щелчок в Проводнике на файле с расширением xls приведет к запуску Microsoft Excel и открытию файла в нем. Это происходит потому, что расширение xls ассоциировано с приложением Microsoft Excel. Примечание Пример, который мы здесь рассмотрим (программа DKSView), умеет ассоциировать файлы с расширением dks с собой, а также проверять, не были ли они ассоциированы с другим приложением. DKSView является MDI-приложением, т.е. может открывать одновременно несколько файлов. Если приложение уже запущено, а пользователь пытается открыть еще один dks-файл, желательно, чтобы он открывался не в новом экземпляре DKSView, а в новом окне уже имеющегося. Поэтому наш пример будет также уметь обнаруживать уже запущенный экземпляр программы и переадресовывать открытие файла ему. 1.3.2.1. Ассоциирование расширения с приложениемФайловые ассоциации прописываются в реестре, в разделе HKEY_CLASSES_ROOT. Чтобы связать расширение с приложением, необходимо выполнить следующие действия: 1. В корне раздела HKEY_CLASSES_ROOTнужно создать новый раздел, имя которого совладает с расширением с точкой перед ним (в нашем случае это будет раздел с именем ".dks"). В качестве значения по умолчанию в этот раздел должна быть записана непустая строка, которая будет идентифицировать соответствующий тип файла. Содержимое этой строки может быть произвольным и определяется разработчиком (в нашем случае эта строка имеет значение "DKS_View_File"). 2. Далее в корне раздела HKEY_CLASSES_ROOTследует создать раздел, имя которого совпадает со значением ключа из предыдущего пункта (т.е. в нашем случае — с именем "DKS_View_File"). В качестве значения по умолчанию для этого ключа нужно поставить текстовое описание типа (это описание будет показываться пользователю в Проводнике в качестве типа файла). 3. В этом разделе создать подраздел Shell, в нем — подраздел Open, а в нем — подраздел Command, значением по умолчанию которого должна стать командная строка для запуска файла. Имя файла в ней заменяется на %1 (подробнее о командной строке чуть ниже). 4. Описанных действий достаточно, чтобы система знала, как правильно открывать файл из Проводника или с помощью ShellExecute. Однако правила хорошего тона требуют, чтобы с файлом была ассоциирована также иконка, которую будет отображать рядом с ним Проводник. Для этого в разделе, созданном во втором пункте, следует создать подраздел "DefaultIcon" и в качестве значения по умолчанию задать ему имя файла, содержащего иконку. Если это ico-файл, содержащий только одну иконку, к имени файла ничего добавлять не нужно. Если иконка содержится в файле, в котором может быть несколько иконок (например, в exe или dll), после имени файла следует поставить запятую и номер требуемой иконки (иконки нумеруются, начиная с нуля). Приведенный список — это самый минимальный набор действий, необходимых для ассоциирования расширения с приложением. Вернемся к третьему пункту. Имя подраздела "Open" задает команду, связанную с данным расширением, т.е. в данном случае — команду "Open". В разделе Shell можно сделать несколько аналогичных подразделов — в этом случае с файлом будет связано несколько команд. У функции ShellExecuteесть параметр lpOperation, в котором задается имя требуемой команды. Пользователь Проводника может выбрать одну из возможных команд через контекстное меню, которое появляется при нажатии правой кнопки мыши над файлом. Существует возможность установить для этих пунктов меню более дружественные имена. Для этого нужно задать значение по умолчанию соответствующего подраздела. В этой строке допустим символ "&" для указания "горячей" клавиши, аналогично тому, как это делается, например, в компоненте TButton. Если в ShellExecuteкоманда не указана явно, используется команда по умолчанию (то же самое происходит при двойном щелчке на файле в Проводнике). Если не оговорено обратное, командой по умолчанию является команда "Open" или, если команды "Open" нет. первая команда в списке. При необходимости можно задать другую команд) по умолчанию. Для этого нужно указать ее название в качестве значения по по умолчанию раздела Shell. В нашем примере будет две команды: Open (открыть для редактирования) и View (открыть для просмотра). Поэтому информация в реестр заносится так, как показано в листинге 1.46. Листинг 1.46. Занесение в реестр информации, необходимой для ассоциирования файла с приложениемconst FileExt = '.dks'; FileDescr = 'DKS_View_File'. FileTitle = 'Delphi Kingdom Sample file'; OpenCommand = '&Открыть'; ViewCommand = '&Просмотреть'; // Занесение в реестр информации об ассоциации // Расширения dks с программой procedure TDKSViewMainForm.SetAssociation(Reg: TRegistry); begin Reg.OpenKey('\' + FileExt, True); Reg.WriteString('' , FileDescr); Reg.OpenKey('\' + FileDescr, True); Reg.WriteString(FileTitle); Reg.OpenKey('Shell', True); Reg.OpenKey('Open', True); Reg.WriteString('', OpenCommand); Reg.OpenKey('command', True); Reg.WriteString('', '"' + ParamStr(0) + '" "%1"'); Reg.OpenKey('\' + FileDescr, True); Reg.OpenKey('Shell', True); Reg.OpenKey('View', True); Reg.WriteString('', ViewCommand); Reg.OpenKey('command', True); Reg.WriteString('' + ParamStr(0) + '" "%1" /v'); Reg.OpenKey('\' + FileDescr, True); Reg.OpenKey('DefaultIcon', True); Reg.WriteString('', ParamStr(0) + ',0'); end; 1.3.2.2. Командная строкаКомандная строка досталась Windows по наследству от DOS. Там основным средством общения пользователя с системой был ввод команд с клавиатуры. Команда запуска приложения выглядела так: <Имя приложения> <Строка параметров> Строка параметров — это произвольная строка, которая без изменений передавалась программе. От имени программы она отделялась пробелом (пробелы в именах файлов и директорий в DOS не допускались). Разработчик конкретного приложения мог, в принципе, интерпретирован, эту строку как угодно, но общепринятым стал способ, когда строка разбивалась на отдельные параметры, которые разделялись пробелами. Вид и смысл параметров зависел от конкретной программы. В качестве параметров нередко передавались имена файлов, с которыми должна была работать программа. В Windows мало что изменилось — функции CreateProcessи ShellExecute, запускающие приложение, по-прежнему используют понятие командной строки. Разве что теперь максимальная длина строки стала существенно больше, и командную строку можно получить в кодировке Unicode. Но, как и раньше, разделителем параметров считается пробел. Однако теперь пробел может присутствовать и в имени файла, как в имени самой программы, так и в именах файлов, передаваемых в качестве параметров. Чтобы отличать такой пробел от пробела-разделителя, параметры, содержащие пробелы, заключаются в двойные кавычки. Если имя программы содержит пробелы, они тоже заключаются в двойные кавычки. И, конечно же, если в кавычки окажется заключенным параметр, в котором нет пробелов, хуже от этого не будет. Для работы с параметрами командной строки в Delphi существуют две стандартные функции: ParamCountи ParamStr. Функция ParamCountвозвращает количество параметров, переданных в командной строке. ParamStr— параметр с заданным порядковым номером. Параметры нумеруются начиная с единицы, нулевым параметром считается имя самой программы (при подсчетах с помощью ParamCountэтот "параметр" не учитывается). Эти функции осуществляют разбор командной строки по описанным ранее правилам: разделитель —пробел, за исключением заключенных в кавычки. Кавычки, в которые заключен параметр, функция ParamStrне возвращает. Ассоциированный файл запускается с помощью механизма командной строки. В реестр записывается командная строка (вместе с именем приложения), в которой имя открываемого файла заменяется на %1. Когда пользователь запускает ассоциированный файл (или он запускается приложением через ShellExecute), система извлекает из реестра соответствующую командную строку, вместо %1подставляет реальное имя файла и пытается выполнить получившуюся команду. Отметим, что если имя файла содержит пробелы, в кавычки оно автоматически не заключается, поэтому о кавычках приходится заботиться самостоятельно, заключая в них %1. Таким образом, в реестр в качестве командной строки должно записываться следующее <Имя программы> "%1" Если существуют разные варианты запуска одного файла (т.е. как в нашем случае — open и view), они различаться дополнительным параметрами. В частности, в нашем примере для открытия для редактирования не будут требоваться дополнительные параметры, для открытия для просмотра в качестве второго параметра должен передаваться кляч v, т.е. в реестр для этой команды будет записана такая строка: <Имя программы> "%1" v Программа должна анализировать переданные ей параметры и открывать соответствующий файл в требуемом режиме. В нашем случае этот код выглядит очень просто (листинг 1.47). Листинг 1.47. Анализ командной строкиprocedure TDKSViewMainForm.FormShow(Sender: TObject); var OpenForView: Bооlean; begin // Проверяем наличие ключа "/v" в качестве второго параметра OpenForView := (ParamCount > 1) and (CompareText(ParamStr(2), '/v') = 0); if ParamCount > 0 then OpenFile(ParamStr(1), OpenForView); ... end; B более сложных случаях (например, при большем числе команд для ассоциированного файла) анализ командной строки будет сложнее, но его принципы останутся теми же. 1.3.2.3. Поиск уже запущенной копии приложенияВо многих случаях желательно не давать пользователю возможности запустить второй экземпляр вашего приложения. В 16-разрядных версиях Windows все приложения выполнялись в одной виртуальной машине, и каждому из них через переменную HPrevInstanceпередавался дескриптор предыдущей копии. По значению HPrevInstanceпрограмма легко могла найти свой предыдущий экземпляр или определить, что других экземпляров нет, если HPrevInstanceравна нулю. В 32-разрядных версиях эта переменная для совместимости оставлена, но всегда равна нулю, т.к. предыдущая копия работает в своей виртуальной машине, и ее дескриптор не имеет смысла. Альтернативного механизма обнаружения уже запущенной копии система не предоставляет, приходится выкручиваться своими силами. Для обнаружения уже запущенного приложения многие авторы предлагают использовать именованные системные объекты (мьютексы, семафоры, атомы и т.п.). При запуске программа пытается создать такой объект с определенным именем. Если оказывается, что такой объект уже создан, программа "понимает", что она — вторая копия, и завершается. Недостаток такого подхода — с его помощью можно установить только сам факт наличия предыдущей копии, но не более того. В нашем случае задача шире: при запуске второго экземпляра приложения должен активизироваться первый, а если второму экземпляру была передана непустая командная строка, первый должен получить эту строку и выполнить соответствующее действие, поэтому описанный способ нам не подходит. Для решения задачи нам подойдут почтовые ящики (mailslots). Это специальные системные объекты для односторонней передачи сообщений между приложениями (ничего общего с электронной почтой эти почтовые ящики не имеют). Под сообщением здесь понимаются не сообщения Windows, а произвольный набор данных (здесь больше подходит скорее термин "дейтаграмма", а не "сообщение"). Каждый почтовый ящик имеет уникальное имя. Алгоритм отслеживания повторного запуска с помощью почтового ящика следующий. Сначала программа пытается создать почтовый ящик как сервер. Если оказывается, что такой ящик уже существует, то она подключается к нему как клиент и передает содержимое своей командной строки и завершает работу. Сервером в таком случае становится экземпляр приложения, запустившийся первым, — он-то и создаёт почтовый ящик. Остальным экземплярам останется только передать ему данные. Примечание Почтовый ящик лучше создать как можно раньше, поэтому мы будем его создавать не в методе формы, а в основном коде проекта, который обычно программист не исправляет. В результате код в dpr-файле проекта будет выглядеть так, как показано в листинге 1.48. Листинг 1.48 Создание почтового ящика в главном файле проектаconst MailslotName = '\\.\mailslot\DelphiKingomSample_Viewer_FileCommand'; EventName = 'DelphiKingdomSamplе_Viewer_Command_Event'; var ClientMailslotHandle: THandle; Letter: string; OpenForView: Boolean; BytesWritten: DWORD; begin // Пытаемся создать почтовый ящик ServerMailslotHandle := CreateMailSlot(MailslotName, 0, MAILSLOT_WAIT_FOREVER, nil); if ServerMailslotHandle = INVALID_HANDLE_VALUE then begin if GetLastError = ERROR_ALREADY_EXISTS then begin // Если такой ящик уже есть, подключаемся к нему, как клиент ClientMailslotHandle := CreateFile(MailslotName, GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); // В зависимости от того, какие переданы параметры, формируем // строку для передачи предыдущему экземпляру. Первый символ // строки - команда: // e - открыть файл для редактирования // v — открыть файл для просмотра // s — просто активизировать предыдущий экземпляр // Для команд e и v к строке, начиная со 2-го символа, // добавляется имя файла if ParamCount > 0 then begin OpenForView := (ParamCount > 1) and (CompareText(ParamStr(2), '/v') = 0); if OpenForView then Letter := 'v' + ParamStr(1) elsе Letter := 'e' + ParamStr(1); end else Letter := 's'; // Отправляем команду в почтовый ящик WriteFile(ClientMailslotHandle, Letter[1], Length(Letter), BytesWritten, nil); // Сигнализируем об отправке данных через специальное событие CommandEvent := OpenEvent(EVENT_MODIFY_STATE, False, EventName); SetEvent(CommandEvent); // Закрываем все дескрипторы CloseHandle(CommandEvent); CloseHandle(ClientMailslotHandle); end; end else begin // Создаем событие для сигнализирования о поступлении данных CommandEvent := CreateEvent(nil, False, False, EventName); // Выполняем обычный для VCL-приложений цикл Application.Initialize; Application.CreateForm(TDKSViewMainForm, DKSViewMainForm); Application.Run; // Закрываем все дескрипторы CloseHandle(ServerMailslotHandle); CloseHandle(CommandEvent); end; end. Теперь осталось "научить" первую копию приложения обнаруживать момент, когда в почтовом ящике оказываются сообщения, и забирать их оттуда. Было бы идеально, если при поступлении данных главная форма получала бы какое-то сообщение, но готового такого механизма, к сожалению, не существует. Из положения можно выйти, задействовав события. Примечание События относятся к именованным объектам, поэтому с их помощью можно синхронизировать разные процессы. В нашем случае первая копия приложения с помощью CreateEventсоздает событие, а последующие копии с помощью OpenEventполучают дескриптор этого события и взводят его. чтобы послать сигнал о появлении данных в почтовом ящике. Для обнаружения этого момента в первой копии приложения создается отдельная нить, которая ожидает событие и, дождавшись, посылает главной форме сообщение (эта нить практически не требует процессорного времени, потому что почти все время находится в режиме ожидания, т.е. квант времени планировщик задач ей не выделяет, по крайней мере, проверка наличие данных в главной нити по таймеру отняла бы больше ресурсов). Это сообщение определяется пользователем и берется из диапазона WM_USER, т.к. его широковещательной рассылки не будет. При получении этого сообщения форма выполняет код, приведенный в листинге 1.49. Листинг 1.49. Реакция формы на поступление данных в почтовый ящик // Реакция на получение команд от других экземпляров приложения procedure TDKSViewMainForm.WMCommandArrived(var Message: TMessage); var Letter: string; begin // Переводим приложение на передний план GoToForeground; // Пока есть команды, читаем их и выполняем Letter := ReadStringFromMailslot; while Letter <> '' do begin // Анализируем и выполняем команду. // Команда "s" не требует никаких действий, кроме перевода // приложения на передний план, поэтому здесь мы ее не учитываем case Letter[1] of 'e': OpenFile(Copy(Letter, 2, MaxInt), False); 'v': OpenFile(Copy(Letter, 2, MaxInt), True); end; Letter := ReadStringFronMailslot; end; end; // Чтение очередного сообщения из почтового ящика function TDksViewMainForm.ReadStringFromMailslot: string; var MessageSize: DWORD; begin // Получаем размер следующего сообщения в почтовом ящике GetMailslotInfo(ServerMailslotHandle, nil, MessageSize, nil, nil); // Если сообщения нет, возвращаем пустую строку if MessageSize = MAILSLOT_NO_MESSAGE then begin Result := ''; Exit; end; // Выделяем для сообщения буфер и читаем его в этот буфер SetLength(Result, MessageSize); ReadFile(ServerMailslotHandle, Result[1], MessageSize, MessageSize, nil); end; Примечание 1.3.2.4. Перевод приложения на передний планПервая копия приложения, получив команду от другой копии, должна вывести себя на передний план. Казалось бы, все просто: с помощью функции SetForegroundWindowмы можем вывести туда любое окно. Однако так было только до Windows 95 и NT 4. В более поздних версиях введены ограничения, и теперь программа не может вывести себя на передний план по собственному усмотрению. Функция SetForegroundWindowпросто заставит мигать соответствующую кнопку на панели задач. Тем не менее, если программа свернута, команда Application.Restoreне только восстанавливает окно, но и выводит его на передний план, что нам и требуется. Ну а если программа не свернута, то "выливаем из чайника воду и тем самым сводим задачу к предыдущей": сначала сворачиваем приложение с помощью Application.Minimize, а потом разворачиваем его. Цели мы добились — главное окно на переднем плане. Дело портит только то, что изменение состояния окна сопровождается анимацией: видно, как главное окно сначала сворачивается, а потом разворачивается. Чтобы убрать этот неприятный эффект, можно на время сворачивания/разворачивания окна запретить анимацию, а потом восстановить ее. С учетом этого метод GoToForegroundвыглядит так, как показано в листинге 1.50. Листинг 1.50. Перевод приложения на передний план // Перевод приложения на передний план procedure TDKSViewMainForm.GoToForeground; var Info: TAnimationInfo; Animation: Boolean; begin // Проверяем, включена ли анимация для окон Info.cbSize := SizeOf(TAnimationInfo); Animation := SystemParametersInfo(SPI_GETANIMATION, SizeOf(Info), @Info, 0 and (Info.iMinAnimate <> 0); // если включена, отключаем, чтобы не было ненужного мерцания if Animation then begin Info.iMinAnimate := 0; SysteParametersInfo(SPI_SETANIMATION, SizeOf(Info), @Info, 0); end; // Если приложение не минимизировано, минимизируем if not IsIconic(Application.Handle) then Application.Minimize; // Восстанавливаем приложение. При этом оно автоматически выводится // на передний план Application.Restorе; // Если анимация окон была включена, снова включаем ее if Animation than begin Info.iMinAnimate := 1; SystemParametersInfo(SPI_SETANIMATION, SizeOf(Info), @Info, 0); end; end; Теперь у нас сделано все, что нужно: приложение умеет ассоциировать расширение с двумя командами; проверять, не ассоциировано ли расширение с другим приложением, и если да, предлагать пользователю установить эту ассоциацию; запрещать запуск второй копии приложения, переводя вместо этого на передний план первую копию; передавать параметры второй копии первой, чтобы она могла выполнить требуемые действия. 1.3.3. Обобщающий пример 3 — "Дырявое" окноВ этом примере мы создадим "дырявое" окно. Те, кто уже знаком с функцией SetWindowRgn, знает, что сделать "дырку" в окне или придать ему какую-либо другую необычную форму не так уж и сложно. Но мы здесь пойдем дальше: у дырки в нашем окне будет рамка, и пользователь сможет изменять размеры и положение дырки так же, как он может изменять положение и размеры окна. Как это выглядит, показано на рис. 1.14. Рассмотрим те средства, которые нам понадобятся для реализации этого. 1.3.3.1. Сообщение WM_NCHCHITTESTКаждое окно в Windows делится на две области: клиентскую и не клиентскую. Клиентской называется та область, в которой отображается содержимое окна. Неклиентская область — это различные служебные области окна: рамка, заголовок, полосы прокрутки, главное меню и т.п. Положение клиентской части окна относительно неклиентской определяет само окно при обработке сообщения WM_NCCALCRECT. Многие окна (особенно различные элементы управления) вообще не имеют неклиентской части. Некоторые сообщения для клиентской части окна имеют аналоги для неклиентской. Например, перерисовка клиентской области осуществляется с помощью сообщения WM_PAINT, а неклиентской — WM_NCPAINT. Нажатие левой кнопки мыши над клиентской частью окна генерирует сообщение WM_LBUTTONDOWN, а над неклиентской — WM_NCLBUTTONDOWNи т.п. Неклиентская область неоднородна: в нее входит заголовок, кнопки сокрытия, разворачивания и закрытия окна, иконка системного меню, главное меню, вертикальная и горизонтальная полосы прокрутки и рамка. Рамка тоже неоднородна — она имеет левую, правую, верхнюю и нижнюю границы и четыре угла. Сообщение WM_NCCALCSIZEпозволяет выяснить, какая область окна является неклиентской, но не позволяет узнать, где какая часть неклиентской области находится. Эта задача решается с помощью другого сообщения — WM_NCHITTEST. В качестве входных параметров WM_NCHITTESTполучает координаты точки, а результат кодирует, к какой части окна относится эта точка (например, HTCLIENTозначает, что точка принадлежит к клиентской части окна, HTCAPTION— к заголовку, HTLEFT— к левой границе рамки, меняющей размер, и т.п.). Рис. 1.14. "Дырявое" окно При любых событиях от мыши система начинает с того, что посылает окну сообщение WM_NCHITTESTс координатами положения мыши. Получив результат, система решает, что делать дальше. В частности, при нажатии левой кнопки мыши окну посылается WM_NCHITTEST. Затем, если результатом был HTCLIENT, посылается сообщение WM_LBUTTONDOWN, в противном случае — WM_NCLBUTTONDOWN. При каждом перемещении мыши окно также получает WM_NCHITTEST— это позволяет системе постоянно отслеживать, над какой частью окна находится курсор, при необходимости меняя его вид (как, например, при прохождении курсора над рамкой). Что будет, если подменить обработчик WM_NCHITTEST? Например, так, чтобы при попадании точки в клиентскую часть окна он возвращал не HTCLIENT, а HTCAPTION? Это приведет к тому, что любые события от мыши над клиентской областью будут восприниматься так же, как над заголовком. Например, можно будет взять окно за клиентскую часть и переместить его, а двойной щелчок на ней приведет к разворачиванию окна. Однако это полностью блокирует нормальную реакцию на мышь, потому что вместо клиентских "мышиных" сообщений окно будет получать неклиентские. С практической точки зрения окно, которое можно таскать за любую точку, обычно не очень интересно (особенно это касается приложений, разработанных с помощью VCL: на мышь перестанет правильно реагировать не только само окно, но и расположенные на нем неоконные элементы управления). Однако обработчик WM_NCHITTESTможно сделать более интеллектуальным и получить довольно интересные эффекты. Например, положив на форму панель и переопределив у панели обработчик WM_NCHITTESTтаким образом, чтобы при нахождении мыши около границ панели возвращался результат, соответствующий различным частям рамки с изменяемым размером, можно получить панель, размеры которой пользователь программы сможет изменять: система будет реагировать на эту область панели как на обычную рамку, которую можно взять и потянуть. (Пример такой панели можно увидеть в статье "Компонент, который меняет свои размеры в режиме run-time аналогично тому, как это происходит в design-time" http://www.delphikingdom.com/asp/viewitem.asp?catalogid=22.) Фантазия может подсказать и многие другие способы получения интересных эффектов с помощью WM_NCHITTEST. 1.3.3.2. РегионыРегионы — это особые графические объекты, представляющие собой области произвольной формы. Ограничений на форму региона нет, они даже не обязаны быть связными. Существует ряд функций для создания регионов простых форм ( CreateRectRgn, CreateEllipticRgn, CreatePolygonRgnи т.п.), а также функция СombineRgnдля объединения регионов различными способами. Все это вместе позволяет получать регионы любых форм. Область применения регионов достаточно широка. Ранее мы уже видели, как с помощью регионов можно ограничить область вывода графики. Здесь же мы будем с помощью функции SetWindowRgnизменять форму окна, придавая ему форму заданного региона. 1.3.3.3. Сообщения WM_SIZE и WM_SIZINGСобытия WM_SIZEи WM_SIZINGпозволяют окну реагировать на перемещение его пользователем. В "классическом" варианте, когда пользователь начинает тянуть рамку окна, на экране рисуется "резиновый" прямоугольник, соответствующая сторона или угол которого движется за курсором мыши. Окно получает сообщение WM_SIZINGпри каждом изменении размера этого прямоугольника. Параметр lParamпри этом содержит указатель на структуру TRectс новыми координатами прямоугольника. Окно может не только прочитать эти координаты, но и изменить их, блокировав тем самым нежелательные изменения размера. На этом, в частности, основано использование свойства Constraints: если размер окна при перемещении становится меньше или больше заданного, при обработке сообщения WM_SIZINGразмер увеличивается или уменьшается до необходимого. Параметр wParamсодержит информацию о том, за какую сторону или угол тянет пользователь, чтобы программа знала, координаты какого из углов прямоугольника нужно смещать, если возникнет такая необходимость. После того как пользователь закончит изменять размеры окна и отпустит кнопку мыши, окно получает сообщение WM_SIZE. При получении этого сообщения окно должно перерисовать себя с учетом новых размеров. (Окно получает сообщение WM_SIZEпосле изменения его размеров по любой причине, а не только из-за действий пользователя.) Описанный "классический" вариант в чистом виде существует только в Windows 95. Во всех более поздних версиях по умолчанию включена опция отображения содержимого окна при перетаскивании и изменении размеров (начиная с Windows ХР эта опция не только включается по умолчанию, но и не отключается средствами пользовательского интерфейса). В таком режиме при изменении размеров окна вместо прямоугольника "резиновым" становится само окно, и любое перемещение мыши при изменении размеров приводит к перерисовке окна. В этом режиме окно получает сообщение WM_SIZEкаждый раз после сообщения WM_SIZING, а не только при завершении изменения размеров. Но в целом логика этих сообщений остается прежней, просто с точки зрения программы это выглядит так, как будто пользователь изменяет размеры окна "по чуть-чуть". 1.3.3.4. А теперь — все вместеКомбинация описанных достаточно простых вещей позволяет построить окно с дыркой, имеющей изменяемые размеры. Для начала объявим несколько констант, которые нам потребуются при вычислении размеров дырки и т.п. (листинг 1.51). Листинг 1.51. Константы примера WndHoleconst // минимальное расстояние от дырки до края окна HoleDistance = 40; // Зона чувствительности рамки панели - на сколько пикселов // может отстоять курсор вглубь от края панели, чтобы его // положение расценивалось как попадание в рамку. BorderMouseSensivity = 3; // Зона чувствительности угла рамки панели - на сколько пикселов // может отстоять курсор от угла панели, чтобы его // положение расценивалось как попадание в угол рамки. CornerMouseSensivity = 15; // Толщина рамки дырки, использующаяся при вычислении региона HoleBorder = 3; // Минимальная ширина и высота дырки MinHoleSize = 10; // Смещение стрелки относительно соответствующего угла ArrowOffset = 8; Теперь приступаем к созданию программы. На форму "кладем" панель. С помощью функции SetWindowRgnустанавливаем такую форму окна, чтобы от панели была видна только рамка, а на всю внутреннюю часть панели пришлась дырка. Рамку выбираем такую, чтобы панель выглядела утопленной, так края дырки будут выглядеть естественней. Для расчета региона используется метод SetRegion(листинг 1.52), он вызывается всегда, когда нужно изменить регион окна. Листинг 1.52. Метод SetRegion, устанавливающий регион окна procedure TFormHole.SetRegion; var Rgn1, Rgn2: HRGN; R, R2: TRect; begin // Создаем регион, соответствующий прямоугольнику окна Rgn1 := CreateRectRgn(0, 0, Width, Height); // Нам потребуются координаты панели относительно левого // верхнего угла окна (а не относительно левого верхнего // угла клиентской области, как это задается свойствами // Left и Тор). Функций для получения смещения клиентской // области относительно левого верхнего угла окна нет. // Придется воспользоваться сообщением WM_NCCalcRect R2 := Rect(Left, Top, Left + Width, Top + Height); Perform(WM_NCCALCSIZE, 0, LParam(@R2)); // Переводим координаты полученного прямоугольника из // экранных в координаты относительно левого верхнего // угла окна OffsetRect(R2, -Left, -Top); // получаем координаты панели относительно левого // верхнего угла клиентской области и пересчитываем их // в координаты относительно верхнего левого угла окна R := Rect(0, 0, PanelHole.Width, PanelHole.Height); OffsetRect(R, PanelHole.Left + R2.Left, PanelHole.Top + R2.Top); // уменьшаем прямоугольник на величину рамки и создаем // соответствующий регион InflateRect(R, -HoleBorder, -HoleBorder); Rgn2 := CreateRectRgnIndirect(R); // вычитаем один прямоугольный регион из другого, получая // прямоугольник с дыркой CombineRgn(Rgn1, Rgn1, Rgn2, RGN_DIFF); // уничтожаем вспомогательный регион DeleteObject(Rgn2); // Назначаем регион с дыркой окну SetWindowRgn(Handle, Rgn1, True); // обратите внимание, что регион, назначенный окну, нигде // не уничтожается. После выполнения функции SetWindowRgn // регион переходит в собственность системы, и она сама // уничтожит его при необходимости end; Сообщения, поступающие с панели, перехватываются через ее свойство WindowProc(подробно эта технология описана в первой части данной главы, здесь мы ее касаться не будем). Сообщение WM_NCHITTESTбудем обрабатывать так, чтобы при попадании мыши на рамку панели возвращались такие значения, чтобы за эту рамку можно было тянуть. В обработчике сообщения WM_SIZEпанели изменяем регион так, чтобы он соответствовал новому размеру панели. Все, дырка с изменяемыми размерами готова. Теперь нужно научить "дырку" менять размеры при изменении размеров окна, если окно стало слишком маленьким, чтобы вместить в себя дырку. Осталось только немного "навести красоту". "Красота" заключается в том, чтобы пользователь не мог уменьшить размеры дырки до нуля и увеличить так, чтобы она вышла за пределы окна, а также уменьшить окно так. чтобы дырка оказалась за пределами окна. Первая из этих задач решается просто: добавляется обработчик сообщения WM_SIZINGдля дырки таким образом, чтобы ее размеры не могли стать меньше, чем MinHoleSizeна MinHoleSizeпикселов, а границы нельзя было придвинуть к границам окна ближе, чем на HoleDistanceпикселов. Вторая задача решается еще проще: в обработчике WM_SIZEдырки меняем свойство Constraintsформы таким образом, чтобы пользователь не мог слишком сильно уменьшить окно. Теперь окно с дыркой ведет себя корректно при любых действиях пользователя с дыркой. Получившийся в результате код обработчика сообщений панели приведен в листинге 1.53. Листинг 1.53. Обработчик сообщений панели, образующей "дырку" procedure TFormHole.PanelWndProc(var Msg: TMessage); var Pt: TPoint; R: TRect; begin POldPanelWndProc(Msg); if Msg.Msg = WM_NCHITTEST then begin // Вся хитрость обработки сообщения WM_NCHITTEST // заключается в правильном переводе экранных координат // в клиентские и в несколько муторной проверке попадания // мыши на сторону рамки или в ее угол. Дело упрощается // тем, что у панели нет неклиентской части, поэтому // верхний левый угол окна и верхний левый угол клиентской // части совпадают. Pt := PanelHole.ScreenToClient(Point(Msg.LParamLo, Msg.LParamHi)); if Pt.X < BorderMouseSensivity then if Pt.Y < CornerMouseSensivity then Msg.Result := HTTOPLEFT else if Pt.Y >= PanelHole.Height - CornerMouseSensivity then Msg.Result := HTBOTTOMLEFT else Msg.Result := HTLEFT else if Pt.X >= PanelHole.Width - BorderMouseSensivity then if Pt.Y < CornerMouseSensivity then Msg.Result := HTTOPRIGHT else if Pt.Y >= PanelHole.Height - CornerMouseSensivity then Msg.Result := HTBOTTOMRIGHT else Msg.Result := HTRIGHT else if Pt.Y < BorderMouseSensivity then if Pt.X < CornerMouseSensivity then Msg.Result := HTTOPLEFT else if Pt.X >= PanelHole.Width - CornerMouseSensivity then Msg.Result := HTTOPRIGHT else Msg.Result := HTTOP else if Pt.Y >= PanelHole.Height - BorderMouseSensivity then if Pt.X < CornerMouseSensivity then Msg.Result := HTBOTTOMLEFT else if Pt.X >= PanelHole.Width - CornerMouseSensivity then Msg.Result := HTBOTTOMRIGHT else Msg. Result := HTBOTTOM; end else if Msg.Msg = WM_SIZE then begin // Пересчитываем регион SetRegion; // Устанавливаем новые ограничения для размеров окна. // учитывающие новое положение дырки Constraints.MinWidth := Width - ClientWidth + PanelHole.Left + MinHoleSize + HoleDistance; Constraints.MinHeight := Height - ClientHeight + PanelHole.Top + MinHoleSize + HoleDistance; end else if Msg.Msg = WM_SIZING then begin // Копируем переданный прямоугольник в переменную R, // одновременно пересчитывая координаты из экранных // в клиентские R.TopLeft := ScreenToClient(PRect(Msg.LParam)^.TopLeft); R.BottomRight := ScreenToClient(PRect(Msg.LParam)^.BottomRight); // Если ширина слишком мала, проверяем, за какую // сторону тянет пользователь. Если за левую - // корректируем координаты левой стороны, если за // правую - ее координаты if R.Right - R.Left < MinHoleSize then if Msg.WParam in [WMSZ_BOTTOMLEFT, WMSZ_LEFT, WMSZ_TOPLEFT] then R.Left := R.Right - MinHoleSize else R.Right := R.Left + MinHoleSize; // Аналогично действуем, если слишком мала высота if R.Bottom - R.Top < MinHoleSize then if Msg.WParam in [WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT] then R.Top := R.Bottom - MinHoleSize else R.Bottom := R.Top + MinHoleSize; // Сдвигаем стороны, слишком близко подошедшие // к границам окна if R.Left < HoleDistance then R.Left := HoleDistance; if R.Top < HoleDistance then R.Top := HoleDistance; if R.Right > ClientWidth - HoleDistance then R.Right := ClientWidth - HoleDistance; if R.Bottom > ClientHeight - HoleDistance then R.Bottom := ClientHeight - HoleDistance; // Копируем прямоугольник R, переводя его координаты // обратно в экранные PRect(Msg.LParam)^.TopLeft := ClientToScreen(R.TopLeft); PRect(Msg.LParam)^.BottomRight := ClientToScreen(R.BottomRight); end; end; Остается еще одна ситуация, когда границы "дырки" могут подойти к границам окна слишком близко: когда пользователь меняет не границы "дырки", а границы самого окна. Чтобы этого не случилось, нужно отслеживать изменения размеров окна и соответствующим образом менять размеры дырки — для этого нам потребуется изменить размеры панели и пересчитать регион. Пересчет региона необходим и в случае увеличения размеров окна: если его не пересчитать, получится, что часть окна не будет попадать в регион и будет отрезана. Все перечисленные действия выполняются в обработчике сообщения WM_SIZEокна (листинг 1.54). Листинг 1.54. Обработчик сообщения WM_SIZEглавного окна procedure TFormHole.WMSize(var Msg: TWMSize); begin inherited; // При уменьшении размеров окна уменьшаем размер дырки, // если границы окна подошли слишком близко к ней if PanelHole.Left + PanelHole.Width > ClientWidth - HoleDistance then PanelHole.Width := ClientWidth - HoleDistance - PanelHole.Left; if PanelHole.Top + PanelHole.Height > ClientHeight - HoleDistance then PanelHole.Height := ClientHeight - HoleDistance - PanelHole.Top; // На случай увеличения окна пересчитываем его регион, // иначе та часть, которая добавилась, окажется за его // пределами и будет отрезана SetRegion; // Пересчитываем координаты стрелок CalculateArrows; Invalidate; end; Напоследок добавим к программе один "бантик": красные стрелки по углам формы, за которые можно потянуть, чтобы изменить ее размер. Каждая стрелка будет представляться отдельным регионом. Координаты регионов должны быть привязаны к соответствующим углам окна, поэтому при изменении размеров окна эти координаты придется пересчитывать (в листинге 1.54 можно найти соответствующий вызов). Все стрелки выглядят одинаково, но являются зеркальным отражением друг друга. Чтобы рассчитывать координаты стрелок, мы зададим координаты одной нарисованной заранее стрелки в виде константы, а потом будем рассчитывать на основе этих данных координаты реальных стрелок, отражая их от нужной плоскости и добавляя требуемое смещение (листинг 1.55). Листинг 1.55. Расчет координат стрелок// координаты верхней левой стрелки, отсчитанные от точки // (0,0). Для получения координат реальных стрелок эти точки // будут смещаться и отражаться const ArrowTemplate: TArrowCoords = ( (X:0; Y:0), (X:24; Y:0), (X:17; Y:7), (X:29; Y:19), (X:19; Y:29), (X:7; Y:17), (X:0; Y:24)); procedure TFomHole.CalculateArrows; var Arrow: TArrowCoords; I: Integer; begin // Вычисление региона левой верхней стрелки // Координаты просто смещаются на постоянную величину for I := 0 to High(Arrow) do begin Arrow[I].X := ArrowTemplate[I].X + ArrowOffset; Arrow[I].Y := ArrowTemplate[I].Y + ArrowOffset; end; // При необходимости уничтожаем старый регион if ArrowTopLeft <> 0 then DeleteObject(ArrowTopLeft); ArrowTopLeft := CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING); // Вычисление региона правой верхней стрелки // Координаты по X отражаются и смещаются // на постоянную величину относительно правого края окна for I := 0 to High(Arrow) do begin Arrow[I].X := ClientWidth - ArrowOffset - 1 - ArrowTemplate[I].X; Arrow[I].Y := ArrowTemplate[I].Y + ArrowOffset; end; if ArrowTopRight <> 0 then DeleteObject(ArrowTopRight); ArrowTopRight := CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING); // Вычисление региона левой нижней стрелки // Координаты по Y отражаются и смещаются // на постоянную величину относительно нижнего края окна for I := 0 to High(Arrow) do begin Arrow[I].X := ArrowTemplate[I].X + ArrowOffset; Arrow[I].Y := ClientHeight - ArrowOffset - 1 - ArrowTemplate[I].Y; end; if ArrowBottomLeft <> 0 then DeleteObject(ArrowBottomLeft); ArrowBottomLeft := CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING); // Вычисление региона правой нижней стрелки // Координаты по обеим осям отражаются и смещаются // на постоянную величину относительно правого нижнего угла окна for I := 0 to High(Arrow) do begin Arrow[I].X := ClientWidth - ArrowOffset - 1 - ArrowTemplate[I].X; Arrow[I].Y := ClientHeight - ArrowOffset - 1 - ArrowTemplate[I].Y; end; if ArrowBottomRight <> 0 then DeleteObject(ArrowBottomRight); ArrowBottomRight := CreatePolygonRgn(Arrow[0], Length(Arrow), WINDING); end; Следующий шаг — рисование стрелки на форме. Делается это очень просто (листинг 1.56). Листинг 1.56. Рисование стрелок на формеprocedure TFormHole.FormPaint(Sender: TObject); begin // Закрашиваем регионы стрелок Canvas.Brush.Style := bsSolid; Canvas.Brush.Color := clRed; FillRgn(Canvas.Handle, ArrowTopLeft, Canvas.Brush.Handle); FillRgn(Canvas.Handle, ArrowTopRight, Canvas.Brush.Handle); FillRgn(Canvas.Handle, ArrowBottomLeft, Canvas.Brush.Handle); FillRgn(Canvas.Handle, ArrowBottomRight, Canvas.Brush.Handle); Остался последний шаг — объяснить системе, что пользователь может, ухватив за стрелки, изменять размеры формы. Очевидно, что делается это через обработчик WM_NCHITTEST. Вопрос только в том, как узнать, когда координаты мыши попадают внутрь нарисованной стрелки, поскольку стрелка является объектом сложной формы, вычислить это не очень просто. Данная задача также решается с помощью регионов: попадание координат курсора в регион каждой из стрелок отслеживается с помощью стандартной функции PtInRegion(листинг 1.57). Листинг 1.57. Обработчик WM_NCHITTESTформы procedure TFormHole.WMNCHitTest(var Msg: TWMNCHitTest); var Pt: TPoint; begin // Чтобы правильно обрабатывать стандартную неклиентскую область, // вызываем унаследованный обработчик inherited; // Не забываем, что параметры WM_NCHITTEST дают экранные, // а не клиентские координаты Pt := ScreenToClient(Point(Msg.XPos, Msg.YPos)); // Проверяем координаты на попадание в регионы стрелок if PtInRegion(ArrowTopLeft, Pt.X, Pt.Y) then Msg.Result := HTTOPLEFT else if PtInRegion(ArrowTopRight, Pt.X, Pt.Y) then Msg.Result := HTTOPRIGHT else if PtInRegion(ArrowBottomLeft, Pt.X, Pt.Y) then Msg.Result := HTBOTTOMLEFT else if PtInRegion(ArrowBottomRight, Pt.X, Pt.Y) then Msg.Result := HTBOTTOMRIGHT; end; Вот и все. С помощью нескольких нехитрых приемов мы получили окно, которое имеет такой необычный вид (см. рис. 1.14). 1.3.4. Обобщающий пример 4 — Линии нестандартного стиляGDI позволяет рисовать линии разных стилей, но бывают ситуации, когда стандартных возможностей по изменению стиля линий не хватает. В этом разделе мы покажем, как рисовать линии произвольного стиля (начнем с прямых, потом перейдем к кривым Безье), а также сделаем "резиновую" линию, которую пользователь может тянуть мышью. 1.3.4.1. Получение координат точек прямойРисование нестандартных линий выполняется следующим образом: вычисляются координаты всех пикселов, составляющих данную прямую, а потом каждый из них (а при необходимости — и какая-либо его окрестность) раскрашиваются нужным цветом. Следовательно, возникает вопрос об определении координат пикселов. Существует ряд алгоритмов вычисления этих координат. Наиболее известный из них — алгоритм Брезенхэма (Bresengham), который заключается в равномерном разбрасывании "ступенек" разной длины вдоль линии. В Windows используется алгоритм GIQ (Grid Intersection Quantization). Каждый пиксел окружается воображаемым ромбом из четырех пикселов. Если прямая имеет общие точки с этим ромбом, то пиксел рисуется. Самостоятельно реализовывать один из таких алгоритмов нет необходимости — в Windows существует функция LineDDA, которая возвращает вызвавшей ее программе координаты линии. Эта функция в качестве параметра принимает координаты начала и конца линии, а также указатель на функцию, которой будут передаваться координаты пикселов. Данная функция должна быть реализована в программе. За время выполнения LineDDAэта функция будет вызвана столько раз, сколько пикселов содержит линия (как обычно в Windows, последний пиксел не считается принадлежащим прямой). Каждый раз при вызове ей будут передаваться координаты очередного пиксела, причем пикселы будут упорядочены от начала к концу прямой. В примере Lines (рис. 1.15) с помощью LineDDAрисуется пять различных типов линий. Рассмотрим на примере самого сложного из реализуемых программой типов линии ("Зеленая елочка"), как это делается (листинг 1.58). Рис. 1.15. Окно программы Lines Листинг 1.58. Рисование линии сложного стиля // константы для типа "Зеленая елочка" const // Угол отклонения "иголки" от направления линии FirNeedleAngle = 30; //Длина иголки FirNeedleLength = 8; var Counter: Integer; // Счетчик точек линии // Вспомогательные переменные для построения "елочки" DX1, DY1, DX2, DY2: Integer; // Линия в виде "елочки" procedure LineDrawFir(X, Y: Integer; Canvas: TCanvas); stdcall; begin with Canvas do case Counter mod 10 of 0: begin MoveTo(X, Y); LineTo(X + DX1, Y + DY1); end; 5: begin MoveTo(X, Y); LineTo(X + DX2, Y + DY2); end; end; Inc(Counter); end; procedure TLinesForm.Line(X1, Y1, X2, Y2: Integer); var Angle: Extended; begin case RGroupLine.ItemIndex of ... 4: begin Counter := 0; Angle := ArcTan2(Y2 - Y1, X2 - X1); DX1 := Round(FirNeedleLength * Cos(Angle + Pi / 180 * FirNeedleAngle)); DY1 := Round(FirNeedleLength * Sin(Angle + Pi / 180 * FirNeedleAngle)); DX2 := Round(FirNeedleLength * Cos(Angle - Pi / 180 * FirNeedleAngle)); DY2 := Round(FirNeedleLength * Sin(Angle - Pi / 180 * FirNeedleAngle)); LineDDA(X1, Y1, X2, Y2, @LineDrawFir, Integer(Canvas)); end; end; end; Каждая "иголка" — это линия длиной FirNeedleLengthпикселов, отклоняющаяся от направления прямой на угол FirNeedleAngleградусов. "Иголки" отклоняются попеременно то в одну, то в другую сторону от прямой. В процедуре Lineсначала рассчитываются смещения координат конца "иголки" относительно начала и результаты помещаются в глобальные переменные DX1, DY1, DX2, DY2. Переменная Counterслужит для определения номера точки. Перед вызовом LineDDAона инициализируется нулем. Затем вызывается функция LineDDA, в качестве одного из параметров которой передается указатель на функцию обратного вызова LineDrawFir. В результате этого функция LineDrawFirбудет вызвана последовательно для каждого из пикселов, составляющих линию, начиная с (X1, Y1). LineDrawFirведет подсчет пикселов, каждый раз увеличивая Counterна единицу. Если остаток от деления номера точки на 10 равен 0, рисуется "иголка", отклоняющаяся в положительном направлении, если 5 — в отрицательном. В остальных случаях не рисуется ничего. Так получается "елочка". 1.3.4.2. "Резиновая" линия и растровые операцииТеперь нужно дать пользователю возможность рисовать линии. Для этого мы используем стандартную "резиновую" линию: пользователь нажимает левую кнопку мыши и, удерживая ее, передвигает мышь. До тех пор, пока кнопка удерживается, за курсором тянется линия. Как только пользователь отпускает кнопку, линия "впечатывается" в рисунок. Сама по себе реализация "резиновой" линии очень проста: при наступлении события OnMouseDownзапоминаются координаты начала линии и взводится флаг, показывающий, что включен режим рисования "резиновой" линии. Также запоминаются координаты конца отрезка, который на данный момент совпадает с началом. В обработчике OnMouseMove, если включен режим рисования "резиновой" линии, стирается линия со старыми координатами конца и рисуется с новыми. При наступлении OnMouseUpпрограмма выходит из режима рисования "резиновой" линии, рисуя окончательный ее вариант с текущими координатами конца. Самое сложное в этой последовательности действий — стереть нарисованную ранее линию. Если бы у нас был однородный фон, можно было бы просто нарисовать старую линию еще раз цветом фона — это выглядело бы как ее стирание. Но поскольку фон не однородный, а составлен из нарисованных ранее линий, этот способ мы применить не можем. Для решения этой задачи мы здесь рассмотрим самый простой метод — инверсное рисование (более сложный метод будет рассмотрен чуть позже). При этом каждая точка, принадлежащая линии, закрашивается не каким-либо фиксированным цветом, а инвертируется (т.е. к текущему цвету точки применяется операция not). Для стирания линии просто рисуем ее еще раз: двойная инверсия восстанавливает предыдущий цвет точек ( not not X = Xдля любого X). При рисовании пером и кистью GDI позволяет использовать различные растровые операции, которые определяют результирующий цвет каждого пиксела в зависимости от цвета фона и пера или кисти. По умолчанию применяется операция R2_COPYPEN, в которой цвет фона игнорируется, а результирующий цвет пиксела совпадает с цветом пера или кисти. Изменить растровую операцию можно с помощью функции SetROP2(двойка в названии функции показывает, что устанавливаемая растровая операция имеет два аргумента — цвет рисования и цвет фона: при выводе растровых рисунков могут применяться растровые операции с тремя аргументами — см. функцию BitBlt). Нас будет интересовать операция R2_NOT, которая инвертирует фоновый цвет, игнорируя цвет пера или кисти. Примечание Код, рисующий "резиновую" линию, приведен в листинге 1.59. Листинг 1.59. Рисование "резиновой" линии инверсным методомprocedure TLinesForm.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin OldX := X; OldY := Y; BegX := X; BegY := Y; LineDrawing := True; end; end; procedure TLinesForm.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if LineDrawing and ((X <> OldX) or (Y <> OldY)) then with Canvas do begin SetROP2(Handle, R2_NOT); Line(BegX, BegY, OldX, OldY); // Стираем старую линию. Line(BegX, BegY, X, Y); // Рисуем новую. OldX := X; OldY := Y; end; end; procedure TLinesFom.FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if (Button = mbLeft) and LineDrawing then begin case RGroupLine.ItemIndex of 2: Canvas.Pen.Color := clBlue; 3: begin Canvas.Brush.Color := clRed; Canvas.Pen.Color := clRed; end; 4: Canvas.Pen.Color := clGreen; end; Line(BegX, BegY, X, Y); LineDrawing := False; end; end; Обратите внимание, что резиновая линия следует за мышью даже тогда, когда мышь выходит за пределы формы, т.е. форма получает сообщения мыши, когда курсор находится за ее пределами. Это становится возможным благодаря захвату мыши окном. Любое окно в Windows может захватить мышь для монопольного использования, и тогда все сообщения от мыши будет получать это окно, независимо от того, где находится курсор. В VCL любой визуальный компонент, у которого установлен стиль csCaptureMouse(а у формы он по умолчанию установлен) автоматически захватывает мышь при нажатии левой кнопки и освобождает при ее отпускании, поэтому мы получаем требуемый нам эффект автоматически. 1.3.4.3. Кривые БезьеСделаем следующий шаг — научимся рисовать произвольным стилем не только прямые, но и кривые. Проще всего это сделать с так называемыми кривыми Безье — они, во-первых, поддерживаются системой Windows, а во-вторых, ими можно аппроксимировать многие другие кривые (в частности, в Windows NT/2000 XP все кривые — окружности, эллипсы, дуги — аппроксимируются кривыми Безье). Теорию кривых Безье разработал П. де Кастело в 1959 году и, независимо от него, П. Безье в 1962 году. Для построения кривой Безье N-го порядка необходимо N+1 точек, две из которых определяют концы кривой, а остальные N-1 называются опорными. В компьютерной графике наибольшее распространение получили квадратичные кривые Безье, строящиеся по трем точкам, и кубические кривые Безье, строящиеся по четырем точкам. Квадратичные кривые Безье используются, например, в шрифтах TrueType при определении контуров символов. Windows API позволяет строить только кубические кривые Безье. Кубические кривые Безье задаются следующей формулой: P(t) = А(1-t)³ + 3Bt(1-t)² + 3Ct²(1-t)+Dt³ (1) где А — начало кривой, D — ее конец, а В и С — первая и вторая опорные точки. Прямая АВ касательная к кривой в точке А, прямая CD — в точке D. Параметр t изменяется от 0 до 1. При t = 0 P(t) = А, при t = 1 P(t) = D. Одним из важнейших свойств кривой Безье является ее делимость. Если кривую разделить на две кривых в точке t = 0,5, каждая из полученных кривых также будет являться кривой Безье. На этом свойстве основывается алгоритм рисования кривых Безье: если кривая может быть достаточно точно аппроксимирована прямой, рисуется отрезок прямой, если нет — она разбивается на две кривых Безье, к каждой из которых вновь применяется этот алгоритм. Для рисования кривых Безье служат функции PolyBezier, PolyBezierToи PolyDraw. В некоторых случаях удобно строить кривую Безье не по опорным точкам, а по точкам, через которые она должна пройти. Пусть кривая начинается в точке А, при t=⅓ проходит через точку В', при t=⅔ — через точку С', и заканчивается в точке D. Подставляя эти точки в уравнение (1), получаем систему, связывающую В' и С' с В и С . Решая систему, получаем (2) Из этих уравнений, в частности, следует, что для любых четырех точек плоскости существует, и притом единственная, кривая Безье, которая начинается в первой точке, проходит при t=⅓ через вторую точку, при t=⅔ — через третью и завершается в четвертой точке. Аналогичным образом можно вычислить опорные точки для кривой, которая должна проходить через заданные точки при других значениях t. 1.3.4.4. ТраекторииAPI Windows реализует поддержку специфических объектов, называемых траекториями (path). Траектория представляет собой запись движения пера и включает один или несколько замкнутых контуров. Каждый контур состоит из отрезков прямых и кривых Безье. Для построения траектории в Windows NT/2000/XP могут быть задействованы все графические функции рисования прямых, кривых и замкнутых контуров, а также функции вывода текста (в этом случае замкнутые контуры будут совпадать с контурами символов). В Windows 9x/Me могут быть использованы только функции рисования прямых, ломаных, многоугольников (за исключением PolyDrawи Rectangle), кривых Безье и функций вывода текста. Функции рисования эллипсов, окружностей и эллиптических дуг не могут быть использованы для создания траектории в Windows 9x/Me, т.к. в этих системах эллиптические кривые рисуются специальным алгоритмом, а не аппроксимируются кривыми Безье. Для создания траектории предусмотрены функции BeginPathи EndPath. Все вызовы графических функций, расположенные между BeginPathи EndPath, вместо вывода в контекст устройства будут создавать в нем траекторию. После того как траектория построена, ее можно отобразить или преобразовать. Мы не будем здесь перечислять все возможные операции с траекториями, остановимся только на преобразовании траектории в ломаную. Как уже отмечалось, все контуры траектории представляют собой набор отрезков прямых и кривых Безье. С другой стороны, при построении кривой Безье она аппроксимируется ломаной. Следовательно, вся траектория может быть аппроксимирована набором отрезков прямой. Функция FlattenPathпреобразует кривые Безье, входящие в состав траектории, в ломаные линии. Таким образом, после вызова этой функции траектория будет состоять из отрезков прямой. Отметим также некоторые другие преобразование траектории, полезные для создания графических редакторов и подобных им программ. Функция PathToRegionпозволяет преобразовать траекторию в регион. Это может понадобиться, в частности, при определении того обстоятельства, попадает ли курсор мыши в область объекта, представляемого сложной фигурой. Функция WidenPathпревращает каждый контур траектории в два контура — внутренний и внешний. Расстояние между ними определяется толщиной текущего пера. Таким образом, траектория как бы утолщается. После преобразования утолщенной траектории в регион можно определить, попадает ли курсор мыши на кривую с учетом погрешности, определяемой толщиной пера. Получить информацию о точках текущей траектории можно с помощью функции GetPath. Для каждой точки траектории эта функция возвращает координаты и тип точки (начальная линии, замыкающая точка отрезка, точка кривой Безье, конец контура). Таким образом, создав траекторию из кривой Безье ( BeginPath/PoliBezier/EndPath), мы можем преобразовать эту траекторию в ломаную ( FlattenPath), а затем получить координаты угловэтой ломаной ( GetPath). А каждое звено этой ломаной мы можем нарисовать произвольным стилем, используя LineDDA. Таким образом, задача построения кривой Безье сведена к уже решенной задаче построения отрезка. В листинге 1.60 реализован метод DrawCurve, выполняющий указанные действия. Здесь FCurve— это поле формы типа TCurve, в котором хранятся координаты четырех точек, образующих кривую. Листинг 1.60. Работа с траекторией на основе кривой Безье type // Тип TCurve хранит координаты кривой в следующем порядке: начало, // первую промежуточную точку, вторую промежуточную точку, конец TCurve = array[0..3] of TPoint; // Функция обратного вызова для LineDDA procedure LineDrawFunc(X, Y: Integer; Canvas: TCanvas); stdcall; begin case CurveForm.RGroupType.ItemIndex of // Разноцветные шарики 0: if CurveForm.FCounter mod 10 = 0 then begin Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Brush.Style := bsSolid; if CurveForm.FCounter mod 15 = 0 then Canvas.Pen.Color := clBlue else if CurveForm.FCounter mod 15 = 5 then Canvas.Pen.Color := сlLime else Canvas.Pen.Color := clRed; Canvas.Brush.Color := Canvas.Pen.Color; Canvas.Ellipse(X - 2, Y - 2, X + 3, Y + 3); end; // Поперечные полосы 1: it CurveForm.FCounter mod 5 = 0 then begin Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Pen.Color := clBlue; Canvas.MoveTo(X - CurveForm.FDX, Y - CurveForm.FDY); Canvas.LineTo(X + CurveForm.FDX, Y + CurveForm.FDY); end; // Плакатное перо 2: begin Canvas.Pen.Style := psSolid; // Предположим, некоторая точка прямой имеет координаты (X, Y), // а соседняя с ней - координаты (Х+1, Y-1). Тогда при проведении // через эти точки наклонной линии одинарной ширины между ними // останутся незаполненные точки, как на шахматной доске. // Поэтому потребовалось увеличить толщину пера Canvas.Pen.Width := 2; Canvas.Pen.Color := clBlack; Canvas.MoveTo(X - 5, Y - 5); Canvas.LineTo(X + 6, Y + 6); end; // Цепочка 3: begin case CurveForm.FCounter mod 15 of 0: begin Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Pen.Color := clBlack; Canvas.Brush.Style := bsClear; Canvas.Ellipse(X - 5, Y - 5, X + 6, Y + 6); end; 2..13: Canvas.Pixels[X, Y] := clBlack; end; end; end; Inc(CurveForm.FCounter); end; procedure TCurveForm.DrawCurve(Canvas: TCanvas); var LCurve: TCurve; I, Size: Integer; PtBuf: array of TPoint; TpBuf: array of Byte; L: Extended; begin // LCurve хранит координаты начала и конца кривой и ее // опорных точек. Если включен режим рисования по опорным // точкам, LCurve совпадает с FCurve, если включен режим // рисования по точкам кривой, опорные точки LCurve[1] // и LCurve[2] рассчитываются по приведенным в книге // формулам на основании точек FCurve LCurve := FCurve; if RGroupDrawMethod.ItemIndex = 1 then begin LCurve[1].X := Round((-5 * FCurve[0].X + 18 * FCurve[1].X - 9 * FCurve[2].X + 2 * FCurve[3].X) / 6); LCurve[1].Y := Round((-5 * FCurve[0].Y + 18 * FCurve[1].Y - 9 * FCurve[2].Y + 2 * FCurve[3]-Y) / 6); LCurve[2].X := Round((2 * FCurve[0].X - 9 * FCurve[1].X + 18 * FCurve[2].X - 5 * FCurve[3].X) / 6); LCurve[2].Y := Round((2 * FCurve[0].Y - 9 * FCurve[1].Y + 18 * FCurve[2].Y - 5 * FCurve[3].Y) / 6); end; // Создаем траекторию на основе кривой BeginPath(Canvas.Handle); Canvas.PolyBezier(LCurve); EndPath(Canvas.Handle); // Аппроксимируем траекторию отрезками прямых FlattenPath(Canvas.Handle); // Получаем число точек траектории. Так как сами точки никуда // пока не копируются, в качестве фиктивного буфера можно указать // любую переменную. В данном случае - переменную I Size := GetPath(Canvas.Handle, I, I, 0); // Выделяем память для хранения координат и типов точек траектории SetLength(PtBuf, Size); SetLength(TpBuf, Size); // Получаем координаты и типы точек. Типы точек нас в данном случае // не интересуют: у первой точки будет тип PT_MOVETO, // а у остальных - PT_LINETO. Появление PT_MOVETO у других точек // невозможно, т.к. траектория содержит только один замкнутый // контур, состояний из кривой и соединяющей ее концы прямой. // Появление точек типа PT_BEZIERTO также исключено, т.к. после // вызова FlattenPath контур содержит только отрезки прямых. // Поэтому значения, записанные в TpBuf, будут в дальнейшем // игнорироваться GetPath(Canvas.Handle, PtBuf[0], TpBuf[0], Size); FCounter := 0; // Рисуем по отдельности каждый из отрезков, составляющих контур for I := 1 to Size - 1 do begin // Вычисляем длину отрезка L := Sqrt(Sqr(PtBuf[I - 1].X - PtBuf[I].X) + Sqr(PtBuf[I - 1].Y - PtBuf[I].Y)); // Практика показала, что аппроксимированный контур может // содержать отрезки нулевой длины - видимо, это издержки // алгоритма аппроксимации. Так как в дальнейшем нам придется // делить на L, такие отрезки мы просто игнорируем, т.к. // на экране они все равно никак не отображаются if L > 0 then begin // переменные FDX и FDY используются только при рисовании // линии типа "поперечные полосы". Если бы линии этого // типа не было, то FDX, FDY, а так же L можно было бы // не рассчитывать FDX := Round (4 * (PtBuf[I - 1].Y - PtBuf[I].Y) / L); FDY := Round(4 * (PtBuf[I].X - PtBuf[I - 1].X) / L); LineDDA(PtBuf[I - 1].X, PtBuf[I - 1].Y, PtBuf[I].X, PtBuf[I].Y, @LineDrawFunc, Integer(Canvas)); end; end; end; 1.3.4.5. Интерактивная криваяОписанная технология создания "резиновой" линии не годится для рисования кривой Безье, т. к. пользователь должен задать координаты не двух точек, а четырех. Удобнее всего это сделать следующим образом: сначала нарисовать "резиновую" прямую, задав тем самым начало и конец кривой, а потом дать пользователю возможность перемещать опорные или промежуточные точки кривой до тех пор, пока она не будет завершена. При этом логично дать возможность перемещать и концы линии, а также менять ее стиль, т.е. свободно манипулировать незавершенной кривой. Для ее завершения будет использоваться кнопка Завершить (рис. 1.16). Чтобы кривая была более дружественной для пользователя, мы не будем применять здесь растровые операции, а попытаемся нарисовать незавершенную кривую без искажения цветов. Для этого нужно хранить картинку с завершенными кривыми, и при выводе нового положения незавершенной кривой сначала выводить эту картинку, а потом поверх нее — незавершенную кривую в новом положении. Так как фон в нашем случае состоит только из нарисованных ранее кривых, то можно было бы просто хранить список, содержащий координаты и стиль каждой кривой, и при перерисовке фона сначала заливать всю форму фоновым цветом, а потом рисовать на ней каждую из этих кривых заново. Но рисование одной кривой — достаточно медленная операция, т.к. на основе кривой нужно создать траекторию, аппроксимировать ее отрезками и нарисовать каждый из них по отдельности с помощью LineDDA. При большом количестве кривых эта реакция на перемещение мыши будет занимать слишком много времени. Поэтому мы выберем другой метод: будет создан растр, содержащий все завершенные кривые, и при перерисовке формы этот растр будет просто копироваться на нее. Так как операции с растрами выполняются очень быстро, мерцания фона не будет. Чтобы незавершенная кривая также не мерцала, будет установлен режим двойной буферизации. Рис. 1.16. Окно программы Bezier. Красные квадратики — области за которые можно перемещать концы и опорные точки незавершенной кривой Когда пользователь нажимает кнопку мыши, программа проверяет, есть ли незавершенная кривая. Если таких кривых нет, начинается создание новой кривой. До тех пор. пока пользователь не отпустит кнопку мыши, рисуется резиновая прямая. Эта прямая становится заготовкой для новой незавершенной кривой. Если в момент нажатия кнопки мыши незавершенная кривая уже существует, координаты мыши сравниваются с координатами опорных и концевых точек и, если они оказываются достаточно близки к одной из них, дальнейшее перемещение мыши (при удерживании кнопки) приводит к перемещению соответствующей точки и перерисовке кривой в новом положении. Изменение типа линии и/или способа построения отражается на незавершенной кривой — она немедленно перерисовывается в соответствии с новыми параметрами. При нажатии кнопки Завершить незавершенная кривая рисуется уже не на самой форме, а на растре, содержащем фон. После этого кривая перестает существовать как кривая и становится набором пикселов на фоновой картинке, а программа вновь переходит в режим, когда нажатие кнопки мыши интерпретируется как создание новой кривой. Реализацию интерактивной кривой в данном случае иллюстрирует листинг 1.61. Листинг 1.61. Реализация интерактивной кривойconst // чтобы перемещать точку кривой, пользователь должен попасть мышью // в некоторую ее окрестность. Константа RectSize задает размер этой // окрестности RectSize = 3; type // Тип TDragPoint показывает, какую точку перемещает пользователь: // ptNone — пользователь пытается тянуть несуществующую точку // ptFirst - пользователь перемещает вторую точку "резиновой" прямой // ptBegin - пользователь перемещает начало кривой // ptInter1, ptInter2 - пользователь перемещает промежуточные точки // ptEnd - пользователь перемещает конец кривой TDragPoint = (dpNone, dpFirst, dpBegin, dpInter1, dpInter2, dpEnd); TCurveForm = class(TForm) BtnEnd: TButton; RGroupType: TRadioGrour; RGroupDrawMethod: TRadioGroup; procedure FormCreate(Sender: TObject); procedure FomMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure FormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure FormPaint(Sender: TObject); procedure BtnEndClick(Sender: TObject); procedure RGroupTypeClick(Sender: TObject); procedure FormDestroy(Sender: TObject); private // Если FNewLine = True, незавершённых кривых нет, и при нажатии на // кнопку мыши начинает рисоваться новая кривая. // Если FNewLine = False, есть незавершенная кривая, и нажатия мыши // интерпретируются как попытки ее редактирования FNewLine: Boolean; // Поле FDragPoint указывает, какую точку перемещает пользователь FDragPoint: TDragPoint; // Поле FCurve хранит координаты незавершенной кривой FCurve: TCurve; // FBack - фоновый рисунок с завершенными кривыми FBack: TBitmap; // FCounter - счетчик точек, использующийся при рисовании отрезков // с помощью LineDDA FCounter: Integer; // FDX, FDY - смещения относительно координаты точки кривой для // рисования поперечной полосы FDX, FDY: Integer; // Функция PtNearPt возвращает True, если точка с координатами // (X1, Y1) удалена от точки Pt по каждой из координат не более // чем на RectSize functionPtNearPt(X1, Y1: Integer; const Pt: TPoint): Boolean; // Процедура DrawCurve рисует кривую по координатам FCurve вида, // задаваемого RadioGroup.ItemIndex procedure DrawCurve(Canvas: TCanvas); end; ... procedure TCurveForm.FormCreate(Sender: TObject); begin FNewLine := True; FDragPoint := dpNone; FBack := TBitmap.Create; FBack.Canvas.Brush.Color := Color; // Устанавливаем размер фонового рисунка равным размеру развернутого // на весь рабочий стол окна FBack.Width := GetSystemMetrics(SM_CXFULLSCREEN); FBack.Height := GetSystemMetrics(SM_CYFULLSCREEN); // Включаем режим двойной буферизации, чтобы незавершенная кривая // не мерцала DoubleBuffered := True; end; procedure TCurveForm.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin // Если незавершенных кривых нет, начинаем рисование новой кривой if FNewLine then begin FDragPoint := dpFirst; FCurve[0].X := X; FCurve[0].Y := Y; FCurve[3] := FCurve[0]; end else begin // Если есть незавершенная кривая, определяем, в какую точку попал // курсор мыши. Строго говоря, необходимо также запоминать, // насколько отстоят координаты курсора мыши от координат // контрольной точки, чтобы при первом перемещении не было скачка. // Но т.к. окрестность точки очень мала, этот прыжок практически // незаметен, и в данном случае этим можно пренебречь, чтобы // не усложнять программу if PtNearPt(X, Y, FCurve[0]) then FDragPoint := dpBegin else if PtNearPt(X, Y, FCurve[1]) then FDragPoint := dpInter1 else if PtNearPt(X, Y, FCurve[2]) then FDragPoint : = dpInter2 else if PtNearPt(X, Y, FCurve[3]) then FDragPoint := dpEnd else FDragPoint := dpNone; end; end; end; procedure TCurveForm.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if ssLeft in Shift then begin case FDragPoint of dpFirst, dpEnd: begin FCurve[3].X := X; FCurve[3].Y := Y; Refresh; end; dpBegin: begin FCurve[0].X := X; FCurve[0].Y := Y; Refresh; end; dpInter1: begin FCurve[1].X := X; FCurve[1].Y := Y; Refresh; end; dpInter2: begin FCurve[2].X := X; FCurve[2].Y := Y; Refresh; end; end; end; end; procedure TCurve Form.FormMouseUp(Sender: TObject; Button: ТМouseButton; Shift: TShiftState; X, Y: Integer); begin // Если кнопка отпущена при отсутствии незавершенной кривой, значит, // пользователь закончил рисование резиновой прямой, на основе которой // нужно делать новую кривую if (Button = mbLeft) and (FDragPoint = dpFirst) then begin FNewLine := False; FDragPoint := dpNone; // Промежуточные точки равномерно распределяем по прямой FCurve[1].X := FCurve[0].X + Round((FCurve[3].X - FCurve[0].X) / 3); FCurve[1].Y := FCurve[0].Y + Round((FCurve[3].Y - FCurve[0].Y) / 3); FCurve[2].X := FCurve[0].X + Round(2 + (FCurve[3].X - FCurve[0].X) / 3); FCurve[2].Y := FCurve[0].Y + Round(2 + (FCurve[3].Y - (Curve[0].Y) / 3); Refresh; end; end; procedure TCurveForm.FormPaint(Sender: TObject); var I: Integer; L: Extended; begin // Сначала выводим фон Canvas.Draw(0, 0, FBack); if FNewLine then begin // Если программа находится в режиме рисования резиновой прямой, // рисуем прямую от точки FCurve[0] до FCurve[3]. Значение FCurve[1] // и FCurve[2] на данном этапе игнорируется if FDragPoint = dpFirst then begin FCounter := 0; L := Sqrt(Sqr(FCurve[0].X - FCurve[3].X) + Sqr(FCurve[0].Y - FCurve[3].Y)); if L > 0 then begin FDX := Round(4 * (FCurve[0].Y -FCurve[3].Y) / L); FDY := Round(4 * (FCurve[3].X - FCurve[0].X) / L); LineDDA(FCurve[0].X, FCurve[0].Y, FCurve[3].X, FCurve[3].Y, @LineDrawFunc, Integer(Canvas)); end; end; end else begin // Если есть незавершённая кривая и установлен режим рисования // по опорным точкам, выводим отрезки, показывающие касательные // к кривой в её начале и конце if RGroupDrawMethod.ItemIndex = 0 then begin Canvas.Pen.Style := psDot; Canvas.Pen.Width := 3; Canvas.Pen.Color := clDkGrey; Canvas.MoveTo(FCurve[0].X, FCurve[0].Y); Canvas.LineTo(FCurve[1].X, FCurve[1].Y); Canvas.MoveTo(FCurve[3].X, FCurve[3].Y); Canvas.LineTo(FCurve[2].X, FCurve[2].Y); end; // Рисуем красные квадраты, показывающие точки, которые пользователь // может перемещать Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Pen.Color := clRed; Canvas.Brush.Style := bsClear; for I := 0 to 3 do Canvas.Rectangle(FCurve[I].X - RectSize, FCurve[I].Y - RectSize, FCurve[I].X + RectSize + 1, FCurve[I].Y + RectSize + 1); end; end; // функция PtNearPt возвращает True, если точка с координатами (X1, Y1) // удалена от точки Pt по каждой из координат не более чем на RectSize function TCurveForm.PtNearPt(X1, Yl: Integer; const Pt: TPoint): Boolean; begin Result := (X1 >= Pt.X - RectSize) and (X1 <= Pt.X + RectSize) and (Y1 >= Pt.Y - RectSize) and (Y1 <= Pt.Y + RectSize); end; procedure TCurveForm.BtnEndClick(Sender: TObject); begin if not FNewLine then begin DrawCurve(FBack.Canvas); FNewLine := True; Refresh; end; end; Размеры фонового растра устанавливаются равными размеру развернутого на весь экран окна. Таким образом, если уменьшить окно, то те завершенные кривые, которые окажутся за его пределами, не пропадут — они вновь будут видимы, если увеличить размеры окна. Однако в программе не предусмотрено, что система позволяет пользователю менять разрешение экрана. Это можно учесть, добавив реакцию на сообщение WM_DISPLAYCHANGEи меняя в нем размеры фонового рисунка. |
|
||||||||||||||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||||
|