Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Основы работы с WinAPI

01.01.2007

Основы работы с Windows API.

Введение.

Цель этого обзора - помочь человеку перейти от использования средств Delphi к функциям Win API. Предполагается, что читатель уже неплохо владеет Delphi, поэтому будет подчёркиваться, прежде всего, разница между этими двумя инструментами. Кроме того, многие авторы книг по Delphi не уделяют достаточно внимания функциям Win API, предназначенным для работы с окнами и графикой, потому что считают, что VCL Delphi достаточно хорошо справляется с этими задачами. Так что часто приходится учиться работе с Win API по книгам по 16-разрядному Borland Pascal'ю. Поэтому я буду обращать внимание и на отличие 32-разрядных версий от 16-разрядных. Но я не буду без особой необходимости останавливаться на подробном описании конкретных функций, так как это всё сделано в справочной системе. Я также остановлюсь и на этой самой справочной системе, потому что начинающему программисту может оказаться не очень просто разобраться с ней.

Что такое Win API

Win API - это набор функций, предоставляемых операционной системой каждой программе. Эти функции находятся в стандартных динамически компонуемых библиотеках (Dynamic Linked Library, DLL), таких как kernel32.dll, user32.dll, gdi32.dll. Эти файлы находятся в директории Window\System. Вообще говоря, каждая программа должна самостоятельно заботится о том, чтобы подключить эти библиотеки. DLL могут подключаться к программе статически и динамически. В первом случае программа <освобождает> DLL только при завершении, во втором освобождение может произойти в любой момент. Если после освобождения DLL оказывается, что её больше не использует ни одна программа, она выгружается из памяти. Так как стандартные библиотеки используются самой системой, они всегда находятся в памяти, и поэтому использование динамического подключения бессмысленно. Чтобы статически подключить в Delphi некоторую функцию Win API, например, функцию GetWindowDC из модуля user32.dll, надо написать конструкцию вида

function GetWindowDC(Wnd: HWnd); HDC; stdcall; external 'user32.dll' name 'GetWindowDC';

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

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

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

Для тех, кто решил использовать Win API, самым необходимым инструментом становится какая-либо документация по этим функциям. Их так много, что запомнить все совершенно нереально, поэтому работа без справочника под рукой просто невозможна. Наиболее доступным справочником для российского программиста является Win32 Developer's Reference, справочная система фирмы Microsoft, потому что фирма Inprise (тогда ещё Borland), получила лицензию на включение её в состав Delphi. Сам я буду постоянно ссылаться на эту справку, потому что подробное описание функций займёт слишком много места, да и нет особого смысла описывать то, что описали и без меня.

Хотя в комплект поставки Delphi и входит эта справочная система, содержащая описание всех функций Win API, получение справки по ним не настолько удобное, как по стандартным функциям Delphi. Если набрать в редакторе Delphi имя какой-нибудь функции Win API, поставить курсор в начало этой функции и нажать F1, то откроется справка по ней, как и в случае обычных функций и процедур. Однако функции Win API не появляются в предметном указателе справочной системы. Авторы Delphi объясняют это ограничениями, накладываемыми самой справочной системой (как обычно, всех собак вешают Windows). Они же советуют создать в меню кнопки <Пуск> ярлык к справочной системе Win32 Developer's Reference. Мне остаётся только присоединиться к этому совету и добавить, что ярлык надо создавать к файлу MSTools.hlp, который находится в директории $(Delphi)\Help.

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

WCHAR = WideChar;

LPSTR = PChar;

LPCSTR = PChar;

LPWSTR = PWideChar;

LPCWSTR = PWideChar;

DWORD = Integer;

BOOL = LongBool;

PBOOL = ^BOOL;

PINT = ^Integer;

PWORD = ^Word;

PDWORD = ^DWORD;

LPDWORD = PDWORD;

UCHAR = Byte;

PUCHAR = ^Byte;

SHORT = Smallint;

UINT = Integer;

PUINT = ^UINT;

ULONG = Longint;

PULONG = ^ULONG;

LCID = DWORD;

LANGID = Word;

int = Integer;

long = LongInt;

PVOID = Pointer;

HANDLE = THandle;

LPPOINT = TPoint;

RECT = TRect;

LPRECT =  PRect;

LPSIZE = PSize;

BITMAP = TBitmap;

Все типы, приведённые в первой части таблицы, в целях совместимости описаны в модуле Windows.dcu, поэтому их можно использовать наравне с обычными типами Delphi. Кроме этих типов общего назначения существуют ещё специальные. Например, дескриптор окна имеет тип HWND, первый параметр сообщения - тип WPARAM. Эти специальные типы также описаны в Windows.dcu. В некоторых имена типов, встречающихся в справке, и соответствующих им типов из Windows.dcu отличаются только добавлением буквы , как это можно видеть из второй части таблицы. Кстати, не следует путать тип TBitmap, определённый в Windows.dcu, с классом TBitmap, определённым в Graphics.dcu. Зачем разработчикам Delphi потребовалось называть разные типы одним именем, не понятно, тем более что во второй версии Delphi был тип BITMAP, который куда-то исчез в третьей. Зато в четвёртой версии снова появился BITMAP, остался TBitmap, да ещё добавился tagBITMAP, и все эти три типа означают то же самое.

Теперь о синтаксисе описания самой функции в Си. Оно имеет вид

<Тип функции> <Имя функции>(<Тип параметра> <Имя параметра>, <Тип параметра> <Имя параметра>, ...);

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

HMETAFILE CopyMetaFile(HMETAFILE  hmfSrc, LPCTSTR  lpszFile);

в Delphi имеет вид

function CopyMetaFile(hmfSrc: HMETAFILE; lpszFile: LPCTSTR): HMETAFILE;

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

function CopyMetaFile(hmfSrc: HMetaFile; lpszFile: PChar): HMetaFile;

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

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

void CloseLogFile(VOID);

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

procedure CloseLogFile;

Не путайте VOID и PVOID. PVOID - это нетипизированный указатель, соответствующий типу Pointer.

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

int GetRgnBox(HRGN  hrgn, LPRECT  lprc);

в файле Windows.pas описана

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

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

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

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

Иногда в справке можно встретить указания или . К этим замечаниям следует относится критически, так как справка написана для Windows 95, когда ещё не было Windows NT 4.0, описывается версия со старым интерфейсом. Так что то, про что написано , может вполне успешно работать и в Windows NT 4.0 и выше, особенно если это <что-то> связано с пользовательским интерфейсом. То же самое относится и к QuickInfo. Такие вещи лучше всего проверять на практике.

Ещё несколько слов о числовых константах. В справке можно встретить числа вида, например, 0xC56F или 0x3341. Префикс <0x> в Си означает шестнадцатеричное число. В Delphi надо его заменить на <$>, то есть вышеназванные числа должны быть записаны как $C56F и $3341 соответственно.

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

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

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

Не следует думать, что при работе с Win API следует полностью отказываться от классов Delphi. Эти методы прекрасно работают вместе. Правда, внутренние механизмы Delphi не могут включиться, если изменение объекта происходит через Win API. Например, если спрятать окно не с помощью метода Hide, а с помощью вызова функции Win API ShowWindow(Handle, SW_Hide), не возникнет событие OnHide, потому что оно запускается теми самыми внутренними механизмами Delphi. Но такие недоразумения случаются обычно только тогда, когда функциями Win API дублируется то, что можно сделать и с помощью Delphi. Для вызова функций Win API объекта, созданного с помощью Delphi, используйте свойство Handle. В нём хранится дескриптор.

В некоторых случаях класс Delphi инкапсулирует несколько объектов Windows. Например, класс TBitmap включает в себя HBitmap и HPalette - картинку и палитру к ней. Соответственно, он хранит два дескриптора - в свойствах Handle и Palette.

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

Формы Delphi и окна Windows

Принято считать, что класс TForm реализует окно. Это не совсем верно, потому что TForm реализует лишь часть тех объектов, которые принято называть окнами. Например, кнопка - это тоже окно, но реализуется она классом TButton.

Каждое окно принадлежит к какому-то оконному классу. Не следует путать оконный класс с классами Delphi. Это некий шаблон, определяющий базовые свойства окна. Каждому такому шаблону присваивается имя, уникальное в его области видимости. Классы делятся на локальные (видимые только в приложении, регистрирующем их) и глобальные (видимые вне прило-жения). В 16-разрядных версиях Windows локальный класс, зарегистрированный приложением, был виден всем другим экземплярам этого же приложения. В 32-разрядных версиях различные экземпляры одного приложения стали более самостоятельными, поэтому каждый экземпляр должен заново регистрировать все свои классы. Перед использованием класс необходимо зарегистрировать ( функция RegisterClassEx). При завершении программы все классы, зарегистрированные в ней, удаляются автоматически, хотя при необходимости их можно удалить и самостоятельно. Отсюда очевидно, что глобальный оконный класс, доступный всем программам, должен быть зарегистрирован динамической библиотекой, постоянно находящейся в памяти. Как это сделать, можно прочитать в Win32 Developer's Reference, тема WNDCLASS. Если же глобальный класс регистрируется программой обычным образом, то это означает, что он будет доступен не только самой программе, но и всем библиотекам, вызванным ею, но никак не другим программам и не другим экземплярам этой программы. Если наоборот, глобальный класс регистрирует DLL, то он становится доступным всем программам, использующим эту DLL. Но в этом случае класс не удаляется автоматически.

При создании окна обязательно указывать его класс. Данный класс должен быть к этому моменту зарегистрирован. В Delphi имя оконного класса для окон, созданных наследниками TForm, всегда совпадает с именем класса Delphi. Существуют предопределённые классы Windows, которые не надо регистрировать. Это 'BUTTON', 'COMBOBOX', 'EDIT', 'LISTBOX', 'MDICLIENT', 'SCROLLBAR' и 'STATIC'. Назначение этих классов понятно из их названий (класс 'STATIC' реализует статические, то есть не реагирующие на мышь и клавиатуру, но имеющие дескриптор элементы, текстовые или графические). Впрочем, можно определить локальный класс с зарезервированным именем, он перекроет глобальный в пределах приложения.

Кроме имени, класс включает в себя другие параметры, такие как стиль, кисть и т. д. Они подробно перечислены в справке по теме WNDCLASS.

Кроме класса нужно указать стиль окна и расширенный стиль. Они определяют поведение конкретного окна и не имеют ничего общего со стилем класса. Возможные значения этих стилей перечислены в справке по функциям CreateWindow и CreateWindowEx. Результатом работы этих функций является дескриптор созданного ими окна.

Функции, создающие окна, требуют указать дескриптор приложения. В Delphi этот дескриптор хранится стразу в двух переменных - MainInstance модуля System и HInstance модуля SysInit. Оба эти модуля автоматически подключаются к любому модулю, созданному в Delphi, так что можно использовать ту или иную переменную по своему вкусу. Кстати, не следует путать автоматическое подключение этих модулей с автоматической генерацией кода IDE Delphi для под-ключения таких модулей, как Windows, Forms, SysUtils и т. д. В первом случае модули подключаются несмотря на то, что не упомянуты в списке uses. Более того, их упоминание там приведёт к ошибке. Во втором случае эти модули явным образом подключаются, просто Delphi автоматически пишет эту часть программы за программиста. Можно написать модуль или даже целую программу, которые не будут использовать SysUtils, но нельзя написать такие, которые не будут использовать System.

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

Другой случай, когда могут понадобиться функции Win API для окон - если приложение должно что-то делать с чужими окнами. Например, хотя бы просто перечислить все окна, открытые в данный момент, как это делает WinSight32. Но в этом случае также не приходится самому создавать окна, работа идёт с уже имеющимися.

Callback функции

Прежде чем двигаться дальше, необходимо разобраться с тем, что такое callback функции. На русский язык это обычно переводится как функции косвенного вызова. Эти функции в программе описываются, но обычно не вызываются напрямую, хотя ничто не запрещает сделать это. В этом они похожи на те методы класса, которые связаны с событиями. Ничто не мешает вызывать напрямую, например, метод FormCreate, но делать это приходится крайне редко. С другой стороны, даже если этот метод не вызывается явно, он всё равно выполняется, потому что VCL автоматически вызывает его без прямого указания программиста. Еще одно общее свойство - конкретное имя метода при косвенном вызове не важно. Можно изменить его, но если этот метод по-прежнему будет связан с событием OnCreate, он так же будет успешно вызываться. Разница заключается только в том, что такие методы вызываются внутренними механизмами Delphi, а callback функции - самой системой Windows. Соответственно, на эти функции налагаются следующие требования: во-первых, эти функции должны быть именно функциями, а не методами класса (впрочем, иногда это условие удаётся обойти); во-вторых, эти функции должны быть написаны в соответствии с моделью вызова stdcall. Справочная система предлагает использовать модель callback, которая в имеющихся версиях Windows совпадает с stdcall. Однако в Delphi такая модель не поддерживается. Что же касается того, как программист сообщает системе о том, что он написал callback функцию, то это в каждом случае по-своему.

Очень часто функции косвенного вызова используются при перечислении некоторых объектов. В качестве примера рассмотрим перечисление окон с помощью функции EnumWindows. В справке она описана так:

BOOL EnumWindows(WNDENUMPROC  lpEnumFunc, LPARAM  lParam);

Соответственно, в Windows.pas она имеет вид

function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

тип TFNWndEnumProc совпадает с типом Pointer. Здесь должен стоять указатель на callback функцию. Синтаксис этой функции описан так:

BOOL CALLBACK EnumWindowsProc(HWND  hwnd, LPARAM  lParam);

Функции с таким именем не существует в Win API. Это так называемый прототип функции, согласно которому следует описывать callback функцию. На самом деле этот прототип предоставляет большую свободу, чем это может показаться на первый взгляд. Как я уже сказал выше, имя может быть любым. Любыми могут быть и типы функции и параметров, при условии что новые типы совпадают по размерам с теми, которые указываются. Что касается типа функции и типа первого параметра, то они имеют определённый смысл и менять их тип практически бессмысленно. Другое дело со вторым параметром. Он предназначен специально для передачи значения, которое программист волен использовать по своему усмотрению, система не имеет на него никаких видов. А программисту может показаться удобнее работать не с типом LPARAM (то есть LongInt), а, например, с указателем или же с массивом из четырёх байт. Лишь бы были именно четыре байта, а не восемь, шестнадцать или ещё какое-то число. Можно даже превратить этот параметр в параметр-переменную, так как при этом функции будут передаваться всё те же четыре байта - адрес переменной. Но эти удовольствия для тех, кто хорошо разбирается с тем, как используется стек для передачи параметров при различных моделях вы-зова.

Как же работает EnumWindows? После вызова эта функция начинает по очереди перебирать все имеющиеся в данный момент окна верхнего уровня, то есть те, у которых нет родителя. Для каждого такого окна вызывается эта самая callback функция, в качестве первого параметра ей передаётся дескриптор данного окна (каждый раз, естественно, новый), в качестве второго - то, что было передано самой функции EnumWindows в качестве второго параметра (каждый раз одно и то же). Что же может делать callback функция с этим дескриптором? А всё, на что у программиста хватит фантазии. Например, можно минимизировать или вообще закрыть все эти окна, хотя не понятно, с чего бы вдруг устраивать такую диверсию. Или можно проверять все эти окна на соответствие какому-то условию, пытаясь найти нужное. А значение, возвращаемое callback функцией, влияет на работу EnumWindows. Если она возвращает False, значит, всё, что нужно, уже сделано, можно не перебирать остальные окна.

Окончательный код для того случая, когда второй параметр имеет тип Pointer, выглядит так:

function MyCallbackFunction(Wnd:HWnd; P: Pointer):Bool; stdcall;
 begin
  {  что-то делаем  }
 end;
..............
var MyPointer:Pointer;
..............
EnumWindows(@MyCallbackFunction, LongInt(MyPointer));

Что бы мы ни делали с типом второго параметра callback функции, тип соответствующего параметра EnumWindows не меняется. Поэтому необходимо явное приведение передаваемого параметра к типу LongInt. Обратное преобразование типов при вызове MyCallbackFunction осуществляется автоматически.

В 16-разрядных версиях Windows вызов callback функций осложнялся тем, что для них необходимо было делать специальный код, называемый прологом. Пролог создавался с помощью функции MakeProcInstance, удалялся после завершения с помощью FreeProcInstance. То есть вызов EnumWindows должен был бы выглядеть так:

var MyProcInstnace: TFarProc;
...................
MyProcInstance := MakeProcInstance(@MyCallbackFunction, HInstance);
EnumWindows(MyProcInstance, LongInt(MyPointer));
FreeProcInstance(MyProcInstance);

В Delphi этот код будет работоспособным, так как для совместимости MyProcInstance и FreeProcInstance оставлены. Но они ничего не делают (в чём легко убедиться, просмотрев ис-ходный файл Windows.pas), поэтому можно обойтись и без них. Другой способ, с помощью которого в 16-разрядных версиях можно сделать пролог - описать функцию с директивой export. Эта директива сохранена для совместимости и в Delphi, но в 32-разрядных версиях она также ничего не делает (несмотря на то, что справка, например, по Delphi 3.0 утверждает обратное; в справке по Delphi 4.3 этой ошибки уже нет).

Сообщения Windows

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

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

В Delphi для реакции на каждое событие обычно создаётся свой метод. В Windows одна процедура, называемая оконной, обрабатывает все сообщения. В языке Си нет понятия <процедура>, поэтому при использовании Паскаля может возникнуть путаница. Дело в том, что то, что называется оконной процедурой, на самом деле является функцией. Тем не менее, я буду использовать общепринятый термин <оконная процедура>. Каждое сообщение имеет свой уникальный номер, а оконная процедура обычно целиком состоит из оператора case, и каждому сообщению соответствует своя альтернатива этого оператора. Номера сообщений учить не надо, потому что можно использовать константы, описанные в модуле Messages.dcu. Эти константы начинаются с префикса, указывающего на принадлежность сообщения к какой-то группе. Например, сообщения общего назначения начинаются с WM_: например, WM_Paint, WM_GetTextLength. Сообщения, специфичные, например, для кнопок, начинаются с префикса BM_. Остальные группы сообщений также связаны либо с теми или иными элементами управления, либо со специальными действиями, например, с динамическим обменом данными (dynamic data exchange, DDE). Обычной программе приходится обрабатывать довольно много сообщений, поэтому оконная процедура бывает, как правило, очень длинной и громоздкой. Оконная процедура описывается программистом как callback функция и указывается при создании оконного класса. Таким образом все окна данного класса имеют одну и ту же оконную процедуру. Впрочем, существует возможность породить так называемый подкласс, то есть новый класс, наследующий все свойства существующего, за исключением оконной процедуры. Несколько подробнее об этом будет сказано далее.

Кроме номера, каждое сообщение содержит два параметра - WParam и LParam. Буквы и означают и , то есть первый параметр 16-разрядный, а второй - 32-разрядный. Однако так было только в старых, 16-разрядных версиях Windows. В 32-разрядных версиях оба параметра 32-разрядные, несмотря на их названия. Конкретный смысл каждого параметра зависит от сообщения. В некоторых сообщениях один или оба параметра могут вообще не использоваться, в других - наоборот, двух параметров даже не хватает. В этом случае один из параметров (обычно LParam) содержит указатель на дополнительные данные. После обработки сообщения оконная процедура должна вернуть какое-то значение. Обычно это значение просто сигнализирует, что сообщение не нуждается в дополнительной обработке, но в некоторых случаях оно более осмысленно, например, WM_SetIcon должно вернуть дескриптор иконки, которая была установлена ранее. Если программист не хочет обрабатывать сообщение самостоятельно, он должен вызвать для его обра-ботки функцию DefWindowProc.

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

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

Кроме параметров WParam и LParam, каждому сообщению приписывается время возникновения и координаты курсора в момент возникновения. Эти параметры можно узнать с помощью функций GetMessagePos и GetMessageTime.

Разумеется, что Delphi предоставляет программисту все средства, необходимые для обработки сообщений. Самый простой способ - описать метод для обработки сообщения с директивой message. Это выглядит примерно так

type TSomeForm = class(TForm)
          ................
          procedure WMSomeMessage(var Message: TMessage); message WM_SomeMessage;
          ................
 
procedure TSomeForm.WMSomeMessage;
 begin
  ..............
  inherited
 end;

Стандартная оконная процедура в Delphi устроена так, что она ищет среди методов класса специальные методы для обработки каждого сообщения. Эти методы во многом подобны обыкновенным виртуальным методам. Другими словами, если переопределить такой метод, будет вы-зван именно новый, а не старый вариант. Вообще говоря, в классе-родителе метода для обра-ботки какого-то конкретного сообщения может и не существовать. Это, однако, никак не сказывается на синтаксисе (в отличие от обычных виртуальных методов, где приходится писать директиву virtual для вновь созданных и override для перекрытых). Кроме того, при перекрытии методов обработки сообщений не важно имя метода, значение имеет только константа, стоящая после message. Именно поэтому при вызове перекрытого метода для обработки данного сообщения достаточно просто написать inherited, без указания имени метода. Такой способ вызова не приведёт к ошибке даже в том случае, если класс-родитель вообще не имел метода для обработки такого сообщения.

Тип TMessage сделан специально для обработки сообщений. Это запись, содержащая 32-разрядные целые поля Msg, WParam, LParam и Result. Первое поле содержит номер сообщения, два следующих - параметры сообщения, а полю Result метод должен присвоить то значение, которое потом вернёт системе оконная процедура. Именно из-за необходимости передавать значение параметр метода обработки сообщения должен быть переменной. При обработке сообщений часто приходится сталкиваться с ситуациями, когда один 32-разрядный параметр используется для передачи двух 16-разрядных значений. Чтобы облегчить программисту работу в таких случаях, тип TMessage описан как вариантная запись, поэтому в нём есть поля WParamLo, WParamHi, LParamLo, LParamHi, ResultLo и ResultHi, имеющие тип Word и дающие доступ к старшему и младшему словам соответствующего параметра.

Так как параметры WParam и LParam могут иметь совершенно различный смысл для разных сообщений, не всегда удобно представлять их в виде чисел. Иногда предпочтительнее, чтобы они имели тип Pointer, или LongBool, или ещё какой-либо. Поэтому тип TMessage - не единственный тип, который может иметь параметр метода обработки сообщения. Для многих сообщений в модуле Messages.dcu описаны собственные типы. Их названия образованы от названия соответствующих сообщений. Например, для сообщения WM_Paint описан тип TWMPaint, для WM_GetText - TWMGetText, и так далее. В этих типах все поля имеют тот тип, который наилучшим образом подходит для обработки именно этого сообщения. Кроме того, поля имеют названия, отражающие их назначения, что делает программу более удобной для чтения. Но такие типы описаны не для всех сообщений, поэтому иногда приходится пользоваться универсальным TMessage. Кстати, если по каким-то причинам в методе обработки сообщения потребуется использовать не тот тип, который используется в соответствующем методе класса-родителя, никаких проблем не возникнет: в данном случае приведение типов выполняется автоматически. Узнать, есть ли специальный тип для данного сообщения, можно двумя способами: либо поискать этот тип в Messages.pas, либо просто проверить, <съест> его компилятор или нет.

Сообщения, определяемые пользователем

Использование сообщений очень удобно в тех случаях, когда нужно заставить окно выполнить какое-то действие. Поэтому Windows предоставляет возможность программисту создавать свои сообщения, которые могут быть локальными или глобальными. Использование локальных сообщений связано с некоторым риском. Дело в том, что эти сообщения должны посылаться только <своим> окнам, то есть тем, оконные процедуры которых написаны так, чтобы правильно интерпретировать это сообщение. Если послать такое сообщение <чужому> окну, его реакция может быть непредсказуемой, потому что человек, писавший его оконную процедуру, мог использовать сообщение с этим же номером для своих целей. Всё это вовсе не значит, что обмен локальными сообщениями возможен только внутри одной программы: если разные программы написаны так, что они правильно понимают одно и то же локальное сообщение, они могут без каких-либо ограничений обмениваться им. Немного повторюсь: важно только чтобы отправитель и получатель сообщения одинаково понимали его. В справочной системе специально указывается, что недопустимо отправлять такие сообщения окнам классов 'BUTTON', 'EDIT', 'LISTBOX' и 'COMBOBOX'.

В Windows (и, соответственно, в модуле Messages.dcu) определена специальная константа WM_User, равная $400 (1024). Впрочем, нет гарантии, что в следующих версиях Windows значение этой константы не изменится. Номера стандартных сообщений лежат в диапазоне от 0 до WM_User-1. Для локальных пользовательских сообщений оставлен диапазон от WM_User до $7FFF (32767). Забегая чуть вперёд, скажу, что для глобальных пользовательских сообщений оставлен диапазон от $C000 до $FFFF (от 49152 до 65535).

Глобальные пользовательские сообщения, называемые также строковыми, предназначены специально для тех случаев, когда локальные сообщения оказываются слишком ненадёжными. Например, может потребоваться написать несколько программ, которые взаимодействуют между собой. Поиск окон, принадлежащих этим программам, можно осуществлять, посылая всем окнам какое-либо специальное сообщение. Те, которые правильно откликнулись - <свои>. Так как сообщения посылаются всем окнам, <чужие> тоже будут его получать. Нужна гарантия, что они никак не отреагируют на такое сообщение. Для этого существует регистрация сообщений, обеспечивающая уникальный номер каждому, кто в нём нуждается.

Прежде чем зарегистрировать сообщение, необходимо придумать ему имя (именно поэтому они называются строковыми). Если давать своим сообщениям осмысленные имена, а не что-то вроде WM_MyMessage1, слишком мала вероятность случайного совпадения. Далее это сообщение регистрируется функцией RegisterWindowMessage, которая возвращает уникальный номер этого сообщения. Если сообщение с таким именем регистрируется впервые, номер выбирается из числа ещё не занятых. Если же сообщение с таким именем уже было зарегистрировано, то возвращается тот же самый номер, который был присвоен ему при первой регистрации. Таким образом разные программы, регистрирующие сообщения с одинаковыми именами, получат одинаковые номера и смогут понимать друг друга. Для прочих же окон это сообщение не будет иметь никакого смысла.

Неудобство использования таких сообщений очевидно - их номера определяются только после начала выполнения программы, при компиляции они ещё неизвестны. Поэтому обработка таких сообщений описанным ранее методом невозможна - мы не знаем, какой номер писать после слова message. Здесь может помочь виртуальный метод WndProc, имеющийся в классе TControl (и в TForm как в его потомке). Этот метод получает все сообщения, поступающие окну. Если перекрыть этот метод, то ничего не мешает сравнивать внутри него номер пришедшего и определённого пользователем сообщения. Например, так:

var WM_MyUserDemoMessage: Cardinal;
.....................
procedure TForm1.FormCreate(Sender: TObject);
 begin
  WM_MyUserDemoMessage := RegisterWndowMessage('WM_MyUserDemoMessage')
 end;
.....................
procedure TForm1.WndProc(var Message: TMessage);
 begin
  if Message.Msg = WM_MyUserDemoMessage then
   begin
    ..............
   end
  else
   inherited WndProc(Message)
 end;

Метод WndProc <первичнее>, чем методы с директивой message. Он раньше получает сообщения, и он же содержит код, который при необходимости ищет и затем вызывает для каждого сообщения соответствующий метод обработки сообщения. И он же вызывает функцию Win API DefWndProc для стандартной реакции на сообщение. Если при перекрытии не вызывать унаследованный метод, то придётся самостоятельно реализовывать эти действия или же подумать, как обойтись без них.

Диапазон номеров сообщений от $8000 (32768) до $BFFF (49151) пока ничем не занят, но зарезервирован Windows для использования в будущем. Авторы Delphi поступили не совсем корректно, использовав верхнюю часть этого диапазона (с адреса $B000 (45046)) для своих собственных сообщений. Именованные константы для этих сообщений находятся в модуле Controls.dcu и начинаются с префикса CM_. Эти сообщения обычно бесполезны для автора готовых программ, но бывают крайне необходимы при написании своих компонентов. Эти сообщения, к сожалению, никак не упомянуты в справке Delphi, поэтому разбираться с ними приходится по исходным файлам VCL.

Особые сообщения

Отправка и обработка некоторых сообщений производится не по общим правилам, а с некоторыми исключениями. Приведённый ниже список таких сообщений не претендует на полноту, но всё-таки может оказаться полезным для начинающего.

Сообщение WM_CopyData используется для передачи блока данных от одного процесса к другому. В 32-разрядных версиях Windows память, выделенная процессу, недоступна для всех остальных процессов. Поэтому просто передать указатель другому процессу нельзя - он не сможет получить доступ к этой области памяти. Для сообщения WM_CopyData приходится делать исключение: блок данных временно становится доступным другому процессу. Это требует определённой синхронности действий от двух процессов, поэтому для отправки этого сообщения можно использовать только SendMessage, прямо вызывающую оконную процедуру. PostMessage использовать нельзя.

Сообщение WM_Paint предназначено для перерисовки клиентской области окна. Если изображение сложное, перерисовка занимает много времени. Чтобы улучшить быстродействие системы, авторы Windows сделали так, что сообщение WM_Paint пропускает все остальные сообщения в очереди, и передаётся окну только тогда, когда в очереди не остаётся никаких других сообщений. Если в очереди оказываются несколько сообщений WM_Paint, они объединяются в одно. Просто так послать сообщение WM_Paint невозможно. Для этого надо сначала объявить, что окно или его часть нуждаются в перерисовке (InvalidateRect, InvalidateRgn).

При обработке сообщений от клавиатуры можно использовать функцию GetKeyState, которая возвращает состояние любой клавиши (нажата-отпущена) в момент возникновения данного события. Именно в момент возникновения, а не в момент вызова функции.

Компоненты, влияющие на обработку событий

Так как стандартные средства Delphi не позволяют использовать все инструменты Win API для работы с окнами, иногда приходится писать компоненты, модифицирующие форму. Мне, например, приходилось писать компоненты, при помещении которых на форму она становится непрямоугольной или полупрозрачной. Очень часто таким компонентам приходится обрабатывать те сообщения, которые предназначены форме-хозяину. Delphi даёт возможность компоненту перехватить сообщения, хотя, на мой взгляд, механизм перехвата оставляет желать лучшего, потому что он не допускает возможности взаимодействия нескольких перехватчиков.

Вместо общего описания алгоритма перехвата я далее просто приведу один из способов сделать это. Способ этот не единственный верный, многие детали можно модифицировать для нужд конкретной задачи, однако основная идея (и основные недостатки) никуда не денутся. Далее я буду предполагать, что компонент перехватывает сообщения владельца (Owner). Что нужно изменить, чтобы он начал перехватывать сообщения родителя (Parent), я скажу чуть позже.

Прежде всего, владелец должен быть окном, поэтому в конструкторе компонента надо проверить класс владельца. Это очень полезно ещё и потому, что тогда в остальных методах объекта можно обойтись без такой проверки, что делает код более эффективным. Чтобы предотвратить создание компонента, достаточно в его конструкторе возбудить какое-либо исключение

Метод компонента не может быть оконной процедурой, потому что методу всегда неявно передаётся <лишний> параметр Self. Поэтому нужна генерация специального кода входа и выхода для того, чтобы вызывать метод вместо оконной процедуры. Этот код генерируется с помощью специальной функции Delphi, которая создаёт в памяти нужный код и возвращает на него указатель. Поэтому компонент должен иметь указатель на этот код (я условно назову этот указатель NewWndProc). Сам метод, обрабатывающий события (условно - HookWndProc) должен иметь один параметр-переменную типа TMessage, и может быть как статическим, тик и виртуальным или динамическим. Кроме того, нужен указатель на старую процедуру, которая была до установки компонента (OldWndProc). Далее, компонент должен содержать два метода для перехвата и освобождения, которые выглядят так:

procedure TMyComponent.HookOwner;
 begin
  if Assigned(Owner) then
   begin
    OldWndProc := Pointer(GetWindowLong(TForm(Owner).Handle, GWL_WndProc));
    NewWndProc := MakeObjectInstance(HookWndProc);
    SetWindowLong(TForm(Owner).Handle, GWL_WndProc, LongInt(NewWndProc))
   end
 end;
 
procedure TMyComponent.UnhookOwner;
 begin
  if Assigned(Owner) and Assigned(OldWndProc) then
   SetWindowLong(TForm(Owner).Handle, GWL_WndProc, LongInt(OldWndProc));
  if Assigned(NewWndProc) then
   FreeObjectInstance(NewWndProc);
  NewWndProc := nil;
  OldWndProc := nil
 end;

Функции Win API GetWindowLong и SetWindowLong предназначены для получения и изменения 32-разрядного значения, связанного с данным окном. В данном случае мы с их помощью работаем с 32-разрядным параметром - адресом оконной процедуры. Изменение адреса оконной процедуры с помощью SetWindowLong и есть то самое порождение оконного подкласса, о котором я писал ранее. Функция MakeObjectInstance - это та самая функция, которая превращает метод в оконную процедуру. FreeObjectInstance освобождает память, выделенную для создания кода входа и выхода функцией MakeObjectInstance.

Было бы глупо перехватывать сообщения и при этом не иметь возможности вызвать ту оконную процедуру, которая была до перехвата. Если необходимо вызвать её для обработки сообщения Msg с параметрами WParam и LParam, нужно воспользоваться следующим кодом:

CallWindowProc(OldWndProc, TForm(Owner).Handle, Msg, WParam, LParam);

Результатом работы этой функции будет число, возвращаемое оконной процедурой.

Вызов процедуры HookOwner я обычно помещаю в самый конец конструктора компонента, UnhookOwner - в самое начало деструктора. Но в некоторых ситуациях VCL Delphi уничтожает окно и вновь создаёт его с новыми свойствами. Это происходит очень быстро, пользователь ничего не замечает. (Такое <пересоздание> формы может потребоваться при изменении во время выполнения свойств FormStyle, BorderStyle и BorderIcons.) Однако VCL ничего не знает о перехвате и поэтому не может корректно удалить его, а уж о восстановлении его потом и речи быть не может. Чтобы избежать такой ситуации, необходимо обрабатывать сообщение CM_RecreateWnd: перед вызовом унаследованного метода для обработки этого события компонент должен снять перехват, после - восстановить его.

Если форма содержит несколько компонентов, перехватывающих сообщения, могут возникнуть конфликты. Снятие и восстановление перехвата через обработку сообщения CM_RecreateWnd безопасно в этом смысле, потому что компоненты обрабатывают это сообщение в порядке, обратном порядку создания. Но если приходится удалять компонент-перехватчик, он не исключает себя из цепочки перехватчиков, а просто обрывает её, и все перехватчики, созданные после него, оказываются не у дел. Именно это я и считаю главным недостатком механизма перехвата.

Если есть необходимость перехватывать сообщения не владельца, а родителя, нужно сделать всё то же самое с точностью до замены Owner на Parent. Но владельца компонент в принципе не может поменять, а вот родителя - вполне. Поэтому нужно ещё перекрыть виртуальный метод SetParent, в котором снимается перехватчик со старого родителя, затем вызывается унаследованный SetParent, затем уже ставится обработчик на нового родителя

Графические функции Win API

Та часть Win API, которая служит для работы с графикой, обычно называется GDI (Graphic Device Interface). Ключевым в GDI является понятие контекста устройства (Device Context, DC). Контекст устройства - это специфический объект, хранящий информацию о возможностях устройства, о способе работы с ним и о разрешённой для изменения области. В Delphi контекст устройства представлен классом TCanvas, свойство Handle которого содержит дескриптор контекста устройства. TCanvas универсален в том смысле, что с его помощью рисование в окне, на принтере или в метафайле выглядит одинаково. То же самое справедливо и для контекста устройства. Разница заключается только в том, как получить в разных случаях дескриптор контекста.

Большинство методов класса TCanvas являются <калькой> с соответствующих и, в большинстве случаев, одноимённых функций GDI. Но в некоторых случаях (прежде всего в методах вывода текста и рисования многоугольников) параметры методов TCanvas имеют более удобный тип, чем функции GDI. Например, метод TCanvas.Polygon требует в качестве параметра открытый массив элементов типа TPoint, а соответствующая функция GDI - указатель на такой массив и число элементов в нём. Это означает, что для массива до вызова функции надо выделить память, а потом - освободить её. Ещё нужен код, который заполнит эту область памяти нужными значениями. И ни в коем случае нельзя ошибаться в количестве элементов массива. Если зарезервировать память для одного числа точек, а при вызове функции указать другое, программа будет работать неправильно. Но для простых функций работа через GDI ничуть не сложнее, чем через TCanvas.

Для получения дескриптора контекста устройства существует много функций. Только для того, чтобы получить дескриптор контекста обычного окна, существуют три функции: BeginPaint, GetDC, GetWindowDC и GetDCEx. Первая из них может использоваться только при обработке сообщения WM_Paint. Вторая даёт контекст клиентской области окна. Третья позволяет получить контекст всего окна, вместе с неклиентской частью. Последняя же позволяет получить контекст определённой области клиентской части окна.

После того, как дескриптор контекста получен, можно воспользоваться преимуществами класса TCanvas. Для этого надо создать экземпляр такого класса, и присвоить его свойству Handle полученный дескриптор. Освобождение ресурсов нужно проводить в следующем порядке: сначала свойству Handle присваивается нулевое значение, затем уничтожается экземпляр класса TCanvas, затем с помощью подходящей функции GDI освободить контекст устройства.

Разумеется, можно вызывать функции GDI при работе через TCanvas. Для этого им просто надо передать в качестве дескриптора контекста Canvas.Handle. Коротко перечислю те возможности GDI, которые разработчики Delphi почему-то не сочли нужным включать в TCanvas: установка прозрачного фона у текста без изменения кисти; рисование кривых Безье; работа с регионами; выравнивание текста по любому углу или по центру; установка собственной координатной системы; получение детальной информации об устройстве; использование геометрических карандашей; вывод текста под углом к горизонтали.

Использование кистей, карандашей и шрифтов в GDI принципиально отличается от того, что привычно в Delphi. Класс TCanvas имеет свойства Brush, Pen и Font, изменение атрибутов которых приводит к выбору того или иного карандаша, шрифта, кисти. В GDI эти объекты самостоятельны, должны создаваться, получать свой дескриптор, <выбираться> в нужный контекст устройства с помощью функции SelectObject и уничтожаться после использования. Причём удалять можно только те объекты, которые не выбраны ни в одном контексте. Есть также несколько стандартных объектов, которые не надо ни создавать, ни удалять. Их дескрипторы можно получить с помощью функции GetStockObject. Чтобы продемонстрировать это, приведу фрагмент программы, рисующей на контексте с дескриптором DC две линии - синюю и красную. В этом фрагменте используется то, что функция SelectObject возвращает дескриптор объекта, родственного выбираемому, который был выбран ранее. Так, при выборе нового карандаша она вернёт дескриптор того карандаша, который был выбран до этого.

SelectObject(DC, CreatePen(PS_Solid, 1, RGB(255, 0, 0)));
MoveToEx(DC, 100, 100, nil);
LineTo(DC, 200, 200);
DeleteObject(SelectObject(DC, CreatePen(PS_Solid, 1, RGB(0, 0, 255))));
MoveToEx(DC, 200, 100, nil);
LineTo(DC, 100, 200);
DeleteObject(SelectObject(DC, GetStockObject(Black_Pen)));

Особым образом следует работать через GDI с растровыми изображениями. Эта тема настолько сложна, что в таком кратком обзоре не стоит и начинать её. Скажу только, что при использовании 24-битных изображений лучше не комбинировать Delphi и GDI. Если передать TBitmap.Handle какой-нибудь функции GDII, у этой картинки иногда портятся последние несколько байт. Так как строки в растровом изображении располагаются снизу вверх, то это приводит к порче правого верхнего угла рисунка. Такой глюк я наблюдал в Delphi 3.0, про остальные версии Delphi ничего сказать не могу.

При переходе на 32-разрядную версию Windows многие функции были исключены из GDI и заменены новыми. Список устаревших функций и соответствующих им новых можно найти в справке в разделе 'Graphics Functions'.

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

Существует одна проблема при работе с метафайлами в Windows 95 (возможно, эта же проблема есть в Windows 98 и NT, но я не проверял). Метафайл создаётся с помощью функции CreateEnhMetaFile. Она возвращает дескриптор контекста метафайла, который можно использовать для рисования. Затем вызывается CloseEnhMetaFile, закрывающая метафайл для рисования, освобождающая контекст устройства и возвращающая дескриптор метафайла. После использования метафайл удаляется функцией DeleteEnhMetaFile, которая освобождает память, связанную с метафайлом, и его дескриптор. Одна из функций, освобождающих дескрипторы, работает неправильно, и дескриптор не освобождается. Если программе часто приходится создавать и уничтожать метафайлы, это быстро приводит к тому, что все дескрипторы оказываются заняты, и система перестаёт работать корректно. Бороться с этим, пользуясь классами TMetafile и TMetafileCanvas, нельзя, потому что они работают через эти же функции.

Работа со строками в Win API

Функции Win API не поддерживают тип string, принятый в Delphi. Они работают со строками, оканчивающимися на #0 (нуль-терминированные строки, null-terminated strings). Это означает, что строкой называется указатель на цепочку символов. Признаком конца такой цепочки является символ #0. Раньше для таких строк использовали термин ASCIIZ. ASCII - обозначение кодировки, Z - zero. Сейчас кодировка ASCII заменена на ANSI, поэтому этот термин больше не применяется, хотя это те же самые по своей сути строки. Обычно программисту приходится работать с кодировкой ANSI, но это не единственная кодировка, поддерживаемая Windows.

В Delphi определён тип PChar, содержащий указатель на такую строку. Если один из параметров функции Win API имеет такой тип, то можно либо передать ему строковую константу, заключённую в одинарные кавычки, как если бы это был тип string, либо выражение PChar(S), где S - параметр типа string, возможно, сложное выражение. Ещё один способ - воспользоваться функциями модуля SysUtils.dcu для работы с нуль-терминированными строками и самостоятельно сформировать строку типа PChar. При этом надо будет самостоятельно выделять и освобождать память для цепочки символов, что обычно приводит только к лишним проблемам. Обычно гораздо проще работать с типом string, и лишь при вызове соответствующей функции преобразовать его к типу PChar. Для любителей оптимизации кода замечу, что такое преобразование не расходует ни память, ни процессорное время, потому что тип string - сам по себе указатель, он указывает именно на строку, завершающуюся нулём, а дополнительная информация, специфическая для типа string, имеет отрицательное смещение относительно этого указателя. Поэтому выражение PChar(S) не приводит к генерации кода, а лишь разрешает компилятору использовать этот указатель в качестве PChar.

Получить строку от функции Win API несколько сложнее, чем передать её. Обычно это делается в несколько этапов. Сначала с помощью функций Win API выясняется, какова длина строки. Затем резервируется место для неё. А только затем вызывается та функция, которая копирует строку в приготовленный буфер. Например, для получения заголовка окна нужно использовать функции GetWindowTextLength и GetWindowText. В некоторых случаях можно облегчить себе жизнь, если существует ограничение на максимальную длину строки. Например, атом не может быть длиннее 255-ти символов. Поэтому можно выделить буфер размером 256 символов (один - для завершающего нуля), и сразу копировать туда атом. В любом случае полученная строка будет нуль-терминированной. Чтобы преобразовать её к обычной, используйте функцию StrPas. Или же можно просто выполнить присвоение S := P, где S - типа string, P - PChar.

Другой тип кодировки, поддерживаемый в Windows, называется Wide. В отличие от ANSI в нём для представления одного символа используется не один, а два байта. Все функции, работающие со строками, написаны в двух модификациях - для ANSI и для Wide. Например, если посмотреть модуль user32, в котором, как утверждает справка, описана функция GetWindowText, то видно, что там нет такой функции. Там есть две другие функции - GetWindowTextA и GetWindowTextW, работающие каждая с соответствующей кодировкой. И это относится ко всем функциям, работающим со строками. К тому имени функции, которое указано в справке, необходимо добавить 'A' или 'W', в зависимости от используемой кодировки.

Разработчики Delphi при написании Windows.pas использовали маленькую хитрость, помогающую начинающему программисту не запутаться. Вот, например, цитата из этого модуля:

function GetWindowTextA(hWnd: HWND; lpString: PAnsiChar; nMaxCount: Integer): Integer; stdcall;
function GetWindowTextW(hWnd: HWND; lpString: PWideChar; nMaxCount: Integer): Integer; stdcall;
function GetWindowText(hWnd: HWND; lpString: PChar; nMaxCount: Integer): Integer; stdcall;
 
{   Это написано в интерфейсной части модуля   }
........................
 
function GetWindowTextA; external user32 name 'GetWindowTextA';
function GetWindowTextW; external user32 name 'GetWindowTextW';
function GetWindowText; external user32 name 'GetWindowTextA';
 
{   А это - в разделе реализации   }

Видно, что функция GetWindowTextA импортируется дважды - один раз под своим настоящим именем, а второй раз - под именем GetWindowText (это и есть тот случай, когда имя функции в библиотеке и то имя, под которым она становится известна компилятору, не совпадают). Поэтому программисту в Delphi нет разницы, писать или , потому что единственное различие у них - тип параметра lpString. Но из исходного текста всё того же модуля видно, что это на самом деле один и тот же тип. По такой же схеме импортируются и все остальные строковые функции Win API.

Заключение

Функции Win API - не такая уж сложная штука. Они часто используют идеологию, не похожую ни на какую другую, но и с этим легко разобраться. Проблема только в том, где и как получить по ним информацию. Будем откровенны: в нашей стране далеко не все, мягко говоря, используют честно купленные программные продукты. Лицензионный Windows сейчас не в диковинку только потому, что его часто устанавливают на новые компьютеры. Лицензионный Delphi приобретают некоторые фирмы. Но много ли людей в России может похвастаться, что они видели документацию по Win API фирмы Microsoft? А эту документацию на русском языке? А ведь авторы западных книг по программированию обычно предполагают, что читателю есть куда заглянуть для справки по этим функциям, и поэтому особенно их не разбирают. Так что нашему программисту доступны следующие пути: по крупицам вытаскивать информацию из тех книг, где Win API упоминается; читать Win32 Develpoer's References; изучать исходные файлы RTL и VCL Delphi; искать информацию в интернете (могу посоветовать сайт http://www.delphikingdom.com). Всё. Если человек не готов часами и даже днями искать информацию о нужной функции, лучше ему не становиться программистом. Главная цель этой статьи - облегчить начало этого поиска. Но дальше человек должен идти сам.

© Григорьев Антон , 1999 Специально для Королевства Delphi

Взято из https://forum.sources.ru

Основы работы с WinAPI