    ГЛАВА         31 

    Работа с интерфейсом Windows API 

    Джо Хечт 

    -- Динамическая компоновка и прототипы функций 
    Изучите библиотеки динамической компоновки Windows и способы создания 
прототипов функций для работы с Windows API. 
    -- Соглашения о вызовах          ; 
    Рассматриваются соглашения     , о вызовах Windows API.           ''. 
    -- Беззнаковые 32-битовые целые 
    Описывается работа с 32битовыми беззнаковыми целыми в Delphi. 
    -- Функции косвенного вызова 
    Приводятся правила работы 
    с функциями косвенного вызова 
    Windows API. 
    -- Передача ввода с клавиатуры     , и мыши в любое окно 
    В этом разделе рассказывается о создании программы, позволяющей направить 
ввод от мыши или клавиатуры к любому активному элементу управления. 

    В те времена, когда компьютеры были дорогими, а Билл Гейтс уверял, что 640 
Кбайт памяти достаточно для выполнения любой программы, которую только может 
вообразить человек, каждая программа содержала в себе весь необходимый код. 
Тогда это было не так важно— ведь все равно в DOS могла выполняться только 
одна программа в один момент времени, и откуда именно загружался код, не имело 
значения. Впрочем, тогда средний компьютер имел 256 Кбайт оперативной памяти и 
два накопителя на гибких дисках. Вы можете не поверить, но первая версия 
Windows создавалась именно для такой конфигурации... Однако уже тогда было 
отмечено, что вовсе ни к чему каждой программе тащить с собой (а программисту 
тратить огромное время на его разработку) код для столь общих и часто 
выполняемых операций, как создание меню, окон, диалогов и т.п. Так родились 
идея библиотек динамической компоновки Windows. 
    Почти все системы Windows построены из библиотек динамической компоновки, 
позволяющих приложениям использовать совместно как код, так и ресурсы. 
Например, файл MORICONS . DLL из поставки Windows представляет собой 
библиотеку DLL, содержащую только ресурсы (множество пиктограмм). Расширение 
DLL не принципиально — в Windows есть множество файлов, по сути своей 
являющихся библиотеками динамической компоновки, с расширениями DRV 
(драйверы), FON (шрифты) и даже ЕХЕ (которые на самом деле не являются 
выполняемыми модулями). 
    Функции, предоставляемые DLL. могут использоваться несколькими 
приложениями одновременно. Более того, в память загружается только та часть 
кода, которая необходима в настоящий момент, и освобождается, как только 
перестанет быть актуальной. 
    Кроме хранения кода и ресурсов, использование DLL имеет ряд других 
преимуществ. Вы можете уменьшить проект, если в нем несколько выполняемых 
модулей, использующих один и тот же код. Если вы выпускаете
   
    716 

обновленную версию своего продукта, достаточно обновить соответствующую DLL 
без создания ЕХЕ-модуля. Использование DLL во время работы программы позволяет 
применять технологию расширения основной программы (plug-ins). 
    Если вы собираетесь использовать в программе вызовы Windows API, научитесь 
работать с DLL, поскольку почти вся Windows построена как набор динамически 
компонуемых библиотек. 

    Динамическая компоновка и прототипы функций 

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

    var NumTicks: Longint; 
    begin 
    NumTicks := GetTickCount; 
    end; 

    В этом примере вызывается функция Windows API GetTickCount, возвращающая 
количество миллисекунд с момента запуска Windows. Заметим, что в Delphi это 
значение становится отрицательным через примерно 24 дня, так как Delphi не 
поддерживает беззнаковые 32-битовые целые и в любой системе через 49 дней 
после запуска Windows произойдет сброс 32-битового счетчика в 0. Обратитесь за 
более детальной информацией к разделу "Беззнаковые 32-битовые целые" этой главы. 
    Компоновка с DLL, у которой точки входа предопределены, известна как 
статическая компоновка (static linking). Обычно описания экспортируемых 
функций в DLL помещаются в отдельный модуль, так что они могут быть вызваны из 
других модулей приложения. Вот фрагмент Windows . раз, содержащий описание 
функции GetTickCount (). 

    unit Windows; 
    interface 
    type DWORD = Integer
    function GetTickCount: DWORD; stdcall; 
    implementation 
    function GetTickCount; external 'kernel32.dll' name 'GetTickCount'; 
    end. 

    Обратите внимание на описание функции GetTickCount: она использует 
соглашение о вызовах stdcall и расположена в DLL-модуле kernel32. Такая 
функция связывается статически, так как ее точка входа определена во время 
создания программы. При статической компоновке для работы программы 
необходимо, чтобы соответствующая DLL находилась в том же каталоге, что и 
программа, или в каталоге WINDOWS, \WINDOWS\SYSTEM или в другом, описанном в 
переменной окружения path. Если DLL не будет найдена, программа не загрузится. 
    Вы можете изменить имя импортируемой функции, описав ее так: 

    function GetTimerTicks; external 'kernel32.dll' name 'GetTickCount'; 

    При этом вы используете в своей программе функцию GetTimerTicks, которая 
фактически указывает на функцию GetTickCount в модуле kernel32. Эта технология 
отображения имен (remapping) довольно часто используется в модулях интерфейса 
Windows. Например, функция IstrlenO в Windows существует в двух вариантах— для 
ANSIсимволов и для строк, состоящих из символов, где на символ приходится 
больше одного байта. Для совместимости в модуле Windows в разделе реализации 
функции описаны следующим образом. 

    function IstrlenA; external 'kernel32' name 'IstrlenA'; 
    function IstrlenW; external 'kernel32' name 'IstrlenW'; 
    function Istrlen; external 'kernel32' name 'IstrlenA'; 

    Такое описание позволяет использовать функцию Istrlen в Delphi для 
обработки ANSI-строк. При использовании версии Delphi, по умолчанию работающей 
с двухбайтовыми символами, будет достаточно изменить в третьей строке 
определения IstrlenA на IstrlenW (это позволит избежать переделки проекта для 
работы с двухбайтовыми символами). 

    Глава 31. Работа с интерфейсом Windows API                            717 

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

    procedure SomeProc(1: Longint); 
    {$IFDEF Win32} stdcall; {$ELSE} far; {$ENDIF} external 'TESTDLL' index 1; 

    Обратите внимание, что здесь описание работает как в 16-, так и в 
32-разр,ядной версии Delphi благодаря использованию директив условной 
компиляции. В данном случае функция определена не по имени, а по порядковому 
номеру (ordinal} вDLL. Импортирование'по порядковому номеру происходит 
быстрое, чем по имени, однако использовать этот способ стоит только при 
гарантии, что номер не изменится в будущих версиях DLL. 
    Динамическую компоновку удобнее использовать вместо статической в тех 
случаях, когда нужен полный контроль над ситуацией при отсутствии DLL. При 
динамической компоновке экономится память за счет того, что DLL загружается 
только при необходимости, а не в процессе загрузки программы и, кроме того, 
позволяет расширять приложение дополнительными модулями без переделки самого 
приложения. При динамической компоновке вы сперва загружаете DLL в память, 
затем определяете адрес интересующей вас функции"и^"ызымете ее. Если DLL 
больше не нужна, просто выгрузите ее из памяти. 
    Однажды я писал приложение, которое должно было использовать специальные 
эффекты для преобразования изображений. Одним из требований было то, что 
эффекты должны были оформляться как модули расширения. При запуске приложения 
просматривался каталог программы и для каждой DLL запрашивалась 
специальная^фун^кция GetPlugInEffectName (). Если таковая находилась, 
программа опознавала DLL и вносила возвращаемое значение (имя эффекта) в 
список эффектов. Когда пользователь выбирал э4)фект, программа знала, в каком 
модуле DLL его искать, и оставалось только загрузить нужную DLL и вызвать 
функцию ApplyEffect (), после чего DLL выгружалась. 
    В следующем примере загружается DLL, находится адрес процедуры, процедура 
выполняется, после чего DLL выгружается из памяти. 

    {$IFDEF Win32} 
    const BadDllLoad = 0; 
    ($ELSE} 
    const BadDllLoad = 32; 
    {$ENDIF} 
    procedure DoEffect(TheBitmap: HBitmap; 
    DllName: pChar); 
    var 
    DllHandle : THandle; 
    EffectProc : procedure(TheBitmap: HBitmap); 
    ($IFDEF Win32) stdcall; ($ENDIF) begin 
    DllHandle := LoadLibrary(DllName); 
    if DllHandle >= BadDllLoad then begin 
    @EffectProc := GetProcAddress(DllHandle, 'ApplyEffect'); 
    if @EffectProc <> nil then EffectProc(TheBitmap); 
    FreeLibrary(DllHandle) ; 
    end; 
    end; 

    В этом примере с помощью директив условной компиляции создан текст, 
совместимый с 16- и 32-разрядной версиями Delphi. В процедуре определены 
дескриптор DLL DllHandle и процедурная переменная EffectProc. Если DLL успешно 
загружена, мы ищем адрес процедуры ApplyEffect и присваиваем его переменной 
EffectProc. Если адрес найден, вызываем процедуру, а затем выгружаем DLL из памяти. 
    Как видите, динамическая компоновка — очень мощный инструмент для 
расширения возможностей приложения, даже если вы уже распространили сам продукт. 

    Соглашения о вызовах 

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

    718                                       Часть VI. Сложный материал 

    В предыдущем примере мы добавили дополнительное описание в случае 
32-раздярного приложения для использования соглашения stdcall, в то время как 
в случае 16-разрядного приложения функция использует pascal-соглашение о 
вызовах. Delphi поддерживает пять типов соглашений о вызовах, четыре из 
которых используются Windows (16разрядная версия поддерживает только два 
соглашения — pascal и cdecl). 
    Кратко опишем каждое из них. 

    cdecl 
    Параметры передаются через стек справа налево, и вызывающий код отвечает 
за очистку стека. Достоинства: функции и процедуры могут получать переменное 
число параметров. 

    pascal 
    Параметры передаются через стек слева направо, вызываемая функция отвечает 
за очистку стека. Достоинства: на 20% быстрее, чем cdecl. 

    stdcall 
    Параметры передаются через стек справа налево, вызываемая функция отвечает 
за очистку стека. Соглашение использует преимущества соглашений cdecl и 
pascal.       " 

    register 
    Известно также как fastcall. До трех первых параметров параметры 
передаются слева направо через регистры CPU ЕАХ, EDX и ЕСХ, остальные 
передаются через стек также слева направо. Вызываемая функция отвечает за 
очистку стека. Достоинства: совмещает скорость соглашения pascal с 
дополнительной скоростью за счет передачи параметров через регистры. 
Вещественные типы данных и указатели на методы никогда не передаются через регистры. 
    Заметим, что 32-разрядные вызовы Windows по умолчанию используют 
соглашение stdcall, а 32-разрядные программы Delphi по умолчанию используют 
вызов register. В случае 16-разрядного кода в обоих случаях используется вызов pascal. 

    safecall 
    То же, что и для stdcall, однако этот вызов помещается в блок try/except и 
используется в OLE. 

    Результаты выполнения функций 

    Большинство соглашений передает результаты стандартно, за исключением 
передачи вещественных значений. Исторически сложилось так, что Borland 
возвращает вещественный тип через вершину стека сопроцессора (ST (0)), в то 
время как Microsoft предпочитает использовать указатель на возвращаемое 
значение. Я полагаю, что при разработке DLL стоит передавать возвращаемое 
значение через переменную-параметр. 

    Исправления WINDOWS.PAS 

    Тысячи констант, типов, функций, процедур и макросов, включенных в Windows 
API, несомненно, приводят программистов в несколько подавленное состояние. К 
тому же может случиться так, что какое-то описание дано с ошибкой. Если в 
вашей поставке Delphi есть исходные тексты VCL (каталог \SOURCE\RTL\WIN), 
можете просмотреть их и убедиться, что интересующие вас функции описаны верно. 
В крайнем случае можете изменить исходный текст, перекомпилировать нужные 
модули (не забудьте при этом добавить в путь к библиотекам в диалоговом окне 
Options каталог RTL\WIN) и затем скопировать вновь созданные модули в свой 
библиотечный каталог, решив тем самым проблему. Обычно это последний путь, 
когда все другие возможности ошибки в вашей программе проверены и отклонены. 
Стоит также убедиться, что ваши функции отображены на корректные функции 
Windows API в тех DLL, где они на самом деле расположены. Случается, что 
функции с одним и тем же именем находятся в разных модулях, и ошибка может 
заключаться в"вызове функции не из того модуля, из которого нужно. Обычно 
такое происходит при вызовах из 16-разрядного кода 32-разрядной функции и наоборот. 
    Возможно, вы столкнетесь с некорректным описанием параметров функций или 
структуры записи или с неудовлетворяющим вас описанием функции. Обычно эту 
ошибку легко устранить, однако следует помнить о согласованности вашего кода и 
VCL. Если вы абсолютно твердо убеждены в наличии ошибки в VCL, исправляйте ее 
и перекомпилируйте модули VCL. Однако если вас не устраивает какая-либо 
функция (например, вам нужно передавать не переменную, а указатель на нее), 
трижды подумайте, семь раз отмерьте, но все равно не режьте, а создайте свою 
локальную функцию-посредника для вызова библиотечной функции.                  

    Глава 31. Работа с интерфейсом Windows API                            719 

    Например, функция GetSystemPaletteEntries () не всегда может подходить для 
ваших нужд, однако ваша программа может не стыковаться с программами других 
программистов, если вы ее измените. Вот ее прототип. 

    function GetSystemPaletteEntries(DC:,HDC; 
    ;')   Startlndex, NumEn tries': UINT; 
    — var PaletteEntries) : UI'NT; stdcall; 

    Обратите внимание, что последний параметр, PaletteEntries, декларирован 
как нетипизированная переменная. Это означает, что вы можете передать как 
параметр любую переменную (компилятор передаст ее адрес в функцию). Теперь 
представим, что вы выделили память динамически и ошибочно передали указатель в 
функцию. Компилятор пропустит его без замечаний, но при выполнении, вероятно, 
у вас возникнут неприятности. Один из способов обойти эту проблему — 
определить локальную версию функции, принимающей указатель вместо переменной. 

    function GetSystemPaletteEntries(DC: нбс; 
    Startlndex, NumEntries: UINT; 
    PaletteEntries: pointer): UINT; 
    ($IFDEF Win32} stdcall; ($ELSE) far; {$ENDIF} external 'gdi32.dll' name 'GetSystemPaletteEntries'; 
    Теперь можете с чистой совестью и ничего не боясь передавать в функцию указатель. 
    procedure TFormI.ButtonlClick(Sender: TObject) ; 
    var 
    dc: hdc ; 
    p : pointer; 
    begin 
    dc := GetDc(0) ; 
    GetMemfp, 256*sizeof(TPaletteEntry) ); 
    GetSystemPaletteEntries(dc,0,256,p) ; 
    ( Что-то делаем с полученной палитрой) 
    FreeMem(p, 256*sizeof(TPaletteEntry)) ; 
    ReleaseDc(O.dc); 
    end; 

    Последнее замечание: если вы нашли ошибку в файле интерфейса с Windows, 
отошлите ее полное описание в Borland (проще всего сделать это, посетив их Web-сервер). 

    Беззнаковые 32-битовые целые 

    Следующая проблема состоит в том, что Delphi не поддерживает работу с 
32-битовыми беззнаковыми целыми числами. Решить ее можно по-разному, включая 
погружение в пучину ассемблера. Я предлагаю перевести 32-битовое беззнаковое 
целое в вещественное (double) и работать с ним, а при необходимости— 
конвертировать обратно. Это весьма удобно, так как вы продолжаете работать с 
значениями переменной, а не с набором битов. 
    Для облегчения жизни здесь представлены две функции, осуществляющие 
конвертирование longint в double и наоборот. Функции корректно преобразуют 
longint в положительный double и наоборот, учитывая, что диапазон 
представления longint — от -2147483648 до 2147483647, а беззнакового целого — 
от 0 до 4294967295. 

    ($IFOPT Q+} 
    {$DEFINE CKOVERFLOW} 
    <$Q-} ($ENDIF) 
    function UlntToDouble(u: longint): double; 
    var 
    d : double; 
    begin 
    if u >= 0 then 
    d := u else 
    if u=-l then b,egin 
    d := $7FFFFFFF; 
    d := d+d+1; 
    end else begin 
    d := $7FFFFFFF; 
    d := d+abs($7FFFFFFF - u) ; 
    end; 


    720                                       Часть VI. Сложный материал 


    result :" d; 
    end; 
    function DoubleToUInt(d: double): longint; 
    var 
    n : double; 
    begin 
    if d<0 then d := Abs; 
    d := Int; 
    n := $7FFFFFFF; 
    n := n+n+2; 
    while d>=n do d := d~n; 
    if d= (n-1) then 
    result :== -1 
    else if d > $7FFFFFFF then 
    result := not ($80000000 - trunc(d-$7FFFFFFF) ) 
    else 
    result := Trunc; 
    end; 
    ($IFDEF CKOVERFLOW} 
    ($UNDEF CKOVERFLOW} 
    {$Q+} 
    {$ENDIF} 
    procedure TFormI.ButtonlClick(Sender: TObject); 
    var 
    NumTicks : longint; 
    RealNumTicks : double; 
    begin 
    {NumTicks может быть и отрицательным.} 
    NumTicks := GetTickCount; 
    (А теперь все будет ОК.} 
    RealNumTicks := UlntToDouble(NumTicks); 
    {Вернем назад.} 
    NumTicks := DoubleToUInt(RealNumTicks); 
    end; 

    В этом примере наши функции оказались внутри бутерброда из директив $IFOPT 
и $IFDEF. Это сделано для отключения проверки (если она включена) на 
переполнение вещественных чисел и включения ее вновь, а также для обеспечения 
корректной и безопасной работы. Обратите внимание на то, что числа, большие 
$7FFPFFFF интерпретируются как отрицательные longint. Внимательно просмотрите 
приведенные тексты (думаю, у вас не должно возникнуть особых вопросов в связи 
с использованием этих функций). Более того, у вас даже остается возможность 
оптимизировать эти функции, так как они написаны для демонстрации принципа 
работы, а не достижения максимального быстродействия. 

    Функции косвенного вызова 

    Функции косвенного вызова (Callback functions) — одна из наиболее полезных 
реализации вызовов в Windows. Вы определяете функцию в приложении, выполняющую 
некоторые действия, и передаете ее адрес в другую функцию, которая и 
осуществляет вызов первой функции, передавая ей параметры, с которыми она 
должна работать. 
    Хороший пример применения функций косвенного вызова — перечисление 
системных шрифтов. Предположим, в вашем приложении есть форма, содержащая 
список, который вы хотите заполнить названиями всех доступных в системе 
шрифтов. Вы создаете функцию, получающую информацию о шрифте в качестве 
параметра и добавляющую его название в список. Затем вы можете вызвать одну из 
системных функций перечисления шрифтов и передать ей адрес своей функции для 
обработки информации о шрифте. Затем ваша функция добавляет название шрифта в 
список и возвращает true, если вам нужны остальные названия шрифтов, или false 
—для прекращения перечисления. 
    Заметьте, что последний параметр, обычно передаваемый в функцию косвенного 
вызова Windows, определен как "определяемые пользователей данные". Обычно он 
называется Iparam и представляет собой 32-битовое значение, определенное 
приложением (вы свободны в выборе передаваемого значения). Это значение будет 
передано в вашу функцию 

    Глава 31. Работа с интерфейсом Windows API                            721 

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

    function FillFontList(Ipelf : PEnumLogFont; 
    Ipntm : PNewTextMetric; 
    FontType: integer; 
    Param : pointer) : integer 
    {$IFDEF Win32) stdcall; {$ELSE} ; export; ($ENDIF) 
    begin 
    TListBox(param^).Items.Add(Ipelfл.elfLogFont.IfFaceName) ; 
    FillFontList := 1; 
    end; 
    procedure TFormI.ButtonlClick(Sender: TObject); 
    var 
    dc : hdc ; 
    fn : pointer; 
    begin 
    dc := GetDc(O) ; 
    fn := MakeProdnstance (QFillFontList, hinstance); 
    EnumFontFamilies(dc, fn, 
    ($IFDEF Win32) 
    longint(@ListBoxl)) ; 
    ($ELSE) 
    OListBoxl) ; 
    {$ENDIF} 
    FreeProcInstance(fn); 
    ReleaseDc(0,dc) ; 
    end; 

    Этот пример начинается с описания функции косвенного вызова FillFontList. 
Данная функция описана так, чтобы ее можно было использовать как с 16-, так и 
с 32-разрядной версиями Windows. Функции передается в качестве параметра Param 
указатель на список, затем к списку добавляется название шрифта и возвращается 
1 (True), чтобы сообщить Windows, что следует продолжать перечисление шрифтов. 
    В методе ButtonlClick определены две переменные типов hdc и pointer, затем 
получен дескриптор Dc рабочего стола и инициализирован указатель функцией 
FillFontList () вызовом функции Windows MakeProcInstance (это необязательно в 
32-разрядно^ мире (функция просто возвращает переданный ей указатель), так как 
адресное пространство DLL автоматически отображается в адресное пространство 
приложения, но в старом 16-разрядном мире все не так просто...). И, наконец, 
мы вызвали функцию EnumFontFamilies (), передав ей в качестве параметров dc, 
для которого требуется перечисление шрифтов, нулевой указатель, говорящий о 
том, что нам требуется огласить весь список, ад-. рее функции косвенного 
вызова и указатель на заполняемый список. В 32-разрядной версии следует 
конвертировать адрес в longint"в связи с изменениями описания функции по 
сравнению с 16-разрядной версией. По окончании работы был освобожден указатель 
на функцию косвенного вызова и dc. 

    Функции перехвата 

    Перехват (hook— крючок, захват. — Прим. перев.) представляет собой функцию 
косвенного вызова, внедренную в систему сообщений Windows для доступа к 
сообщением до того, как их получат другие приложения. Это напоминает перехват 
прерываний резидентными программами времен DOS. И так же, как при перехвате 
прерываний, вероятно, вы захотите не только перехватить сообщение, но и 
вызвать следующий перехватчик в цепочке, чтобы сообщение все же дошло до 
своего адресата (пусть и в искаженном цензорами-перехватчиками виде).          
    Когда вы устанавливаете функцию перехвата с использованием функции Windows 
SetWindowsHook (), вы получаете 32-битовый дескриптор, идентифицирующий ваш 
перехватчик в системе. В отличие от большинства дескрипторов 'Windows, этот 
имеет размер 32 бит как в 32-, так и в 16-разрядных версиях приложения. 
Дескриптор потребуется при вызове следующего перехвата в цепочке вызовов 
функции CallNextHookEx () (конечно, если вы не намерены спрятать это сообщение 
от того, кому оно адресовано). Кроме того, дескриптор понадобится в тот 
момент, когда вы решите, что перехватили достаточно сообщений и хотите 
деактивизировать свой перехватчик вызовом UnHookWindowsHookEx (). Заметьте, 
что все эти функции присутствуют и в 32-, и в 16-разрядной версиях Windows. 

    722                                       Часть VI. Сложный материал 


    Перехваты Windows 

    Windows определяет следующие типы перехватов. 

    WH.CALLWNDPROC 
    Перехватчик вызывается при вызове функции SendMessage () . 

    WH.CALLWNDPROCRET 
    Перехватчик вызывается при возврате из функции SendMessage (). Доступно 
только в Windows 95. 

    WH_CBT 
    Перехватчик вызывается перед активизацией, созданием, удалением, 
сворачиванием, восстановлением, перемещением окна и изменением его размеров, 
перед выполнением системной команды, удалением события мыши или клавиатуры, 
установкой фокуса ввода и синхронизацией системной очереди сообщений. 

    WH.DEBUG 
    Отладочный перехват вызывается перед любым другим перехватом. 

    WH.GETMESSAGE 
    Данный перехват вызывается при получении сообщения из очереди приложения 
функцией GetMessage (). 

    WH.HARDWARE 
    Аппаратный перехват сообщения вызывается при вызове приложением GetMessage 
() или PeekMessage () для события нестандартного аппаратного обеспечения (не 
мыши или клавиатуры). 

    WH.JOURNALRECORD 
    Регистрирующий перехват вызывается при удалении системой сообщения из 
системной очереди сообщений. 

    WH.JOURNALPLAYBACK 
    Воспроизводящий перехват вызывается для вставки сообщений мыши или 
клавиатуры в системную очередь. 

    WH.KEYBOARD 
    Клавиатурный перехват вызывается при вызове приложением GetMessage () или 
PeekMessage () для сообщений WM_KEYDOWN или WM_KEYUP. 

    WH.MOUSE 
    Перехват мыши вызывается при вызове приложением GetMessage () или 
PeekMessage () для сообщений мыши. 

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

    WH.SHELL 
    Этот перехват используется приложениями оболочки для получения уведомлений 
от системы. 

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


    Дополнительные замечания о перехватах 

    Функции перехвата специфичных для приложения сообщений могут находиться 
как в сообщении, так и в DLL, в то время как перехватчики системных сообщений 
должны обязательно находиться в DLL. К сожалению, документация по перехватам 
Windows не всегда полна и корректна и не всегда отражает все причуды очередной 
версии Windows. Хоро- . шим источником информации может служить Microsoft's 
KnowledgeBase на компакт-диске MSDN. В крайнем случае, если ваш перехватчик 
загнал систему в тупик, нажмите <Ctrl+Alt+Del>; это должно помочь удалить все 
установленные системные перехватчики. 

    Глава 31. Работа с интерфейсом Windows API                            723 


    Передача ввода с клавиатуры и мыши в любое окно 

    Теперь — маленький пример использования перехвата Windows, без которого 
глава была бы неполной и оставляла бы у читателя чувство неуверенности в своих 
возможностях и в возможностях автора. Итак, наша задача — отослать из вашего 
приложения ввод от клавиатуры или мыши в любое окно (может пригодиться, если 
SendMessage () по какимлибо причинам для этого не подходит). Размер буфера 
сообщений Windows, в отличие от размеров самой системы, весьма ограничен, и 
его очень легко переполнить. Для того чтобы избежать этой неприятности, 
создадим перехватчик JournalPlayback и позволим Windows самой решать, когда 
можно посылать очередное сообщение. Создадим DLL с перехватчиком и приложение 
для демонстрации его использования. Поскольку одни из вас работают в 
16-разрядной версии, а другие — в 32-разрядной, я добавил несколько строк в 
исходный текст для его универсальности. 
    Создадим буфер сообщений, содержащий сообщения, которые необходимо 
"воспроизвести". Он будет начинаться с заголовочной записи, в которой 
содержатся указатели на первое и последнее сообщения в буфере. Каждое 
сообщение будет содержать само сообщение для отправления и указатель на 
следующее сообщение (односвязный список). Дополнительные функции и их название 
описаны ниже. 

    CreateMsgBuff       Создает новый буфер сообщений 
    Is Playing            Возвращает статус "воспроизведения" 
    AddKeyString        Добавляет строку нажатий клавиш для проигрывания 
    AddMouseMessage     Добавляет сообщения мыши 
    AddVkMessage        Добавляет сообщения от виртуальных клавиш 
    AddVkMsgPair        Добавляет пары KeyUp/KeyDown 
    PlayMouseKeys       "Воспроизводит" сообщения в буфере 
    DeleteMsgBuff       Удаляет буфер сообщений 

    Загрузите проект PLAYKEYS . DPR с дополнительно распространяемой дискеты. 
В листинге 31.1 содержится исходный текст PLAYKEYS . DLL. 
    
    ЛИСТИНГ 31.1. \UDELPHI3\CHP31\BAREBONES.DPR                                
                       :| 
    {$С FIXED PRELOAD PERMANENT) 
    library PLAYKEYS; 
    ($IFDEF Win32} 
    uses Windows, Messages; 
    type 
    TwMessage = Longint; 
    TwParameter = Longint; 
    TIParameter = Longint; 
    {$ELSE} 
    uses WinTypes, WinProcs, Messages; 
    type 
    TwMessage = Word; 
    TwParameter = Word; 
    TIParameter = Longint; 
    {$ENDIF) 
    type PMsgItem = ^TMsgItem; 
    TMsgItem = record 
    Event : TEventMsg; 
    Next : PMsgItem; 
    end; 
    type PMsgHeader = ^TMsgHeader; 
    TMsgHeader = record 
    First : PMsgItem; 


    724                                       Часть VI. Сложный материал 


    Last : PMsgItem; 
    end; 
    var 
    Playing  : LongBool; 
    IpMsg   : pMsgItem; 
    HookHandle : HHook; 
    function PlaybackProc(Code: Integer; 
    wParam: TwParameter; 
    IParam: TIParameter): Longint; 
    {$IFDEF Win32) stdcall; {$ELSE} export; ($ENDIF} 
    begin 
    PlaybackProc := 0; 
    case Code of 
    HC_SKIP : begin 
    {fetch the next message} 
    IpMsg := IpMsg".Next; 
    {are we done?} 
    if IpMsg = nil then 
    if UnHookWindowsHookEx(HookHandle) then 
    Playing := false; 
    exit; 
    end; 
    HC_GETNEXT : begin 
    {play the current message) 
    PEventMsg(IParam)" := IpMsg^.Event; 
    PEventMsg(IParam)/\ .Time := GetTickCount; 
    exit; 
    end; 
    HC_SYSMODALON : begin 
    {system error - let the next hook know about it} 
    CallNextHookEx(HookHandle, Code, wParam, IParam); 
    exit; 
    end; 
    HC_SYSMODALOFF : begin 
    {system error - let the next hook know about it} 
    {note : our hook has been canceled by the system} 
    CallNextHookEx(HookHandle, Code, wParam, IParam); 
    exit; 
    end; 
    end; 
    {request to call the next hook} 
    if code < 0 then 
    PlaybackProc := CallNextHookEx(HookHandle, Code, wParam, IParam); 
    end; 
    function PlayMouseKeys(IpMessages : PMsgHeader): LongBool; 
    {$IFDEF Win32) stdcall; {$ELSE} export; {$ENDIF) begin 
    PlayMouseKeys := false; 
    {avoid data segement corruption} 
    if Playing then exit; 
    Playing := true; 


    Глава 31. Работа с интерфейсом Windows API                            725 


    (is the buffer a valid pointer?) 
    if IpMessages = nil then begin 
    Playing := false; 
    exit; 
    end; 
    {are there messages to play?} 
    if IpMessages".First = nil then begin 
    Playing := false; 
    exit; 
    end; 
    {get the first message) 
    IpMsg := IpMessages".First; 
    {set the hook} 
    HookHandle := SetWindowsHookEx(WH_JOURNALPLAYBACK, 
    PlayBackProc,  ~——-—""""''' 
    hinstance, 
    0); 
    {was there an error setting the hook?} 
    if HookHandle = 0 then begin 
    Playing := false; 
    exit; 
    end; 
    {everything is ok!} 
    PlayMouseKeys := true; 
    end; 
    function IsPlaying : LongBool; 
    ($IFDEF Win32) stdcall; ($ELSE) export; {$ENDIF} 
    begin 
    IsPlaying := Playing; 
    end; 
    procedure CreateMsgBuff(var IpMessages : PMsgHeader); 
    {$IFDEF Win32) stdcall; ($ELSE) export; {$ENDIF} 
    begin 
    {create and the message buffer } 
    GetMem'(lpMessages, sizeof(TMsgHeader)) ; 
    {Was there an error?} 
    if IpMessages = nil then exit; 
    {initialize the message buffer} 
    IpMessages".First := nil; 
    IpMessages".Last := nil; 
    end; 
    procedure DeleteMsgBuff(var IpMessages : PMsgHeader); 
    {$IFDEF Win32} stdcall; {$ELSE} export; {$ENDIF) 
    var 
    Next : PMsgItem; 
    Temp : PMsgItem; 
    begin 
    {is the buffer pointer valid?} 
    if IpMessages = nil then exit; 
    {loop though buffer and destory items} 
    Next := IpMessages".First; 
    while Next <> nil do begin 
    Temp := Next".Next; 
    FreeMem(Next, sizeof(TMsgItem)) ; 


    726                                       Часть VI. Сложный материал 


    Next := Temp; 
    end; 
    {destroy the header record) 
    FreeMem(lpMessages, sizeof(TMsgHeader)); 
    {invalidate the message buffer) 
    IpMessages := nil; 
    end; 
    procedure CreateNextMsgItem(lpMessages : PMsgHeader) ; 
    begin             " 
    (is the buffer pointer valid?) 
    if IpMessages = nil then exit; 
    {is this the first item?) 
    if IpMessages".First = nil then begin 
    GetMemdpMessages" . First, sizeof (TMsgItem) ) ; 
    IpMessages".Last := IpMessages".First; 
    end else begin 
    GetMem(IpMessages".Last".Next, sizeof(TMsgItem)) ; 
    IpMessages".Last := IpMessages".Last".Next; 
    end; 
    (was there an error?) 
    if IpMessages".Last = nil then exit; 
    {initialize the new item with zeros) 
    FillChar(lpMessages".Last", sizeof(TMsgItem), #0); 
    end; 
    function AddVkMessage(IpMessages : PMsgHeader; 
    TheMsg : Word; 
    TheVkKey : Word) : LongBool; 
    ($IFDEF Win32) stdcall; {$ELSE} export; ($ENDIF) 
    begin 
    AddVkMessage := false; 
    (is the buffer pointer valid?) 
    if IpMessages = nil then exit; 
    {create a new message item) 
    CreateNextMsgItem(lpMessages) ; 
    {was there an error?) 
    if IpMessages".Last = nil then exit; 
    (fill in the new message item) 
    IpMessages".Last".Event.Message := TheMsg; 
    IpMessages".Last".Event.ParamL := TheVkKey; 
    IpMessages".Last".Event.ParamH := 
MapVirtualKey(IpMessages".Last".Event.ParamL, 0) ; 
    AddVkMessage := true; 
    end; 
    function AddVkMsgPair(IpMessages : PMsgHeader; 
    ThelstMsg : Word; 
    The2ndMsg : Word; 
    TheVkKey : Word) : LongBool; 
    ($IFDEF Win32) stdcall; {$ELSE} export; ($ENDIF) 
    begin 
    AddVkMsgPair := false; 
    (add the first message) 
    if AddVkMessage(IpMessages, 
    ThelstMsg, 
    TheVkKey) = false then exit; 


    Глава 31. Работа с интерфейсом Windows API  727 


    {add the second message) 
    if AddVkMessage(IpMessages, 
    The2ndMsg, 
    TheVkKey) = false then exit; 
    AddVkMsgPair := true; 
    end; 
    function AddKeyString(IpMessages : PMsgHeader; 
    IpBuffer : PChar) : LongBool; 
    f$IFDEF Win32} stdcall; {$ELSE} export; {$ENDIF} var 
    i : integer; 
    VkCode : word; 
    begin 
    AddKeyString := false; 
    i := 0; 
    (is the character buffer valid?} 
    if IpBuffer = nil then exit; 
    {loop through the character buffer) 
    while IpBuffer[i] <> #0 do begin 
    {get the characters vkKeyCode and shift state) 
    {$IFDEF Win32) 
    VkCode := VkKeyScan(lpBuffer[i]); 
    {$ELSE) 
    VkCode := VkKeyScan(ord(lpBuffer[i])) ; 
    {$ENDIF) 
    (does the character require a shift key down?) 
    if (HiByte(VkCode) and 1) = 1 then 
    if not AddVkMessage(IpMessages, 
    WM_KEYDOWN, 
    VK_SHIFT) then exit; 
    {make the character message) 
    if not AddVkMsgPair(IpMessages, 
    WM_KEYDOWN, 
    WM_KEYUP, 
    vkCode) then exit; 
    (does the character require a shift key up?) 
    if (HiByte(VkCode) and 1) = 1 then 
    if not AddVkMessage(IpMessages, 
    WM_KEYUP, 
    VK_SHIFT) then exit; 
    {point to the next character) 
    Inc(i) ; 
    end; 
    AddKeyString := true; 
    end; 
    function AddMouseMessage(IpMessages : PMsgHeader; 
    Msg : integer; 
    x : Smallint; 
    у : Smallint) : LongBool; 
    ($IFDEF Win32) stdcall; ($ELSE) export; ($ENDIF) 
    begin 
    AddMouseMessage := false; 
    (is the buffer pointer valid?} 
    if IpMessages = nil then exit; 
    {create next message} 


    728                                       Часть VI. Сложный материал 


    CreateNextMsgItem(IpMessages); 
    (was there an error?} 
    if IpMessages^.Last = nil then exit; 
    {fill in the message} 
    IpMessages".Last''.Event.Message := Msg; 
    IpMessages".Last^.Event.ParamL := x; 
    IpMessages"-Last^.Event.ParamH := y; 
    AddMouseMessage := true; 
    end; 
    exports 
    PlayBackProc index 1 name 'PLAYBACKPROC' resident 
    PlayMouseKeys index 2 name ' PLAYMOUSEKEYS"'' resident, 
    IsPlaying index 3 name 'ISPLAYING' resident, 
    CreateMsgBuff index 4 name 'CREATEMSGBUFF' resident, 
    DeleteMsgBuff index 5 name 'DELETEMSGBUFF' resident, 
    AddVkMessage index 6 name 'ADDVKMESSAGE' resident, 
    AddVkMsgPair index 7 name 'ADDVKMSGPAIR' resident, 
    AddKeyString index 8 name 'ADDKEYSTRING' resident, 
    AddMouseMessage index 9 name 'ADDMOUSEMESSAGE' resident; 
    begin 
    Playing := false; 
    end. 

    В первой строке сегмент кода определяется как предварительно загружаемый и 
фиксированный (чтобы в 16разрядной версии его не перемещали в памяти с места 
на место). Затем определяются типы TMsgItem и TMsgHeader для двух версий 
Windows и глобальные переменные. 
    Playing. Флаг, определяющий, выполняется ли макрос в текущий момент. 
    IpMsg. Указатель на текущий буфер сообщений. 
    • TheHook. Дескриптор нашей функции перехвата. 
    После этого определяем функцию перехвата и обрабатываем четыре типа запросов. 
    • HC_SKIP. Получение следующего сообщения. Если сообщений больше нет, 
снимаем перехват. 
    • НС GETNEXT. Воспроизвести текущее сообщение. 
    • HC_SYSMODALON. Произошла системная ошибка и необходимо вызвать следующий 
перехватчик в цепочке, чтобы все перехватчики могли узнать о неприятностях. 
    • HC_SYSMODALOFF. Система удаляет все перехватчики из-за произошедшей 
ошибки. Необходимо вызвать следующий перехватчик в цепочке, чтобы все 
перехватчики могли узнать о том, что их удаляют. 
    Если перехватчик получает значение, которое мы не в состоянии обработать, 
вызываем следующий перехватчик в цепочке и возвращаем полученное от него значение. 
    Функция PlayMouseKeys () устанавливает наш перехватчик. Она получает 
указатель на список сообщений для передачи и устанавливает перехватчик в том 
случае, если буфер корректен и в настоящий момент с ним не работает уже 
установленный перехватчик. Если перехватчик корректно установлен, функция 
возвращает true. 
    Функция IsPlaying () просто возвращает значение true, если перехватчик 
установлен и работает, так что ваше приложение может определить, что передача 
еще не окончена, и отложить передачу очередной порции сообщений до полной 
передачи текущего буфера. Это можно сделать с помощью простейшего фрагмента кода. 

    while IsPlaying do 
    Application.ProcessMessages; 

    Следующие две функции (CreateMsgBuff и DeleteMsgBuff) создают и уничтожают 
буфер сообщений. Первая возвращает nil при ошибке, вторая — при удачном 
освобождении буфера. 
    Процедура CreateNextMsgItem добавляет новое сообщение в список. Поскольку 
она не экспортируется из DLL, можете изменить ее описание так, как вам необходимо. 
    Функции AddVkMessage и AddVkMsgPair добавляют сообщения виртуальных клавиш 
в буфер. Фактически вторая функция дважды вызывает первую для пересылки двух 
сообщений одним вызовом. 
    AddKeyString пересылает серию сообщений WM_KEYDOWN и WM_KEYUP для каждого 
символа в строке. 

    Глава 31. Работа с интерфейсом Windows API                            729 

    Последняя функция, AddMouseMessage, добавляет в список сообщение мыши. 
    Затем мы экспортируем функции как по именам, так и по индексам и 
инициализируем переменную Playing значением false. 


    Взаимодействие с DLL PlayKeys 

    Простейший способ взаимодействия с разработанной DLL — создание 
интерфейсного модуля. При этом, когда мы хотим использовать DLL в нашем 
приложении, мы просто добавляем этот модуль в раздел uses нашей программы и 
используем функции из него. Исходный текст такого модуля приведен в листинге 31.2. 

    ЛИСТИНГ 31.2, \UDELPHI3\CHP31\KEYIMP 

    unit keyimp; 
    interface 
    function PlayMouseKeys(IpMessages : pointer): LongBool; 
    {$IFDEF Win32) atdcall; {$ELSE} far; ($ENDIF) 
    function IsPlaying : LongBool; 
    t$IFDEF Win32} stdcall; ($ELSE) far; ($ENDIF) 
    procedure CreateMsgBuff(var IpMessages : pointer); 
    {$IFDEF Win32} stdcall; {$ELSE} far; ($ENDIF) 
    procedure DeleteMsgBuff(var IpMessages : pointer); 
    {$IFDEF Win32) stdcall; ($ELSE) far; ($ENDIF) 
    function AddVkMessage(IpMessages : pointer; 
    TheMsg : Word; 
    TheVkKey : Word) : LongBool; 
    ($IFDEF Win32) atdcall; ($ELSE) far; {$ENDIF} 
    function AddVkMsgPair(IpMessages : pointer; 
    ThelstMsg : Word; 
    The2ndMsg : Word; 
    TheVkKey : Word) : LongBool; 
    {$IFDEF Win32) stdcall; {$ELSE} far; <$ENDIF) 
    function AddKeyString(IpMessages : pointer; 
    IpBuffer : PChar) : LongBool; 
    {$IFDEF Win32) stdcall; ($ELSE) far; ($ENDIF) 
    function AddMouseMessage(IpMessages : pointer; 
    Msg : integer; 
    x : Smallint; 
    у : Smallint) : LongBool; 
    {$IFDEF Win32) stdcall; {$ELSE} far; ($ENDIF) 
    implementation 
    function PlayMouseKeys; external 'PLAYKEYS' index 2; 
    function IsPlaying; external 'PLAYKEYS' index 3; 
    procedure CreateMsgBuff; external 'PLAYKEYS' index 4; 
    procedure DeleteMsgBuff; external 'PLAYKEYS' index 5; 
    function AddVkMessage; external 'PLAYKEYS' index 6; 


    730                                       Часть VI. Сложный материал 


    function AddVkMsgPair; external 'PLAYKEYS' index 7; 
    function AddKeyString; external 'PLAYKEYS' index 8; 
    function AddMouseMessage; external'PLAYKEYS' index 9; 
    end. 

    Обратите внимание, что мы описали прототипы функций в разделе интерфейса, 
а их входные точки в DLL — в разделе реализации.                               

    Использование PlayKeys 

    Использовать разработанную DLL весьма просто. Сначала необходимо 
установить фокус_ввода_в_получающее окно или управляющий элемент (если это 
VCL-компонент, вызовите функцию setfocus). Если вы хотите отослать ввод в 
окно, используйте функцию Windows FindWindow, а затем передайте полученный 
дескриптор функции BringWindowToTop для активизации окна. Независимо от этого 
вызовите Application. ProcessMessage. Убедившись, что нужное окно или элемент 
управления получил фокус ввода, вызовите CreateMsgBuff для создания буфера и 
поместите в него необходимые сообщения. Создав список сообщений, убедитесь, 
что они не "воспроизводятся" в настоящий момент вызовом IsPlaying, и с чистой 
совестью вызывайте функцию PlayMouseKeys (). Теперь вы должны дождаться, пока 
все сообщения не будут переданы, вызывая для этого цикл while IsPlaying do 
Application. ProcessMessages;. Последнее, что нужно сделать, — освободить 
буфер, вызвав DeleteMsgBuff (). Вот пример такой передачи строки: 

    procedure TFormI.ButtonlClick(Sender: TObject); 
    var 
    p: pointer; 
    begin 
    {Устанавливаем фокус ввода.} 
    Memol.SetFocus; 
    Application.ProcessMessages ; 
    (Создаем буфер сообщений.) 
    CreateMsgBuff(p) ; 
    (Добавляем строку в буфер сообщений.} 
    AddKeyString(p,'Now Is The Time!'); 
    (Ждем окончания передачи, если таковая выполняется в настоящий момент.} 
    while Is Playing do 
    Application.ProcessMessages; 
    (Начинаем передачу.} 
    PlayMouseKey(p); 
    (Ждем окончания передачи.) 
    while Is Playing do 
    Application.ProcessMessages; 
    (Удаляем буфер.} 
    DeleteMsgBuff(p) ; 
    end; 

    Конечно, метод передачи компоненту Memo строки с помощью нашей DLL кажется 
несколько диким и странным, но не надо забывать о том, что это лишь пример и 
что настоящая сила нашего метода — в возможности эмулировать события наподобие 
нажатия клавиш <Alt+Shift*Aa: .и <PrintScreen>. В приведенном ниже примере 
активизируется меню DelpHi, выбираются пункт Help и команда About и отсылается 
посдедявательност.ь клавиш. <ТЕДМ> при нажатой клавише <Alt>. Не пожалейте 
времени и посмотрите,'что из ЭТоТо'получится! Сначала используйте FindWindow 
() и BringWindowToTop () для активизации главного окна Delphi, а затем 
сделайте следующее. 

    (Активизируем меню, нажав и отпустив клавишу <Alt>.) AddVkMsgPair(p, 
WMJ3YSKEYDOWN, WM_SYSKEYUP, VK_MENU) ; 


    Глава 31. Работа с интерфейсом Windows API                            731 


    {Вводим 'Н' для выбора меню Help 
    и 'А' — для вызова диалогового окна About. ) 
    AddKeyString(p,'НА'); 
    {Нажимаем клавишу <Alt>.} 
    AddVkMessagefp, WM_SYSKEYDOWN, VK_MENU); 
    (Вводим 'team'.} 
    AddKeyString(p,'team' ) ; 
    {Отпускаем клавишу <Alt>.) 
    AddVkMessagefp, WM_SYSKEYUP, VK_MENU); 

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

    procedure TFormI.ButtonlClick(Sender: TObject) ; 
    var 
    pt : TPoint; 
    p : pointer; 
    begin 
    (Создаем буфер.} 
    CreateMsgBuff(p) ; 
    (Получаем координаты центра кнопки.) 
    pt.x := Button2.Left + (Button2.Width div 2); 
    pt.y := Button2.Top + (Button2.Height div 2); 
    (Получаем экранные координаты.) 
    pt := ClientToScreen(pt); 
    (Добавляем сообщения мыши.} 
    AddMouseMessage(p,WM_MOUSEMOVE,pt.x,pt.у); 
    AddMouseMessage(p,WM_LBUTTONDOWN,pt.x,pt.y); 
    AddMouseMessage(p,WM_LBUTTONUP,pt. x, pt. y) ; 
    (Ждем окончания передачи, если она выполняется.) 
    while IsPlaying do 
    Application.PrосеssMessages; 
    ("Проигрываем" наши сообщения.} 
    PlayMouseKeys(p); 
    (Ждем окончания передачи.} 
    while IsPlaying do 
    Application.ProcessMessages;. 
    (Удаляем буфер.) 
    DeleteMsgBuff(p) ; 
    end; 

    В этом примере координаты в форме переведены в экранные координаты, так 
как сообщения мыши должны содержать именно их. 

    Резюме 

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

    732                                       Часть VI. Сложный материал 
