-------
| Библиотека iknigi.net
|-------
|  Александр Анатольевич Чиртик
|
|  Программирование в Delphi. Трюки и эффекты
 -------

   Александр Анатольевич Чиртик
   Программирование в Delphi. Трюки и эффекты


   Введение

   В настоящее время количество книг, посвященных различным языкам программирования, настолько велико, что иногда просто не знаешь, какую выбрать. Цель этой книги – не просто тривиальное изложение материала о Delphi. Она поможет вам получить опыт в решении многих задач. В итоге вы получите необходимый базис знаний, который даст возможность легко и быстро усваивать что-то новое. Здесь вы найдете ответы на вопросы, которые возникают у большинства людей при разработке своих собственных приложений. Вам больше не придется задумываться над тем, как решать мелкие задачи, которые составляют значительную часть повседневной работы большинства программистов. У вас появится возможность тратить больше времени именно на основную цель, поставленную перед вами, а не на второстепенную.
   Данная книга рассчитана на читателей, которые уже имеют некий опыт в программировании, причем достаточный, чтобы не излагать тривиальные вещи заново. Однако сразу отмечу, пусть даже вы делаете свои первые шаги на пути к написанию приложений на высоком уровне, – книга окажет вам неоценимую помощь. Она построена так, чтобы вы смогли с высокой степенью эффективности узнавать новый материал. В конце книги есть приложения в удобном для восприятия виде. В них вы найдете информацию, которая часто используется при написании программ.
   Зачастую люди выбирают Delphi за его простоту. Она подкупает начинающих, которые хотят почти сразу писать программы, а не разбираться в особенностях синтаксиса языка. Простота в совокупности с мощью дают вам целый набор инструментов для воплощения задуманного. Однако запомните: чтобы научиться хорошо программировать, недостаточно иметь огромный объем теоретических знаний, хотя и он немаловажен. Следует научиться думать в концепции выбранного вами языка, и тогда вас ждет успех. Ведь не понимая, зачем все это нужно, вы не сможете эффективно воспользоваться ресурсами языка для наиболее удачного решения поставленных задач.
   В этой книге описано множество примеров. Есть как относительно простые, так и довольно сложные, но пусть последнее вас не пугает: к тому моменту, когда вы начнете их рассматривать, они не покажутся вам особенно трудными.


   От издательства

   Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты gromakovski@minsk.piter.com (издательство «Питер», компьютерная редакция).
   Мы будем рады узнать ваше мнение!
   Все примеры, приведенные в книге, вы можете найти по адресу http://www.piter.com/download.
   На сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах.


   Глава 1
   Окна


   • Привлечение внимания к приложению
   • Окно приложения
   • Полупрозрачные окна
   • Окна и кнопки нестандартной формы
   • Немного о перемещении окон
   • Масштабирование окон
   • Добавление команды в системное меню окна
   • Отображение формы поверх других окон

   Почему было решено начать книгу именно с необычных приемов использования оконного интерфейса? Причиной стало то, что при работе с операционной системой Windows мы видим окна постоянно и повсюду (отсюда, собственно, и название этой операционной системы). Речь идет не только об окнах приложений, сообщений, свойств: понятие о таких окнах есть у любого начинающего пользователя Windows.
   В своих собственных окнах рисуются и элементы управления (текстовые поля, панели инструментов, таблицы, полосы прокрутки, раскрывающиеся списки и т. д.). Взгляните на интерфейс, например, Microsoft Word. Здесь вы увидите, что даже содержимое документа находится в своем собственном окне с полосами прокрутки (правда, это необязательно элемент управления). Окна элементов управления отличаются от «самостоятельных» окон (упрощенно) отсутствием стиля, позволяющего им иметь заголовок, а также тем, что они являются дочерними по отношению к другим окнам. Понимание этого является важным, так как на нем основана часть примеров данной главы.
   Рассматриваемые примеры частично используют средства, предусмотренные в Borland Delphi, а частично – возможности «чистого» API (см. гл. 2). Практически все API-функции работы с окнами требуют задания параметра с типом значения HWND – дескриптора окна. Это уникальное значение, идентифицирующее каждое существующее в текущем сеансе Windows окно. В Delphi дескриптор окна формы и элемента управления хранится в параметре Handle соответствующего объекта.
   Нужно также уточнить, что в этой главе термины «окно» и «форма» употребляются как синонимы, когда речь идет о форме. Когда же речь идет об элементах управления, то так и говорится: «окно элемента управления».


   Привлечение внимания к приложению


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


   Инверсия заголовка окна

   Вероятно, вы не раз могли наблюдать, как некоторые приложения после выполнения длительной операции или при возникновении ошибки как бы подмигивают. При этом меняется цвет кнопки приложения на Панели задач, а состояние открытого окна меняется с активного на неактивное. Такой эффект легко достижим при использовании API-функции FlashWindow или ее усовершенствованного, но более сложного варианта – функции FlashWindowEx.

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

   Первая из этих функций позволяет один раз изменить состояние заголовка окна и кнопки на Панели задач (листинг 1.1).
 //-- Листинг 1.1. Простая инверсия заголовка окна --// 
   procedure TForm1.cmbFlashOnceClick(Sender: TObject);
   begin
   FlashWindow(Handle, True);
   end;

   Как видите, функция принимает дескриптор нужного окна и параметр (тип BOOL) инверсии. Если значение флага равно T rue, то состояние заголовка окна изменяется на противоположное (из активного становится неактивным и наоборот). Если значение флага равно False, то состояние заголовка окна дважды меняет свое состояние, то есть восстанавливает свое первоначальное значение (активно или неактивно).
   Более сложная функция FlashWindowEx в качестве дополнительного параметра (кроме дескриптора окна) принимает структуру FLASHWINFO, заполняя поля которой можно настроить параметры мигания кнопки приложения и/или заголовка окна.
   В табл. 1.1 приведено описание полей структуры FLASHWINFO.
 //-- Таблица 1.1. Поля структуры FLASHWINFO --// 
   Значение параметра dwFlags формируется из приведенных ниже флагов с использованием операции побитового ИЛИ:
   • FLASHW_CAPTION – инвертирует состояние заголовка окна;
   • FLASHW_TRAY – заставляет мигать кнопку на Панели задач;
   • FLASHW_ALL – сочетание FLASHW_CAPTION и FLASHW_TRAY;
   • FLASHW_TIMER – периодически измененяет состояния заголовка окна и/или кнопки на Панели задач до того момента, пока функция FlashWindowEx не будет вызвана с флагом FLASHW_STOP;
   • FLASHW_TIMERNOFG – периодически измененяет состояния заголовка окна и/или кнопки на Панели задач до тех пор, пока окно не станет активным;
   • FLASHW_STOP – восстанавливает исходное состояние окна и кнопки на Панели задач.
   Далее приведены два примера использования функции FlashWindowEx.
   В первом примере состояние заголовка окна и кнопки на Панели задач изменяется десять раз в течение двух секунд (листинг 1.2).
 //-- Листинг 1.2. Десятикратная инверсия заголовка окна --// 
   procedure TForm1.cmbInverse10TimesClick(Sender: TObject);
   var
   fl: FLASHWINFO;
   begin
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Handle;
   fl.dwFlags:= FLASHW_CAPTION or FLASHW_TRAY; //аналогично FLASHW_ALL
   fl.uCount:= 10;
   fl.dwTimeout:= 200;
   FlashWindowEx(fl);
   end;

   Второй пример демонстрирует использование флагов FLASHW_TIMER и FLASHW_ STOP для инверсии заголовка окна в течение заданного промежутка времени (листинг 1.3).
 //-- Листинг 1.3. Инверсия заголовка окна в течение определенного промежутка времени --// 
   //Запуск процесса периодической инверсии заголовка
   procedure TForm1.cmbFlashFor4SecClick(Sender: TObject);
   var
   fl: FLASHWINFO;
   begin
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Handle;
   fl.dwTimeout:= 200;
   fl.dwFlags:= FLASHW_ALL or FLASHW_TIMER;
   fl.uCount:= 0;
   FlashWindowEx(fl);
   Timer1.Enabled:= True;
   end;
   //Остановка инверсии и заголовка
   procedure TForm1.Timer1Timer(Sender: TObject);
   var
   fl: FLASHWINFO;
   begin
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Handle;
   fl.dwFlags:= FLASHW_STOP;
   FlashWindowEx(fl);
   Timer1.Enabled:= False;
   end;

   В данном примере используется таймер, срабатывающий каждые четыре секунды. Таймер первоначально неактивен. Конечно, можно было бы не использовать его, а просто посчитать количество инверсий, совершаемых в течение требуемого интервала времени (в данном случае четырех секунд) и задать его в поле uCount. Но приведенный пример предназначен именно для демонстрации использования флагов FLASHW_TIMER и FLASHW_STOP.


   Активизация окна

   Теперь рассмотрим другой, гораздо более гибкий способ привлечения внимания к окну приложения. Он базируется на использовании API-функции SetForegroundWindow. Данная функция принимает один единственный параметр – дескриптор окна. Если выполняется ряд условий, то окно в заданным дескриптором будет выведено на передний план, и пользовательский ввод будет направлен в это окно. Функция возвращает нулевое значение, если не удалось сделать окно активным.
   В приведенном ниже примере окно активизируется при каждом срабатывании таймера (листинг 1.4).
 //-- Листинг 1.4. Активизация окна --// 
   procedure TForm1.Timer1Timer(Sender: TObject);
   begin
   SetForegroundWindow(Handle);
   end;

   В операционных системах старше Windows 95 и Windows NT 4.0 введен ряд ограничений на действие функции SetForegroundWindow. Приведенный выше пример как раз и является одним из случаев недружественного использования активизации окна – но это всего лишь пример.
   Чтобы активизировать окно, процесс не должен быть фоновым либо должен иметь право устанавливать активное окно, назначенное ему другим процессом с таким правом, и т. д. Все возможные нюансы в пределах одного трюка рассматривать не имеет смысла. Стоит отметить, что в случае, когда окно не может быть активизировано, автоматически вызывается функция FlashWindow для окна приложения (эта функция заставляет мигать кнопку приложения на Панели задач). Поэтому даже при возникновении ошибки при вызове функции SetForegroundWindow приложение, нуждающееся во внимании, не останется незамеченным.



   Окно приложения

   Обратите внимание на то, что название приложения, указанное на кнопке, расположенной на Панели задач, совпадает в названием проекта (можно установить на вкладке Application окна Project options, вызываемого командой меню Project ► Options). Но это название не совпадает с заголовком главной формы приложения. Взгляните на приведенный ниже код, который можно найти в DPR-файле (несущественная часть опущена).

   program ...
   begin
   Application.Initialize;
   Application.CreateForm(TForm1, Form1);
   Application.Run;
   end;

   В конструкторе класса TApplication, экземпляром которого является глобальная переменная Application (ее объявление находится в модуле Forms), происходит неявное создание главного окна приложения. Заголовок именно этого окна отображается на Панели задач (кстати, этот заголовок можно также изменить с помощью свойства Title объекта Application). Дескриптор главного окна приложения можно получить с помощью свойства Handle объекта Application.
   Главное окно приложения делается невидимым (оно имеет нулевую высоту и ширину), чтобы создавалась иллюзия его отсутствия и можно было считать, что главной является именно форма, создаваемая первой.
   Для подтверждения вышесказанного можно отобразить главное окно приложения, используя следующий код (листинг 1.5).
 //-- Листинг 1.5. Отображение окна приложения --// 
   procedure TForm1.Button1Click(Sender: TObject);
   begin
   SetWindowPos(Application.Handle, 0, 0, 0, 200, 100,
   SWP_NOZORDER or SWP_NOMOVE);
   end;

   В результате использования этого кода ширина окна станет равной 200, а высота 100, и вы сможете посмотреть на главное окно. Кстати, можно заметить, что при активизации этого окна (например, щелчке кнопкой мыши на заголовке) фокус ввода немедленно передается созданной первой, то есть главной, форме.
   Теперь должно стать понятно, почему не мигала кнопка приложения при применении функций FlashWindow или FlashWindowEx к главной форме приложения. Недостаток этот теперь можно легко устранить, например, следующим образом (листинг 1.6).
 //-- Листинг 1.6. Мигание кнопки приложения на Панели задач --// 
   procedure TForm1.Button2Click(Sender: TObject);
   var
   fl: FLASHWINFO;
   begin
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Application.Handle;
   fl.dwFlags:= FLASHW_ALL;
   fl.uCount:= 10;
   fl.dwTimeout:= 200;
   FlashWindowEx(fl);
   end;

   В данном случае одновременно инвертируется и заголовок окна приложения. Убедиться в этом можно, предварительно выполнив код листинга 1.5. Наконец, чтобы добиться одновременного мигания кнопки приложения на Панели задач и заголовка формы (произвольной, а не только главной), можно выполнить следующий код (листинг 1.7).
 //-- Листинг 1.7. Мигание кнопки приложения и инверсия заголовка формы --// 
   procedure TForm1.Button3Click(Sender: TObject);
   var
   fl: FLASHWINFO;
   begin
   //Мигание кнопки
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Application.Handle;
   fl.dwFlags:= FLASHW_TRAY;
   fl.uCount:= 10;
   fl.dwTimeout:= 200;
   FlashWindowEx(fl);
   //Инверсия заголовка
   fl.cbSize:= SizeOf(fl);
   fl.hwnd:= Handle;
   fl.dwFlags:= FLASHW_CAPTION;
   fl.uCount:= 10;
   fl.dwTimeout:= 200;
   FlashWindowEx(fl);
   end;

   В данном случае инвертируется заголовок формы Form1. Кнопка на Панели задач может не только мигать, но и, например, быть скрыта или показана, когда в этом есть необходимость. Так, для скрытия кнопки приложения можно применить API-функцию ShowWindow:

   ShowWindow(Application.Handle, SW_HIDE);

   Чтобы показать кнопку приложения, можно функцию ShowWindow вызвать с равным SW_NORMAL вторым параметром.


   Полупрозрачные окна

   В Windows 2000 впервые появилась возможность использовать прозрачность окон (в англоязычной документации такие полупрозрачные окна называются Layered windows). Сделать это можно, задав дополнительный стиль окна (о назначении и использовании оконных стилей вы можете узнать из материалов, представленных в гл. 2). Здесь не будет рассматриваться использование API-функций для работы с полупрозрачными окнами, так как их поддержка реализована для форм Delphi. Соответствующие свойства включены в состав класса TForm.
   • AlphaBlend – включение или выключение прозрачности. Если параметр имеет значение True, то прозрачность включена, если False – выключена.
   • AlphaBlendValue – значение, обратное прозрачности окна (от 0 до 255). Если параметр имеет значение 0, то окно полностью прозрачно, если 255 – непрозрачно.
   Значения перечисленных свойств можно изменять как с помощью окна Object Inspector, так и во время выполнения программы (рис. 1.1).
   Рис. 1.1. Свойства для настройки прозрачности в окне Object Inspector

   На рис. 1.2 наглядно продемонстрировано, как может выглядеть полупрозрачное окно (форма Delphi).
   Рис. 1.2. Форма с коэффициентом прозрачности, равным 14 %

   В качестве примера ниже показано, как используются свойства AlphaBlend и AlphaBlendValue для задания прозрачности окна во время выполнения программы (сочетание положения ползунка tbAlpha, состояния флажка chkEnableAlpha и подписи lblCurAlpha на форме, представленной на рис. 1.2) (листинг 1.8).
 //-- Листинг 1.8. Динамическое изменение прозрачности окна --// 
   procedure TForm1.chkEnableAlphaClick(Sender: TObject);
   begin
   AlphaBlendValue:= tbAlpha.Position;
   AlphaBlend:= chkEnableAlpha.Checked;
   end;
   procedure TForm1.tbAlphaChange(Sender: TObject);
   var
   pos, perc: Integer;
   begin
   pos:= tbAlpha.Position;
   //Новое значение прозрачности
   AlphaBlendValue:= pos;
   //Обновим подпись под ползунком
   perc:= pos * 100 div 255;
   lblCurAlpha.Caption:= IntToStr(pos) + 'из 255 ('+IntToStr(perc) + '%)';
   end;

   Применив следующий код, можно реализовать довольно интересный эффект постепенного исчезновения, а затем появления формы (листинг 1.9).
 //-- Листинг 1.9. Исчезновение и появление формы --// 
   implementation
   var
   isInc: Boolean; //Если True, то значение AlphaBlend формы
   //увеличивается, если False, то уменьшается
   //(форма скрывается)
   procedure TForm1.cmbHideAndShowClick(Sender: TObject);
   begin
   if AlphaBlend then chkEnableAlpha.Checked:= False;
   //Включаем прозрачность (подготовка к плавному скрытию)
   AlphaBlendValue:= 255;
   AlphaBlend:= True;
   Refresh;
   //Запускаем процесс скрытия формы
   isInc:= False;
   Timer1.Enabled:= True;
   end;
   procedure TForm1.Timer1Timer(Sender: TObject);
   var val: Integer;
   begin
   if not isInc then
   begin
   //"Растворение " окна
   val:= AlphaBlendValue;
   Dec(val, 10);
   if val <= 0 then
   begin
   //Окно полностью прозрачно
   val:= 0;
   isInc:= True;
   end
   end
   else begin
   //Появление окна
   val:= AlphaBlendValue;
   Inc(val, 10);
   if val >= 255 then
   begin
   //Окно полностью непрозрачно
   val:= 255;
   Timer1.Enabled:= False; //Процесс закончен
   AlphaBlend:= False;
   end
   end;
   AlphaBlendValue:= val;
   end;

   Единственная сложность (если это можно назвать сложностью) приведенного в листинге 1.9 алгоритма кроется в использовании таймера (Timerl) для инициирования изменения прозрачности окна. Так сделано для того, чтобы окно могло принимать пользовательский ввод, даже когда оно скрывается или постепенно показывается, и чтобы приложение не «съедало» все ресурсы на относительно слабой машине. Попробуйте сделать плавное изменение прозрачности в простом цикле, запустите его на каком-нибудь Pentium III 600 МГц без навороченной видеокарты – и сами увидите, что станет с бедной машиной.
   Грамотное, а главное, уместное использование прозрачности окон может значительно повысить привлекательность интерфейса приложения (взгляните хотя бы на Winamp 5 при включенном параметре прозрачности окон).


   Окна и кнопки нестандартной формы


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


   Регионы. Создание и использование

   Рассматриваемые эффекты по изменению формы окон базируются на использовании регионов (областей) отсечения – в общем случае сложных геометрических фигур, ограничивающих область рисования окна. По умолчанию окна (в том числе и окна элементов управления) имеют область отсечения, заданную прямоугольным регионом с высотой и шириной, равной высоте и ширине самого окна.
   Однако использование прямоугольных регионов для указания областей отсечения совсем не обязательно. Использование отсечения по заданному непрямоугольному региону при рисовании произвольного окна наглядно представлено на рис. 1.3: а – исходный прямоугольный вид формы; б – используемый регион, формирующий область отсечения; в – вид формы, полученный в результате рисования с отсечением по границам заданного региона.
   Рис. 1.3. Использование области отсечения при рисовании окна

   Рассмотрим операции, позволяющие создавать, удалять и модифицировать регионы.
 //-- Создание и удаление регионов --// 
   Создать несложные регионы различной формы можно с помощью следующих API-функций:

   function CreateRectRgn(p1, p2, p3, p4: Integer): HRGN;
   function CreateEllipticRgn(p1, p2, p3, p4: Integer): HRGN;
   function CreateRoundRectRgn(p1, p2, p3, p4, p5, p6: Integer): HRGN;

   Все перечисленные здесь и ниже функции создания регионов возвращают дескриптор GDI-объекта «регион». Он впоследствии и передается в различные функции, работающие с регионами.
   Первая из приведенных функций (CreateRectRgn) предназначена для создания регионов прямоугольной формы. Параметры этой функции необходимо толковать следующим образом:
   • p1 и p2 – горизонтальная и вертикальная координаты левой верхней точки прямоугольника;
   • p3 и p4 – горизонтальная и вертикальная координаты правой нижней точки прямоугольника.
   Следующая функция (CreateEllipticRgn) предназначена для создания региона в форме эллипса. Параметры этой функции – координаты прямоугольника (аналогично функции CreateRectRgn), в который вписывается требуемый эллипс.
   Третья функция (CreateRoundRectRgn) создает регион в виде прямоугольника с округленными углами. При этом первые четыре параметра функции аналогичны соответствующим параметрам функции CreateRectRgn. Параметры p5 и p6 – ширина и высота сглаживающих углы эллипсов (рис. 1.4).
   Рис. 1.4. Округление прямоугольника функцией CreateRoundRectRgn

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

   function CreatePolygonRgn(const Points; Count, FillMode: Integer): HRGN;

   Функция CreatePolygonRgn использует следующие параметры:
   • Points – указатель на массив записей типа TPoint, каждый элемент которого описывает одну вершину многоугольника (координаты не должны повторяться);
   • Count – количество записей в массиве, на который указывает параметр Points;
   • FillMode – способ заливки региона (в данном случае определяет, попадает ли внутренняя область многоугольника в регион).
   Параметр FillMode принимает значения WINDING (попадает любая внутренняя область) и ALTERNATE (попадает внутренняя область, если она находится между нечетной и следующей четной сторонами многоугольника).

   Примечание
   При создании регионов с помощью любой из указанных выше функций координаты точек задаются в системе координат того окна, в котором предполагается использовать регион. Так, если у вас есть кнопка размером 40 х 30 пикселов, левый верхний угол которой расположен на форме в точке (100; 100), то для того, чтобы создать для кнопки прямоугольный регион 20 х 15 пикселов с левой верхней точкой (0;0) относительно начала координат кнопки, следует вызвать функцию CreateRectRgn с параметрами (0, 0, 19, 14), а не (100, 100, 119, 114).

   Поскольку регион является GDI-объектом (подробнее в гл. 6), то для его удаления, если он не используется системой, применяется функция удаления GDI-объектов DeleteObject:

   function DeleteObject(p1: HGDIOBJ): BOOL;

 //-- Регион как область отсечения при рисовании окна --// 
   Обычно регион нужно удалять в том случае, если он не используется системой, однако после того, как регион назначен окну в качестве области отсечения, удалять его не следует. Функция назначения региона окну имеет следующий вид:

   function SetWindowRgn(hWnd: HWND; hRgn: HRGN; bRedraw: BOOL): Integer;

   Функция возвращает 0, если произвести операцию не удалось, и ненулевое значение в случае успешного выполнения операции. Параметры функции SetWindowRgn следующие:
   • hWnd – дескриптор окна, для которого устанавливается область отсечения (свойство Handle формы или элемента управления);
   • hRgn – дескриптор региона, назначаемого в качестве области отсечения (в простейшем случае является значением, возвращенным одной из функций создания региона);
   • bRedraw – флаг перерисовки окна после назначения новой области отсечения (для видимых окон обычно используется значение True, для невидимых – False).
   Чтобы получить копию региона, формирующего область отсечения окна, можно использовать API-функцию GetWindowRgn:

   function GetWindowRgn(hWnd: HWND; hRgn: HRGN): Integer;

   Первый параметр функции – дескриптор (Handle) интересующего окна. Второй параметр – дескриптор предварительно созданного региона, который в случае успеха модифицируется функцией GetWindowRgn так, что становится копией региона, формирующего область отсечения окна. Значения целочисленных констант – возможных возвращаемых значений функции – следующие:
   • NULLREGION – пустой регион;
   • SIMPLEREGION – регион в форме прямоугольника;
   • COMPLEXREGION – регион сложнее, чем прямоугольник;
   • ERROR – при выполнении функции возникла ошибка либо окну задана область отсечения.
   Далее приведен пример использования функции GetWindowRgn (предполагается, что приведенный ниже код является телом одного из методов класса формы).

   var rgn: HRGN;
   begin
   rgn:= CreateRectRgn(0,0,0,0); //Первоначальная форма региона не важна
   if GetWindowRgn(Handle, rgn) <> ERROR then
   begin
   //Операции с копией региона, формирующего область отсечения окна...
   end;
   DeleteObject(rgn); //Мы пользовались копией региона, которую должны
   //удалить (здесь или в ином месте, но сами)
   end;

 //-- Операции над регионами --// 
   При рассказе о функциях создания регионов неоднократно упоминалось о возможности комбинирования регионов для получения сложных форм. Пришло время кратко рассмотреть операции над регионами. Все операции по комбинированию регионов осуществляются с помощью функции CombineRgn:

   function CombineRgn(p1, p2, p3: HRGN; p4: Integer): Integer;

   Параметры этой функции следующие:
   • p1 – регион (предварительно созданный), предназначенный для сохранения результата;
   • p2, p3 – регионы-аргументы операции;
   • p4 – тип операции над регионами.
   Более подробно действие функции CombineRgn при различных значениях параметра p4 поясняется в табл. 1.2.
 //-- Таблица 1.2. Операции функции CombineRgn --// 
   Кроме приведенных в табл. 1.2 констант, в качестве параметра p4 функции CombineRgn можно использовать параметр RGN_COPY. При его использовании копируется регион, заданный параметром p2, в регион, заданный параметром p1.
   Тщательно рассчитывая координаты точек регионов-аргументов, можно с использованием функции CombineRgn создавать регионы самых причудливых форм, в чем вы сможете убедиться ниже.
   Наконец, после теоретического отступления можно рассмотреть несколько примеров создания и преобразования регионов, предназначенных для формирования области отсечения окон (форм и элементов управления на формах).


   Закругленные окна и многоугольники

   Сначала самые простые примеры: создание регионов без операций над ними. Формы всех трех приведенных здесь примеров содержат по три кнопки различной ширины и высоты, которым также задаются области отсечения.

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

   В приведенном ниже обработчике события FormCreate создается окно в форме эллипса с тремя кнопками такой же формы (листинг 1.10).
 //-- Листинг 1.10. Окно и кнопки в форме эллипсов --// 
   procedure TfrmElliptic.FormCreate(Sender: TObject);
   var
   formRgn, but1Rgn, but2Rgn, but3Rgn: HRGN;
   begin
   //Создаем регионы кнопок
   but1Rgn:= CreateEllipticRgn(0, 0, Button1.Width–1, Button1.Height–1);
   SetWindowRgn(Button1.Handle, but1Rgn, False);
   but2Rgn:= CreateEllipticRgn(0, 0, Button2.Width–1, Button2.Height–1);
   SetWindowRgn(Button2.Handle, but2Rgn, False);
   but3Rgn:= CreateEllipticRgn(0, 0, Button3.Width–1, Button3.Height–1);
   SetWindowRgn(Button3.Handle, but3Rgn, False);
   //Регион для окна
   formRgn:= CreateEllipticRgn(0, 0, Width–1, Height–1);
   SetWindowRgn(Handle, formRgn, True);
   end;

   Ширина и высота эллипсов в приведенном примере равна, соответственно, ширине и высоте тех окон, для которых создаются регионы. При необходимости это можно изменить, например, если требуется, чтобы все кнопки были одной величины независимо от размера, установленного во время проектирования формы.
   Результат выполнения кода листинга 1.10 можно увидеть на рис. 1.5.
   Рис. 1.5. Окно и кнопки в форме эллипсов

   Далее рассмотрим не менее интересный (возможно, даже более полезный на практике) пример – округление углов формы и кнопок на ней, то есть применение области отсечения в форме прямоугольника с округленными углами. Ниже приведен код реализации соответствующего обработчика события FormCreate (листинг 1.11).
 //-- Листинг 1.11. Окно и кнопки с округленными краями --// 
   procedure TfrmRoundRect.FormCreate(Sender: TObject);
   var
   formRgn, but1Rgn, but2Rgn, but3Rgn: HRGN;
   begin
   //Создаем регионы для кнопок
   but1Rgn:= CreateRoundRectRgn(0, 0, Button1.Width–1, Button1.Height–1,
   Button1.Width div 5, Button1.Height div 5);
   SetWindowRgn(Button1.Handle, but1Rgn, False);
   but2Rgn:= CreateRoundRectRgn(0, 0, Button2.Width–1, Button2.Height–1,
   Button2.Width div 5, Button2.Height div 5);
   SetWindowRgn(Button2.Handle, but2Rgn, False);
   but3Rgn:= CreateRoundRectRgn(0, 0, Button3.Width–1, Button3.Height–1,
   Button3.Width div 5, Button3.Height div 5);
   SetWindowRgn(Button3.Handle, but3Rgn, False);
   //Регион для окна
   formRgn:= CreateRoundRectRgn(0, 0, Width–1, Height–1,
   Width div 5, Height div 5);
   SetWindowRgn(Handle, formRgn, False);
   end;

   В листинге 1.11 размеры округляющих эллипсов вычисляются в расчете из размеров конкретного окна (20 % от его ширины и 20 % от высоты). Это смотрится не всегда красиво. В качестве альтернативы для ширины и высоты скругляющих эллипсов можно использовать фиксированные небольшие значения.
   Результат выполнения кода листинга 1.11 можно увидеть на рис. 1.6.
   Рис. 1.6. Окно и кнопки с округленными краями

   Теперь самый интересный из предусмотренных примеров – создание окна и кнопок в форме многоугольников, а именно: окна в форме звезды, кнопок в форме треугольника, пяти– и шестиугольника (рис. 1.7).
   Рис. 1.7. Окно и кнопки в форме многоугольников

   Код создания регионов для областей отсечения данного примера приведен в листинге 1.12.
 //-- Листинг 1.12. Окно и кнопки в форме многоугольников --// 
   procedure TfrmPoly.FormCreate(Sender: TObject);
   var
   points: array [0..5] of TPoint;
   formRgn, but1Rgn, but2Rgn, but3Rgn: HRGN;
   begin
   //Создаем регионы для окна и кнопок
   //..шестиугольная кнопка
   Make6Angle(Button1.Width, Button1.Height, points);
   but1Rgn:= CreatePolygonRgn(points, 6, WINDING);
   SetWindowRgn(Button1.Handle, but1Rgn, False);
   //..треугольная кнопка
   Make3Angle(Button2.Width, Button2.Height, points);
   but2Rgn:= CreatePolygonRgn(points, 3, WINDING);
   SetWindowRgn(Button2.Handle, but2Rgn, False);
   //..пятиугольная кнопка
   Make5Angle(Button3.Width, Button3.Height, points);
   but3Rgn:= CreatePolygonRgn(points, 5, WINDING);
   SetWindowRgn(Button3.Handle, but3Rgn, False);
   //..форма в виде звезды
   MakeStar(Width, Height, points);
   formRgn:= CreatePolygonRgn(points, 5, WINDING);
   SetWindowRgn(Handle, formRgn, False);
   end;

   Особенностью создания регионов в приведенном листинге является использование дополнительных процедур для заполнения массива points координатами точек-вершин многоугольников определенного вида. Все эти процедуры принимают, помимо ссылки на сам массив points, ширину и высоту прямоугольника, в который должен быть вписан многоугольник. Описание процедуры создания треугольника приведено в листинге 1.13.
 //-- Листинг 1.13. Создание треугольника --// 
   procedure Make3Angle(width, height: Integer; var points: array of TPoint);
   begin
   points[0].X:= 0;
   points[0].Y:= height – 1;
   points[1].X:= width div 2;
   points[1].Y:= 0;
   points[2].X:= width – 1;
   points[2].Y:= height – 1;
   end;

   В листинге 1.14 приведено описание процедуры создания шестиугольника.
 //-- Листинг 1.14. Создание шестиугольника --// 
   procedure Make6Angle(width, height: Integer; var points: array of TPoint);
   begin
   points[0].X:= 0;
   points[0].Y:= height div 2;
   points[1].X:= width div 3;
   points[1].Y:= 0;
   points[2].X:= 2 * (width div 3);
   points[2].Y:= 0;
   points[3].X:= width – 1;
   points[3].Y:= height div 2;
   points[4].X:= 2 * (width div 3);
   points[4].Y:= height – 1;
   points[5].X:= width div 3;
   points[5].Y:= height – 1;
   end;

   Листинг 1.15 содержит описание процедуры создания пятиугольника (неправильного).
 //-- Листинг 1.15. Создание пятиугольника --// 
   procedure Make5Angle(width, height: Integer; var points: array of TPoint);
   var a: Integer; //Сторона пятиугольника
   begin
   a:= width div 2;
   points[0].X:= a;
   points[0].Y:= 0;
   points[1].X:= width – 1;
   points[1].Y:= a div 2;
   points[2].X:= 3 * (a div 2);
   points[2].Y:= height – 1;
   points[3].X:= a div 2;
   points[3].Y:= height – 1;
   points[4].X:= 0;
   points[4].Y:= a div 2;
   end;

   Пятиугольная звезда, используемая как область отсечения формы, создается с помощью описанной в листинге 1.15 процедуры Make5Angle. После ее создания изменяется порядок следования вершин пятиугольника, чтобы их обход при построении региона выполнялся в той же последовательности, как рисуется звезда карандашом на бумаге (например, 1-3-5-2-4) (листинг 1.16).
 //-- Листинг 1.16. Создание пятиугольной звезды --// 
   procedure MakeStar(width, height: Integer; var points: array of TPoint);
   begin
   Make5Angle(width, height, points);
   //При построении звезды точки пятиугольника обходятся не по порядку,
   //а через одну
   Swap(points[1], points[2]);
   Swap(points[2], points[4]);
   Swap(points[3], points[4]);
   end;

   Процедура MakeStart (листинг 1.16) использует дополнительную процедуру Swap, меняющую местами значения двух передаваемых в нее аргументов. Процедура Swap реализуется чрезвычайно просто и потому в тексте книги не приводится.


   Комбинированные регионы

   Вы уже научились создавать и использовать простые регионы. Однако многим может показаться недостаточным тех форм окон, которые можно получить с использованием в качестве области отсечения лишь одного несложного региона. Пришло время заняться созданием окон более сложной формы, применяя рассмотренные ранее операции над регионами.
 //-- «Дырявая» форма --// 
   Этот простейший пример сомнительной полезности предназначен для первого знакомства с операциями над регионами. Здесь применяется только одна из возможных операций – операция XOR для формирования «дырок» в форме (рис. 1.8).
   Рис. 1.8. «Дырки» в форме

   На рис. 1.8 явно видно, как в «дырках» формы просвечивает одно из окон среды разработки Delphi. При этом, когда указатель находится над «дыркой», сообщения от мыши получают те окна, части которых видны в «дырке».
   Программный код, приводящий к созданию формы столь необычного вида, приведен в листинге 1.17.
 //-- Листинг 1.17. Создание «дырок» в форме --// 
   procedure TfrmHole.FormCreate(Sender: TObject);
   var
   rgn1, rgn2: HRGN; //"Регионы-дырки" в форме
   formRgn: HRGN;
   begin
   //Создание региона для формы
   formRgn:= CreateRectRgn(0, 0, Width – 1, Height – 1);
   //Создание регионов для "дырок"
   rgn1:= CreateEllipticRgn(10, 10, 100, 50);
   rgn2:= CreateRoundRectRgn(10, 60, 200, 90, 10, 10);
   //Создание "дырок" в регионе формы
   CombineRgn(formRgn, formRgn, rgn1, RGN_XOR);
   CombineRgn(formRgn, formRgn, rgn2, RGN_XOR);
   SetWindowRgn(Handle, formRgn, True);
   //Регионы для "дырок" больше не нужны
   DeleteObject(rgn1);
   DeleteObject(rgn2);
   end;

 //-- Сложная комбинация регионов --// 
   Теперь пришла очередь рассмотреть более сложный, но и гораздо более интересный пример. Последовательное применение нескольких операций над регионами приводит к созданию формы, показанной на рис. 1.9 (белое пространство – «вырезанные» части формы).
   Рис. 1.9. Сложная комбинация регионов

   Описание процедуры, выполняющей операции над регионами, приведено в листинге 1.18.
 //-- Листинг 1.18. Сложная комбинация регионов --// 
   procedure TfrmManyRgn.FormCreate(Sender: TObject);
   var
   r1, r2, r3, r4, r5, r6, r7: HRGN;
   formRgn: HRGN;
   butRgn: HRGN;
   begin
   //Создание регионов
   r1:= CreateRoundRectRgn(100, 0, 700, 400, 40, 40);
   r2:= CreateRectRgn(280, 0, 300, 399);
   r3:= CreateRectRgn(500, 0, 520, 399);
   r4:= CreateEllipticRgn(140, 40, 240, 140);
   r5:= CreateEllipticRgn(0, 300, 200, 500);
   r6:= CreateEllipticRgn(500, 40, 600, 140);
   r7:= CreateEllipticRgn(540, 40, 640, 140);
   //Комбинирование
   //..разрезы в основном регионе
   CombineRgn(r1, r1, r2, RGN_XOR);
   CombineRgn(r1, r1, r3, RGN_XOR);
   //..круглая "дырка" в левой стороне
   CombineRgn(r1, r1, r4, RGN_XOR);
   //..присоединение круга в левой нижней части
   CombineRgn(r1, r1, r5, RGN_OR);
   //..создание "дырки" в форме полумесяца
   CombineRgn(r7, r7, r6, RGN_DIFF);
   CombineRgn(r1, r1, r7, RGN_XOR);
   formRgn:= CreateRectRgn(0, 0, 0, 0);
   CombineRgn(formRgn, r1, 0, RGN_COPY);
   DeleteObject(r1);
   DeleteObject(r2);
   DeleteObject(r3);
   DeleteObject(r4);
   DeleteObject(r5);
   DeleteObject(r6);
   DeleteObject(r7);
   //Создание круглой кнопки закрытия
   butRgn:= CreateEllipticRgn(50, 50, 150, 150);
   SetWindowRgn(Button1.Handle, butRgn, False);
   SetWindowRgn(Handle, formRgn, True);
   end;

   В этом листинге подписано, какие операции предназначены для создания каких элементов итогового региона. В операциях участвуют семь регионов. Расположение используемых в операциях регионов показано на рис. 1.10.
   Рис. 1.10. Элементарные регионы, используемые для получения формы, представленной на рис. 1.9

 //-- Использование шаблона --// 
   Предыдущий пример наглядно демонстрирует мощь функции CombineRgn при построении регионов сложной формы. Однако существует огромное количество предметов, контуры которых крайне сложно повторить, комбинируя простые регионы. Построение многоугольных регионов с большим количеством точек может в этом случае выручить, но ведь это крайне нудно и утомительно.
   Если есть изображение предмета, контуры которого должны совпадать с контурами региона, то гораздо проще при построении региона обрабатывать само изображение, выбирая все точки, для которых выполняется определенное условие. Используемое изображение и будет тем шаблоном, по которому «вырезается» регион нужной формы.
   Рассмотрим простейший пример: есть изображение, каждая точка которого должна попасть в результирующий регион, если ее цвет не совпадает с заданным цветом фона. При этом изображение анализируется по так называемым «скан-линиям», то есть построчно. Из подряд идущих точек не фонового цвета формируются прямоугольные регионы, которые объединяются с результирующим регионом. Пример возможного используемого шаблона приведен на рис. 1.11.
   Рис. 1.11. Пример растрового изображения-шаблона

   Код функции построения региона указанным способом приведен в листинге 1.19.
 //-- Листинг 1.19. Построение региона по шаблону --// 
   function RegionFromPicture(pict:TPicture; backcolor: TColor): HRGN;
   var
   rgn, resRgn: HRGN;
   x, y, xFirst: Integer;
   begin
   resRgn:= CreateRectRgn(0, 0, 0, 0); //Результирующий регион
   //Анализируем каждую скан-линию рисунка (по горизонтали)
   for y:= 0 to pict.Height – 1 do
   begin
   x:= 0;
   while x < pict.Width do
   begin
   if (pict.Bitmap.Canvas.Pixels[x, y] <> backcolor) then
   begin
   xFirst:= x;
   Inc(x);
   //Определим часть линии, окрашенной не цветом фона
   while (x < pict.Width) and
   (pict.Bitmap.Canvas.Pixels[x, y] <> backcolor) do Inc(x);
   //Создаем регион для части скан-линии и добавляем его к
   //результирующему региону
   rgn:= CreateRectRgn(xFirst, y, x–1, y+1);
   CombineRgn(resRgn, resRgn, rgn, RGN_OR);
   DeleteObject(rgn);
   end;
   Inc(x);
   end;
   end;
   RegionFromPicture:= resRgn;
   end;

   Загрузка изображения-шаблона и создание региона могут происходить, например, при создании формы (листинг 1.20).
 //-- Листинг 1.20. Создание региона для области отсечения формы --// 
   procedure TfrmTemplate.FormCreate(Sender: TObject);
   var
   pict: TPicture;
   begin
   //Загрузка изображения и создание региона
   //(считаем, что цвет фона – белый)
   pict:= TPicture.Create;
   pict.LoadFromFile('back.bmp');
   SetWindowRgn(Handle, RegionFromPicture(pict, RGB(255,255,255)), True);
   end;

   В листинге 1.20 подразумевается, что используется файл back.bmp, находящийся в той же папке, что и файл приложения. Цвет фона – белый. Таким образом, если шаблон, показанный на рис. 1.11, хранится в файле back.bmp, то в результате получается форма, показанная на рис. 1.12.
   Рис. 1.12. Результат построения региона по шаблону



   Немного о перемещении окон


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


   Перемещение за клиентскую область

   Здесь на конкретном примере (перемещение формы за любую точку клиентской области) продемонстрировано, как можно самостоятельно определять положение некоторых важных элементов окна. Под элементами окна здесь подразумеваются:
   • строка заголовка (предназначена не только для отображения текста заголовка, но и служит областью захвата при перемещении окна мышью);
   • границы окна (при щелчке кнопкой мыши на верхней, нижней, правой и левой границе можно изменять размер окна, правда, если стиль окна это допускает);
   • четыре угла окна (предназначены для изменения размера окна с помощью мыши);
   • системные кнопки закрытия, разворачивания, сворачивания, контекстной справки (обычно расположены в строке заголовка окна);
   • горизонтальная и вертикальная полосы прокрутки;
   • системное меню (раскрывается щелчком кнопкой мыши на значке окна);
   • меню – полоса меню (обычно расположена вверху окна);
   • клиентская область – по умолчанию все пространство окна, кроме строки заголовка, меню и полос прокрутки.
   Каждый раз, когда над окном перемещается указатель мыши либо происходит нажатие кнопки мыши, система посылает соответствующему окну сообщение WM_ NCHITTEST для определения того, над которой из перечисленных выше областей окна находится указатель. Обработчик этого сообщения, вызываемый по умолчанию, информирует систему о расположении элементов окна в привычных для пользователя местах: заголовка – сверху, правой границы – справа и т. д.
   Как вы, наверное, уже догадались, реализовав свой обработчик сообщения WM_ NCHITTEST, можно изменить назначение элементов окна. Этот прием как раз и реализован в листинге 1.21.
 //-- Листинг 1.21. Перемещение окна за клиентскую область --// 
   procedure TfrmMoveClient.WMNCHitTest(var Message: TWMNCHitTest);
   var
   rc: TRect;
   p: TPoint;
   begin
   //Если точка приходится на клиентскую область, то заставим систему
   //считать эту область частью строки заголовка
   rc:= GetClientRect();
   p.X:= Message.XPos;
   p.Y:= Message.YPos;
   p:= ScreenToClient(p);
   if PtInRect(rc, p) then
   Message.Result:= HTCAPTION
   else
   //Обработка по умолчанию
   Message.Result:= DefWindowProc(Handle, Message.Msg, 0, 65536 * Message.YPos + Message.XPos);
   end;

   Приведенный в листинге 1.21 обработчик переопределяет положение только строки заголовка, возвращая значение HTCAPTION. Этот обработчик может возвращать следующие значения (целочисленные константы, возвращаемые функцией DefWindowProc):
   • HTBORDER – указатель мыши находится над границей окна (размер окна не изменяется);
   • HTBOTTOM, HTTOP, HTLEFT, HTRIGHT – указатель мыши находится над нижней, верхней, левой или правой границей окна соответственно (размер окна можно изменить, «потянув» за границу);
   • HTBOTTOMLEFT, HTBOTTOMRIGHT, HTTOPLEFT, HTTOPRIGHT – указатель мыши находится в левом нижнем, правом нижнем, левом верхнем или правом верхнем углу окна (размер окна можно изменять по диагонали);
   • HTSIZE, HTGROWBOX – указатель мыши находится над областью, предназначенной для изменения размера окна по диагонали (обычно в правом нижнем углу окна);
   • HTCAPTION – указатель мыши находится над строкой заголовка окна (за это место окно перемещается);
   • HTCLIENT – указатель мыши находится над клиентской областью окна;
   • HTCLOSE – указатель мыши находится над кнопкой закрытия окна;
   • HTHELP – указатель мыши находится над кнопкой вызова контекстной справки;
   • HTREDUCE, HTMINBUTTON – указатель мыши находится над кнопкой минимизации окна;
   • HTZ OOM, HTMAXBUTTON – указатель мыши находится над кнопкой максимизации окна;
   • HTMENU – указатель мыши находится над полосой меню окна;
   • HTSYSMENU – указатель мыши находится над значком окна (используется для вызова системного меню);
   • HTHSCROLL, HTVSCROLL – указатель находится над вертикальной или горизонтальной полосой прокрутки, соответственно;
   • HTTRANSPARENT – если возвращается это значение, то сообщение пересылается окну, находящемуся под данным окном (окна должны принадлежать одному потоку);
   • HTNOWHERE – указатель не находится над какой-либо из областей окна (например, на границе между окнами);
   • HTERROR – то же, что и HTNOWHERE, только при возврате этого значения обработчик по умолчанию (DefWindowProc) воспроизводит системный сигнал, сигнализирующий об ошибке.


   Перемещаемые элементы управления

   В завершение материала о перемещении окон приведу один совсем несложный, но довольно интересный пример, позволяющий прямо «на лету» изменять внешний вид приложения. Достигается это благодаря возможности перемещения и изменения размера элементов управления так, будто это обычные перекрывающиеся окна.
   Чтобы вас заинтересовать, сразу приведу результат работы примера. На рис. 1.13 показан внешний вид формы в начале работы примера.
   Рис. 1.13. Первоначальный вид формы

   После установки флажка Перемещение элементов управления получается результат, показанный на рис. 1.14.
   Рис. 1.14. Элементы управления можно перемещать (флажок не учитывается)

   В результате выполнеия произвольных перемещений, изменения размера окон, занявших место элементов управления, снятия флажка получаем измененный интерфейс формы (рис. 1.15).
   Рис. 1.15. Внешний вид формы после перемещения элементов управления

   Как же достигнут подобный эффект? Очень просто. Вы уже знаете, что элементы управления рисуются внутри своих собственных окон (дочерних по отношению к окну формы). Окна элементов управления отличает отсутствие в их стиле флагов (подробнее в гл. 2), позволяющих отображать рамку и изменять размер окна. Это легко изменить, самостоятельно задав нужные флаги в стиле окна с помощью API-функции SetWindowLong. Для удобства можно написать отдельную процедуру, которая будет дополнять стиль окна флагами, необходимыми для перемещения и изменения размера (как, собственно, и сделано в примере) (листинг 1.22).
 //-- Листинг 1.22. Разрешение перемещения и изменения размера --// 
   procedure MakeMovable(Handle: HWND);
   var
   style: LongInt;
   flags: UINT;
   begin
   //Разрешаем перемещение элемента управления
   style:= GetWindowLong(Handle, GWL_STYLE);
   style:= style or WS_OVERLAPPED or WS_THICKFRAME or WS_CAPTION;
   SetWindowLong(Handle, GWL_STYLE, style);
   style:= GetWindowLong(Handle, GWL_EXSTYLE);
   style:= style or WS_EX_TOOLWINDOW;
   SetWindowLong(Handle, GWL_EXSTYLE, style);
   //Перерисуем в новом состоянии
   flags:= SWP_NOMOVE or SWP_NOSIZE or SWP_DRAWFRAME or SWP_NOZORDER;
   SetWindowPos(Handle, 0, 0, 0, 0, 0, flags);
   end;

   Как можно увидеть, дополнительные флаги задаются в два этапа. Сначала считывается старое значение стиля окна. Потом с помощью двоичной операции ИЛИ стиль (задается целочисленным значением) дополняется новыми флагами. Это делается для того, чтобы не пропали ранее установленные значения стиля окна.
   Вообще, процедура MakeMovable изменяет два стиля окна: обычный и расширенный. Расширенный стиль окна изменяется лишь для того, чтобы строка заголовка получившегося окна занимала меньше места (получаем так называемое окно панели инструментов). Полный перечень как обычных, так и расширенных стилей можно просмотреть в приложении 2.
   Логично также реализовать процедуру, обратную MakeMovable, запрещающую перемещение окон элементов управления (листинг 1.23).
 //-- Листинг 1.23. Запрещение перемещения и изменения размера --// 
   procedure MakeUnmovable(Handle: HWND);
   var
   style: LongInt;
   flags: UINT;
   begin
   //Запрещаем перемещение элемента управления
   style:= GetWindowLong(Handle, GWL_STYLE);
   style:= style and not WS_OVERLAPPED and not WS_THICKFRAME
   and not WS_CAPTION;
   SetWindowLong(Handle, GWL_STYLE, style);
   style:= GetWindowLong(Handle, GWL_EXSTYLE);
   style:= style and not WS_EX_TOOLWINDOW;
   SetWindowLong(Handle, GWL_EXSTYLE, style);
   //Перерисуем в новом состоянии
   flags:= SWP_NOMOVE or SWP_NOSIZE or SWP_DRAWFRAME or SWP_NOZORDER;
   SetWindowPos(Handle, 0, 0, 0, 0, 0, flags);
   end;

   Осталось только реализовать вызовы процедур MakeMovable и MakeUnmovable в нужном месте программы. В рассматриваемом примере вызовы заключены внутри обработчика изменения состояния флажка на форме (листинг 1.24).
 //-- Листинг 1.24. Управление перемещаемостью элементов управления --// 
   procedure TfrmMovingControls.chkSetMovableClick(Sender: TObject);
   begin
   if chkSetMovable.Checked then
   begin
   //Разрешаем перемещение элементов управления
   MakeMovable(Memo1.Handle);
   MakeMovable(ListBox1.Handle);
   MakeMovable(Button1.Handle);
   end
   else
   begin
   //Запрещаем перемещение элементов управления
   MakeUnmovable(Memo1.Handle);
   MakeUnmovable(ListBox1.Handle);
   MakeUnmovable(Button1.Handle);
   end;
   end;



   Масштабирование окон

   Возможность масштабирования окон (форм) является интересным приемом, который может быть заложен в дизайн приложения. При этом имеется в виду масштабирование в буквальном смысле этого слова: как пропорциональное изменение размера элементов управления формы, так и изменение размера шрифта.
   Использовать масштабирование при работе с Delphi крайне просто, ведь в класс TWinControl, от которого наследуются классы форм, встроены методы масштабирования. Вот некоторые из них:
   • ScaleControls – пропорциональное изменение размера элементов управления на форме;
   • ChangeScale – пропорциональное изменение размера элементов управления с изменением шрифта, который используется для отображения текста в них.
   Оба приведенных метода принимают два целочисленных параметра: числитель и знаменатель нового масштаба формы. Пример задания параметров для методов масштабирования приведен в листинге 1.25.
 //-- Листинг 1.25. Масштабирование формы с изменением шрифта --// 
   procedure TfrmScaleBy.cmbSmallerClick(Sender: TObject);
   begin
   ChangeScale(80, 100); //Уменьшение на 20 % (новый масштаб – 80 %)
   end;
   procedure TfrmScaleBy.cmbBiggerClick(Sender: TObject);
   begin
   ChangeScale(120, 100); //Увеличение на 20 % (новый масштаб – 120 %)
   end;

   Чтобы размер шрифта правильно устанавливался, для элементов управления нужно использовать шрифты семейства TrueType (в данном примере это шрифт Times New Roman).
   На рис. 1.16 показан внешний вид формы до изменения масштаба.
   Рис. 1.16. Форма в оригинальном масштабе

   Внешний вид формы после уменьшения масштаба в 1,25 раза (новый масштаб составляет 80 % от первоначального) показан на рис. 1.17.
   Рис. 1.17. Форма в масштабе 80 %

   То, что форма не изменяет свой размер при масштабировании, можно легко исправить, установив, например, свойство AutoSize в значение True с помощью редактора свойств объектов (Object Inspector).
   Если по каким-либо причинам использование свойства AutoSize вас не устраивает, то можно рассчитать новый размер формы самостоятельно. Только пересчитывать нужно не размер всего окна, а его клиентской области, ведь строка заголовка при масштабировании не изменяется. Расчет размера окна можно выполнить следующим образом.
   1. Получить прямоугольник клиентской области окна (GetClientRect).
   2. Вычислить новый размер клиентской области.
   3. Рассчитать разницу между новой и первоначальной шириной, новой и первоначальной высотой клиентской области; сложить полученные значения с первоначальными размерами самой формы.
   Пример расчета для увеличения размера клиентской области в 1,2 раза приведен ниже:

   GetClientRect(Handle, rc);
   newWidth:= (rc.Right – rc.Left) * 120 div 100;
   newHeight:= (rc.Bottom – rc.Top) * 120 div 100;
   Width:= Width + newWidth – (rc.Right – rc.Left);
   Height:= Height + newHeight – (rc.Bottom – rc.Top);


   Примечание
   Чтобы после изменения масштаба формы можно было вернуться в точности к исходному масштабу (с помощью соответствующей обратной операции), нужно для уменьшения и увеличения использовать коэффициенты, произведение которых равно единице. Например, при уменьшении масштаба на 20 % (в 0,8 раз) его нужно увеличивать при обратной операции на 25 % (в 1/0,8 = 1,25 раза).



   Добавление команды в системное меню окна

   Обратите внимание на меню, раскрывающееся при щелчке кнопкой мыши на значке окна. В этом системном меню обычно присутствуют пункты, выполняющие стандартные действия над окном, такие как закрытие, минимизация, максимизация и др. Для доступа к этому меню предусмотрены специальные функции, что дает возможность использовать его в своих целях.
   Для получения дескриптора (HMENU) системного меню окна используется API-функция GetSystemMenu, а для добавления пункта в меню – функция AppendMenu. Пример процедуры, добавляющей пункты в системное меню, приведен в листинге 1.26.
 //-- Листинг 1.26. Добавление пунктов в системное меню окна --// 
   procedure TForm1.FormCreate(Sender: TObject);
   var hSysMenu: HMENU;
   begin
   hSysMenu:= GetSystemMenu(Handle, False);
   AppendMenu(hSysMenu, MF_SEPARATOR, 0, '');
   AppendMenu(hSysMenu, MF_STRING, 10001, 'Увеличить на 20%');
   AppendMenu(hSysMenu, MF_STRING, 10002, 'Уменьшить на 20 %');
   end;

   В результате выполнения этого кода системное меню формы Form1 станет похожим на меню, показанное на рис. 1.18.
   Рис. 1.18. Пользовательские команды в системном меню

   Однако недостаточно просто создать команды меню – нужно предусмотреть обработку их выбора. Это делается в обработчике сообщения WM_SYSCOMMAND (листинг 1.27).
 //-- Листинг 1.27. Обработка выбора пользовательских пунктов в системном меню --// 
   procedure TForm1.WMSysCommand(var Message: TWMSysCommand);
   begin
   if Message.CmdType = 10001 then
   //Увеличение масштаба
   ChangeScale(120, 100)
   else if Message.CmdType = 10002 then
   ChangeScale(80, 100)
   else
   //Обработка по умолчанию
   DefWindowProc(Handle, Message.Msg, Message.CmdType, 65536 * Message.YPos+ Message.XPos);
   end;

   Обратите внимание на то, что числовые значения, переданные в функцию Append-Menu, используются для определения, какой именно пунктменю выбран. Чтобы меню работало стандартным образом, все поступающие от него команды должны быть обработаны. Поэтому для всех команд, реакция на которые не заложена в реализованном обработчике, вызывается обработчик по умолчанию (функция DefWindowProc).


   Отображение формы поверх других окон

   Иногда вам может пригодиться возможность отображения формы поверх всех окон. За примером далеко ходить не надо: посмотрите на окно Диспетчера задач Windows. Теперь вспомните, терялось ли хоть раз окно Свойства: Экран среди других открытых окон. Это происходит благодаря тому, что это окно перекрывается другими окнами и при этом не имеют никакого значка на Панели задач (правда, это окно все же можно найти с помощью Диспетчера задач).
   Из сказанного выше можно заключить, что отображение окна поверх других окон может пригодиться как минимум в двух случаях: для важных окон приложения (например, окна ввода пароля) и/или в случае, если значок приложения не выводится на Панели задач (как скрыть значок, было рассказано выше).
   После небольшого отступления можно рассмотреть способы, позволяющие задать положение формы так, чтобы другие окна не могли ее закрыть.
   Первый способ прост до предела: достаточно присвоить свойству FormStyle в окне Object Inspector значение fsStayOnTo. Результат этого действия показан на рис. 1.19 (обратите внимание, что форма закрывает Панель задач, которая по умолчанию также отображается поверх всех окон).
   Рис. 1.19. Форма, отображаемая поверх других окон

   Второй способ пригодится, если форма постоянно отображается обычным образом, но в определенные моменты времени требует к себе более пристального внимания, для чего и помещается наверх. Способ основан на использовании API-функции SetWindowPos, которая, кроме позиции и размера окна, может устанавливать и порядок рисования окна (Z-order).

   Примечание
   Под Z-order подразумевается порядок следования окон вдоль оси Z, направленной перпендикулярно экрану (оси X и Y лежат в плоскости экрана).

   Вызов функции SetWindowPos для перемещения окна наверх выглядит следующим образом (Handle – дескриптор нужного окна):

   SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE)

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

   SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE)

   После этого другие окна, не отображаемые поверх других, смогут снова перекрывать форму.



   Глава 2
   Уменьшение размера EXE-файла. Использование Windows API


   • Источник лишних килобайт
   • Создание окна вручную
   • Окно с элементами управления
   • Стандартные окна Windows
   • Установка шрифта элементов управления

   Не секрет, что размер скомпилированного EXE-файла Delphi часто значительно превосходит размер программ, написанных с использованием сред разработки от Microsoft (например, Visual C++, Visual Basic).

   Примечание
   Здесь и далее имеются в виду приложения с оконным интерфейсом (не консольные).

   При разработке крупных проектов этот факт абсолютно не смущает. Однако что делать, если программисту на Delphi нужно написать программу, занимающую как можно меньше места (например, инсталлятор) или загружающуюся за минимальное время (например, сервисную программу)? Конечно, такое приложение можно написать на C++, но опять же, что делать, если осваивать новый язык программирования нет времени?
   В этой главе будет рассмотрен способ уменьшения размера EXE-файла: отказ от библиотеки Borland за счет прямого использования Windows API. Данный способ позволяет уменьшить размер приложения, однако написание Delphi-приложения (да еще и с оконным интерфейсом) с использованием только API-функций является задачей весьма трудоемкой, хотя и интересной, да к тому же и экзотичной.
   Вначале небольшое отступление. Операционная система (в данном случае Windows) предоставляет интерфейс для программирования внутри себя – набор функций, заключенных в нескольких системных библиотеках, называемый Windows API (Windows Application Programming Interface – интерфейс программирования Windows-приложений). Любой проект под Windows на любом языке программирования в конечном счете сводится именно к приложению, использующему функции WindowsAPI. Использование этих функций может быть как явным, так и скрытым за использованием библиотек, поставляемых вместе со средой программирования.
   Еще один момент: в тексте постоянно говорится о Windows API, а не просто API. Это потому, что само понятие Application Programming Interface применяется ко многим системам, а не только к операционным системам, и уж тем более не только к Windows. Вот несколько примеров: UNIX API, Linux API, Oracle API (интерфейс для работы с СУБД Oracle) и т. д.

   Примечание
   В книге описаны только те возможности Window API, которые непосредственно используются в примерах. Полное описание Windows API является слишком большой задачей, для которой не хватит и книги. Если вам захочется изучить или хотя бы узнать больше о Windows API, можно будет обратиться к специализированным изданиям по этой теме. Однако никакое издание не заменит MSDN (огромная справочная система от Microsoft для Visual Studio).

   Теперь выясним, за счет чего разрастается EXE-файл приложения при использовании среды программирования Delphi.


   Источник лишних килобайт

   Для начала создадим новый проект Windows-приложения (Project1.exe). По умолчанию создается и показывается одна пустая форма (объявлена в модуле Unit1.pas). Ничего менять не будем, просто скомпилируем и посмотрим размер EXE-файла. Больше 300 Кбайт – не многовато ли для такого простого приложения?
   Кстати, простейшее оконное приложение, написанное на Visual C++ 6.0 (в Release-конфигурации, то есть без отладочной информации в EXE-файле) без использования MFC имеет размер 28 Кбайт, а с использованием библиотеки MFC – 20 Кбайт. Простейшее оконное приложение на Visual Basic 6.0 занимает всего 16 Кбайт.
   Из-за чего такая разница? Посмотрим, какие библиотеки используются приложениями, написанными на этих языках программирования. Это можно сделать, например, с помощью программы Dependency Walker, входящей в комплект Microsoft Visual Studio (рис. 2.1).
   Рис. 2.1. Библиотеки, используемые приложениями

   Как видно, приложение на Delphi (правый верхний угол окна на рис. 2.1) использует приличный набор функций, помещенных в стандартные библиотеки операционной системы Windows. Кроме библиотек операционной системы, приложение на Delphi ничего не использует.
   Приложение WinAPI.exe (левое верхнее окно) является примером чистого Windows API-приложения в том смысле, что в нем не задействованы библиотеки-оболочки над API-функциями, каким-либо образом облегчающие программирование. Собственно, именно такой объем и занимает простейшее оконное приложение.
   С приложением MFC.exe уже интереснее: размер самого EXE-файла уменьшился за счет того, что часть кода работы с API-функциями переместилась в библиотеки. С приложением на Visual Basic еще интереснее (правое нижнее окно). Приложение, написанное на этом языке, компилируется в исполняемый бинарный код, фактически представляющий собой вызовы функций одной библиотеки, в которой и реализована вся поддержка программирования на Visual Basic (при детальном рассмотрении этой библиотеки в ней можно найти объявления встроенных функций Visual Basic).
   К чему это все? Да к тому, что приложения на других языках программирования (в данном случае речь идет о продуктах Microsoft) не менее «тяжеловесны», чем приложения, написанные на Borland Delphi, если при их написании программист пользуется не только API-функциями. Особенно примечателен в этом случае пример исполняемого файла Visual Basic, который, хотя и имеет малый размер, требует наличия библиотеки, размер которой равен около 1,32 Мбайт. Программа на Visual C++ с использованием, например MFC, в которой реализованы классы оболочки над функциями Windows API (правда, не только они), требует наличия нескольких DLL-файлов. Для Microsoft это не проблема, так как операционная система Windows выпускается именно этой компанией, а следовательно, обеспечить переносимость (здесь – работоспособность без установки) приложений, написанных с использованием ее же сред разработки, очень просто – достаточно добавить нужные библиотеки в состав операционной системы.
   Что же в таком случае осталось сделать Borland? Дабы не лишать программиста возможности пользоваться библиотеками, реализующими самые полезные классы (VCL и не только), код с реализацией этих самых классов приходится компоновать в один файл с самой программой. Вот и получается, что реализация этих самых классов в EXE-файле может занимать места гораздо больше, чем реализация собственно приложения. Так в данном случае и получилось.

   Примечание
   Кстати, проект на Visual C++ также можно статически скомпоновать с библиотекой MFC (то есть включить код реализации классов в сам EXE-файл). Таким способом можно добиться независимости приложения от различных библиотек, кроме тех, которые гарантированно поставляются с Windows, но при этом размер EXE-файла рассмотренного выше приложения (в Release-конфигурации) возрастет до 192 Кбайт.

   Теперь обратимся к проекту на Delphi. Посмотрим, что записано в файлах Unit1.pas и Project1.dpr. Текст файла Unit1.pas приведен ниже (листинг 2.1).
 //-- Листинг 2.1. Содержимое файла Unitl.pas --// 
   unit Unit1;
   interface
   uses
   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
   Dialogs;
   type
   TForm1 = class(TForm)
   private
   {Private declarations}
   public
   {Public declarations}
   end;
   var
   Form1: TForm1;
   implementation
   {$R *.dfm}
   end.

   Обратите внимание на секцию uses. Здесь перечислены девять подключаемых модулей, объявлен собственно класс формы TForm1, а также записана строка, указывающая компилятору на использование файла ресурсов. Все модули, кроме первых двух, – труды компании Borland, облегчающие жизнь простым программистам. Модуль такого же рода используется и в файле Project1.dpr (листинг 2.2).
 //-- Листинг 2.2. Содержимое файла Project1.dpr --// 
   program Project1;
   uses
   Forms,
   Unit1 in 'Unit1.pas'{Form1};
   {$R *.res}
   begin
   Application.Initialize;
   Application.CreateForm(TForm1, Form1);
   Application.Run;
   end.

   Теперь обратите внимание на модули Windows и Messages. В первом определены константы, структуры данных, необходимые для работы с функциями Windows API, и, конечно же, объявлены импортируемые из системных библиотек API-функции. В модуле Messages можно найти определения констант и структур, предназначенных для работы с Windows-сообщениями (подробнее об этом смотрите в подразделе «Реакция на сообщения элементов управления» гл. 2 (стр. 66)).
   Собственно, этих двух модулей должно хватить для того, чтобы реализовать оконное приложение, использующее, правда, только стандартные функции Windows API и стандартные элементы управления. В листинге 2.3 приведен пример элементарного Windows-приложения. Главное, на что сейчас стоит обратить внимание, – это размер приложения: всего 15 Кбайт.
 //-- Листинг 2.3. Элементарное приложение --// 
   program WinAPI;
   uses
   Windows, Messages;
   {$R *.res}
   begin
   MessageBox(0, 'This is a test', 'Little application', MB_OK);
   end.

   Зачастую полностью отказываться от классов, реализованных Borland, неоправданно, но для чистоты эксперимента в этой главе будут рассмотрены радикальные примеры, построенные на использовании только Windows API.


   Создание окна вручную

   Раз уж речь зашла о приложениях с оконным интерфейсом, то самое время приступить к его реализации средствами Windows API. Итак, чтобы создать и заставить работать окно приложения, нужно выполнить следующие операции.
   1. Зарегистрировать класс окна с использованием функции RegisterClass или RegisterClassEx.
   2. Создать экземпляр окна зарегистрированного ранее класса.
   3. Организовать обработку сообщений, поступающих в очередь сообщений.
   Пример того, как можно организовать регистрацию класса окна, приведен в листинге 2.4.
 //-- Листинг 2.4. Регистрация класса окна --// 
   function RegisterWindow():Boolean;
   var
   wcx: WNDCLASSEX;
   begin
   ZeroMemory(Addr(wcx), SizeOf(wcx));
   //Формирование информации о классе окна
   wcx.cbSize:= SizeOf(wcx);
   wcx.hInstance:= GetModuleHandle(nil);
   wcx.hIcon:= LoadIcon(0, IDI_ASTERISK); //Стандартный значок
   wcx.hIconSm:= wcx.hIcon;
   wcx.hCursor:= LoadCursor(0, IDC_ARROW); //Стандартный указатель
   wcx.hb rBackground:= GetStockObject(WHITE_BRUSH); //Серый цвет фона
   wcx.style:= 0;
   //..самые важные параметры
   wcx.lpszClassName:= 'MyWindowClass'; //Название класса
   wcx.lpfnWndProc:= Addr(WindowFunc); //Адрес функции обработки сообщений
   //Регистрация класса окна
   RegisterWindow:= RegisterClassEx(wcx) <> 0;
   end;

   Здесь существенным моментом является обязательное заполнение структуры WNDCLASSEX информацией о классе окна. Наиболее необычной вам должна показаться следующая строка:

   wcx.lpfnWndProc:= Addr(WindowFunc); //Адрес функции обработки сообщений

   Здесь сохранен адрес функции WindowFunc (листинг 2.5) – обработчика оконных сообщений (называемый также оконной процедурой). После вызова функции RegisterClassEx система запомнит этот адрес и будет вызывать указанную функцию-обработчик каждый раз при необходимости обработать сообщение, пришедшее окну. Код простейшей реализации функции WindowFunc приведен в листинге 2.5.
 //-- Листинг 2.5. Функция обработки сообщений --// 
   //Функция обработки сообщений
   function WindowFunc(hWnd:HWND; msg:UINT; wParam:WPARAM; lParam:LPARAM):LRESULT; stdcall;
   var
   ps: PAINTSTRUCT;
   begin
   case msg of
   WM_CLOSE:
   if (hWnd = hMainWnd) then
   PostQuit Message(0); //При закрытии окна – выход
   WM_PAINT:
   begin
   //Перерисовка содержимого окна
   BeginPaint(hWnd, ps);
   Text Out(ps.hdc, 10, 10, 'Текст в окне', 12);
   EndPaint(hWnd, ps);
   end;
   else
   begin
   //Обработка по умолчанию
   WindowFunc:= DefWindowProc(hWnd, msg, wParam, lParam);
   Exit;
   end;
   end;
   WindowFunc:= S_OK; //Сообщение обработано
   end;

   В этой функции реализована обработка сообщения WM_PAINT – запроса на перерисовку содержимого окна. Обработка сообщения WM_CLOSE предусмотрена для того, чтобы при закрытии главного окна происходил выход из приложения. Для всех остальных сообщений выполняется обработка по умолчанию.
   Обратите особое внимание на прототип этой функции: типы возвращаемых значений, типы параметров и способ вызова функции (stdcall) должны быть именно такими, как в листинге 2.5. Возвращаемое значение зависит от конкретного сообщения. Чаще всего это S_OK (константа, равная 0) в случае успешной обработки сообщения.
   В листинге 2.6 приведена часть программы, использующая регистрацию, создание окна, а также организующая обработку сообщений для созданного окна.
 //-- Листинг 2.6. Регистрация и создание окна. Цикл обработки сообщений --// 
   program Window;
   uses
   Windows, Messages;
   {$R *.res}
   var
   hMainWnd: HWND;
   mess: MSG;
   ...
   begin
   //Создание окна
   if not RegisterWindow() then Exit;
   hMainWnd:= CreateWindow(
   'MyWindowClass', //Имя класса окна
   'Главное окно', //Заголовок окна
   WS_VISIBLE or WS_OVERLAPPEDWINDOW,//Стиль окна (перекрывающееся, видимое)
   CW_USEDEFAULT, //Координата X по умолчанию
   CW_USEDEFAULT, //Координата Y по умолчанию
   CW_USEDEFAULT, //Ширина по умолчанию
   CW_USEDEFAULT, //Высота по умолчанию
   HWND(nil), //Нет родительского окна
   HMENU(nil), //Нетменю
   GetModuleHandle(nil),
   nil);
   //Запуск цикла обработки сообщений
   while (Longint(GetMessage(mess, HWND(nil), 0, 0)) <> 0)
   do begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;
   end.

   В листинге 2.6 на месте многоточия должны находиться коды функций WindowFunc и Regis terWindow. При создании окна использовались только стили WS_VI SIBLE и WS_OVERLAPPEDWINDOWS. Но это далеко не все возможные стили окон. В приложении 2 приведен список всех стилей окон (если другого не сказано, то стили можно комбинировать с помощью оператора Or). Кроме функции CreateWindow, для создания окон можно использовать функцию CreateWindowEx. При этом появится возможность указать дополнительный (расширенный) стиль окна (первый параметр функции CreateWindowEx). Список расширенных стилей также приведен в приложении 2.
   В конце листинга 2.6 записан цикл обработки сообщений:

   while (Longint(GetMessage(mess, hMainWnd, 0, 0)) > 0)
   do begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;

   Здесь API-функция GetMessage возвращает значения больше нуля, пока в очереди не обнаружится сообщение WM_QUIT. В случае возникновения какой-либо ошибки функция GetMessage возвращает значение -1. Функция TranslateMessage преобразует сообщения типа WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN иWM_ SYSKEYUP в сообщения символьного ввода(WM_CHAR, WM_SYS CHAR, WM_ DEADCHAR, WM_SYSDEADCHAR). Функция DispatchMessage в общем случае (за исключением сообщения WM_TIMER) вызывает функцию обработки сообщений нужного окна.
   Внешний вид самого окна, создаваемого в этом примере, показан на рис. 2.2.
   Рис. 2.2. Окно, созданное вручную

   Кстати, пока размер приложения равен всего 16 Кбайт.


   Окно с элементами управления


   После того как вы ознакомились с созданием простейшего окна, самое время позаботиться о его наполнении элементами управления. Для стандартных элементов управления в системе уже зарегистрированы классы окон:
   • BUTTON – оконный класс, реализующий работу обычной кнопки, флажка, переключателя и даже рамки для группы элементов управления (GroupBox);
   • COMBOBOX – раскрывающийся список;
   • EDIT – текстовое поле, может быть как однострочным, так и многострочным, с полосами прокрутки и без;
   • LISTBOX – список;
   • SCROLLBAR – полоса прокрутки;
   • STATIC – статический текст (он же Label, надпись, метка и пр.), кроме текста, может содержать изображение.
   Ввиду большого количества возможных стилей окон элементов управления их перечень здесь не приводится, но его можно найти в приложении 2.


   Создание элементов управления

   Целесообразно написать более краткие функции создания элементов управления, чтобы, формируя интерфейс формы «на лету», не приходилось усложнять код громоздкими вызовами функций CreateWindow или CreateWindowEx. Этим мы сейчас и займемся. Сразу необходимо отметить: предполагается, что все функции помещены в модуль (модуль Controls в файле Controls.pas), в котором объявлены глобальные переменные hAppInst и hParentWnd. Эти переменные инициализируются перед вызовом первой из перечисленных ниже процедур или функций создания и работы с элементами управления (инициализацию можно посмотреть в листинге 2.21).


   Внимание!
   Обратите внимание на параметр id функций создания и манипулирования элементами управления. Это целочисленное значение идентифицирует элементы управления в пределах родительского окна.

   Для создания обычных кнопок можно использовать функцию из листинга 2.7 (все рассмотренные далее функции создания элементов управления возвращают дескриптор созданного окна).
 //-- Листинг 2.7. Создание кнопки --// 
   function CreateButton(x, y, width, height, id:Integer;
   caption: String):HWND;
   begin
   CreateButton:=
   CreateWindow('BUTTON', PAnsiChar(caption), WS_CHILD or WS_VISIBLE or
   BS_PUSHBUTTON or WS_TABSTOP, x, y, width, height,
   hParentWnd, HMENU(id), hAppInst, nil);
   end;

   Приведенная в листинге 2.8 функция создает флажок и устанавливает его.
 //-- Листинг 2.8. Создание флажка --// 
   function CreateCheck(x, y, width, height, id: Integer; caption: String;
   checked: Boolean):HWND;
   var
   res: HWND;
   begin
   res:=
   CreateWindow('BUTTON', PAnsiChar(caption), WS_CHILD or WS_VISIBLE or
   BS_AUTOCHECKBOX or WS_TABSTOP, x, y, width, height,
   hParentWnd, HMENU(id), hAppInst, nil);
   if ((res <> 0) and checked) then
   SendMessage(res, BM_SETCHECK, BST_CHECKED, 0); //Флажок установлен
   CreateCheck:= res;
   end;

   Следующая функция создает переключатель (листинг 2.9). Если нужно, то он устанавливается. Новый переключатель может начинать новую группу переключателей, для чего нужно параметру group присвоить значение True.
 //-- Листинг 2.9. Создание переключателя --// 
   function CreateOption(x, y, width, height, id: Integer; caption: String;
   group: Boolean; checked: Boolean):HWND;
   var
   res: HWND;
   nGroup: Integer;
   begin
   if (checked) then nGroup:= WS_GROUP else nGroup:= 0;
   res:=
   CreateWindow('BUTTON', PAnsiChar(caption), WS_CHILD or WS_VISIBLE or
   BS_AUTORADIOBUTTON or nGroup or WS_TABSTOP, x, y, width,
   height, hParentWnd, HMENU(id), hAppInst, nil);
   if ((res <> 0) and checked) then
   //Переключатель установлен
   SendMessage(res, BM_SETCHECK, BST_CHECKED, 0);
   CreateOption:= res;
   end;

   Для создания подписанной рамки, группирующей элементы управления, можно воспользоваться функцией CreateFrame, приведенной в листинге 2.10.
 //-- Листинг 2.10. Создание рамки --// 
   function CreateFrame(x, y, width, height, id: Integer;
   caption: String):HWND;
   begin
   CreateFrame:=
   CreateWindow('BUTTON', PAnsiChar(caption), WS_CHILD or WS_VISIBLE or
   BS_GROUPBOX, x, y, width, height, hParentWnd,
   HMENU(id), hAppInst, nil);
   end;

   Для создания раскрывающегося списка (ComboBox) пригодится функция Create-Combo, приведенная в листинге 2.11.
 //-- Листинг 2.11. Создание раскрывающегося списка --// 
   function CreateCombo(x, y, width, height, id: Integer):HWND;
   begin
   CreateCombo:=
   CreateWindow('COMBOBOX', nil, WS_CHILD or WS_VISIBLE or CBS_DROPDOWN
   or CBS_AUTOHSCROLL or WS_TABSTOP, x, y, width, height,
   hParentWnd, HMENU(id), hAppInst, nil);
   end;

   Для создания простого списка (ListBox) вполне подойдет функция CreateList, описанная в листинге 2.12.
 //-- Листинг 2.12. Создание простого списка --// 
   function CreateList(x, y, width, height, id: Integer):HWND;
   begin
   CreateList:=
   CreateWindowEx(WS_EX_CLIENTEDGE, 'LISTBOX', nil, WS_CHILD or WS_VISIBLE
   or LBS_NOTIFY or WS_BORDER or WS_TABSTOP, x, y, width,
   height, hParentWnd, HMENU(id), hAppInst, nil);
   end;

   Функция CreateLabel, приведенная в листинге 2.13, создает статическую надпись (Label), предназначенную только для вывода текста.
 //-- Листинг 2.13. Создание надписи --// 
   function CreateLabel(x, y, width, height, id: Integer;
   caption: String):HWND;
   begin
   CreateLabel:=
   CreateWindow('STATIC', PAnsiChar(caption), WS_CHILD or WS_VISIBLE, x,
   y, width, height, hParentWnd, HMENU(id), hAppInst, nil);
   end;

   Однострочное текстовое поле с привычной рамкой создается функцией CreateEdit (листинг 2.14).
 //-- Листинг 2.14. Создание однострочного текстового поля --// 
   function CreateEdit(x, y, width, height, id: Integer; strInitText: String):HWND;
   begin
   CreateEdit:=
   CreateWindowEx(WS_EX_CLIENTEDGE, 'EDIT', PAnsiChar(strInitText),
   WS_CHILD or WS_VISIBLE or ES_AUTOHSCROLL or WS_TABSTOP,
   x, y, width, height, hParentWnd,
   HMENU(id), hAppInst, nil);
   end;

   Код создания многострочного текстового поля (аналог Memo) отличается от кода создания однострочного поля только указанием дополнительного флага ES_MULTILINE (листинг 2.15).
 //-- Листинг 2.15. Создание многострочного текстового поля --// 
   function CreateMemo(x, y, width, height, id: Integer; strInitText: String):HWND;
   begin
   CreateMemo:=
   CreateWindowEx(WS_EX_CLIENTEDGE, 'EDIT', PAnsiChar(strInitText),
   WS_CHILD or WS_VISIBLE or ES_AUTOVSCROLL or ES_MULTILINE
   or WS_TABSTOP, x, y, width, height, hParentWnd,
   HMENU(id), hAppInst, nil);
   end;

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


   Использование элементов управления

   Элементы управления, как и все окна, управляются путем отсылки им сообщений. Этим же способом они уведомляют родительские окна о некоторых произошедших событиях (например, выделении элемента в списке, нажатии кнопки и т. д.).
   Описание наиболее используемых сообщений для рассматриваемых элементов управления приведено в приложении 3. Сейчас же будет показано, как можно упростить работу с элементами управления в некоторых частных случаях с помощью специальных функций.
   Итак, в демонстрационном проекте для управления переключателями и флажками предусмотрены следующие функции и процедуры (листинг 2.16).
 //-- Листинг 2.16. Управление флажками и переключателями --// 
   //Установка/снятие флажка (установка/снятие переключателя)
   procedure SetChecked(id: Integer; checked: BOOL);
   var state: Integer;
   begin
   if (checked) then state:= BST_CHECKED
   else state:= BST_UNCHECKED;
   SendDlgItemMessage(hParentWnd, id, BM_SETCHECK, state, 0);
   end;
   //Получение информации о том, установлен ли флажок
   //(установлен ли переключатель)
   function GetChecked(id: Integer):BOOL;
   begin
   if (SendDlgItemMessage(hParentWnd, id, BM_GETCHECK, 0, 0) = BST_CHECKED)
   then GetChecked:= True
   else GetChecked:= False;
   end;

   Функции и процедуры, описанные в листинге 2.17, предназначены для управления раскрывающимся списком (элементом ComboBox).
 //-- Листинг 2.17. Управление раскрывающимся списком --// 
   //Добавление строки в список
   procedure AddToCombo(id: Integer; str: String);
   begin
   SendDlgItemMessage(hParentWnd, id, CB_ADDSTRING, 0,
   Integer(PAnsiChar(str)));
   end;
   //Удаление строки из списка
   procedure DeleteFromCombo(id: Integer; index: Integer);
   begin
   SendDlgItemMessage(hParentWnd, id, CB_DELETESTRING, index, 0);
   end;
   //Выделение строки с заданным номером
   procedure SetComboSel(id: Integer; index: Integer);
   begin
   SendDlgItemMessage(hParentWnd, id, CB_SETCURSEL, index, 0);
   end;
   //Получение номера выделенной строки (CB_ERR, если нет выделения)
   function GetComboSel(id: Integer): Integer;
   begin
   GetComboSel:= SendDlgItemMessage(hParentWnd, id, CB_GETCURSEL, 0, 0);
   end;
   //Получение количества строк
   function GetComboCount(id: Integer): Integer;
   begin
   GetComboCount:= SendDlgItemMessage(hParentWnd, id, CB_GETCOUNT, 0, 0);
   end;
   //Получение текста строки по ее индексу
   function GetComboItemText(id: Integer; index: Integer):String;
   var buffer: String;
   begin
   SetLength(buffer,
   SendDlgItemMessage(hParentWnd, id, CB_GETLBTEXTLEN, index, 0)
   );
   SendDlgItemMessage(hParentWnd, id, CB_GETLBTEXT, index,
   Integer(Addr(buffer)));
   GetComboItemText:= buffer;
   end;

   Сходные функции и процедуры, приведенные в листинге 2.18, предназначены для управления списком (элементом ListBox).
 //-- Листинг 2.18. Управление списком --// 
   //Добавление строки в список
   procedure AddToList(id: Integer; str: String);
   begin
   SendDlgItemMessage(hParentWnd, id, LB_ADDSTRING, 0, Integer(PAnsiChar(str)));
   end;
   //Удаление строки из списка
   procedure DeleteFromList(id: Integer; index: Integer);
   begin
   SendDlgItemMessage(hParentWnd, id, LB_DELETESTRING, index, 0);
   end;
   //Выделение строки с заданным номером
   procedure SetListSel(id: Integer; index: Integer);
   begin
   SendDlgItemMessage(hParentWnd, id, LB_SETCURSEL, index, 0);
   end;
   //Получение номера выделенной строки (LB_ERR, если нет выделения)
   function GetListSel(id: Integer): Integer;
   begin
   GetListSel:= SendDlgItemMessage(hParentWnd, id, LB_GETCURSEL, 0, 0);
   end;
   //Получение количества строк
   function GetListCount(id: Integer): Integer;
   begin
   GetListCount:= SendDlgItemMessage(hParentWnd, id, LB_GETCOUNT, 0, 0);
   end;
   //Получение текста строки по ее индексу
   function GetListItemText(id: Integer; index: Integer):String;
   var buffer: String;
   begin
   SetLength(buffer,
   SendDlgItemMessage(hParentWnd, id, LB_GETTEXTLEN, index, 0)
   );
   SendDlgItemMessage(hParentWnd, id, LB_GETTEXT, index,
   Integer(Addr(buffer)));
   GetListItemText:= buffer;
   end;

   Функции и процедуры, приведенные в листинге 2.19, дают возможность управлять текстовыми полями (элементы Edit и Memo).
 //-- Листинг 2.19. Управление текстовыми полями --// 
   //Получение позиции первого выделенного символа (нумерация с нуля)
   function GetSelStart(id: Integer): Integer;
   var selStart, selEnd: Integer;
   begin
   SendDlgItemMessage(hParentWnd, id, EM_GETSEL, Integer(Addr(selStart)),
   Integer(Addr(selEnd)));
   GetSelStart:= selStart;
   end;
   //Получение длины выделенного фрагмента текста
   function GetSelLength(id: Integer): Integer;
   var selStart, selEnd: Integer;
   begin
   SendDlgItemMessage(hParentWnd, id, EM_GETSEL, Integer(Addr(selStart)),
   Integer(Addr(selEnd)));
   GetSelLength:= selEnd – selStart;
   end;
   //Выделение фрагмента текста (позиция первого символа с нуля)
   procedure SetSel(id: Integer; start, length: Integer);
   begin
   SendDlgItemMessage(hParentWnd, id, EM_SETSEL, start, start + length);
   end;
   //Получение выделенного фрагмента текста
   function GetSelText(id: Integer): String;
   var allText: String;
   begin
   allText:= GetText(id);
   GetSelText:= Copy(allText, GetSelStart(id)+1,GetSelLength(id));
   end;
   //Замена выделенного текста
   procedure ReplaceSelText(id: Integer; newText: String);
   begin
   SendDlgItemMessage(hParentWnd, id, EM_REPLACESEL,
   0, Integer(PAnsiChar(newText)));
   end;

   В листинге 2.20 приведены функции и процедуры, которые можно применять ко всем элементам управления с одинаковым успехом.
 //-- Листинг 2.20. Общие функции и процедуры --// 
   //Установка текста окна
   procedure SetText(id: Integer; str: String);
   begin
   SetWindowText(GetDlgItem(hParentWnd, id), PAnsiChar(str));
   end;
   //Получение текста окна
   function GetText(id: Integer): String;
   var buffer: String;
   begin
   SetLength(buffer, GetWindowTextLength(hParentWnd));
   GetWindowText(hParentWnd, PAnsiChar(buffer), Length(buffer));
   GetText:= buffer;
   end;
   //Активизация/деактивизация окна
   procedure SetEnabled(id: Integer; fEnabled: BOOL);
   begin
   EnableWindow(GetDlgItem(hParentWnd, id), fEnabled);
   end;


   Реакция на сообщения элементов управления

   При возникновении какого-либо предусмотренного для элемента управления события родительскому окну посылается сообщение WM_COMMAND.

   Примечание
   Сообщение WM_COMMAND приходит также при перерисовке так называемых «самоперерисовывающихся» (Owner Draw) элементов управления. Однако ввиду специфики данного вопроса и ограниченности объема главы оно рассматриваться не будет.

   Таким образом, когда родительское окно получает сообщение WM_COMMAND, то из двух прилагающихся параметров (lParam и wParam) можно извлечь следующие сведения:
   • старшие 16 бит wParam представляют собой целочисленный код уведомления, позволяющий определить, что именно произошло с элементом управления;
   • младшие 16 бит wParam представляют собой идентификатор элемента управления, состояние которого изменилось (именно этот идентификатор передавался вместо дескриптора меню при создании элементов управления);
   • lParam содержит дескриптор (HWND) окна элемента управления, состояние которого изменилось.
   Для выделения старших 16 бит из 32-битного значения можно использовать функцию HiWord. Для получения младших 16 бит можно использовать функцию с именем LoWord. Обе функции объявлены в модуле Windows.
   В качестве примеров можно привести следующие коды уведомлений:
   • BN_CLICKED – нажата кнопка;
   • EN CHANGE – изменен текст в текстовом поле;
   • LBN_SELCHANGE – изменилось выделение в списке;
   • CBN_SELCHANGE – изменилось выделение в раскрывающемся списке.
   Эти и все остальные константы уведомлений стандартных элементов управления объявлены в модуле Messages.

   Примечание
   Коды и описания уведомлений рассматриваемых в этой главе элементов управления приведены в приложении 3.



   Пример приложения

   Рассмотрим небольшой пример, иллюстрирующий принцип работы с элементами управления, помещенными на форму описанным ранее способом. Проект этого приложения называется ControlsDemo.
   Не буду заострять внимание на регистрации класса главного окна приложения, так как она аналогична приведенной в листинге 2.4. Рассмотрим создание окна с элементами управления в нем (листинг 2.21).
 //-- Листинг 2.21. Создание главного окна приложения (с элементами управления) --// 
   program ControlsDemo;
   uses
   Windows, Messages,
   Controls in 'Controls.pas';
   {$R *.res}
   var
   hMainWnd: HWND;
   hInst: Cardinal;
   mess: MSG;
   //Функция обработки сообщений
   ...
   //Создание окна и цикл обработки сообщений
   begin
   hInst:= GetModuleHandle(nil);
   //Регистрация и создание главного окна
   if not RegisterWindow() then Exit;
   hMainWnd:= CreateWindow(
   'MyWindowClass', //Имя класса окна
   'Главное окно', //Заголовок окна
   WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT, //Координата X по умолчанию
   CW_USEDEFAULT, //Координата Y по умолчанию
   CW_USEDEFAULT, //Ширина по умолчанию
   CW_USEDEFAULT, //Высота по умолчанию
   HWND(nil), //Нет родит ельского окна
   HMENU(nil), //Нетменю
   hInst,
   nil);
   if (hMainWnd = HWND(nil)) then Exit;
   //Инициализация модуля Controls для работы с главным окном
   Controls.hParentWnd:= hMainWnd;
   Controls.hAppInst:= hInst;
   //Создание элементов управления
   CreateFrame(10, 80, 170, 70, –1, 'Кнопки');
   CreateButton(2 0, 100, 70, 30, 1001, 'Кнопка 1');
   CreateButton(100, 100, 70, 30, 1002,'Кнопка 2');
   CreateFrame(2 00, 10, 200, 180, –1, 'Флажки и переключатели');
   CreateCheck(2 10, 30, 180, 20, 2001, 'Флажок 1');
   CreateCheck(2 10, 60, 180, 20, 2002, 'Флажок 2', True);
   CreateOption(2 10, 100, 180, 20, 3001, 'Переключатель 1', True);
   CreateOption(2 10, 130, 180, 20, 3002, 'Переключатель 2', False, True);
   CreateOption(2 10, 160, 180, 20, 3003, 'Переключатель 3', True);
   CreateFrame(420, 10, 300, 180, –1, 'Списки и статические надписи');
   CreateLabel(430, 30, 70, 20, -1, 'Надпись');
   CreateCombo(510, 30, 200, 100, 4001);
   CreateList(430, 60, 280, 120, 5001);
   CreateFrame(2 00, 200, 200, 240, –1, 'Текстовые поля');
   CreateEdit(2 10, 22 0, 180, 20, 6001, 'Текст в текстовом поле');
   CreateMemo(2 10, 25 0, 180, 180, 6002, 'Текст в многострочном'+ #13
   + #10 + 'текстовом поле');
   //Добавление строк в списки
   AddToCombo(4001, 'Строка 1');
   AddToCombo(4001, 'Строка 2');
   AddToCombo(4001, 'Строка 3');
   AddToList(5 001, 'Строка 1');
   AddToList(5 001, 'Строка 2');
   AddToList(5 001, 'Строка 3');
   ShowWindow(hMainWnd, SW_NORMAL);
   //Запуск цикла обработки сообщений
   while (Longint(GetMessage(mess, 0, 0, 0)) <> 0)
   do begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;
   end.

   Код листинга 2.21 заодно демонстрирует использование некоторых из приведенных ранее функций работы с элементами управления. Выглядит созданное окно так, как показано на рис. 2.3.
   Рис. 2.3. Окно с элементами управления

   Принцип построения функции обработки сообщений для этого окна описан в листинге 2.22.
 //-- Листинг 2.22. Функция обработки сообщений --// 
   //Функция обработки сообщений
   function WindowFunc(hWnd:HWND; msg:UINT;
   wParam:WPARAM; lParam:LPARAM):LRESULT; stdcall;
   var
   ps: PAINTSTRUCT;
   begin
   case msg of
   WM_PAINT:
   begin
   //Перерисовка содержимого окна
   BeginPaint (hWnd, ps);
   Text Out (ps.hdc, 10, 10, 'Текст в окне', 12);
   EndPaint(hWnd, ps);
   end;
   WM_CLOSE:
   if (hWnd = hMainWnd) then
   PostQuit Message(0); //При закрытии этого окна завершается
   //приложение
   WM_COMMAND:
   begin
   case LOWORD(wParam) of
   //нажата "Кнопка 1"
   1001: if HIWORD(wParam) = BN_CLICKED then;
   //нажата "Кнопка 2"
   1002: if HIWORD(wParam) = BN_CLICKED then;
   //установлен "Флажок 1"
   2001: if HIWORD(wParam) = BN_CLICKED then;
   //установлен "Флажок 2"
   2002: if HIWORD(wParam) = BN_CLICKED then;
   //установлен "Переключатель 1"
   3001: if HIWORD(wParam) = BN_CLICKED then;
   //установлен "Переключатель 2"
   3002: if HIWORD(wParam) = BN_CLICKED then;
   //установлен "Переключатель 3"
   3003: if HIWORD(wParam) = BN_CLICKED then;
   //выделение в ComboBox
   4001: if HIWORD(wParam) = CBN_SELCHANGE then;
   //выделение в ListBox
   5001: if HIWORD(wParam) = LBN_SELCHANGE then;
   //изменен текст в Edit
   6001: if HIWORD(wParam) = EN_CHANGE then;
   //изменен текст в Memo
   6002: if HIWORD(wParam) = EN_CHANGE then;
   end;
   end;
   else
   begin
   //Обработка по умолчанию
   WindowFunc:= DefWindowProc(hWnd, msg, wParam, lParam);
   Exit;
   end;
   end;
   WindowFunc:= S_OK; //Сообщение обработано
   end;

   Приведенная в листинге 2.22 функция не претендует на то, чтобы быть эталоном в порядке классификации сообщений от элементов управления. Иногда бывает полезно сразу классифицировать сообщения не по элементам управления, которые их прислали, а по типу самих сообщений. К тому же в ряде случаев можно предусмотреть один обработчик сообщений сразу для нескольких элементов управления, например, для группы переключателей. В таком случае полезным окажется параметр lParam сообщения WM COMMAND.
   Кстати, размер исполняемого файла этого приложения равен всего 19 Кбайт.



   Стандартные окна Windows


   Теперь рассмотрим, как можно с помощью только функций Windows API вызывать некоторые распространенные окна. Чтобы использовать API-функции и структуры с информацией для этих окон, необходимо подключить следующие модули:
   • CommDlg – для окон открытия и сохранения файла, выбора цвета и шрифта, поиска и замены текста;
   • ShlObj и ActiveX – для окна выбора папки (второй модуль нужен для доступа к интерфейсу IMalloc, зачем – будет рассказано далее);
   • Windows – помимо объявления основных структур и API-функций, этот модуль содержит объявления функций для работы с окнами подключения и отключения от сетевого ресурса (сетевого диска);
   • ShellAPI – для системного окна О программе.
   Вариант использования рассматриваемых в этом разделе окон приведен в подразделе «Демонстрационное приложение» данной главы (стр. 81).

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



   Окно открытия и сохранения файла

   Чтобы использовать возможности окна открытия файла, достаточно воспользоваться функцией, представленной в листинге 2.23.
 //-- Листинг 2.23. Окно открытия файла --// 
   function ShowOpen(strFilter: string; nFilterIndex: Integer= 0;
   strInitFileName: string = '';
   var
   ofn: OPENFILENAME;
   begin
   ZeroMemory(Addr(ofn), SizeOf(ofn));
   //Формирование буфера (260 символов)
   SetLength(strInitFileName, MAX_PATH);
   PrepareFilterString(strFilter);
   //Заполнение структуры для окна
   ofn.lStructSize:= SizeOf(ofn);
   ofn.hWndOwner:= hParentWnd;
   ofn.hInstance:= hAppInst;
   ofn.lpstrFilter:= PAnsiChar(strFilter);
   ofn.nFilterIndex:= nFilterIndex;
   ofn.lpstrFile:= PAnsiChar(strInitFileName);
   ofn.nMaxFile:= MAX_PATH;
   ofn.lpstrTitle:= pAnsiChar(strTitle);
   ofn.Flags:= OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_HIDEREADONLY;
   //Отображение окна и обработка результата
   if(GetOpenFileName(ofn) = True) then
   ShowOpen:= ofn.lpstrFile;
   end;

   Приведенная в листинге 2.23 функция возвращает непустую строку – полный путь файла в случае, если пользователь выбрал или ввел имя файла. Здесь главной трудностью является заполнение довольно большой структуры OPENFILENAME. В данном примере используются только базовые возможности окна открытия файла и лишь некоторые из поддерживаемых им флагов (поле Flags):
   • OFN_FILEMUSTEXIST – при этом установленном флаге, если окно успешно завершило свою работу, можно быть уверенным, что результирующий путь является путем существующего файла;
   • OFN_PATHMUSTEXIST – данный флаг не дает ввести имя файла в несуществующей папке (например, при вводе c: \docs\mydoc1.doc, если папки docs не существует, будет выдано соответствующее сообщение);
   • OFN_HIDEREADONLY – позволяет скрыть флажок Только для чтения.
   Теперь отдельно рассмотрим, зачем в приведенном примере вызывается дополнительная функция PrepareFilterString (листинг 2.24).
 //-- Листинг 2.24. Преобразование строки фильтра --// 
   procedure PrepareFilterString(var strFilter: string);
   var
   i: Integer;
   begin
   for i:= 1 to length(strFilter) do
   if (strFilter[i] = '|') then strFilter[i]:= #0;
   end;

   Дело в том, что при задании фильтров (поле lpstrFile) требуется, чтобы каждое название и обозначение фильтров были отделены символом #0, а за последним фильтром шла последовательность из двух нулевых символов. На практике задавать строку из нескольких фильтров в следующем виде недостаточно удобно:

   'Текстовые файлы'+ #0 + '*.txt'+ #0 + 'Все файлы'+ '*.*'+ #0 + #0

   Поэтому часто применяются другие разделители, которые впоследствии преобразуются в символы #0. В данном случае в качестве разделителя используется символ |, благодаря чему приведенная выше строка фильтра может быть записана так:

   'Текстовые файлы|*.txt |Все файлы|*.*||'

   Согласитесь, что получилось более кратко и понятно.
   Теперь обратимся к окну сохранения файла. Для его вызова достаточно переделать код из листинга 2.23 следующим образом (листинг 2.25).
 //-- Листинг 2.25. Окно сохранения файла --// 
   function ShowSave(strFilter: string; nFilterIndex: Integer = 0;
   strInitFileName: string = '';
   strTitle: string = 'Сохранение файла'):string;
   var
   ofn: OPENFILENAME;
   begin
   ZeroMemory(Addr(ofn), SizeOf(ofn));
   //Формирование буфера (260 символов)
   SetLength(strInitFileName, MAX_PATH);
   PrepareFilterString(strFilter);
   //Заполнение структуры для окна
   ofn.lStructSize:= SizeOf(ofn);
   ofn.hWndOwner:= hParentWnd;
   ofn.hInstance:= hAppInst;
   ofn.lpstrFilter:= PAnsiChar(strFilter);
   ofn.nFilterIndex:= nFilterIndex;
   ofn.lpstrFile:= PAnsiChar(strInitFileName);
   ofn.nMaxFile:= MAX_PATH;
   ofn.lpstrTitle:= pAnsiChar(strTitle);
   ofn.Flags:= OFN_PATHMUSTEXIST or OFN_OVERWRITEPROMPT;
   //Отображение окна и обработка результата
   if (GetSaveFileName(ofn) = True) then
   ShowSave:= ofn.lpstrFile;
   end;

   Здесь дополнительно к упомянутому ранее флагу OFN_PATHMUSTEXIST добавлен флаг OFN_OVE RWRITEPROMPT для того, чтобы при указании имени уже существующего файла был задан вопрос, желает ли пользователь заменить этот файл.


   Окно для выбора цвета

   Код вызова следующего окна – окна для выбора цвета – приведен в листинге 2.26.
 //-- Листинг 2.26. Окно для выбора цвета --// 
   function ShowChooseColor(lastColor: COLORREF = 0):COLORREF;
   var
   choose: TChooseColor;
   begin
   ZeroMemory(Addr(choose), SizeOf(choose));
   //Заполнение структуры для окна
   choose.lStructSize:= SizeOf(choose);
   choose.hWndOwner:= hParentWnd;
   choose.hInstance:= hAppInst;
   choose.rgbResult:= lastColor;
   choose.lpCustColors:= Addr(colors);
   choose.Flags:= CC_RGBINIT or CC_ANYCOLOR or CC_FULLOPEN;
   //Отображение окна и обработка результата
   if (ChooseColor(choose) = True) then ShowChooseColor:= choose.rgbResult
   else ShowChooseColor:= lastColor;
   end;

   Здесь также заполняется специальная структура. Для этого используются следующие флаги:
   • CC_RGBINIT – использовать значение поля rgbResult в качестве предустановленного значения цвета (по умолчанию как ранее выбранного);
   • CC_ANYCOLOR – отображать все доступные предопределенные цвета (левая половина, рис. 2.4);
   Рис. 2.4. Окно для выбора цвета

   • CC_FULLOPEN – раскрывать панель подбора цвета (правая половина окна, рис. 2.4).
   Стоит пояснить, что за переменная, а точнее, адрес, сохраняется в поле lpCustColors – это массив из 16 значений типа COLORREF:

   colors: array [1..16] of COLORREF;

   Обратите внимание на 16 квадратов в левой нижней области окна (рис. 2.4) – это места для определенных пользователем цветов. Для заполнения этой области окна и используются значения из массива colors. Массив может быть как локальным, так и глобальным (что иногда удобнее, так как значения определенных пользователем цветов в этом случае сохраняются между вызовами окна выбора цвета).


   Окно для выбора шрифта

   Для вывода окна для выбора шрифта подойдет функция, приведенная в листинге 2.27.
 //-- Листинг 2.27. Окно для выбора шрифта --// 
   function ShowChooseFont(var font: LOGFONT):BOOL;
   var
   choose: TChooseFont;
   begin
   ZeroMemory(Addr(choose), SizeOf(choose));
   //Заполнение структуры для окна
   choose.lStructSize:= SizeOf(choose);
   choose.hWndOwner:= hParentWnd;
   choose.hInstance:= hAppInst;
   choose.lpLogFont:= Addr(font);
   choose.Flags:= CF_BOTH or CF_INITTOLOGFONTSTRUCT;
   //Отображение окна и обработка результата
   if (ChooseFont (choose) = True) then
   begin
   CopyMemory(Addr(font), choose.lpLogFont, SizeOf(font));
   ShowChooseFont:= True;
   end
   else ShowChooseFont:= False;
   end;

   Здесь используются флаги окна, имеющие следующие значения:
   • CF_BOTH – позволяет отображать экранные и принтерные шрифты (для отображения либо экранных, либо принтерных шрифтов можно использовать флаги CF_SCREENFONTS и CF_PRINTERFONTS соответственно);
   • CF_INITTOLOGFONTSTRUCT – позволяют выбрать в окне шрифт, соответствующий (или максимально похожий) шрифту, описываемому структурой LOGFONT, указатель на которую сохраняется в поле lpLogFont.


   Окно для выбора папки

   Чтобы иметь возможность пользоваться окном Обзор папок, можно использовать функцию, представленную в листинге 2.28.
 //-- Листинг 2.28. Окно для выбора папки --// 
   function ShowChooseFolder(strTitle: string):string;
   var
   choose: BROWSEINFO;
   buffer: string;
   pidl: PItemIDList;
   begin
   ZeroMemory(Addr(choose), SizeOf(choose));
   SetLength(buffer, MAX_PATH);
   //Заполнение структуры для окна
   choose.hwndOwner:= hParentWnd;
   choose.pi dlRoot:= nil; //Корень – папка Рабочего стола
   choose.pszDisplayName:= PAnsiChar(buffer);
   choose.lpszTitle:= PAnsiChar(strTitle);
   choose.ulFlags:= 0;
   //Вывод окна и обработка результата
   pidl:= SHBrowseForFolder(choose);
   if (pidl <> nil) then
   begin
   //Получение полного пути выбранной папки
   SHGetPathFromIDList(pidl, PAnsiChar(buffer));
   ShowChooseFolder:= buffer;
   DeletePIDL(pidl);
   end
   else
   ShowChooseFolder:= '';
   end;

   Представленная в листинге 2.28 функция ShowChooseFolder возвращает полный путь указанной папки, если она выбрана, и пустую строку в противном случае. Само окно Обзор папок показано на рис. 2.5.
   Рис. 2.5. Окно для выбора папки

   Особенностью использованной в данном примере функции SHBrowseForFolder является то, что она возвращает не путь выбранной папки, а указатель на структуру ItemlDList (что-то вроде внутреннего представления путей). Для извлечения построения пути по содержимому этой структуры используется функция SHGetPathFromIDList. После этого структура становится больше не нужна, и ее следует удалить (с использованием специального интерфейса IMalloc). Для этого используется процедура DeletePIDL, реализованная в листинге 2.29.
 //-- Листинг 2.29. Удаление структуры ItemlDList --// 
   procedure DeletePIDL(pidl: PItemIDList);
   var
   pMalloc: IMalloc;
   begin
   SHGetMalloc(pMalloc);
   if (pMalloc <> nil) then
   begin
   pMalloc.Free(pidl);
   pMalloc._Release();
   end;
   end;

   Функцию SHBrowseForFolder (листинг 2.28) можно использовать и для указания принтеров или компьютеров. Для этого достаточно установить флаги BIF_ BROWSEFORCOMPUTER и BIF_BROWSEFORPRINTER соответственно:

   choose.ulFlags:= BIF_BROWSEFORCOMPUTER;

   и

   choose.ulFlags:= BIF_BROWSEFORPRINTER;

   Чтобы в окне отображались еще и значки файлов, нужно установить флаг BIF_ BROWSEINCLUDEFILES.


   Окна подключения и отключения сетевого ресурса

   Часто бывает удобно осуществлять доступ к сетевым папкам так же, как и к локальным дискам компьютера (с использованием того же принципа построения пути). Окна подключения и отключения сетевого ресурса дают пользователю возможность выбрать, какие папки считать сетевыми дисками и какие сетевые диски можно отключить.
   Вид окна подключения сетевого ресурса в Windows XP показан на рис. 2.6.
   Рис. 2.6. Окно подключения сетевого диска

   Для вызова окна подключения сетевого ресурса можно использовать функцию, приведенную в листинге 2.30.
 //-- Листинг 2.30. Окно подключения сетевого ресурса --// 
   function ShowConnection(): BOOL;
   begin
   ShowConnection:=
   WNetConnectionDialog(hParentWnd, RESOURCETYPE_DISK) = NO_ERROR;
   end;

   Функция ShowConnection возвращает значение True в случае удачного подключения и False – в противном случае.
   Окно отключения сетевого диска показано на рис. 2.7.
   Рис. 2.7. Отключение сетевого ресурса

   Функция, показывающая окно отключения сетевого диска, приведена в листинге 2.31.
 //-- Листинг 2.31. Окно отключения сетевого ресурса --// 
   function ShowDisconnect(): BOOL;
   begin
   ShowDisconnect:=
   WNetDisconnectDialog(hParentWnd, RESOURCETYPE_DISK) = NO_ERROR;
   end;

   Аналогично ShowConnection функция ShowDisconnect возвращает значение True, если отсоединен хотя бы один диск, и False – в противном случае.


   Системное окно О программе

   Этот последний и довольно специфический пример приведен на тот случай, если возникнет желание или необходимость использовать окно О программе, которое выводится для самой операционной системы Windows и ее компонентов. Процедура, выводящая это окно, приведена в листинге 2.32.
 //-- Листинг 2.32. Окно О программе --// 
   procedure ShowAbout(strAppName: string; strInfo: string);
   begin
   ShellAbout(hParentWnd, PAnsiChar(strAppName), PAnsiChar(strInfo),
   LoadIcon(0, IDI_ASTERISK));
   end;

   Правда, в окне О программе Windows XP на информацию о приложении отведено всего две строки (и место для значка). Все остальное место занимают информация о регистрации операционной системы и фирменная эмблема Microsoft Windows XP.


   Демонстрационное приложение

   Теперь пришла очередь рассмотреть небольшое приложение, использующее описанные выше окна (проект StandartWindows). Окно этого приложения показано на рис. 2.8.
   Рис. 2.8. Окно демонстрационного приложения

   Размер EXE-файла приложения равен 22 Кбайт.
   В листинге 2.33 приведены объявления используемых глобальных переменных, код, реализующий создание окна и элементов управления в нем, а также цикл обработки сообщений (файл StandartWindows.dpr). Функции работы с рассмотренными выше окнами вынесены в отдельный модуль StdWindows (файл StdWindows.pas).
   В листингах 2.33-2.34 используются уже знакомые вам функции из модуля Controls.
 //-- Листинг 2.33. Глобальные переменные, код создания окна и цикл обработки сообщений --// 
   program StandartWindows;
   {$R *.res}
   uses
   Windows, Messages, CommDlg,
   Controls in 'Controls.pas',
   StdWindows in 'StdWindows.pas';
   var
   hMainWnd: HWND;
   hInst: Cardinal;
   mess: MSG;
   curColor: COLORREF;
   font: LOGFONT;
   hCurFont: HFONT;
   ...
   function RegisterWindow():Boolean;
   ...
   begin
   hInst:= GetModuleHandle(nil);
   //Регистрация и создание главного окна
   if not RegisterWindow() then Exit;
   hMainWnd:= CreateWindow(
   'MyWindowClass', //Имя класса окна
   'Стандартные окна Windows', //Заголовок окна
   WS_CAPTION or WS_SYSMENU or WS_CLIPCHILDREN or WS_CLIPSIBLINGS,
   CW_USEDEFAULT, //Координата X по умолчанию
   CW_USEDEFAULT, //Координата Y по умолчанию
   470, 420,
   HWND(nil), //Нет родительского окна
   HMENU(nil), //Нетменю
   hInst,
   nil);
   if (hMainWnd = HWND(nil)) then Exit;
   //Инициализация модуля Controls для работы с главным окном приложения
   Controls.hParentWnd:= hMainWnd;
   Controls.hAppInst:= hInst;
   //Инициализация модуля StdWindows для работы с главным окном приложения
   StdWindows.hParentWnd:= hMainWnd;
   StdWindows.hAppInst:= hInst;
   //Создание кнопок для открытия окон
   CreateButton(2 0, 20, 200, 30, 1001, 'Открытие файла');
   CreateButton(2 0, 60, 200, 30, 1002, 'Сохранение файла');
   CreateButton(2 0, 100, 200, 30, 1003, 'Выбор цвета');
   CreateButton(2 0, 140, 200, 30, 1004, 'Выбора шрифта');
   CreateButton(2 0, 180, 200, 30, 1005, 'Окно поиска текста');
   CreateButton(2 0, 22 0, 200, 30, 1006, 'Окно поиска и замены');
   CreateButton(23 0, 20, 22 0, 30, 1010, 'Выбор папки');
   CreateButton(23 0, 60, 22 0, 30, 1011, 'Подключение сетевого ресурса');
   CreateButton(23 0, 100, 22 0, 30, 1012, 'Отключение сетевого ресурса');
   CreateButton(23 0, 140, 22 0, 30, 1013, 'Системное окно "О программе"');
   //Текстовое поле для результата
   CreateMemo (20, 270, 430, 100, 2001);
   ShowWindow(hMainWnd, SW_NORMAL);
   //Запуск цикла обработки сообщений
   while (Longint(GetMessage(mess, 0, 0, 0)) <> 0) do
   begin
   if (IsDialogMessage(hMainWnd, mess) = False) then
   begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;
   end;
   end.

   Код функции RegisterWindow опущен, поскольку он аналогичен приведенному в листинге 2.4. Функции работы с рассмотренными ранее окнами вынесены в модуль StdWindows(файл StdWindows.pas).
   Особенностью цикла обработки сообщений в этом примере является использование API-функции IsDialogMessage, которая позволяет реагировать на некоторые действия пользователя так, как это делается в рассмотренных выше окнах. Примером может служить перемещение фокуса между окнами при нажатии клавиши Tab.
   Перед функцией RegisterWindow (на месте многоточия перед ее объявлением в листинге 2.33) находится функция обработки сообщений, имеющая следующий вид (листинг 2.34).
 //-- Листинг 2.34. Функция обработки сообщений --// 
   function WindowFunc(hWnd:HWND; msg:UINT; wParam:WPARAM; lParam:LPARAM):LRESULT; stdcall;
   var
   hOldFont: HFONT;
   strBuf: String;
   hEditDC: HDC;
   begin
   case msg of
   WM_CLOSE:
   if (hWnd = hMainWnd) then PostQuitMessage(0);
   WM_CTLCOLOREDIT: //Сообщения от Edit перед перерисовкой
   begin
   //Зададим тексту Edit выбранный цвет
   hEditDC:= HDC(wParam);
   SetTextColor(hEditDC, curColor);
   GetCurrentObject(hEditDC, OBJ_BRUSH);
   end;
   WM_COMMAND:
   if (HIWORD(wParam) = BN_CLICKED) then
   begin
   //Определим, какая кнопка нажата
   case LOWORD(wParam) of
   1001: //Открытие файла
   begin
   SetText(2 001, 'Открыт файл:'+ #13 + #10 +
   ShowOpen('Все файлы|*.*||'));
   end;
   1002: //Сохранение файла
   begin
   SetText(2001, 'Путь для сохранения:'+ #13 + #10 +
   ShowSave('Все файлы|*.*||'));
   end;
   1003: //Выбор цвета
   begin
   curColor:= ShowChooseColor(curColor);
   Str(curColor, strBuf);
   SetText(2001, 'Выбранный цвет:'+ #13 + #10 + strBuf);
   end;
   1004: //Выбор шрифта
   begin
   if (ShowChooseFont(font) = True) then
   begin
   //Замена шрифта в Edit
   hOldFont:= HFONT(
   SendDlgItemMessage(hMainWnd,2001,WM_GETFONT, 0,0));
   hCurFont:= CreateFontIndirect(font);
   SendDlgItemMessage(hMainWnd, 2001, WM_SETFONT,
   Integer(hCurFont), Integer(True));
   SetText(2001, 'Текст, записанный выбранным шрифтом');
   if (hOldFont <> 0) then DeleteObject(hOldFont);
   end;
   end;
   1010: //Выбор папки
   begin
   SetText(2 001, 'Выбранная папка:'+ #13 + #10 +
   ShowChooseFolder());
   end;
   1011: //Подключение сетевого ресурса
   begin
   ShowConnection();
   end;
   1012: //Отключение сетевого ресурса
   begin
   ShowDisconnect();
   end;
   1013: //Окно "О программе"
   begin
   ShowAbout('Standart windows',
   'Демонстрация использования стандартных '+
   'окон из чистого API-приложения');
   end;
   end;
   end;
   else
   begin
   //Обработка по умолчанию
   WindowFunc:= DefWindowProc(hWnd, msg, wParam, lParam);
   Exit;
   end;
   end;
   WindowFunc:= S_OK; //Сообщение обработано
   end;

   Обработка сообщений здесь довольно проста, за исключением изменения шрифта текстового поля. Обратите внимание на следующий отрывок листинга 2.34:

   //Замена шрифта в Edit
   hOldFont:= HFONT(SendDlgItemMessage(hMainWnd,2001,WM_GETFONT, 0,0));
   hCurFont:= CreateFontIndirect(font);
   SendDlgItemMessage(hMainWnd, 2001, WM_SETFONT,
   Integer(hCurFont), Integer(True));
   SetEdit Text(2001, 'Текст, записанный выбранным шрифтом');
   if (hOldFont <> 0) then DeleteObject(hOldFont);

   Этот довольно объемный фрагмент кода всего лишь заменяет шрифт в текстовом поле. Подобную операцию можно использовать для задания шрифта любого элемента управления. В частности, в приведенных в этой главе примерах текст на кнопках, надписях и т. д. выглядит довольно невзрачно потому, что используется системный шрифт, установленный по умолчанию.
   Способ, которым можно установить шрифт всех элементов управления окна, рассмотрен ниже. Остался еще один существенный момент: не забывайте удалять объекты GDI (в данном случае – шрифт) после того, как они стали не нужны. Дело в том, что приложение может содержать не более 65 ООО объектов GDI, и при наличии так называемой «утечки» ресурсов GDI может произойти аварийное завершение приложения из-за исчерпания лимита ресурсов GDI.



   Установка шрифта элементов управления

   Существует множество способов установки шрифта текста, отображаемого в элементах управления. Можно, например, при создании элемента управления посылать ему сообщение WM_SETFONT, передавая дескриптор (HFONT) созданного ранее объекта шрифта. В таком случае код создания и установки шрифта элементов управления (с использованием рассмотренных в этой главе функций) может выглядеть, как показано в листинге 2.35.
 //-- Листинг 2.35. Установка шрифта во время создания элементов управления --// 
   //Шрифт для элементов управления
   font:= CreateFont(16, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET,
   OUT_CHARACTER_PRECIS, CLIP_DEFAULT_PRECIS,
   DEFAULT_QUALITY, DEFAULT_PITCH, 'Courier new');
   //Создание элементов управления
   ctrl:= CreateButt on(20, 30, 70, 30, 1001, 'Кнопка 1');
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateButt on(100, 30, 70, 30, 1002,'Кнопка 2');
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateCheck(210, 30, 180, 20, 2001, 'Флажок 1');
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateCheck(210, 60, 180, 20, 2001, 'Флажок 2', True);
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateOpti on(210, 100, 180, 20, 3001, 'Переключатель 1', True);
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateOpti on(210,130,180,20,3002, 'Переключатель 2', False, True);
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   ctrl:= CreateOpti on(210, 160, 180, 20, 3003, 'Переключатель 3', True);
   SendMessage(ctrl, WM_SETFONT, HFONT(font), 1);
   //Запуск цикла обработки сообщений
   while (Longint(GetMessage(mess, 0, 0, 0)) <> 0)
   do begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;
   //Удаление шрифта
   DeleteObject(font);

   Вид окна с элементами управления, шрифт которых установлен любым из рассмотренных способов, показан на рис. 2.9.
   Рис. 2.9. Шрифт элементов управления, отличный от системного

   Способ задания шрифта, приведенный в листинге 2.35, легко реализовать. Его существенным недостатком является двукратное увеличение количества строк кода, выполняющих создание элементов управления. Для окон, содержащих большое количество элементов управления, можно предложить более универсальный способ (листинг 2.36).
 //-- Листинг 2.36. Установка шрифта перебором элементов управления --// 
   //Шрифт для элементов управления
   font:= CreateFont(16, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET,
   OUT_CHARACTER_PRECIS, CLIP_DEFAULT_PRECIS,
   DEFAULT_QUALITY, DEFAULT_PITCH, 'Courier new');
   //Создание элементов управления
   CreateButton(20, 30, 70, 30, 1001, 'Кнопка 1');
   CreateButton(100, 30, 70, 30, 1002,'Кнопка 2');
   CreateCheck(210, 30, 180, 20, 2001, 'Флажок 1');
   CreateCheck(210, 60, 180, 20, 2001, 'Флажок 2', True);
   CreateOption(210, 100, 180, 20, 3001, 'Переключатель 1', True);
   CreateOption(210, 130, 180, 20, 3002, 'Переключатель 2', False, True);
   CreateOption(210, 160, 180, 20, 3003, 'Переключатель 3', True);
   //Установка шрифта элементов управления
   EnumChildWindows(hMainWnd, Addr(EnumFunc), font);
   //Запуск цикла обработки сообщений
   while (Longint(GetMessage(mess, 0, 0, 0)) <> 0)
   do begin
   TranslateMessage(mess);
   DispatchMessage(mess);
   end;
   DeleteObject(font);

   Собственно, за установление шрифта отвечает в приведенном листинге только одна строка:

   EnumChildWindows(hMainWnd, Addr(EnumFunc), font);

   При этом также нужно определить функцию обратного вызова (в данном случае это функция EnumFunc), которая будет вызываться по одному разу для каждого дочернего окна. В данном примере функция EnumFunc имеет следующий вид (листинг 2.37).
 //-- Листинг 2.37. Реализация функции EnumFunc --// 
   function EnumFunc(wnd: HWND; param: LPARAM): BOOL; stdcall;
   begin
   SendMessage(wnd, WM_SETFONT, WPARAM(param), LPARAM(True));
   EnumFunc:= True; //Продолжать перечисление
   end;

   В принципе, имя этой функции и названия параметров могут быть любыми, а вот типы параметров, возвращаемого значения и способ вызова функции должны быть именно такими, какие представлены в листинге 2.37. Функция должна возвращать True, если нужно продолжать перечисление окон, и False – в противном случае. Значение, которое было передано в качестве третьего параметра API-функции EnumChildWindows, передается в функцию обратного вызова. В данном случае этим параметром является дескриптор шрифта.



   Глава 3
   Мышь и клавиатура


   • Мышь
   • Клавиатура

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


   Мышь


   Кто из нас не пользовался мышью? Это незамысловатое устройство помогает быстро, а главное просто работать с программами. Начнем с простых операций с мышью. Научимся определять координаты, а также программно перемещать и делать невидимым указатель мыши. Кроме того, вы узнаете, как «захватить» указатель мыши, ограничить область его перемещения и вычислить расстояние, проходимое указателем мыши на экране монитора.


   Координаты и указатель мыши

   Для начала программным путем определим наличие мыши в системе, а именно выясним, что мышь подключена к компьютеру и правильно настроена (то есть устройство используется). Один из способов определения наличия мыши демонстрирует следующий пример (листинг 3.1).
 //-- Листинг 3.1. Определение присутствия в системе мыши --// 
   function MousePresent: Boolean;
   begin
   //С помощью вызова GetSystemMetrics определяем наличие мыши в системе
   if GetSystemMetrics(SM_MOUSEPRESENT) <> 0 then
   Result:= True
   else
   Result:= False;
   end;

   Описанная выше функция MousePresent позволяет проверить наличие мыши. Если мышь подключена к компьютеру и ее можно использовать, то MousePresent возвращает значение True, в противном случае – False.
   После того как мышь обнаружена, можно приступать к определению координат ее указателя на экране монитора (листинг 3.2).
 //-- Листинг 3.2. Определение координат указателя мыши --// 
   procedure MouseForm.Button1Click(Sender: TObject);
   var
   pt: TPoint;
   begin
   //Получаем координаты указателя мыши
   GetCursorPos(pt);
   ShowMessage('('+ IntToStr(pt.X) + ','+ IntToStr(pt.Y) +')');
   end;

   Здесь для определения координат указателя мыши использовалась API-функция GetCursorPos. Передав в эту функцию переменную pt типа TPoint, вы получите текущие координаты указателя мыши на экране монитора.
   В следующем примере демонстрируется, как программным путем скрыть указатель мыши. При нажатии кнопки Button2 указатель будет исчезать, а при нажатии кнопки Button3 (например, с помощью клавиатуры) – появляться (листинг 3.3).
 //-- Листинг 3.3. Скрытие указателя мыши --// 
   procedure MouseForm.Button2Click(Sender: TObject);
   begin
   //Прячем указатель
   ShowCursor(False);
   end;
   procedure MouseForm.Button3Click(Sender: TObject);
   begin
   //Показываем указатель
   ShowCursor(True);
   end;

   В приведенном примере для управления отображением указателя мыши используется функция ShowCursor, которая либо скрывает его (принимая значение False), либо снова показывает (принимая значение True). По причине того, что управление мышью при скрытом указателе невозможно, исходный текст, который управляет видимостью указателя, помещен в обработчики нажатия кнопок формы. Когда указатель скрыт, можно использовать клавишу Tab для выбора и нажатия кнопки формы, используя клавиатуру.
   Существуют и другие способы скрыть указатель: рассмотрим пример управления его видимостью посредством установки свойства Cursor компонента:

   TempForm.Cursor:= crNone;

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

   Screen.Cursor:= crNone;

   Изменять положение указателя на экране можно не только физически передвигая мышь, но и программно задавая новые экранные координаты. Следующий код демонстрирует, каким образом это можно сделать (листинг 3.4).
 //-- Листинг 3.4. Изменение координат мыши --// 
   procedure TForm1.Button1Click(Sender: TObject);
   var
   pt: TPoint;
   begin
   Application.ProcessMessages;
   Screen.Cursor:= CrHourglass;
   GetCursorPos(pt);
   SetCursorPos(pt.x + 1, pt.y + 1);
   Application.ProcessMessages;
   SetCursorPos(pt.x – 1, pt.y – 1);
   end;


   Захват указателя мыши

   Существует ряд задач, для выполнения которых полезно иметь возможность получать сообщения от мыши даже тогда, когда указатель находится за пределами формы. Обычно рабочая область программы ограничена размерами окна приложения. Это значит, что действия, выполняемые пользователем за пределами рабочей области, попросту не воспринимаются приложением. Попробуйте сделать следующее: откройте редактор Paint, сделайте размер его окна меньше размера холста, затем, зажав кнопку мыши, нарисуйте линию так, чтобы в ходе рисования указатель вышел за пределы окна редактора. Если теперь развернуть окно редактора на весь экран, то можно увидеть, что рисунок содержит часть линии, которую вы рисовали, перемещая указатель за пределами окна редактора. Так происходит, потому что редактор Paint «захватывает» указатель, как только пользователь начинает рисовать линию, и «освобождает» указатель, когда пользователь отпускает кнопку мыши.
   Захват указателя полезен и в других случаях, потому стоит рассмотреть способ его реализации (а сделать это действительно просто). В листинге 3.5 приведены обработчики нажатия и отпускания кнопки мыши, которые реализуют захват указателя на время от нажатия до отпускания кнопки.
 //-- Листинг 3.5. Захват и освобождение указателя мыши --// 
   procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
   Shift: TShiftState; X, Y: Integer);
   begin
   //Захватываем указатель мыши
   SetCapture(Handle);
   end;
   procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;
   Shift: TShiftState; X, Y: Integer);
   begin
   //Отменяем захват указателя
   ReleaseCapture();
   end;

   Вся хитрость состоит в использовании API-функций захвата SetCapture и ReleaseCapture. При вызове первой функции происходит регистрация окна, которое захватывает указатель мыши: это окно будет получать сообщения от мыши даже тогда, когда указатель переместится за его пределы. Функция возвращает дескриптор окна, которое захватило указатель ранее, либо 0, если такого окна нет. Соответственно, функция ReleaseCapture используется для освобождения указателя.

   Примечание
   При использовании SetCapture окно получает сообщения, когда указатель находится не над окном, только в том случае, если кнопка мыши нажата либо указатель находится над одним из окон, созданных тем же потоком (независимо от того, нажата ли кнопка мыши).

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


   Ограничение области перемещения указателя

   С помощью несложных манипуляций можно ограничить перемещение указателя мыши определенной областью экрана (прямоугольником). Для этого используется API-функция ClipCursor. Она принимает в качестве параметра структуру TRect с координатами прямоугольника, в пределах которого может перемещаться указатель. Если ограничивающая область движения курсора заданна успешно, то функция возвращает ненулевое значение.
   С функцией ClipCursor тесно связана функция GetClipCursor, позволяющая получить координаты прямоугольника, которым ограничено перемещение указателя в данный момент.
   Способ использования функций ClipCursor и GetClipCursor показан в листинге 3.6.
 //-- Листинг 3.6. Ограничение области перемещения указателя --// 
   var
    lastRect: TRect;
   cursorClipped: Boolean = False;
   procedure SetCursorRect(newRect: TRect);
   begin
   if not cursorClipped then
   begin
   //Сохраняем старую область перемещения указателя
   GetClipCursor(lastRect);
   //Устанавливаем ограничение на перемещения указателя
   cursorClipped:= ClipCursor(Addr(newRect)) <> False;
   end;
   end;
   procedure RestoreCursorRect();
   begin
   if cursorClipped then
   begin
   //Восстанавливаем область перемещения указателя
   cursorClipped:= ClipCursor(Addr(lastRect)) = False;
   end;
   end;

   В данном примере реализованы две функции. Первая (SetCursorRect) ограничивает перемещение указателя мыши заданной областью экрана (параметр newRect). Перед ограничением на перемещения указателя в процедуре SetCursorRect происходит сохранение области перемещения, установленной ранее, чтобы действие процедуры можно было отменить. Для отмены ограничения перемещения указателя служит вторая функция – RestoreCursorRect.

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



   Изменение назначений кнопок мыши

   Как известно, операционная система Windows предоставляет возможность работать с компьютером широкому кругу людей. Большинство производителей манипуляторов типа «мышь» предусматривают возможность простой адаптации манипулятора под правшу или левшу. К тому же, мышь адаптировать к таким различиям намного проще, нежели выпускать манипуляторы различных типов: конструкцию изменять не надо, достаточно программно поменять функции кнопок мыши.
   Способ программного изменения функций левой и правой кнопок мыши продемонстрирован в листинге 3.7.
 //-- Листинг 3.7. Изменение назначений кнопок мыши --// 
   procedure TForm1.Button1Click(Sender: TObject);
   begin
   //Меняем местами функции левой и правой кнопок мыши
   SwapMouseButton(True);
   end;
   procedure TForm1.Button2Click(Sender: TObject);
   begin
   //Восстанавливаем функции кнопок мыши
   SwapMouseButton(False);
   end;

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


   Подсчет расстояния, пройденного указателем мыши

   Далее будет рассмотрена небольшая программа, которая носит скорее познавательный, нежели практический характер. Она подсчитывает количество метров (в буквальном смысле), пройденное указателем мыши за время ее работы. Внешний вид формы приложения показан на рис. 3.1.
   Рис. 3.1. Программа для измерения пробега указателя мыши

   Использование этой программы крайне просто: сразу после запуска она начинает измерять пройденное указателем мыши расстояние в пикселах. Нижняя группа элементов управления формы нужна для правильного вывода пройденного расстояния в метрах. При нажатии кнопки Изменить масштаб становятся активными два текстовых поля (предназначенных для ввода ширины и высоты прямоугольника). Чтобы программа правильно преобразовывала пройденное расстояние, нужно линейкой измерить ширину белого прямоугольника и ввести полученное значение (в миллиметрах) в текстовое поле. При повторном нажатии кнопки Изменить масштаб введенные значения принимаются, и с этого момента показания пройденного расстояния начинают переводиться в метры с учетом текущего разрешения и размера монитора.
   Теперь перейдем к рассмотрению способа реализации этого приложения. В табл. 3.1 приведены сведения о настройке элементов управления, не являющихся рамками или статическими надписями.
 //-- Таблица 3.1. Параметры элементов управления формы, показанной на рис. 3.1 --// 
   В коде листинга 3.8 объявляются переменные (члены класса TForm1) и методы, добавленные вручную.
 //-- Листинг 3.8. Форма для измерения пробега указателя --// 
   type
   TForm1 = class(TForm)
   ...
   private
   isUp dating: Boolean; //Если равен False, то показания в txtDistance
   //не обновляются
   lastPos: TPoint; //Координаты указателя во время прошлого замера
   distance: Real; //Пройденное расстояние в пикселах
   procedure StartUpdating();
   procedure StopUpdating();
   procedure ShowDistance();
   end;

   Суммарное расстояние в пикселах, пройденное указателем, сохраняется в переменной distance. Ниже представлен способ перевода этого расстояния в метры (листинг 3.9).
 //-- Листинг 3.9. Перевод расстояния в метры с учетом масштаба --// 
   procedure TForm1.ShowDistance();
   var
   scale: Real;
    distanceMetters: Real;
   begin
   //Пересчитываем текущий пробег в метры и показываем его
   //в текстовом поле
   //..определяем масштаб для перевода измерений в метры
     scale:= 0.001 * StrToInt(txtWidth.Text) / Shape1.Width;
     //..подсчитываем расстояние с учетом масштаба
   distanceMetters:= scale * distance;
   //..округляем до трех знаков (мм) и показываем
     distanceMetters:= Int(distanceMetters * 1000) * 0.001;
   txtDistance.Text:= FloatToStr(distanceMetters);
   end;

   Главная процедура приложения – обработчик для таймера Timer1. Таймер срабатывает с максимальной для него частотой (около 18 раз в секунду). Текст обработчика Timer1Timer приведен в листинге 3.10.
 //-- Листинг 3.10. Подсчет разницы между положениями указателя мыши --// 
   procedure TForm1.Timer1Timer(Sender: TObject);
   var
   curPos: TPoint;
   delta: Real;
   begin
   if (curPos.X <> lastPos.X) or (curPos.Y <> lastPos.Y) then
   begin
   GetCursorPos(curPos);
   //Вычисляем разницу между текущим и прошлым положением мыши
     delta:= Sqrt(Sqr(curPos.X – lastPos.X) + Sqr(curPos.Y – lastPos.Y));
     distance:= distance + delta;
   //Не забываем сохранить новые координаты указателя
     lastPos:= curPos;
   if isUpdating then
   begin
     //Обновим показания в текстовом поле
   ShowDistance();
   end;
   end;
   end;

   Из данного листинга видно, что обновление показаний происходит при истинном (True) значении переменной isUpdating. Значение этой переменной устанавливается в False во время изменения масштаба, чтобы во время ввода значений в текстовые поля не выводились неправильные цифры (листинг 3.11).
 //-- Листинг 3.11. Активизация и деактивизация режима изменения масштаба --// 
   procedure TForm1.cmbScaleClick(Sender: TObject);
   begin
   if cmbScale.Caption = 'Изменить масштаб' then
   begin
   //Начинаем изменение масштаба
   StopUpdating();
   cmbScale.Caption:= 'Принять масштаб';
   txtWidth.Enabled:= True;
   end
   else
   begin
   //Заканчиваем изменение масштаба
   txtWidth.Enabled:= False;
   cmbScale.Caption:= 'Изменить масштаб';
   StartUpdating();
   end;
   end;

   Процедуры StartUpdating и StopUpdating скрывают действия, которые необходимо произвести для остановки и возобновления отображения пройденного в текстовом поле указателем мыши расстояния. В данном примере они выглядят достаточно просто (листинг 3.12).
 //-- Листинг 3.12. Включение и выключение обновления результатов измерения --// 
   procedure TForm1.StartUpdating();
   begin
   //Включаем обновление показаний в текстовом поле
   isUpdating:= True;
   end;
   procedure TForm1.StopUpdating();
   begin
   //Отключаем обновление показаний в текстовом поле
   isUpdating:= False;
   end;

   В завершение остается реализовать код инициализации координат указателя мыши при запуске программы и обработчик события Click для кнопки cmbClear (листинг 3.13).
 //-- Листинг 3.13. Инициализация при запуске и код сброса счетчика --// 
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   //Инициализируем координаты мыши
   GetCursorPos(lastPos);
   StartUpdating();
   end;
   procedure TForm1.cmbClearClick(Sender: TObject);
   begin
   //Сбрасываем счетчик пройденного расстояния
   distance:= 0;
   GetCursorPos(lastPos); //Начинаем отсчет с текущей позиции указателя
   ShowDistance();
   end;

   Вот, собственно, и все, что нужно для работы рассматриваемой программы. Остается лишь уточнить, что способ установки масштаба, используемый в программе, предназначен для таких разрешений мониторов, при которых нет искажений по горизонтали или вертикали. Чаще всего это такие разрешения, при которых размеры изображения по горизонтали и вертикали подчиняются пропорции 4:3 (640 х 480, 800 х 600 и т. д.). При этом такими же пропорциями должен обладать и экран монитора.


   Подсвечивание элементов управления

   В реальных приложениях часто возникает необходимость изменять внешний вид элементов интерфейса программы в ответ на определенные действия пользователя. Поэтому стоит рассмотреть несложный, но достаточно полезный пример, позволяющий сделать более «живым» интерфейс приложения: изменение внешнего вида элементов управления при наведении на них указателя мыши.
   В листинге 3.14 продемонстрирован способ создания статической надписи, похожей на гиперссылку (для большего эффекта для такой надписи можно установить свойство Cursor равным crHandPoint на этапе проектирования формы).
 //-- Листинг 3.14. Подчеркивание и изменение цвета надписи --// 
   procedure TForm1.lblUnderlineMouseEnter(Sender: TObject);
   begin
   lblUnderline.Font.Style:= [fsUnderline];
   lblUnderline.Font.Color:= RGB(0, 0, 255);
   end;
   procedure TForm1.lblUnderlineMouseLeave(Sender: TObject);
   begin
   lblUnderline.Font.Style:= [];
   lblUnderline.Font.Color:= RGB(0, 0, 0);
   end;

   Для надписи, чтобы получилась довольно правдоподобная гиперссылка, осталось добавить только обработчик события Click, правда, выполнять она сможет любое действие, а не только переход по ссылке (достаточно лишь определить обработчик).
   Для стандартной кнопки начертание шрифта также можно изменить (листинг 3.15).
 //-- Листинг 3.15. Изменение начертания шрифта --// 
   procedureTForm1. cmbItalicBoldMouseMove (Sender: TObject;
     Shift: TShiftState; X, Y: Integer);
   begin
   cmbItalicBold.Font.Style:= [fsItalic, fsBold];
   end;
   procedure TForm1.lblItalicMouseEnter(Sender: TObject);
   begin
   lblItalic.Font.Style:= [fsItalic];
   end

   В листинге 3.15 используется обработчик MouseMove для кнопки потому, что обработчики событий MouseEnter и MouseLeave для нее (по крайней мере, с вкладки Standard) не предусмотрены.



   Клавиатура


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


   Получение информации о клавиатуре

   Начнем с небольшого примера, позволяющего определить некоторую информацию о клавиатуре (листинг 3.16). Данный пример основан на использовании API-функции GetKeyboardType.
 //-- Листинг 3.16. Получение информации о клавиатуре --// 
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   //Определяем тип клавиатуры
   case GetKeyboardType(0) of
     1: txt Type.Text:= 'PC/XT или совместимая (83 клавиши)';
   2: txt Type.Text xtxt:= 'Olivetti" ICO " (102 клавиши)';
   3: txt Type.Text xtxt:= 'PC/AT (84 клавиши) или похожая';
   4: txt Type.Text:= 'Расширенная (101 или 102 клавиши)';
   5: txt Type.Text:= 'Nokia 1050 или похожая';
   6: txt Type.Text:= 'Nokia 9140 или похожая';
   7: txt Type.Text:= 'японская';
   end;
   //Определяем код типа производителя
   txtSubtype.Text:= IntToStr(GetKeyboardType(1));
   //Определяем количество функциональных клавиш
     txtKeys.Text:= IntToStr(GetKeyboardType(2));
   end;

   При создании формы происходит заполнение текстовых полей информацией о типе клавиатуры, коде типа, присвоенном производителем, и количестве функциональных клавиш.
   На рис. 3.2 показан возможный результат определения информации о клавиатуре.
   Рис. 3.2. Информация о клавиатуре


   Опрос клавиатуры

   Существует достаточно удобная альтернатива обработке событий клавиатурного ввода, которая может оказаться особенно полезной, если необходима информация о состоянии сразу нескольких клавиш. Это может понадобиться, если пользователь должен одновременно удерживать нажатыми несколько клавиш. Например, в гоночных симуляторах, чтобы проезжать поворот, необходимо одновременно удерживать клавишу ↑ (газ) и одну из клавиш поворота (← или →).
   В листинге 3.17 приведен пример обработчика события Timer1Timer, определяющего, нажаты ли клавиши ↑, ↓, ←, →, а также пробел, Enter, Ctrl (правый), Shift (правый) и Alt (правый).
 //-- Листинг 3.17. Определение состояния некоторых клавиш --// 
   procedure TForm1.Timer1Timer(Sender: TObject);
   var
   buttons: TKeyBoardstate;
   begin
   //Получаем состояния клавиш
   GetKeyboardState(buttons);
   //Отобразим состояния клавиш
   //..пробел
   if buttons[VK_SPACE] and 128 <> 0 then
   SendMessage(cmbSpace.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbSpace.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..enter
   if buttons[VK_RETURN] and 128 <> 0 then
   SendMessage(cmbEnter.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbEnter.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..правый Ctrl
   if buttons[VK_RCONTROL] and 128 <> 0 then
   SendMessage(cmbRCtrl.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbRCtrl.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..правый Alt
   if buttons[VK_RMENU] and 128 <> 0 then
   SendMessage(cmbRAlt.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbRAlt.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..правый Shift
   if buttons[VK_RSHIFT] and 128 <> 0 then
   SendMessage(cmbRShift.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbRShift.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..вверх
   if buttons[VK_UP] and 128 <> 0 then
   SendMessage(cmbUp.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbUp.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..вниз
   if buttons[VK_Down] and 128 <> 0 then
   SendMessage(cmbDown.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbDown.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..влево
   if buttons[VK_LEFT] and 128 <> 0 then
   SendMessage(cmbLeft.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbLeft.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   //..вправо
   if buttons[VK_RIGHT] and 128 <> 0 then
   SendMessage(cmbRight.Handle, BM_SETSTATE, BST_CHECKED, 0)
   else
   SendMessage(cmbRight.Handle, BM_SETSTATE, BST_UNCHECKED, 0);
   end;

   Для определения состояния клавиш используется API-функция GetKeyboardS tate, заполняющая массив buttons значениями, характеризующими, нажата ли клавиша (на самом деле тип TKeyBoardstate определен как array [0..255] of Byte). Причем значения в массиве buttons трактуются следующим образом:
   • если установлен старший бит (проверка чего производится в листинге 3.17), то считается, что клавиша в данный момент нажата;
   • если установлен младший бит, то считается, что функция, закрепленная за этой клавишей (например, Caps Lock) включена.
   Для индексации массива можно использовать ASCII-коды символов, а также константы, соответствующие несимвольным клавишам (обозначения и коды для таких клавиш приведены в приложении 1).
   Каждой контролируемой клавише (листинг 3.17) соответствует кнопка на форме. Для принудительной установки клавиши в нажатое или ненажатое состояние используется сообщение BM_SETSTATE. На рис. 3.3 приведен пример результата определения состояния клавиш в некоторый момент времени.
   Рис. 3.3. Состояние некоторых клавиш клавиатуры

   Рассмотренный способ работы с клавиатурой можно использовать даже для определения неисправных клавиш на клавиатуре, как это сделано, например, в одной из программ пакета Norton Utilities.


   Имитация нажатия клавиш

   Как и в случае с мышью, когда программно изменялось положение указателя мыши, состояние клавиш клавиатуры также может изменяться программно. Рассмотрим один из способов программного нажатия клавиш, который крайне прост благодаря наличию API-функции keybd_event, предназначенной именно для имитации клавиатурного ввода. Таким образом, имитация нажатия любой клавиши на клавиатуре сводится к вызову keybd_event с определенными параметрами.
   Назначения параметров этой функции пояснено на примере (листинг 3.18).
 //-- Листинг 3.18. Вызов меню «Пуск» --// 
   procedureTForm1. cmbStartClick(Sender: TObject);
   begin
   //Имитируем нажатие клавиши Windows
     keybd_event (VK_LWIN, 0, 0, 0);
   //Имитируем отпускание клавиши Windows
   keybd_event (VK_LWIN, 0, KEYEVENTF_KEYUP, 0);
   end;

   Здесь нас интересуют, прежде всего, первый и третий параметры функции keybd_ event (второй не используется, а четвертый предназначен для установки дополнительной информации, относящейся к нажатию клавиши). В первом параметре функции передается код «нажимаемой» или «отпускаемой» клавиши. Третий же параметр равен нулю при «нажатии» и константе KEYEVENTF_KEYUP при «отпускании» клавиши.

   Внимание!
   При использовании функции keybd_event главное – не забывать «отпускать» программно нажатые клавиши (что и делается в приведенных в данном разделе примерах). В противном случае существует риск «залипания» клавиш и, как следствие, ухудшения работы операционной системы.

   Аналогичный приведенному в листинге 3.18 код программного нажатия клавиши Print Screen (создания копии экрана) приведен в листинге 3.19.
 //-- Листинг 3.19. Создание копии изображения на экране --// 
   procedure TForm1.cmbPrintScreenClick(Sender: TObject);
   begin
   //Нажимаем Print Screen
   keybd_event(VK_SNAPSHOT, 0, 0, 0);
   keybd_event(VK_SNAPSHOT, 0, KEYEVENTF_KEYUP, 0);
   end;

   В листинге 3.20 приведен пример программного нажатия сочетания клавиш (Windows+M, выполняющей сворачивание всех окон).
 //-- Листинг 3.20. Сворачивание всех окон --// 
   procedureTForm1. cmbMinimizeAllClick(Sender: TObject);
   begin
   //Нажимаем Windows+M
   keybd_event(VK_LWIN, 0, 0, 0);
   keybd_event(Byte('M'), 0, 0, 0);
   keybd_event(Byte('M'), 0, KEYEVENTF_KEYUP, 0);
   keybd_event(VK_LWIN, 0, KEYEVENTF_KEYUP, 0);
   end;

   Добавление к этому сочетанию клавиши Shift приведет к восстановлению прежнего состояния окон.
   Последний пример иллюстрирует способ использования программного нажатия клавиш для оптимизации быстрого доступа к программам. Имеется в виду программное нажатие сочетаний клавиш, ассоциированных с ярлыками, расположенными на Рабочем столе или находящимися в меню Пуск. Для примера будет выбрано сочетание клавиш CtrL+ALt+E, обычно используемое для запуска браузера Internet Explorer (листинг 3.21).
 //-- Листинг 3.21. Быстрый запуск программы --// 
   procedureTForm1.cmbEIxplorerClick(Sender: TObject);
   begin
   //Нажимаем сочетание Ctrl+Alt+E
   keybd_event(VK_CONTROL, 0, 0, 0);
   keybd_event(VK_MENU, 0, 0, 0);
   keybd_event(Byte('E'), 0, 0, 0);
   keybd_event(Byte('E'), 0, KEYEVENTF_KEYUP, 0);
   keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
   keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
   end;

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


   Бегущие огни на клавиатуре

   В завершение будет рассмотрен довольно любопытный пример, так же, как и предыдущий, основанный на программном нажатии клавиш Caps Lock, Num Lock и ScroLL Lock. Как известно, этим клавишам на клавиатуре соответствуют три индикатора (по крайней мере, на большинстве клавиатур). Суть примера состоит в последовательном включении и выключении упомянутых режимов, которые автоматически сопровождаются включением и выключением соответствующих индикаторов на клавиатуре.
   Перед рассмотрением основных процедур примера стоит привести текст процедуры PressKey, которая далее используется практически на каждом шагу (листинг 3.22). Она имитирует нажатие и отпускание одной клавиши. Конкретное наименование программно нажимаемой и отпускаемой клавиши определяется единственным параметром (кодом клавиши), который принимает процедура PressKey.
 //-- Листинг 3.22. Нажатие отдельной клавиши --// 
   procedurePressKey (keyCode: Byte);
   begin
   keybd_event(keyCode, 0, 0, 0);
   keybd_event(keyCode, 0, KEYEVENTF_KEYUP, 0);
   end;

   Запуск и остановка мерцания индикаторов на клавиатуре осуществляется при нажатии кнопки на форме (помимо кнопки, на форме должны присутствовать текстовое поле, предназначенное для указания интервала изменения состояния индикаторов, а также таймер со свойством Enabled, равным False) (листинг 3.23).
 //-- Листинг 3.23. Запуск и остановка мигания индикаторов --// 
   var
   init Caps, init Num, init Scroll: Boolean; //Первоначальные состояния
   //клавиш
   curCaps, curNum, curScroll: Boolean; //Текущие состояния клавиш
   procedure TForm1.cmbStartClick(Sender: TObject);
   begin
   if cmbStart.Caption = 'Старт' then
   begin
   //Сохраняем первоначальные состояния клавиш
   initCaps:= (GetKeyState(VK_CAPITAL) and 1) <> 0;
   initNum:= (GetKeyState(VK_NUMLOCK) and 1) <> 0;
   initScroll:= (GetKeyState(VK_SCROLL) and 1) <> 0;
   //Включаем только Caps Lock
   if not initCaps then PressKey(VK_CAPITAL);
   curCaps:= True;
   if initNum then PressKey(VK_NUMLOCK);
   curNum:= False;
   if initScroll then PressKey(VK_SCROLL);
   curScroll:= False;
   //Запускаем " бегущие огни "
   Timer1.Interval:= StrToInt(txtInterval.Text);
   Timer1.Enabled:= True;
   cmbStart.Caption:= 'Стоп';
   end
   else
   begin
   //Останавливаем "бегущие огни "
   Timer1.Enabled:= False;
   cmbStart.Caption:= 'Старт';
   //Восстанавливаем первоначальные состояния клавиш
   if initCaps <> curCaps then PressKey(VK_CAPITAL);
   if initNum <> curNum then PressKey(VK_NUMLOCK);
   if initScroll <> curScroll then PressKey(VK_SCROLL);
   end;
   end;

   В начале кода листинга 3.23 приведены следующие используемые глобальные переменные:
   • initCaps, initNum, initScroll – предназначены для сохранения первоначального состояния клавиш Caps Lock, Num Lock и ScroLL Lock для последующего восстановления;
   • curCaps, curNum, curScroll – предназначены для быстрого определения текущего состояния клавиш (вместо постоянного обращения к функциям типа GetKeyboardState).
   Изменение состояния индикторов происходит при каждом срабатывании таймера Timerl (листинг 3.24).
 //-- Листинг 3.24. Перемещение огней --// 
   procedure TForm1.Timer1Timer(Sender: TObject);
   begin
   //Изменяем состояние лампочек на клавиатуре
   if curCaps then
   begin
   //Caps Lock на Num Lock
   PressKey(VK_NUMLOCK);
   PressKey(VK_CAPITAL);
   curCaps:= False;
   curNum:= True;
   end
   else if curNum then
   begin
   //Num Lock на Scroll Lock
   PressKey(VK_SCROLL);
   PressKey(VK_NUMLOCK);
   curNum:= False;
   curScroll:= True;
   end
   else
   begin
   //Scroll Lock на Caps Lock
   PressKey(VK_CAPITAL);
   PressKey(VK_SCROLL);
   curScroll:= False;
   curCaps:= True;
   end;
   end;


   Примечание
   Если у вашей клавиатуры порядок следования индикаторов отличается от приведенного в примере (в какую-нибудь сторону), то достаточно изменить порядок переключения клавиш в листинге 3. 24, чтобы бегущие огни действительно «бежали».

   Теперь можно запустить соответствующую заставку, и получится неплохое украшение, например, для новогодней елки… из компьютера.




   Глава 4
   Диски, папки, файлы


   • Диски
   • Папки и пути
   • Файлы

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


   Диски


   Начнем со способов определения информации о дисках компьютера. Как вы, наверное, не раз могли убедиться, ряд приложений (тот же Internet Explorer) обладают гораздо большей информацией о дисках, нежели только их обозначение (буква) или размер. Далее будет рассмотрено, как определить буквы всех установленных на компьютере дисков, их метки, серийные номера томов и другую информацию о файловой системе. Вы также узнаете, как можно программно изменять метки дисков.
   Все рассмотренные далее функции работы с дисками вы можете найти в модуле DriveTools, расположенном на диске, прилагаемом к книге, в папке с названием раздела.


   Сбор информации о дисках

   Начнем по порядку. Получить список дисков компьютера (строк вида <буква>:\) поможет функция, представленная в листинге 4.1.
 //-- Листинг 4.1. Определение букв дисков --// 
   function GetDriveLetters (letters: TStrings):Integer;
   var
   buffer: String;
   i, len, start: Integer;
   begin
   SetLength(buffer, 110);
   len:= GetLogicalDriveStrings(110, PAnsiChar(buffer));
   //Разбираем строку вида 'c:\#0d:\#0...#0#0', возвращаемую функцией
   //GetLogicalDriveStrings
   start:= 1;
   for i:= 2 to len do
   if (bu ffer[i] = #0) and (start <> i) then
   begin
   //Нашли обозначение очередного диска
   letters.Append(Copy(buffer, start, i–start));
   start:= i+1;
   end;
   GetDriveLetters:= letters.Count;
   end;

   Функция принимает ссылку на список и заполняет его строками, содежащими пути корневых папок каждого из дисков (например, C:\). Вся сложность использования этой функции состоит в необходимости выделения путей из строки, заполняемой API-функцией GetLogicalDriveStrings. Функция GetDriveLetters возвращает количество строк, добавленных в список letters.
   Кроме API-функции GetLogicalDriveStrings, для получения информации о том, за какими буквами закреплены диски, можно использовать еще, по меньшей мере, одну функцию – GetLogicalDrives. Эта функция не имеет аргументов и возвращает значение типа DWORD, представляющее собой битовую маску. Состояние каждого бита маски (от первого до 26-го) соответствует наличию либо отсутствию диска под соответствующей номеру буквой латинского алфавита. Код выделения информации из маски (и, соответственно, составления списка дисков) может выглядеть так же, как в листинге 4.2.
 //-- Листинг 4.2. Составление списка дисков --// 
   function GetDriveLetters (letters: TStrings):Integer;
   var
   mask: DWORD;
   i: Integer;
    letter: Char;
   begin
   //Получаем маску, характеризующую наличие дисков
   mask:= GetLogicalDrives();
   //Разбираем маску (определяем значения первых 26 битов)
   i:= 1;
   for letter:= 'A' to 'Z' do
   begin
   if mask and i <> 0 then
   //Есть тьдиск кпод текущей йбуквой
   lett ers.App end(letter + ':\');
   i:= i * 2; //Переходим к следующему биту
   end;
   GetDriveLetters:= letters.Count;
   end;

   Теперь запишем несложные функции, позволяющие определить полный размер и размер свободного пространства диска (листинг 4.3).
 //-- Листинг 4.3. Определение полного размера и размера свободного пространства диска --// 
   //Функция возвращает полный размер диска (в байтах)
   function GetDriveSize(root: String): Int64;
   var
   freeToCaller, totalBytes, freeBytes: Int64;
   begin
   if GetDiskFreeSpaceEx(PAnsiChar(root), freeToCaller, totalBytes,
   PLargeInteger(Addr(freeBytes))) <> False
   then
   GetDriveSize:= totalBytes
   else
   GetDriveSize:= -1;
   end;
   //Функция возвращает размер свободного места на диске (в байтах)
   function GetDriveFreeSpace(root: String): Int64;
   var
   freeToCaller, totalBytes, freeBytes:Int64;
   begin
   if GetDiskFreeSpaceEx(PAnsiChar(root), freeToCaller, totalBytes,
   PLargeInteger(Addr(freeBytes))) <> False
   then
   GetDriveFreeSpace:= freeBytes
   else
     GetDriveFreeSpaceee:= –1;
   end;

   В обеих функциях, представленных в листинге 4.3, для достижения двух разных целей используется API-функция GetDiskFreeSpaceEx:

   function GetDiskFreeSpaceEx(lpDirectoryName: PChar;
   var lpFreeBytesAvailableToCaller,
   lpTotalNumberOfBytes;
   lpTotalNumberOfFreeBytes: PLargeInteger): BOOL;

   Данная функция принимает путь файла или папки на требуемом диске и заполняет три параметра:
   • lpFreeBytesAvailableToCaller – содержит размер свободного пространства, доступного пользователю, под правами которого работает поток, вызывающий данную функцию (в байтах);
   • lpTotalNumberOfBytes – содержит полный размер диска (в байтах);
   • lpTotalNumberOfFreeBytes – содержит размер свободного пространства на диске (в байтах).
   Чтобы можно было оперировать размерами дисков более 4 Гбайт, все перечисленные значения являются 64-битными. Если вызов функции GetDiskFreeSpaceEx выполняется неудачно, то возвращается значение False. В этом случае функции листинга 4.3 возвращают значение -1, сигнализируя об ошибке.
   Теперь самое интересное – получение детальной информации о файловой системе на дисках. Много полезной информации о файловой системе на каждом диске можно получить с помощью API-функции GetVolumelnformation. Эта функция имеет следующий вид:

   function GetVolumeInformation(lpRootPathName: PChar;
   lpVolumeNameBuffer: PChar; nVolumeNameSize: DWORD;
   lpVolumeSerialNumber: PDWORD; var lpMaximumComponentLength,
   lpFileSystemFlags: DWORD; lpFileSystemNameBuffer: PChar;
   nFileSystemNameSize: DWORD): BOOL;

   Код объявления функции выглядит довольно громоздким за счет большого количества параметров, однако использовать функцию GetVolumelnformation очень просто. Чтобы не вдаваться в долгое описание ее параметров, рассмотрим ее использование на примере (листинг 4.4).
 //-- Листинг 4.4. Определение информации о диске --// 
   //Функция определяет информацию о диске
   //Возвращает False, если возникла ошибка
   function GetDriveInformation(root: String; var info: DriveInfo):Boolean;
   var
   bufDriveName, bufFSNAme: String;
   SN: DWORD;
   maxFileName, fsOptions: Cardinal;
   begin
   SetLength(bufDriveName, 101);
   SetLength(bufFSName, 101);
   //Определение информации о диске
   if GetVolumeInformation(PAnsiChar(root), PAnsiChar(bufDriveName), 100,
   Addr(SN), maxFileName, fsOptions,
   PAnsiChar(bufFSName), 100) <> False then
   begin
   //Заполняем структуру информацией о диске
   with info do
   begin
   DriveLabel:= bufDriveName;
   FileSystemName:= bufFSName;
   SerialNumber:= SN;
   MaxFileNameLen:= maxFileName;
   //..параметры файловой системы
   with info.FileSystemOptions do
   begin
   CaseSensitive:= fsOptions and FS_CASE_SENSITIVE <> 0;
   SupportCompression:= fsOptions and FS_FILE_COMPRESSION <> 0;
   IsCompressed:= fsOptions and FS_VOL_IS_COMPRESSED <> 0;
   end;
   end;
   //Функция отработала успешно
   GetDriveInformation:= True;
   end
   else
   //Ошибка
   GetDriveInformation:= False;
   end;

   Анализируя приведенный в листинге 4.4 код, можно увидеть, что функции GetVolumelnformation, кроме пути, принадлежащего диску, передаются также:
   • буфер для метки диска (и длина этого буфера);
   • указатель на переменную типа DWORD для записи в нее серийного номера тома диска, который присваивается при каждом создании файловой системы, например, после форматирования диска;
   • ссылка на переменную типа Cardinal для сохранения в ней максимальной длины компонента пути (имени файла или папки);
   • ссылка на переменную типа Cardinal для сохранения в ней набора битовых флагов с некоторыми параметрами файловой системы;
   • буфер для названия файловой системы (и его длина).
   Как вы могли заметить, в результате выполнения приведенной в листинге 4.4 функции GetDriveInformation заполняется структура Drivelnfo. Код определения этой структуры (а также вложенной в нее структуры, хранящей некоторые извлеченные из битовой маски fsOptions флаги) приведен в листинге 4.5.
 //-- Листинг 4.5. Определение записей для хранения информации о диске --// 
   Type
   //Запись некоторых параметров о файловой системе
   FSOptions = record
   CaseSensitive: Boolean; //При уравнении путей учитывает регистр
   SupportCompression: Boolean; //Файловая система поддерживает сжатие
   IsCompressed: Boolean; //Диск сжат
   end;
   //Запись, содержащая информацию о диске
   DriveInfo = record
   DriveLabel: String; //Метка диска
   FileSystemName: String; //Файловая система диска
   FileSystemOptions: FSOptions; //Параметры файловой системы
   SerialNumber: DWORD; //Серийный номер тома
   MaxFileNameLen: Cardinal; //Максимальная длина имени файла
   end;

   Напоследок будет рассмотрена еще одна полезная возможность – определение типа носителя диска с помощью API-функции GetDriveType. Эта функция принимает единственный параметр, задающий корневую папку диска (например, C:\, причем обратный слэш на конце обязателен). Функция GetDriveType возвращает целочисленное значение, идентифицирующее тип диска. Вариант кода получения текстового описания типов дисков с использованием этой API-функции приведен в листинге 4.6.
 //-- Листинг 4.6. Определение типа носителя диска --// 
   function GetDriveTypeName(root: String): String;
   begin
   case GetDriveType(PAnsiChar(root)) of
   DRIVE_UNKNOWN: GetDriveTypeName:= ' Не определен ';
   DRIVE_REMOVABLE: GetDriveTypeName:= ' Сменный';
   DRIVE_FIXED: GetDriveTypeName:= ' Фиксированный';
   DRIVE_REMOTE: GetDriveTypeName:= 'Удаленный (сетевой)';
   DRIVE_CDROM: GetDriveTypeName:= 'Компакт-диск';
   DRIVE_RAMDISK: GetDriveTypeName:= 'RAM-диск';
   else
   GetDriveTypeName:= ''//Возвращается в случае ошибки
   end;
   end;


   Изменение метки диска

   Как вы думаете, сложно ли изменить метку диска? Совсем нет: сложность состоит лишь в поиске необходимой функции. В данном случае можно применить API-функцию SetVolumeLabel (листинг 4.7).
 //-- Листинг 4.7. Изменение метки диска --// 
   function SetDriveLabel(root, newLabel: String): Boolean;
   begin
   SetDriveLabel:=
   SetVolumeLabel(PAnsiChar(root), PAnsiChar(newLabel)) <> False;
   end;

   В листинге 4.7 приведена функция-оболочка для API-функции изменения метки диска, избавляющая вас от необходимости преобразования типов и интерпретации значения, возвращаемого API-функцией.


   Программа просмотра свойств дисков

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

   Работа формы, приведенной на рис. 4.1, организована предельно просто. Сначала при создании формы происходит получение списка дисков (а также выделение первого диска и загрузка информации о нем) (листинг 4.8).
 //-- Листинг 4.8. Составление списка дисков --// 
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   //Загрузка букв дисков
   if GetDriveLetters(cboDrives.Items) > 0 then
   begin
   //Выделим первый диск
   cboDrives.ItemIndex:= 0;
   cboDrivesSelect(self);
   end
   else
   Button1.Enabled:= False;
   end;

   Загрузка информации о дисках происходит при выборе буквы диска из списка (листинг 4.9).
 //-- Листинг 4.9. Загрузка информации о выбранном диске --// 
   procedure
   TForm1.cboDrivesSelect(
   Sender: TObject);
   var info: DriveInfo;
   root: String;
   fullSize, freeSize: Int64;
   begin
   root:= cboDrives.Items[cboDrives.ItemIndex];
   //Загружаем информацию о выбранном диске
   GetDriveInformation(root, info);
   //Общая информация о диске и файловой системе
   txtLabel.Text:= info.DriveLabel;
   txtDriveType.Text:= GetDriveTypeName(root);
   txtFSName.Text:= info.FileSystemName;
   txtSN.Text:= IntToHex(Int64(info.SerialNumber), 8);
   txtMaxFileName.Text:= IntToStr(Integer(info.MaxFileNameLen));
   //Флажки некоторых свойств файловой системы
   chkCaseSensitive.Checked:= info.FileSystemOptions.CaseSensitive;
   chkCompression.Checked:= info.FileSystemOptions.SupportCompression;
   chkCompressed.Checked:= info.FileSystemOptions.IsCompressed;
   //Размер диска
   fullSize:= GetDriveSize(root);
   if fullSize <> –1 then
   freeSize:= GetDriveFreeSpace(root)
   else
   begin //Ошибка при обращении к диску
   fullSize:= 0;
   freeSize:= 0;
   end;
   //..формирование диаграммы
   driveSize.Series[0].Clear;
   driveSize.Series[0].Add(freeSize, 'Свободно');
   driveSize.Series[0].Add(fullSize – freeSize, 'Занято')
   end;

   При нажатии кнопки Изменить производится попытка присвоить диску, выбранному в списке, метку, введенную в соответствующее текстовое поле (txtLabel) (листинг 4.10).
 //-- Листинг 4.10. Задание новой метки диска --// 
   procedure TForm1.Button1Click(Sender: TObject);
   begin
   //Изменение метки диска
   if not SetDriveLabel(cboDrives.Items[cboDrives.ItemIndex],
   txtLabel.Text)
   then
   MessageBox(self.Handle, 'Не удалось поменять метку диска', 'Ошибка ',
   MB_ICONEXCLAMATION)
   else
   //Перечитаем информацию о диске
   cboDrivesSelect(self);
   end;

   Табличное или иное описание свойств элементов управления не приводится, так как имена элементов управления соответствуют виду информации, помещаемой в них. Следует только уточнить, что в элементе управления TChart создан один ряд типа Pie (круговая диаграмма). У этого ряда отключено отображение подписей к диаграмме, чтобы не дублировать данные, приведенные в легенде.



   Папки и пути


   В этом разделе описываются некоторые примеры, позволяющие узнавать расположение важных папок операционной системы Windows. Здесь также рассматриваются вопросы преобразования путей и приводятся некоторые алгоритмы обхода папок, применяемые для поиска.
   Прежде чем рассматривать решения конкретных задач, следует сказать пару слов о неком магическом числе, а точнее, целочисленной константе, используемой в некоторых примерах, приведенных в этой главе. Речь идет о константе MAX_PATH, равной 260. Она используется явно или неявно (функциями API) в качестве максимальной возможной длины пути. Здесь налицо небольшой парадокс: хотя такая файловая система, как FAT32, и реализована так, что может поддерживать неограниченную вложенность папок, в реальности не получится создать даже две вложенные папки с именами длиной 255 символов.

   Примечание
   При доскональной проверке вышесказанного выяснилось, что не получится создать даже одну папку с именем длиной 255 символов в корневой папке диска (например, C:\). Папка может иметь имя длиной максимум 244 символа. С учетом длины имени корневой папки (в сумме получается уже 247 символов) можно предположить, что система резервирует оставшиеся 13 символов, чтобы в папке можно было сохранять файлы с именем в формате 8. 3 (формат, применявшийся в MS-DOS).



   Системные папки WINDOWS и system

   Приходилось ли вам хоть раз писать приложения, работоспособность которых зависела бы от места расположения системных папок Windows? Если да, то вы наверняка хорошо знаете, насколько неустойчиво предположение о том, что папка WINDOWS – всегда C:\WINDOWS, а system – всегда C:\WINDOWS\system. Ведь при установке операционной системы ничто не мешает поместить ее, например, на диск E:\а папку для Windows назвать Linux. Кроме того, системная папка Windows на платформе NT имеет имя system32, и кто знает, какое имя она будет иметь в следующей версии Windows?
   В таких и многих других случаях выручают API-функции GetWindowsDirectory и GetSystemDirectory. Они обе принимают в качестве своих параметров строковый буфер и его длину и возвращают количество символов, записанных в переданный буфер, или 0 в случае ошибки.
   Для этих функций удобно реализовать функции-оболочки, работающие со стандартными для Delphi строками, что, собственно, и сделано при написании этой главы (все реализованные функции вы можете найти в модуле PathFunctions, расположенном на диске, прилагаемом к книге, в папке с названием подраздела). Итак, код функции определения папки WINDOWS приведен в листинге 4.11.
 //-- Листинг 4.11. Определение адреса папки WINDOWS --// 
   function GetWinDir(): String;
   var
   buffer: String;
   len: UINT;
   begin
   SetLength(buffer, MAX_PATH + 1);
   len:= GetWindowsDirectory(PAnsiChar(buffer), MAX_PATH);
   SetLength(buffer, len);
   GetWinDir:= buffer;
   end;

   Аналогичным образом реализуется функция, позволяющая определить расположение системной папки (только вместо GetWindowsDirectory вызывается функция GetSystemDirectory).


   Имена временных файлов

   Для централизованного хранения временных данных, необходимых при работе приложений, в Windows предусмотрена специальная папка Temp. Ее расположение может изменяться. Причем в многопользовательских версиях Windows (NT, 2000, XP) местоположение папки для временных файлов может быть различным для различных пользователей. Итак, расположение папки Temp можно определить с помощью API-функции GetTempPath. Это функция принимает строковый буфер и длину этого буфера и возвращает количество символов, записанных в переданную строку, или 0, если возникла ошибка. Функция-оболочка, скрывающая работу со строковым буфером и преобразование типов, реализуется аналогично двум ранее рассмотренным функциям (листинг 4.12).
 //-- Листинг 4.12. Определение расположения папки временных файлов --// 
   function GetTempDir(): String;
   var
   buffer: String;
   len: UINT;
   begin
   SetLength(buffer, MAX_PATH + 1);
   len:= GetTempPath(MAX_PATH, PAnsiChar(buffer));
   SetLength(buffer, len);
   GetTempDir:= buffer;
   end;
   Кроме того, Windows API предусматривает очень полезную функцию, избавляющую программиста от необходимости подбирать имена временным файлам так, чтобы они были уникальными в пределах заданной папки (это не обязательно должна быть папка Temp). Имя этой функции – GetTempFileName. Пример ее использования приведен в листинге 4.13.
 //-- Листинг 4.13. Определение имени временного файла --// 
   function GetTempFile (prefix: String= '~mytmp'): String;
   var
   buffer, dir: String;
   begin
   dir:= GetTempDir();
   //Получение имени временного файла (система сама определяет имя,
   //уникальное для заданной папки)
   SetLength(buffer, MAX_PATH + 1);
   GetTempFileName(PAnsiChar(dir), PAnsiChar(prefix), 0, PAnsiChar(buffer));
   GetTempFile:= buffer;
   end;

   Приведенная в листинге 4.13 функция в качестве папки временных файлов использует папку Temp, однако функцию GetTempFileName можно использовать для получения имен файлов в пределах любой папки.
   Кроме пути папки, в которой необходимо создать временный файл, функция GetTempFileName принимает строку-префикс для имени временного файла и целочисленное значение (третий параметр). Если третий параметр не равен нулю, то его значение в шестнадцатеричной форме просто прибавляется справа к строке prefix. Никаких проверок на уникальность получившегося имени файла при этом не производится. Если же третьему параметру присвоить значение 0, то система сама сформирует шестнадцатеричное значение так, чтобы имя файла было уникальным в заданной папке. При этом, кроме того, создается и сам файл.
   Буфер (последний параметр функции GetTempFileName) должен вмещать как минимум MAX_PATH символов, так как функция записывает в него полный путь временного файла.
   Пример работы функций определения папки временных файлов, получения имени для временного файла, а также определения системных папок Windows показан на рис. 4.2.
   Рис. 4.2. Расположения папок WINDOWS, system, Temp и имя для временного файла


   Определение прочих системных путей

   В Windows существует ряд других системных путей, которые так или иначе могут пригодиться. Определяются они не менее просто, чем пути к системным папкам (листинг 4.14).
 //-- Листинг 4.14. Определение прочих системных путей --// 
   function GetSpecialDir(dirtype: Integer): String;
   var
   buffer: String;
   begin
   SetLength(buffer, MAX_PATH + 1);
   SHGetSpecialFolderPath(0, PAnsiChar(buffer), dirtype, False);
   GetSpecialDir:= buffer;
   end;

   Здесь используется функция командной оболочки файловой системы (Windows Shell) SHGetSpecialFolderPath, объявление которой находится в модуле ShlObj. Среди параметров этой функции самыми значимыми (кроме буфера длиной минимум MAX_PATH символов для помещения в него пути) являются два последних параметра. Третий параметр функции SHGetSpecialFolderPath используется для указания папки, расположение которой необходимо определить. Если четвертый параметр функции SHGetSpecialFolderPath не равен False и запрашиваемой папки не существует, то папка будет создана.
   Пример использования функции GetSpesialDir для составления списка (в элементе управления ListView) некоторых системных путей приведен в листинге 4.15. Из него вы также можете узнать имена целочисленных констант, идентифицирующих некоторые папки.
 //-- Листинг 4.15. Использование функции GetSpecialDir --// 
   procedure TForm3.Button1Click(Sender: TObject);
   var
   item: TListItem;
   begin
   lvwPathes.Clear;
   //Определение путей некоторых системных папок
   //..Рабочий стол
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Рабочий стол';
    item.SubItems.Insert(0, GetSpecialDir(CSIDL_DESKTOPDIRECTORY));
   //..Избранное
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Избранное';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_FAVORITES));
   //..Шрифты
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Шрифты';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_FONTS));
   //..Мои документы
   item:= lvwPathes.Items.Add();
   item.Caption:= ' Мои документы';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_PERSONAL));
   //..Последние документы
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Последние документы';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_RECENT));
   //..История
   item:= lvwPathes.Items.Add();
   item.Caption:= 'История';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_HISTORY));
   //..Отправить
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Отправить';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_SENDTO));
   //..Меню Пуск
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Пуск';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_STARTMENU));
   //..Меню Программы
   item:= lvwPathes.Items.Add();
   item.Caption:= ' Программы';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_PROGRAMS));
   //..Меню 'Автозагрузка'
   item:= lvwPathes.Items.Add();
   item.Caption:= ' Авто загрузка ';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_STARTUP));
   //..Папка с шаблонами документов
   item:= lvwPathes.Items.Add();
   item.Caption:= 'Шаблоны';
   item.SubItems.Insert(0, GetSpecialDir(CSIDL_TEMPLATES));
   end;

   Пример возможного результата выполнения процедуры, представленной в листинге 4.14, показан на рис. 4.3.
   Рис. 4.3. Прочие системные пути Windows

   В коде приведенной в листинге 4.15 процедуры определены не все пути, доступные с использованием функции SHGetSpecialFolderPath. Дело в том, что существует ряд виртуальных (не существующих реально на диске) папок: Мой компьютер, Принтеры, Сетевое окружение и т. д.
   Для некоторых упоминаемых в листинге 4.15 папок существуют также аналогичные папки, содержимое которых доступно всем пользователям:
   • CSIDL_COMMON_DESKTOPDIRECTORY – содержимое этой папки отображается на Рабочем столе всех пользователей;
   • CSIDL_COMMON_DOCUMENTS – содержит общие документы;
   • CSIDL_COMMON_FAVORITES – содержит общие элементы папки Избранное;
   • CSIDL_COMMON_PROGRAMS – содержит общие для всех пользователей программы (пункт Программы меню "Пуск");
   • CSIDL_COMMON_STARTMENU – содержит общие элементы, отображаемые в меню "Пуск";
   • CSIDL_COMMON_STARTUP – содержит общие элементы меню Автозагрузка;
   • CSIDL_COMMON_TEMPLATES – папка с общими для всех пользователей шаблонами документов.

   Примечание
   Большинство из перечисленных выше путей определяются только в системах Windows на ядре NT, но недоступны в Windows 95/98/Me.



   Определение и установка текущей папки

   Во время работы каждого приложения для него назначается папка, которая считается текущей (для этого приложения). При управлении текущей папкой удобно использовать рассмотренные ниже относительные пути.
   Для определения текущей папки приложения используется функция GetCurrentDir, приведенная в листинге 4.16.
 //-- Листинг 4.16. Определение текущей папки --// 
   function GetCurrentDir(): String;
   var
   len: Integer;
   buffer: String;
   begin
   SetLength(buffer, MAX_PATH + 1);
   len:= GetCurrentDirectory(MAX_PATH, PAnsiChar(buffer));
   GetCurrentDir:= Copy(buffer, 1, len);
   end;

   Функция определения пути текущей папки основана на применении соответствующей API-функции GetCurrentDirectory. Вполне естественно, что эта функция имеет себе пару – функцию для задания текущей папки SetCurrentDirectory. Объявление этой функции происходит следующим образом:

   function SetCurrentDirectory(lpPathName: PChar): BOOL; stdcall;

   Функция принимает путь папки и возвращает ненулевое значение в случае успешного своего выполнения.


   Преобразование путей

   Далее будет рассмотрено несколько функций, которые могут пригодиться, если возникнет необходимость преобразования путей. Имеется в виду прежде всего преобразование имен файлов в формат MS-DOS и обратно. Этот вид преобразования наглядно продемонстрирован на рис. 4.4 (верхняя часть формы).
   Рис. 4.4. Преобразования путей

   Иногда удобно представлять пути относительно какой-либо вложенной, а не корневой папки диска. Например, представьте, что вы разрабатываете приложение, документы которого, являющиеся неделимыми для пользователя, могут фактически состоять из большого количества файлов, расположенных в разных папках (Images, Movies, Embed и др.). Сами папки расположены в той же папке, что и основной файл документа или ниже по иерархии (во вложенных папках). Как добиться того, чтобы при копировании приложения со всеми нужными папками в другое место (на другой диск или компьютер, в другую папку) его по-прежнему можно было открыть, рассчитывая, что в папках Images, Movies и Embed содержится нужная информация. Для этого приложение должно «знать», какие файлы и в каких папках ему действительно необходимы. В таком случае пригодится относительный путь, который содержит в себе информацию о количестве и направлении переходов из папки, заданной в качестве корневой, чтобы можно было найти указанный в этом пути файл или папку.
   Преобразование абсолютного пути в относительный и наоборот вы можете увидеть на рис. 4.4 (нижняя часть формы). Здесь в качестве исходного пути берется информация, содержащаяся в текстовом поле Исходный длинный путь, а в качестве пути папки для построения относительного пути – в текстовом поле Текущая папка.
   На всякий случай стоит уточнить, что в относительном пути символ. указывает на текущую папку (никуда переходить не надо), а символ… означает папку, расположенную на один уровень выше (родительскую папку). Также следует уточнить, что под абсолютным путем понимается путь, корневым элементом которого является \\ или <диск>:\(C:\, D:\ и т. д.).

   Примечание
   Все приведенные далее функции преобразования вы можете найти в модуле PathConvert, расположенном на диске, в папке с названием подраздела.

 //-- Преобразование длинных имен файлов в короткие, и наоборот --// 
   Теперь рассмотрим реализацию преобразования путей. Сначала – преобразование между длинной и короткой формами. Выполняется это предельно просто (благо Windows API предусматривает соответствующие функции).
   Функция преобразования длинного пути в короткий приведена в листинге 4.17.
 //-- Листинг 4.17. Преобразование длинного пути в короткую форму --// 
   function LongPathToShort(path: String): String;
   var
   buffer: String;
   len: Integer;
   begin
   SetLength(buffer, MAX_PATH);
   len:= GetShortPathName(PAnsiChar(path), PAnsiChar(buffer), MAX_PATH);
   SetLength(buffer, len);
   LongPathToShort:= buffer;
   end;

   Соответственно, функция обратного преобразования пути может выглядеть следующим образом (листинг 4.18).
 //-- Листинг 4.18. Преобразование пути из короткой в длинную форму --// 
   function ShortPathToLong(path: String): String;
   var
   buffer: String;
   len: Integer;
   begin
   SetLength(buffer, MAX_PATH);
   len:= GetLongPathName(PAnsiChar(path), PAnsiChar(buffer), MAX_PATH);
   SetLength(buffer, len);
   ShortPathToLong:= buffer;
   end

   При тестировании последнего листинга в Delphi 7 выяснилось, что API-функция GetLongPathName объявлена в модуле Windows. Возможно, в других версиях Delphi это не так, но, в любом случае, импортировать эту функцию из библиотеки Kernel32. dll предельно просто – достаточно поместить в модуль следующую строку:

   function GetLongPathName(lpszLongPath: PChar; lpszShortPath: PChar;
   cchBuffer: DWORD): DWORD; stdcall;
   external kernel32 name 'GetLongPathNameA';

 //-- Преобразование абсолютного пути в относительный, и наоборот --// 
   Теперь пришла очередь рассмотреть реализацию преобразований между абсолютной и относительной формами путей. Однако сначала рассмотрим небольшую, но полезную процедуру, используемую при преобразованиях. Процедура GetPathElements (листинг 4.19) формирует список строк из компонентов переданного ей пути (имен папок и именицелевого файла или папки).
 //-- Листинг 4.19. Разбиение пути на составляющие --// 
   procedure GetPathElements(path: String; elements: TStrings);
   var
   start, pos: Integer;
   begin
   start:= 1;
   for pos:= 1 to Length(path) do
   if path [pos] = '\' then
   begin
   if start <> pos then
   //Выделим имя папки
   elements.Add(Copy(path, start, pos – start))
   else
   //Сочетание типа '\\' в середине пути пропускаем
   ;
   start:= pos + 1;
   end;
   pos:= Length(path) + 1;
   if start <> pos then
   //Выделим имя последней папки или файла
   elements.Add(Copy(path, start, pos – start));
   end;

   После применения процедуры GetPathElements работать с компонентами пути становится достаточно удобно, да к тому же и упрощается код функций преобразования, так как при их написании не приходится уделять внимание, правильно ли выделены подстроки из строк полного пути.
   Функция преобразования абсолютного пути в относительный (относительно заданной в параметре curdir папки) приведена в листинге 4.20.
 //-- Листинг 4.20. Преобразование абсолютного пути в относительный --// 
   function AbsPathToRelative(path, curdir: String): String;
   var
   pathElements, curElements: TStrings;
   outPath: String;
   i, j: Integer;
   begin
   if Copy(path, 1, 2) <> Copy(curdir, 1, 2) then
   begin
   //Папки на разных дисках
   AbsPathToRelative:= path;
   Exit;
   end;
   //Получение составляющих абсолютного и текущего пути
   pathElements:= TStringList.Create;
   GetPathElements(path, pathElements);
   curElements:= TStringList.Create;
   GetPathElements(curdir, curElements);
   //Пропускаем одинаковые папки
   i:= 0;
   while (i < curElements.Count) and (i < pathElements.Count)
   and (CompareText(curElements[i], pathElements[i]) = 0) do Inc(i);
   //Добавляем небходимое количество переходов вверх для того, чтобы из
   //папки curdir попасть в общую для path и curdir папку
   for j:= i to curElements.Count -1 do
   out Path:= out Path + '..\';
   //Заходим из папки полученной (общей) в папку path
   for j:= i to pathElements.Count – 2 do
   out Path:= out Path + path Elements[j] + '\';
   //Последним добавляем имя конечной папки или файла
   AbsPathToRelative:= outPath + pathElements[pathElements.Count – 1];
   //Списки строк больше не нужны
   pathElements.Free;
   curElements.Free;
   end;

   При преобразовании нужно учитывать, что пути, не принадлежащие одной иерархии (например, локальный и сетевой пути или пути, принадлежащие разным дискам), не могут быть представлены один относительно другого: у них нет общей родительской папки.
   Функция обратного преобразования относительного пути в абсолютный приведена в листинге 4.21. Здесь нужно отметить, что если путь папки curdir относительный, то в итоге получается также относительный путь (относительно другой папки). Поэтому функция и называется RelativePathToRelative, а не RelativePathToAbs.
 //-- Листинг 4.21. Преобразование относительного пути в абсолютный --// 
   function RelativePathToRelative(path, curdir: String): String;
   var
   pathElements, curElements: TStrings;
   outPath: String;
   i: Integer;
   begin
   //Получение списка составляющих абсолютного и текущего пути
   pathElements:= TStringList.Create;
   GetPathElements(path, pathElements);
   curElements:= TStringList.Create;
   GetPathElements(curdir, curElements);
   //Изначально находимся в последней папке пути curdir
   //"Путешествуем" от текущей папки вверх или вниз по дереву папок
   //(прибавляя или удаляя компоненты пути из списка curElements)
   for i:= 0 to pathElements.Count–1 do
   begin
   if pathElements[i] = '..' then
   //Вверх по дереву
   if (curElements.Count > 0)then
   curElements.Delete(curElements.Count – 1)
   else
   curElements.Append('..')
   else if pathElements[i] <> '.' then
   //Вниз по дереву (знак текущей папки "." не изменяет положение)
   curElements.Append(pathElements[i]);
   end;
   //Формируем результирующий путь
   if (curElements.Count > 0) then outPath:= curElements[0];
   for i:= 1 to curElements.Count -1 do
   out Path:= out Path + '\'+ curElements[i];
   RelativePathToRelative:= outPath;
   //Списки строк больше не нужны
   pathElements.Free;
   curElements.Free;
   end;


   Поиск

   Поиск является неотъемлемой частью работы с файловой системой. Даже простой просмотр содержимого любой папки сопряжен с использованием простейших, но все же поисковых средств (перебор и, возможно, отсеивание элементов папки). Поэтому далее будут рассмотрены возможные варианты реализации двух функций поиска: поиска по маске и атрибутам файлов в пределах заданной папки и такого же поиска по всему дереву папок, начиная от заданной корневой папки. Все рассмотренные далее функции поиска можно найти в модуле Search, расположенном на диске, в папке с названием подраздела.
   Однако сначала немного сведений о масках, используемых для поиска, и атрибутах файлов и папок.
 //-- Маски и атрибуты --// 
   Маска имени файла или папки представляет собой строку, в которой неизвестный одиночный символ можно заменять символом? а произвольное количество (0 и более) неизвестных заранее символов можно заменять символом *. Остальные (допустимые в имени) символы обозначают сами себя. Например, имена файлов SomeFile.exe и Some.exe удовлетворяют каждой из масок: Some* и Some*.exe.
   Атрибуты определяют некоторые важные особенности файла. Так, например, при просмотре папки с помощью API-функций папка может отличаться от файла только наличием атрибута FILE_ATTRIBURE_DIRECTORY. Вообще содержимое папки записано на диске в самый обычный файл. Его отличает наличие указанного неизменяемого вручную атрибута и строго заданный формат записей, а также наличие специальных функций, скрывающих все особенности работы с данными папки (открытие файла с записями, поиск нужных записей).
   Теперь об атрибутах. Ниже приведен перечень наиболее часто используемых атрибутов файлов и папок (идентификаторы целочисленных констант, объявленных в модуле Windows) (если не сказано иного, атрибут можно изменить):
   • FILE_ATTRIBUTE_ARCHIVE – архивный файл или папка (на опыте замечено, что этот атрибут появляется практически у всех файлов, находящихся на диске некоторое время);
   • FILE_ATTRI BUTE_DI RECTORY – атрибут папки (атрибут нельзя самостоятельно снять или назначить);
   • FILE_ATTRIBUTE_HIDDEN – скрытый файл или папка;
   • FILE_ATTRIBUTE_NORMAL – означает отсутствие особых атрибутов у файла или папки (у папки, естественно, всегда установлен атрибут FILE_ATTRIBUTE_ DIRECTORY);
   • FILE_ATTRIBUTE_READONLY – файл или папка, доступные только для чтения;
   • FILE_ATTRIBUTE_SYSTEM – системный файл или папка;
   • FILE_ATTRIBUTE_TEMPORARY – временный файл (для ускорения доступа к находящимся в нем данным файловая система стремится по возможности хранить все содержимое открытого временного файла в оперативной памяти).
   Выше были рассмотрены основные атрибуты, которые могут быть присвоены объектам файловой системы (файлам и папкам), но не было сказано, как получить или установить атрибуты файла или папки. Атрибуты можно получить при просмотре содержимого папки (как в рассмотренных далее функциях поиска), а также с помощью API-функции GetFileAttributes. Эта функция принимает путь файла (PChar) и возвращает значение типа DWORD (32-битное целое значение), представляющее собой битовую маску. Если функция GetFileAttributes завершается неудачно, то возвращаемое значение равно $FFFFFFFF (-1 при переводе к беззнаковому целому).
   Каждому из рассмотренных атрибутов соответствует бит в возвращаемом функцией GetFileAttributes значении. Ниже приведен отрывок программы, определяющей, является ли файл системным:

   var attrs: DWORD;
   begin
   attrs:= GetFileAtt ribut e(PAnsiChar('C:\boot.ini'));
   if (attrs and FILE_ATTRIBUTE_SYSTEM <> 0) then {файл системный};

   Атрибуты устанавливаются с помощью API-функции SetFileAttributes. Данная функция принимает два параметра: путь файла или папки (PChar) и битовую маску атрибутов и возвращает ненулевое значение в случае успешного выполнения задачи и 0 (False) в противном случае.
   Поскольку в функцию SetFileAttributes передается маска, хранящая сведения сразу обо всех атрибутах файла или папки, то изменять атрибуты нужно аккуратно (чтобы не удалить установленные ранее). Пример (отрывок программы) назначения одного и одновременного снятия другого атрибута файла приведен в листинге 4.22 (для простоты проверка ошибок не производится).
 //-- Листинг 4.22. Изменение атрибутов файла --// 
   var attrs: DWORD;
   begin
   attrs:= GetFileAttributes('C:\text .txt');
   attrs:= attrs or FILE_ATTRIBUTE_HIDDEN; //Установка атрибута "скрытый"
   attrs:= attrs and not FILE_ATTRIBUTE_ARCHIVE; //Снятие атрибута "архивный"
   SetFileAttributes('C:\text.txt', attrs);

 //-- Поиск в указанной папке --// 
   Поиск в пределах одной папки представляет собой простой перебор всех ее элементов с отбором тех, имена которых удовлетворяют маске и заданному набору атрибутов. В приведенном ниже примере (листинг 4.23) используется API-функция FindFirstFile, которая начинает просмотр указанной папки, автоматически отсеивая имена файлов и папок, не удовлетворяющие маске, и возвращает дескриптор (THandle), используемый для идентификации начатого сеанса просмотра папки при продолжении поиска (передается в функции FindNextFile и FindClose).
   После окончания просмотра папки вызывается функция FindClose, завершающая просмотр папки. Весь процесс очень напоминает работу с обычным файлом (открытие, просмотр, закрытие), не так ли?
 //-- Листинг 4.23. Поиск в заданной папке --// 
   function SearchInFolder(folder, mask: String; flags: DWORD;
   names: TStrings; addpath: Boolean = False): Boolean;
   var
   hSearch: THandle;
   FindData: WIN32_FIND_DATA;
   strSearchPath: String;
   bRes: Boolean; //Если равен True, то найден хотя бы один файл или папка
   begin
   strSearchPath:= folder + '\'+ mask;
   bRes:= False;
   //Начинаем поиск
   hSearch:= FindFirstFile(PAnsiChar(strSearchPath), FindData);
   if (hSearch <> INVALID_HANDLE_VALUE) then
   begin
   //Ищем все похожие элементы (информация о первом элементе уже
   //записана в FindData функцией FindFirstFile)
   repeat
   if (String(FindData.cFileName) <> '..') and
   (String(FindData.cFileName) <> '.') then //Пропускаем . и ..
   begin
   if MatchAttrs(flags, FindData.dwFileAttributes) then
   begin
   //Нашли подходящий объект
   if addpath then
   names.Add(folder + '\'+ FindData.cFileName)
   else
   names.Add(FindData.cFileName);
   bRes:= True;
   end;
   end;
   until FindNextFile(hSearch, FindData) = False;
   //Заканчиваем поиск
   FindClose(hSearch);
   end;
   SearchInFolderr r:= bRes;
   end;

   В результате работы функции SearchInFolder заполняется список names именами или, если значение параметра addpath равно True, полными путями найденных файлов и папок. Значение параметра flags (битовая маска атрибутов) формируется так же, как для функции SetFileAttributes, только одновременно могут быть установлены любые интересующие атрибуты. При обнаружении хотя бы одного файла или папки функция SearchlnFolder возвращает значение True.
   В функции поиска проверка соответствия атрибутов найденных файлов и папок маске производится с помощью дополнительной функции MatchAttrs. Код этой функции приведен в листинге 4.24.
 //-- Листинг 4.24. Фильтр атрибутов --// 
   function MatchAttrs(flags, attrs: DWORD): Boolean;
   begin
   MatchAttrs:= (flags and attrs) = flags;
   end;

   Может показаться, что проверка из одной строки – недостаточная причина для создания отдельной функции. Тем не менее в рассматриваемом примере отдельная функция MatchAttrs выделена для того, чтобы сделать отсеивание файлов (и папок) по атрибутам более очевидным.
   В листинге 4.24 приведена реализация нестрогого фильтра: он принимает файл или папку, если они имеют все установленные во flags атрибуты, независимо от того, имеет ли файл или папка дополнительные атрибуты. Так, если, например, задан flags:= FILE_ATTRIBUTE_READONLY, то будут найдены скрытые, системные и прочие как файлы, так и папки, имеющие среди своих атрибутов атрибут FILE_ATTRIBUTE_READONLY. Для реализации строгого фильтра можно заменить выражение в функции MatchAttrs простым равенством: flags = attrs.
   Возможный результат поиска с использованием функции SearchInFolder показан на рис. 4.5.
   Рис. 4.5. Поиск в заданной папке

   Пример кода вызова функции SearchlnFolder (для показанного на рис. 4.5 приложения) приведен в листинге 4.25.
 //-- Листинг 4.25. Использование функции SearchlnFolder --// 
   //Запуск поиска файла в заданной папке
   procedure TForm2.Button1Click(Sender: TObject);
   var
   flags: DWORD;
   begin
   //Формируем набор атрибутов (по установленным флажкам на форме)
   flags:= 0;
   if (chkDirs.Checked) then flags:= flags or FILE_ATTRIBUTE_DIRECTORY;
   if (chkHidden.Checked) then flags:= flags or FILE_ATTRIBUTE_HIDDEN;
   if (chkSystem.Checked) then flags:= flags or FILE_ATTRIBUTE_SYSTEM;
   if (chkReadOnly.Checked) then flags:= flags or FILE_ATTRIBUTE_READONLY;
   if (chkArchive.Checked) then flags:= flags or FILE_ATTRIBUTE_ARCHIVE;
   lblFound.Caption:= 'Поиск...';
   lstFiles.Clear;
   Refresh;
   //Поиск (файлы записываются прямо в список на форме)
   if not SearchInFolder(txtFolder.Text, txtMask.Text, flags,
   lstFiles.Items)
   then
   lblFound.Caption:= 'Поиск не дал результатов'
   else
   lblFound.Caption:= ' Найдено объектов: '+ IntToStr(lstFiles.Count);
   end;

 //-- Поиск по всему дереву папок --// 
   В листинге 4.26 приведена одна из возможных реализаций рекурсивного поиска по дереву папок. Алгоритм поиска следующий.
   1. Выполняется поиск в папке folder (все найденные файлы или папки добавляются в список names).
   2. Функция SearchlnTree вызывается для каждой подпапки папки folder для продолжения поиска в поддереве, определяемом каждой подпапкой.
 //-- Листинг 4.26. Поиск по дереву папок --// 
   function SearchInTree(folder, mask: String; flags: DWORD;
   names: TStrings; addpath: Boolean = False): Boolean;
   var
   hSearch: THandle;
   FindData: WIN32_FIND_DATA;
   bRes: Boolean; //Если равен True, то найден хотя бы один файл или папка
   begin
   //Осуществляем пои ск в текущей папке
   bRes:= SearchInFolder(folder, mask, flags, names, addpath);
   //Продолжим поиск в каждой из подпапок
   hSearch:= FindFirstFile(PAnsiChar(folder + '\*'), FindData);
   if (hSearch <> INVALID_HANDLE_VALUE) then
   begin
   repeat
   if (String(FindData.cFileName) <> '..') and
   (String(FindData.cFileName) <> '.') then //Пропускаем . и ..
   begin
   if (FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY <> 0)
   then
   //На йдена подпапка – выполним в ней поиск
   if SearchInTree(folder + '\'+ String(FindData.cFileName),
   mask, flags, names, addpath)
   then
   bRes:= True;
   end;
   until FindNextFile(hSearch, FindData) = False;
   FindClose(hSearch);
   end;
   SearchInTree:= bRes;
   end;

   В функции SearchInTree используется просмотр папки folder вручную (с помощью API-функций) из соображений эффективности. Если захотите, можете реализовать поиск подпапок с помощью функции SearchlnFolder. Правда, для этого придется завести дополнительный список (TStringList) для сохранения найденных в текущей папке подпапок. Элементы списка будут использоваться только один раз: для поиска в подпапках.
   Возможный результат поиска с использованием функции SearchInTree приводится на рис. 4.6.
   Рис. 4.6. Поиск по дереву папок

   С небольшими изменениями алгоритм рекурсивного обхода дерева папок, реализованный в листинге 4.25, можно использовать и при операциях, отличных от простого поиска: например, при копировании или удалении дерева папок. Для этого достаточно выполнять нужные операции над каждым находимым объектом.


   Построение дерева папок

   Рассмотрим довольно интересный пример, основанный на использовании функции поиска SearchInFolder, – построение дерева папок для определенного диска (рис. 4.7).
   Рис. 4.7. Дерево папок

   Для простоты (и чтобы не отвлекать внимание от построения дерева) диск задается в программе жестко – при необходимости это можно легко исправить (как определять диски, вы уже знаете).
   Рассмотрим работу приложения по порядку. Элемент управления TreeView на форме имеет имя tree. На рис. 4.8 показано содержимое списка изображений (ImageList), используемого деревом.
   Рис. 4.8. Изображения для элементов дерева

   Первый элемент дерева, соответствующий диску, создается при обработке события FormCreate (листинг 4.27).
 //-- Листинг 4.27. Создание первого элемента дерева – диска --// 
   procedure TForm3.FormCreate(Sender: TObject);
   begin
   //Корневой элемент дерева (диск)
   root:= tree.Items.Add(tree.Items.GetFirstNode, 'C:');
   root.ImageIndex:= 0;
   root.SelectedIndex:= 0;
   SetExpanded(root, False);
   end;

   Здесь и далее в примере глобальная в пределах модуля переменная root позволяет быстро получать доступ к корневому элементу дерева. Используемая в листинге 4.27 процедура применяется для установки состояния элементов дерева (листинг 4.28).
 //-- Листинг 4.28. Установка состояния элемента дерева --// 
   procedure TForm3.SetExpanded(Node: TTreeNode; isExpanded: Boolean);
   begin
   if isExpanded then
   begin
   //Подготавливаем элемент к загрузке содержимого папки
   Node.Data:= Pointer(1);
   Node.DeleteChildren;
   end
   else
   begin
   //Содержимое папки не прочитано (или его следует обновить)
   Node.Data:= Pointer(0);
   Node.Collapse(False);
   Node.DeleteChildren;
   tree.Items.AddChild(Node, ''); //Фиктивный элемент (чтобы отображался
   //"+", позволяющий развернуть элемент)
   end;
   end;

   Если после создания элементов дерева вызвать процедуру SetExpanded с параметром isExpanded, равным False (как в листинге 4.27), то для переданного в процедуру элемента дерева создастся фиктивный дочерний элемент. Это делается для того, чтобы не зачитывать содержимое каждого еще неразвернутого элемента дерева (при работе с папками, содержащими большое количество файлов, программа будет сильно «тормозить»). В этом случае у каждого еще не развернутого элемента отображается символ +, позволяющий развернуть элемент в нужный момент. При этом не нужно забывать удалять созданный фиктивный элемент дерева, что и выполняет процедура SetExpanded при вызове с параметром isExpanded, равным True.
   Каждый неразвернутый еще элемент дерева помечается значением поля Node. Data, равным 0. Каждый элемент, содержимое которого уже прочитано с диска, помечается значением поля Node. Data, равным 1. Для проверки, было ли прочитано содержимое папки, соответствующей элементу дерева, используется простая функция IsExpanded (листинг 4.29).
 //-- Листинг 4.29. Проверка, загружено ли содержимое папки --// 
   function TForm3.IsExpanded(Node: TTreeNode): Boolean;
   begin
   IsExpanded:= Integer(Node.Data) = 1;
   end;

   Загрузка содержимого папки и одновременное формирование дочерних элементов в дереве происходит при разворачивании элемента дерева (листинг 4.30).
 //-- Листинг 4.30. Загрузка содержимого папки --// 
   procedure TForm3.treeExpanding(Sender: TObject; Node: TTreeNode;
   var AllowExpansion: Boolean);
   var
   strFolder: String;
   subfolders: TStrings;
   i: Integer;
   item: TTreeNode;
   begin
   if not IsExpanded(Node) then
   //Содержимое папки нужно зачитать
   SetExpanded(Node, True)
   else
   begin
   //Список подпапок для выделенной папки был составлен ранее
   AllowExpansion:= True;
   Exit;
   end;
   //Составление списка подпапок
   strFolder:= NodeToFolderPath(Node);
   subfolders:= TStringList.Create;
   if SearchInFolder(strFolder, '*', FILE_ATTRIBUTE_DIRECTORY, subfolders)
   then begin
   //Добавим в дерево элементы, соответствующие подпапкам
   for i:= 0 to subfolders.Count – 1 do
   begin
   item:= tree.Items.AddChild(Node, subfolders[i]);
   item.ImageIndex:= 1;
   item.SelectedIndex:= 2;
   SetExp anded(item, False); //Содержимое подпапки еще не прочитано
   end;
   AllowExpansion:= True;
   end
   else
   //В папке нет подпапок
   AllowExpansion:= False;
   subfolders.Free;
   end;

   В листинге 4.30 для определения пути папки, заданной элементом дерева, используется функция NodeToFolderPath. Реализуется она совсем не сложно (листинг 4.31).
 //-- Листинг 4.31. Определение полного пути элемента дерева --// 
   function TForm3.NodeToFolderPath(Node: TTreeNode): String;
   var
   path: String;
   item: TTreeNode;
   begin
   item:= Node;
   while item <> nil do
   begin
   if path <> '' then
   path:= item.Text + '\'+ path
   else
   path:= item.Text;
   item:= item.Parent;
   end;
   NodeToFolderPath:= path;
   end;

   Приведенный здесь пример построения дерева может пригодиться при решении некоторых задач. Дополнительно же нужно сказать, что на вкладке Samples (Delphi 7) можно найти компоненты, прекрасно подходящие для построения пользовательского интерфейса приложений, предназначенных для просмотра содержимого не только физически существующих дисков: компонент ShellTreeView, используемый для построения полного дерева папок (включая корневой элемент Рабочий стол и прочие виртуальные папки), компонент ShellComboBox, позволяющий построить список основных элементов системы папок, а также компонент ShellListView, создающий элемент управления, используемый для просмотра содержимого папки.



   Файлы


   В завершение главы будут рассмотрены три несложных примера работы с файлами: копирование файла (с отображением хода копирования в ProgressBar), определение значков, ассоциированных с файлами, и извлечение значков из EXE– и DLL-файлов.


   Красивое копирование файла

   Казалось бы, что особенного в организации копирования большого файла с отображением процесса: читай файл порциями и записывай прочитанные данные в файл назначения, попутно отображая в ProgressBar или где-то еще отношение объема переписанной информации к размеру файла. Однако зачем такие сложности? Ведь у API-функции CopyFile, выполняющей простое копирование файла, есть и расширенный вариант – функция CopyFileEx, поддерживающая отображение процесса копирования (и не только это). Прототип функции CopyFileEx имеет следующий вид:

   function CopyFileEx(lpExistingFileName, lpNewFileName: PChar;
   lpProgressRoutine: TFNProgressRoutine; lpData: Pointer; pbCancel: PBool;
   dwCopyFlags: DWORD): BOOL; stdcall;

   Кроме пути исходного и конечного файлов, а также флагов (последний параметр) функция принимает ряд дополнительных параметров: адрес функции обратного вызова (IpProgressRoutine), указатель на данные, передаваемые в функцию обратного вызова (lpData), а также адрес переменной типа BOOL (pbCancel), при установке значения которой в состояние True копирование прерывается.
   Пример использования функции CopyFileEx в программе приведен в листинге 4.32. Здесь подразумевается, что кнопка cmbCopy используется как для запуска, так и для прерывания процесса копирования. Также на форме присутствуют следующие элементы управления:
   • индикатор pbCopyProgress, диапазон значений которого составляет от О до 1ОО;
   • текстовое поле txtFrom, содержащее имя копируемого файла;
   • текстовое поле txtTo, содержащее имя файла назначения.
 //-- Листинг 4.32. Использование функции CopyFileEx --// 
   procedure TForm1.cmbCopyClick(Sender: TObject);
   begin
   if cmbCopy.Caption = 'Копировать' then
   begin
   //Запускаем копирование
   progress:= pb CopyProgress; //Настроен от 0 до 100 %
   bCancelCopy:= False;
   cmbCopy.Caption:= 'Отмена ';
   if CopyFileEx(PAnsiChar(txtFrom.Text), PAnsiChar(txtTo.Text),
   Addr(CopyProgressFunc), nil, Addr(bCancelCopy),
   COPY_FILE_FAIL_IF_EXISTS) = False
   then
   MessageBox(Handle, 'Не удается скопировать файл', 'Копирование',
   MB_ICONEXCLAMATION);
   end
   else
   begin
   //Останавливаем процесс копирования
   bCancelCopy:= True;
   cmbCopy.Caption:= 'Копировать';
   end;
   end;

   Из кода листинга 4.32 видно, что в качестве значения последнего параметра функции CopyFileEx можно передавать константу COPY_FILE_FAIL_IF_EXISTS (функция возвращает False, если файл назначения уже существует, и не запускает процесс копирования). На самом деле значение параметра dwCopyFlags функции CopyFileEx может являться комбинацией значений COPY_FILE_FAIL_IF_EXISTS и COPY_FILE_RESTARTABLE, то есть представлять собой битовый флаг. Последнее значение используется для того, чтобы копирование файла можно было остановить и потом возобновить в случае прерывания процесса. В этом случае функция CopyFileEx сохраняет в файле назначения информацию, достаточную для возобновления процесса копирования.
   В листинге 4.32 используется переменная progress – глобальная переменная-ссылка на TProgressBar, используемая в функции обратного вызова. Переменная bCancelCopy, адрес которой передается в функцию CopyFileEx, также объявлена глобальной (в пределах модуля).
   Теперь, наконец, рассмотрим функцию обратного вызова, выполняющую в данном случае отображение хода копирования на индикаторе ProgressBar (листинг 4.33).
 //-- Листинг 4.33. Функция, отображающая ход копирования файла --// 
   function CopyProgressFunc(TotalFileSize: Int64;
   TotalBytesTransferred: Int64;
   StreamSize: Int64;
   StreamBytesTransferred: Int64;
   dwStreamNumber: DWORD;
   dwCallbackReason: DWORD;
   hSourceFile: THandle;
   hDestinationFile: THandle;
   lpData: Pointer): DWORD; stdcall;
   begin
   progress.Position:= 100 * TotalBytesTransferred div TotalFileSize;
   Application.ProcessMessages; //Чтобы не "зависал " интерфейс приложения
   CopyProgressFunc:= PROGRESS_CONTINUE;
   end;

   Пусть вас не смущает большое количество параметров функции CopyProgressFunc. Применять их все далеко не обязательно (но они должны быть объявлены), хотя ничего сложного здесь нет. В листинге 4.33 использование параметров реализовано, на мой взгляд, наиболее простым и очевидным образом: значения параметров TotalBytesTransferred и TotalFileSize применяются для определения доли скопированной информации.
   В листинге 4.33 вызов метода ProcessMessages объекта Application используется потому, что функция CopyFileEx возвращает управление программе только после завершения (или прерывания) копирования. В противном случае пришлось бы создавать для копирования отдельный поток, усложняя листинг и отвлекаясь от главной цели примера.
   Теперь несколько слов о возвращаемых функцией CopyProgressFunc значениях (в данном примере используется только одно из четырех доступных значений). Список целочисленных констант, значения которых может возвращать функция CopyProgressFunc, следующий:
   • PROGRESS_CONTINUE – продолжать процесс копирования;
   • PROGRESS_CANCEL – отменить копирование;
   • PROGRESS_STOP – остановить копирование (можно возобновить);
   • PROGRESS_QUIET – при возврате этого значения система продолжает копирование, но перестает вызывать функцию CopyProgressFunc.
   Внешний вид формы при копировании большого файла показан на рис. 4.9.
   Рис. 4.9. Копирование большого файла

   Только не забывайте останавливать копирование при закрытии приложения или в прочих экстренных ситуациях. Так, если не предусмотреть обработку события CloseQuery для формы (рис. 4.9), то закрыть ее в ходе копирования обычным способом не удастся, зато после завершения копирования (или после нажатия кнопки Отмена) форма исчезнет. Странное поведение, не правда ли? Вариант кода, реализующего более или менее адекватную реакцию на закрытие формы, приведен в листинге 4.34.
 //-- Листинг 4.34. Прерывание копирования при закрытии формы --// 
   procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
   begin
   //Останавливаем процесс копирования
   bCancelCopy:= True;
   end;

   Как вариант, можно запретить закрытие формы (установить параметр CanClose в значение False), не прерывая копирования.
   Если копируется несколько файлов, можно ввести дополнительный элемент управления ProgressBar, отображающий ход всего процесса копирования. Только при этом придется заранее определить общий размер копируемых файлов.


   Определение значков, ассоциированных с файлами

   Рассмотрим еще один пример, позволяющий получить значок файла, отображаемый, например, в Проводнике Windows. Приведенная в листинге 4.35 функция принимает в качестве параметра путь файла и флаг, определяющий, какой нужен значок – малый или большой. Эта функция возвращает дескриптор экземпляра значка, ассоциированного с файлом. Реализация функции представлена в модуле ShellFunctions, расположенном на диске, в папке с названием раздела.
 //-- Листинг 4.35. Определение значка файла --// 
   function GetFileIcon(filename: String; small: Boolean = False): HICON;
   var
   info: SHFILEINFO;
   flags: Cardinal;
   begin
   flags:= SHGFI_ICON;
   if small then
   //Получение малого значка
   flags:= flags or SHGFI_SMALLICON
   else
   //Получение большого значка
   flags:= flags or SHGFI_LARGEICON;
   ZeroMemory(Addr(info), SizeOf(info));
   //Получение значка
   SHGetFileInfo(PAnsiChar(filename), 0, info, SizeOf(info), flags);
   GetFileIcon:= info.hIcon;
   end;

   Используемая в листинге 4.35 API-функция SHGetFilelnfo объявлена в модуле ShellApi. В этом же модуле объявлена и структура SHFILEINFO.
   В листинге 4.36 приведен пример использования функции GetFileIcon: здесь полученные значки сохраняются в элементах управления Image (по одному для большого и маленького значка).
 //-- Листинг 4.36. Пример получения значка заданного файла (или папки) --// 
   procedure TForm1.cmbLoadIconClick(Sender: TObject);
   begin
   //Определение большого и малого значков файла
   imgLarge.Picture.Icon.Handle:= GetFileIcon(txtFile.Text);
   imgSmall.Picture.Icon.Handle:= GetFileIcon(txtFile.Text, True);
   end;

   На рис. 4.10 показан пример определения значка файла.
   Рис. 4.10. Определение значка, ассоциированного с файлом

   На самом деле функция, представленная в листинге 4.35, может определять значки не только файлов, но и папок, дисков и виртуальных папок (Мой компьютер, Рабочий стол, Панель управления и т. д.). Правда, в последнем случае используемая в листинге API-функция SHGetFileInfo потребует первый параметр специального вида (не строка). Работа с таким представлением путей частично рассмотрена в подразделе «Окно для выбора папки» главы 2 (стр. 77).
   В заключение стоит сказать несколько слов о прочих полезных возможностях API-функции SHGetFileInfo. Недаром она называется не SHGetFileIcon или как-то иначе: она позволяет получить гораздо больше информации, нежели просто значок файла. Состав возвращаемой функцией информации зависит от набора флагов, передаваемых в функцию в качестве последнего параметра, но сначала рассмотрим поля, из которых состоит структура SHFILEINFO, поскольку результат (за редким исключением) помещается именно в ее поля:
   • hIcon (типа HICON) – содержит дескриптор значка объекта, заданного путем (первый параметр функции SHGetFileInfo);
   • iIcon (типа Integer) – содержит номер значка, отображаемый в системном компоненте ImageList;
   • dwAttributes (типа DWORD) – содержит атрибуты объекта, заданного путем;
   • szDisplayName (типа array [0..MAX_PATH-1] of AnsiChar) – буфер для имени заданного объекта (например, сочетание имени и метки диска, отображаемое в Проводнике Windows);
   • szTypeName (типа array [0..7 9] of AnsiChar) – буфер для названия типа файла (например, Документ Microsoft Word).
   Поля dwAttributes и ilcon подробно описывать не стану. Вместо этого остановлюсь на способе, позволяющем заставить функцию SHGetFilelnfo заполнить остальные поля структуры (их проще всего использовать в Delphi). Используемые для этого флаги (имена целочисленных констант) следующие.
   • SHGFI_ICON – поле hIcon заполняется дескриптором значка, ассоциированного с объектом. Если при использовании данного флага дескриптор не сохраняется в каком-либо контейнере или прочем объекте, автоматически удаляющем ненужные значки (как в листинге 4.36), то значок после использования необходимо удалить вручную (для удаления значков предназначена API-функция DestroyIcon).
   • SHGFI_LARGEICON, SHGFI_SMALLICON – применяются в сочетании с флагом SHGFI_ICON для получения большого или малого значка соответственно. Совместное использование флагов не имеет смысла (при этом будет получен малый значок).
   • SHGFI_DISPLAYNAME – при наличии этого флага в поле szDisplayName будет помещено имя объекта (например, System(C:)).
   • SHGFI_EXETYPE – при наличии этого атрибута поле s zTypeName будет заполнено текстовым описанием типа файла.
   Значения в приведенном списке можно, если не сказано иного, комбинировать с помощью операции битового ИЛИ (Or).


   Извлечение значков из EXE– и DLL-файлов

   Наверняка вы знаете, что исполняемый файл, помимо кода программы, данных и прочей системной информации, может содержать также ресурсы. Из секции ресурсов берутся значки для EXE-файлов. Кроме того, в EXE– и DLL-файлах содержатся значки, используемые для ассоциированных с приложениями документов. Итак, в завершение главы будет рассмотрен еще один пример: создадим программу, способную извлекать упомянутые значки из DLL– и EXE-файлов (а также из ICO-файлов).
   Пусть есть путь файла, а также два списка (ImageList) для больших и малых значков соответственно. Тогда процедура, заполняющая списки значками, извлеченными из файла, может выглядеть следующим образом (листинг 4.37).
 //-- Листинг 4.37. Составление списков значков --// 
   procedure LoadIcons(filename: String; lgImages, smImages: TImageList);
   var
   icon: TIcon;
   smIconHandle, lgIconHandle: HICON;
   i: Integer;
   begin
   //Загрузка каждого значка (неоптимально, но просто)
   i:= 0;
   while Integer(
   ExtractIconEx(PAnsiChar(filename), i, lgIconHandle, smIconHandle, 1)
   ) > 0 do
   begin
   Inc(i);
   //Большой значок
   icon:= TIcon.Create;
   icon.Handle:= lgIconHandle;
   lgImages.AddIcon(icon);
   //Малый значок
   icon:= TIcon.Create;
   icon.Handle:= smIconHandle;
   smImages.AddIcon(icon);
   end;
   end;

   В листинге 4.37 для извлечения значков из файла используется очередная функция модуля ShellApi – ExtractlconEx. Прототип функции имеет следующий вид:

   function n n ExtractIconEx (lpszFile: PChar; nIconIndex: Integer;
   var phiconLarge, phiconSmall: HICON;
   nIcons: UINT): UINT;

   Функция ExtractlconEx принимает следующие параметры:
   • lpszFile – путь файла, из которого извлекаются значки;
   • nIconIndex – номер первого извлекаемого значка; нумерация начинается с нуля (если номер равен -1 и параметры piconLarge и piconSmall нулевые, то функция возвращает количество значков в файле);
   • piconLarge, piconSmall – ссылки на переменные типа HICON (либо на первые элементы массива array. of HICON) для помещения в них дескрипторов больших и малых значков соответственно;
   • nIcons – количество извлекаемых значков (по сути, может быть количеством элементов в передаваемых в функцию массивах – лишние элементы не будут заполнены).
   Функция возвращает количество значков, извлеченных из файла, или количество значков в файле при соответствующем значении параметра nIconlndex.
   В листинге 4.36 используется не совсем оптимальный способ извлечения значков из файла – по одному. Однако данный способ подходит для большинства случаев. Другой (но не единственный вариант) – использование массива. В этом случае функции ExtractlconEx передаются первые элементы массивов для дескрипторов значков (функции нужен адрес начала массива), а в качестве последнего параметра – количество элементов в массиве. Таким образом, если количество значков в файле превосходит количество элементов в массиве, вызов функции ExtractlconEx можно будет повторить, передав в качестве параметра nI conlndex значение, возвращенное функцией ExtractlconEx, умноженное на номер вызова функции (начиная с нуля).
   Кроме того, можно использовать динамический массив, предварительно установив его размер, вызвав для этого функцию ExtractlconEx с параметром nIconlndex, равным -1. Установить в качестве значения параметров piconLarge, piconSmall ноль (не меняя объявление функции) можно, объявив указатель на HICON ( -------
| Библиотека iknigi.net
|-------
|  
 -------


HICON), присвоив ему значение nil и передав его в качестве упомянутых параметров в функцию.
   На рис. 4.11 показан внешний вид формы приложения после извлечения значков из файла Explorer.exe.
   Рис. 4.11. Извлеченные из EXE-файла значки

   Обработчик нажатия кнопки Загрузить значки формы, показанной на рис. 4.11, приведен в листинге 4.38.
 //-- Листинг 4.38. Составление списков значков и их отображение --// 
   procedureTForm1. cmbLoadIconClick(Sender: TObject);
   var
   i: Integer;
   item: TListItem;
   begin
   lvwIconsLg.Clear;
   lvwIconsSm.Clear;
   //Загрузка значков в ImageList
   ImageListLg.Clear;
   ImageListSm.Clear;
   LoadIcons(txtFile.Text, ImageListLg, ImageListSm);
   //Создание элементов в ListView с большими и малыми значками
   for i:= 0 to ImageListLg.Count – 1 do
   begin
   item:= lvwIconsLg.Items.Add();
   item.Caption:= 'Icon'+ IntToStr(i+1);
   item.ImageIndex:= i;
   item:= lvwIconsSm.Items.Add();
   item.Caption:= 'Icon'+ IntToStr(i+1);
   item.ImageIndex:= i;
   end;
   end;

   Здесь подразумевается, что lvwIconLg и lvwIconSm – имена элементов управления ListView для отображения больших и малых значков соответственно. На форме также расположены два элемента управления ImageList для хранения больших и малых значков соответственно: ImageListLg и ImageListSm.
   С помощью окна Object Inspector список ImageListLg назначен в качестве источника больших изображений (свойство LargeImages) для lvwIconLg. Соответственно, список ImageListSm назначен в качестве источника малых изображений (свойство SmallImages) для lvwIconSm.




   Глава 5
   Мультимедиа


   • Воспроизведение звука с помощью системного динамика
   • Использование компонента MediaPlayer
   • Компонент Animate
   • Разработка звукового проигрывателя
   • Видеопроигрыватель

   Использование мультимедийных технологий позволяет повысить качество программ и придает им профессиональный, более привлекательный для пользователя, вид. Среди множества областей применения мультимедиа наиболее интересны аудио– и видеовозможности компьютера. Использование звуков и видео в программах позволяет иным образом взаимодействовать с пользователем: озвучивать его действия, информировать о различных событиях, просматривать видеоролики и т. п.
   В рамках этой главы будут рассмотрены основные возможности мультимедийных средств и компонентов среды Delphi. Будут описаны компоненты Animate и MediaPLayer, способы использования API-функций для генерации звука системным динамиком и воспроизведения звука из ресурсных файлов.
   В отличие от языков Turbo Pascal и Borland Pascal, Delphi не содержит процедур типа Sound или NoSound, предназначенных для работы со звуком. Для использования мультимедийных возможностей компьютера в Delphi служат специальные компоненты Animate и MediaPLayer.
   Компонент MediaPLayer является основным элементом воспроизведения аудио– и видеофайлов. Многофункциональный элемент MediaPLayer обладает рядом возможностей и обеспечивает управление мультимедийными устройствами.
   Для создания и воспроизведения простейшей анимации предназначен компонент Animate. Он позволяет воспроизводить файлы в формате AVI (Audio-Video Interleaved – аудио-и видеосмесь).


   Воспроизведение звука с помощью системного динамика

   Звуковое сопровождение является важной частью большинства современных мультимедийных приложений. В простейших случаях для генерации звукового сигнала достаточно использовать процедуру Beep модуля SysUtils. В этом случае отсутствует необходимость использовать компоненты Animate и MediaPLayer, а звук создается встроенным системным динамиком. Процедура Beep осуществляет вызов одноименной API-функции, поэтому ее использование не составляет большого труда (листинг 5.1).
 //-- Листинг 5.1. Генерация звукового сигнала посредством функции Beep --// 
   procedure TForm1.Button1Click(Sender: TObject);
   begin
   Beep; //Генерация иязвукового огосигнала
   MessageDlg(‘Звуковой сигнал был подан' , mtError, [mbOK], 0);
   end;

   Наряду с функцией Beep для получения звукового сигнала используется API-функция MessageBeep(uType: UINT): Boolean, генерирующая стандартный системный звук, тип которого указывается параметром uType. Параметр функции MessageBeep может задаваться двумя способами: в виде шестнадцатеричного числа и поименованной константы. Например, системный звук по умолчанию задается константой MB_OK, а стандартный системный звук задается шестнадцатеричным числом $FFFFFFFF. Функция возвращает параметр типа Boolean, который в случае успешного выполнения (воспроизведения звука) имеет значение True.


   Использование компонента MediaPlayer

   Мультимедийный проигрыватель MediaPlayer является многофункциональным управляющим элементом. Он предоставляет программисту набор свойств и методов, позволяющих манипулировать файлами и устройствами мультимедиа, воспроизводить файлы и перемещаться между фонограммами (дорожками, записями), а также идентифицировать подключенные устройства.
   Компонент MediaPlayer содержит следующие кнопки (рис. 5.1, слева направо).
   • Play – воспроизведение.
   • Pause – пауза.
   • Stop – остановка.
   • Next – переход к следующей фонограмме (дорожке). Если в списке воспроизведения присутствует всего одна фонограмма, то выполняется переход в ее конец.
   • Prev – переход к предыдущей фонограмме. Если в списке воспроизведения присутствует всего одна фонограмма, то выполняется переход в ее начало.
   • Step – переход на несколько кадров вперед.
   • Back – переход на несколько кадров назад.
   • Record – включение режима записи.
   • Eject – извлечение носителя.
   Рис. 5.1. Вид компонента MediaPlayer

   Компонент MediaPlayer обладает следующим рядом свойств, позволяющих управлять воспроизведением файлов.
   • AutoOpen – определяет, должно ли устройство автоматически открываться после загрузки.
   • AutoRewind – если свойство имеет значение True, то после завершения списка воспроизведения будет осуществлен переход в его начало.
   • DeviceType – определяет тип устройства, которым должен управлять объект MediaPlayer. Свойство принимает одно из следующих значений:
   · dtAVIVideo – файл AVI;
   · dtCDAudio – аудио компакт-диски;
   · dtDAT – цифровой кассетный аудиопроигрыватель;
   · dtDigitalVideo – цифровое видео (AVI, MPG, MOV-файлы или ММ-фильм);
   · dtMMMovie – формат multimedia movie;
   · dtOther – неопределенный формат;
   · dtSequencer – MIDI-файл;
   · dtVCR – видеомагнитофон;
   · dtVideodisc – проигрыватель видеодисков;
   · dtWaveAudio – звуковой файл типа WAV;
   · dtAutoSelect – компонент выбирает устройство автоматически (устанавливается по умолчанию).
   • Display – задает оконный элемент, в котором будут воспроизводиться видеоданные. Если свойство не задано, то будет открываться новое дополнительное окно.
   • DisplayRec – задает прямоугольную область для воспроизведения данных.
   • EnableButtons – определяет набор командных кнопок, которые можно использовать в компоненте.
   • StartPos – определяет начальную позицию для воспроизводимых данных. Если свойство не задано, то воспроизведение идет с начала.
   • EndPos – определяет конечную позицию для воспроизведимых данных. Если свойство не задано, то воспроизведение идет до конца.
   • Position – текущая позиция при воспроизведении.
   • Tracks – определяет количество дорожек для компакт-дисков.
   • Frames – определяет число кадров, на которое перемещается позиция устройства при вызове методов Back и Next.
   • Length – длина файла (носителя).
   • TimeFormat – устанавливает временной формат, используемый конкретным устройством.
   • Wait – определяет, будет управление возвращено вызывающему приложению немедленно или после завершения воспроизведения.
   Одним из важных свойств является свойство Capabilities типа TMPDevCapsSet, которое позволяет определить возможности выбранного и открытого устройства. Это свойство может принимать следующие значения, определяющие доступность соответствующих операций:
   • mpCanEject – извлечение носителя;
   • mpCanPlay – воспроизведение;
   • mpCanRecord – запись на носитель;
   • mpCanStep – перемотка вперед или назад на определенное количество кадров;
   • mpUsedWindow – использование окна для вывода изображения.
   Перед использованием устройства его нужно открыть, поскольку большинство методов, например, Play и StartRecording, можно вызвать только после открытия устройства. Открытие выполняется путем вызова метода Open (листинг 5.2). Если необходимо выполнить автоматическое открытие устройства, то свойству AutoOpen типа Boolean следует присвоить значение True (по умолчанию ему присвоено значение False). После открытия какого-либо мультимедийного устройства свойство DeviseID типа Word проигрывателя будет содержать идентификатор открытого устройства. Если открытых устройств нет, то значение свойства DeviseID будет равно 0.
 //-- Листинг 5.2. Открытие устройства проигрывания компакт-дисков --// 
   procedure TForm1.Button2Click(Sender: TObject);
   begin
   //Задаем устройства воспроизведения
   MyMediaPlayer.DeviceType:= dtCDAudio;
   //Открываем устройство
   MyMediaPlayer.Open;
   end;

   После использования мультимедийного устройства его нужно закрыть, вызвав метод Close.
   Открыв устройство с помощью свойства Tracks типа Longint, можно получить информацию о количестве фонограмм (дорожек). Если устройство не поддерживает дорожки, то значение этого свойства не определено. Свойство TrackLength [TrackNum: Integer] типа Longint содержит длину фонограммы с индексом TrackNum (отсчет начинается с единицы). Длина дорожки указывается в формате времени, заданным свойством TimeFormat.
   Свойство TimeFormat типа TMPTimeFormats задает формат значений свойств, которые связаны со временем. Это свойство влияет на способ интерпретации и отображение значений таких свойств, как TrackLength, Length, StartPos, EndPos и Position. Основными значениями свойства TimeFormat являются следующие:
   • tfMilliseconds – целое четырехбайтовое число (счетчик миллисекунд);
   • tfHMS – количество часов, минут и секунд, размещенных побайтно, начиная с младшего байта, в четырехбайтовом целом (старший байт не учитывается);
   • tfMSF – количество минут, секунд и кадров, размещенных побайтно, начиная с младшего байта, в четырехбайтовом целом (старший байт не учитывается);
   • tfFrames – целое четырехбайтовое число, содержащее количество кадров.
   Теперь, когда вы ознакомились с основными свойствами мультимедиа-компонента MediaPlayer, можно приступать к непосредственному применению его на практике. Ниже представлен пример исходного текста программы, при загрузке которой проигрывается звук (в формате WAV) (листинг 5.3).
 //-- Листинг 5.3. Воспроизведение звука при создании формы приложения --// 
   //Функция вызывается при создании формы
   procedureTForm1. FormCreate (Sender: TObject);
   begin
   //Скрываем компонент
   MyMediaPlayer.Visibleee:= false;
   //Автоматически определяем устройство воспроизведения
   MyMediaPlayer.DeviceType:= dtAutoSelect;
   //Загружаем файл воспроизведения
   MyMediaPlayer.FileName:= ‘start.wav’;
   //Открываем устройство
   if not MyMediaPlayer.AutoOpen then MyMediaPlayer.Open;
   //Воспроизводим файл
   MyMediaPlayer.Play;
   end

   При создании формы Form1 воспроизводится звуковой файл start.wav.
   В некоторых случаях удобнее хранить и использовать данные (например, звуковые записи) прямо в запускаемом модуле (EXE-файле). Такой метод предусматривает хранение звука в файлах ресурсов (RES). На этапе сборки программы файлы ресурсов прикрепляются к запускаемому модулю, тем самым увеличивая размер модуля, но количество файлов, необходимых для корректной работы программы, уменьшается. Так, в предыдущем примере для нормальной работы программы (воспроизведения звука при загрузке) необходим файл start. wav. Следующий пример демонстрирует создание приложения, запускаемый модуль которого будет содержать все необходимые ресурсы (в данном случае это звуковой файл).
   В начале необходимо создать файл ресурса, который будет содержать звуковую запись. Для этого понадобится компилятор ресурсов, который находится в папке Borland\Delphi7\Bin\и имеет имя brcc32.exe. Далее нужно создать файл ресурса. Все ресурсы (значки, указатели, изображения, таблицы строк и т. п.), используемые приложением, описываются в специальном файле. Такое описание имеет фиксированный формат:

   <имя> <тип> <параметры> <имя файла>

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

   LOADSOUND RCDATA LOADONCALL start.wav

   Далее в командной строке нужно записать brcc32.exe source.rc, где source. rc – текстовый файл, содержащий описание ресурса.
   После компиляции получается готовый файл ресурса source. res. Переместите его в папку проекта. На этом этапе ресурс уже может использоваться для сборки приложения.
   Чтобы подключить файл ресурса, напишите в исходном тексте программы следующее:

   //Подключение ресурса
   {$R SOURCE.RES}

   Теперь, когда файл ресурса подключен и готов к использованию, необходимо создать процедуру, которая будет загружать звуковой файл из ресурса и воспроизводить его. Процедура, выполняющая эти действия, выглядит следующим образом (листинг 5.4).
 //-- Листинг 5.4. Использование ресурсов для хранения звуковых записей --// 
   //Функция, которая воспроизводит звук, находящийся в ресурсе
   procedure RetrieveLoadSound;
   var
   hResource: THandle;
   pData: Pointer;
   begin
   //Загружаем файл ресурса и находим звук под именем 'LOADSOUND'
   hResource:= LoadResource(hInstance, FindResource(hInstance, 'LOADSOUND', RT_RCDATA));
   try
   //Находим адрес загруженного ресурса
   pData:= LockResource(hResource);
   if pData = nil then raise Exception.Create('Ошибка чтения ресурса LOADSOUND');
   //Воспроизводим звуковой файл
   sndPlaySound(pData, SND_MEMORY);
   finally
   //Освобождаем ресурс
   FreeResource(hResource);
   end;
   end;

   Для работы функции RetrieveLoadSound понадобятся две переменные: hResource (дескриптор ресурса) и pData (указатель на память, расположение ресурса). Прежде чем приступить к непосредственному использованию, необходимо загрузить ресурс в память приложения (функция LoadResource), но, чтобы загрузить именно тот ресурс, который необходим (звук LOADSOUND), с помощью функции FindResource найдите LOADSOUND во всех ресурсах, подключенных к этому экземпляру приложения (hInstance). Теперь осталось получить указатель на память, в которой находится звуковой файл, и записать его в переменную pData. Если ресурс не будет найден, то программа выдаст сообщение об ошибке. После того как будет получен указатель на память, его можно будет использовать в функции sndPlaySound для воспроизведения звука. Параметр SND_MEMORY означает, что воспроизведение будет осуществляться из памяти приложения (а не из файла, как было показано ранее).
   Функция RetrieveLoadSound может использоваться в любом месте программы для воспроизведения файла start.wav. При ее использовании данные звукового файла помещаются в запускаемый модуль, тем самым увеличивая его объем, но сокращая количество файлов приложения. Такой подход эффективен при создании небольших приложений, которые используют короткие звуковые сопровождения.
   В конце главы будет подробно описан процесс создания универсального проигрывателя, работа которого целиком построена на использовании компонента MediaPLayer. Пока же рассмотрим следующий мультимедийный компонент Delphi – Animate, который позволяет воспроизводить как стандартную (встроенную в Windows), так и пользовательскую анимацию.


   Компонент Animate

   Видеоклип представляет собой файл в формате AVI, содержащий последовательность отдельных кадров, при отображении которых создается эффект движения. Наряду с изображением AVI-файлы могут содержать и звук. Для воспроизведения видеоклипов можно использовать любой из мультимедийных компонентов Delphi – Animate или MediaPLayer.
   Компонент Animate позволяет проигрывать AVI-файлы, а также отображать стандартную анимацию, используемую в Windows. AVI-файлы, воспроизводимые компонентом Animate, имеют следующие ограничения:
   • они не могут содержать звука;
   • информация в них должна быть несжатой;
   • размер файла не должен превышать 64 Кбайт.
   Для определения воспроизводимого видеоклипа используются свойства FileName и CommonAVI, которые не могут использоваться одновременно. Проигрываемый AVI-файл, расположенный на диске, указывается с помощью свойства FileName, при этом свойству CommonAVI автоматически присваивается значение aviNone.
   Свойство CommonAVI позволяет выбрать один из стандартных клипов Windows и принимает следующие значения:
   • aviNone – стандартная анимация отсутствует;
   • aviCopyFile – копирование файла;
   • aviCopyFiles – копирование файлов;
   • aviDeleteFile – удаление файла;
   • aviEmptyRecycle – очистка Корзины;
   • aviFindComputer – поиск компьютера;
   • aviFindFile – поиск файла;
   • aviFindFolder – поиск папки;
   • aviRecycleFile – перемещение файла в Корзину.
   При назначении свойству CommonAVI значения, отличного от aviNone, свойство FileName автоматически очищается, принимая в качестве значения пустую строку.
   Для воспроизведения видеоклипа также можно использовать свойства ResHandle типа THandle и ResID типа Integer, которые составляют альтернативу свойствам CommonAVI и FileName. Значение свойства ResHandle содержит ссылку на модуль, содержащий изображение в виде ресурса, а значение свойства ResID в этом модуле содержит номер ресурса.
   После выбора видеоклипа свойства FrameCount, FrameHeight и FrameWidth типа Integer определяют количество, высоту и ширину кадров (в пикселах) клипа соответственно. Эти свойства являются свойствами времени выполнения и, следовательно, доступны только для чтения.
   По умолчанию размеры компонента Animate автоматически подстраиваются под размеры кадров видеоклипа, что определяется свойством AutoSize, имеющим значение True. Если этому свойству присвоить значение False, то отсечение части кадра изображения будет возможно, если его размеры превысят размеры компонента Animate.
   Воспроизведение видеоклипа начинается при установке свойству Active значения True. Первый и последний кадры определяют диапазон воспроизведения и определяются значениями свойств StartFrame и StopFrame типа SmallInt соответственно. По умолчанию свойство StartFrame указывает на первый кадр анимации и его значение равно 1.
   Свойство Repetitions типа Integer определяет количество повторений воспроизведения видеоклипа. По умолчанию его значение равно нулю, при котором видеоклип проигрывается до тех пор, пока процесс воспроизведения не будет остановлен (например, при закрытии приложения).
   Для запуска и остановки воспроизведения клипов можно использовать методы Play, Stop и Reset. Процедура Play (FromFrame: Word, ToFrame: Word, Count: Integer) проигрывает видеоклип, начиная с кадра, заданного параметром FromFrame, и заканчивая кадром, заданным параметром ToFrame. Параметр Count определяет количество повторений. Таким образом, процедура Play позволяет одновременно управлять свойствами StartFrame, StopFrame и Repetitions, задавая для них требуемые при воспроизведении значения, а также устанавливает свойство Active в значение True.
   Свойство Open типа Boolean доступно при выполнении программы и позволяет определить, готов ли компонент Animate к воспроизведению. Если выбор и загрузка видеоклипа выполнены успешно, то свойству Open автоматически устанавливается значение True (компонент можно открыть и воспроизвести анимацию). При неуспешной загрузке видеоклипа это свойство получает значение False. При необходимости программист может сам присваивать свойству Open значение False, тем самым отключая компонент Animate.
   Процедура Stop прерывает воспроизведение видеоклипа и присваивает свойству Active значение False. Процедура Reset, кроме того, дополнительно сбрасывает свойства StartFrame и StopFrame, устанавливая им значения по умолчанию (первый и последний кадры воспроизводимой анимации).
   В качестве примера, наглядно отражающего работу компонента Animate, будет рассмотрено приложение, позволяющее просматривать стандартную анимацию операционной системы Windows.
   Стандартный видеоклип можно просмотреть, нажав кнопку Просмотр, предварительно выбрав анимацию с помощью группы независимых переключателей. Клип воспроизводится непрерывно с первого до последнего кадра до тех пор, пока не будет нажата кнопка Стоп. Вид окна приложения показан на рис. 5.2.
   Рис. 5.2. Приложение для просмотра стандартной анимации

   Рассмотрим исходный текст приложения подробно. Для работы программы необходим набор констант, значения которых может принимать свойство CommonAVI, поэтому в начале программы нужно объявить константный массив Typeof AVI типа TCommonAVI, который и будет содержать необходимые значения:

   const TypeofAVI: array[0..8] of TCommonAVI =
   (aviNone, aviCopyFile, aviCopyFiles,
   aviDeleteFile, aviEmptyRecycle, aviFindComputer,
   aviFindFile, aviFindFolder, aviRecycleFile);

   При создании главного окна приложения определите положение переключателя в группе выбора стандартной анимации Windows:

   procedure TFormViewAnim.FormCreate(Sender: TObject);
   begin
   //Стандартная анимация “Копирование файла ”
   RadioGroupSelectAnimEffects.ItemIndex:= 1;
   end;

   Затем создайте обработчик выбора группы независимых переключателей. При выборе анимации первым делом устанавливается доступность кнопок управления. Далее задается вид воспроизводимого ролика (например Копирование файлов). В блоке if производится проверка индекса выбранной анимации и, если она не выбрана (индекс равен нулю), блокируется кнопка Просмотр, так как в этом случае просмотр стандартной анимации невозможен (листинг 5.5).
 //-- Листинг 5.5. Обработчик выбора анимационных роликов --// 
   //Выбор стандартной анимации
   procedure TFormViewAnim.RadioGroupSelectAnimEffectsClick(Sender: TObject);
   begin
   //Устанавливаем доступность кнопок управления
   bnStopView.Enabled:= False;
   bnStartView.Enabled:= True;
   //Устанавливаем значение свойства CommonAVI
   StandartAnimate.CommonAVI:= TypeofAVI[RadioGroupSelectAnimEffects.ItemIndex];
   //Если анимация не выбрана, делаем недоступной кнопку старта показа
   if RadioGroupSelectAnimEffects.ItemIndex = 0
   then bnStartView.Enabled:= False
   else bnStopView.Enabled:= True;
   end;

   Значения индексов (RadioGroupSelectAnimEffects.ItemIndex) переключателей соответствуют порядковым номерам в массиве TypeofAVI, который содержит возможные значения свойства CommonAVI.
   При нажатии кнопки начала показа производится вызов метода Play, компонента Animate и устанавливается доступность кнопок управления показом:

   //Старт показа
   procedure TFormViewAnim.bnStartViewClick(Sender: TObject);
   begin
   //Начинаем показ выбранной анимации
   StandartAnimate.Play(1, StandartAnimate.FrameCount, 0);
   bnStartView.Enabled:= False;
   bnStopView.Enabled:= True;
   end;

   Обработчик кнопки Стоп основан на вызове метода Stop компонента Animate и выглядит следующим образом:

   procedure TFormViewAnim.bnStopViewClick(Sender: TObject);
   begin
   //Остановка показа анимации
   StandartAnimate.Stop;
   bnStartView.Enabled:= True;
   bnStopView.Enabled:= False;
   end;

   Зачастую компонент Animate используется при создании панелей инструментов для добавления в них анимационных пиктограмм, которые оживляют форму и служат для индикации выполнения программой той или иной обработки данных. Воспроизведение изображения может осуществляться, например, при нажатии кнопки на панели инструментов или по истечении заданного интервала времени.
   Компонент Animate обеспечивает воспроизведение только простых AVI-файлов. С этой же целью можно использовать компонент MediaPlayer, который, с функциональной точки зрения, значительно сложнее и обеспечивает больше мультимедийных возможностей.


   Разработка звукового проигрывателя

   Обладая достаточным багажом знаний о мультимедийных компонентах Delphi, мы вплотную подошли к созданию программы-проигрывателя. В рамках данной книги разработка многофункционального сложного проигрывателя не предусматривается, но создание легкореализуемого приложения с набором необходимых функций все же будет рассмотрено.
   Приступим к проектированию проигрывателя. Для начала нужно определить набор необходимых функций. В качестве базовых возможностей любого проигрывателя как видео-, так и аудиофайлов выделяют: непосредственно воспроизведение выбранного файла, возможность кратковременной остановки и возобновления воспроизведения (функция паузы), остановка, перемещение позиции воспроизведения (перемотка). Необходимым также являются отображение времени проигрывания и имени воспроизводимого файла. Как известно, компонент MediaPLayer поддерживает почти все эти функции, за исключением двух последних. Следовательно, MediaPLayer почти идеально подходит на роль основного элемента разрабатываемого проигрывателя.
   Свойства формы приложения настраиваются следующим образом. Убирается кнопка максимизации (в данном случае она является лишней): для этого свойству biMaximaze, которое находится на вкладке BorderIcons, присваивается значение False. Затем свойство BorderStyle устанавливается равным bsSingle (это не позволит пользователю изменять размеры формы). Для удобства использования проигрыватель будет появляется в центре экрана, следовательно, свойство Position нужно установить как poScreenCenter. После этого настраиваются цвета: в данном случае свойство Color равно clInactiveCaptionText.
   Для отображения текстовой динамической информации удобно использовать компонент LabeL (метка). Время и позиция указателя воспроизведения в файле будут выводиться в специальный индикатор. Индикатор (в данном случае lbMainTime типа TLabel) будет отображать текущее время проигрывания. Создаваемый проигрыватель должен обладать удобным интерфейсом, поэтому он настраивается следующим образом: цвет фона Color устанавливается как clSkyBlue, цвет и размер шрифта индикатора – clMenuHighlight и 28 соответственно. Другой индикатор (надпись с именем воспроизводимого файла) будет иметь свойства, установленные по умолчанию.
   Управление воспроизведением будет осуществляться частично с помощью кнопок проигрывателя. Функции перемотки реализуются в обработчиках двух других дополнительных кнопок, поэтому все кнопки компонента MediaPLayer, кроме кнопок воспроизведения, паузы и остановки, скрываются. Делается это с помощью присвоения свойству VisibleButtons массива значений [btPlay,btPause,btStop]. Кнопки управления перемоткой будут выглядеть стандартно. Кроме всего сказанного, также необходимо создать кнопку открытия файла для выбора файла воспроизведения. Для этого поместите на форму стандартную кнопку и оставьте ее настройки по умолчанию.
   Далее нужно максимально эргономично разместить на форме вышеперечисленные компоненты – и можно переходить от создания дизайна к реализации функциональных возможностей. Для корректной работы индикатора времениего необходимо периодически обновлять. Для достижения этой цели понадобится таймер. Среда Delphi содержит компонент, выполняющий функции таймера Timer (вкладка System).
   На форму приложения также помещается стандартное окно открытия файлов. Находится этот компонент на вкладке Dialogs. Один из вариантов размещения компонентов интерфейса показан на рис. 5.3.
   Рис. 5.3. Интерфейс проигрывателя

   Начнем рассмотрение исходного текста приложения. В программе присутствует секция констант с единственной константой, необходимой для задания интервала (положения указателя воспроизведения в файле), на которое будет осуществляться перемотка. В данном случае перемотка будет осуществляться на 10 секунд:

   const
   //Константа для перемотки на 10000 миллисекунд
   perem=10000;

   Далее необходимо создать функцию, которая будет преобразовывать численные значения времени (миллисекунды) в более удобный для вывода строковый формат с указанием минут и секунд (листинг 5.6).
 //-- Листинг 5.6. Функция преобразования времени --// 
   function TSoundPlayerForm.FileLangToStr(leng: longint): string;
   var
   //Переменная результирующей строки
   strTime: string;
   sec: longint;
   min: longint;
   begin
   //Получаем секунды и минуты из миллисекунд (leng)
   sec:= trunc(leng/1000);
   min:= trunc(sec/60);
   secc c cc:= sec– min*60;
   strTimeee:= IntToStr(min);
   //Если секунд меньше десяти, то преобразуем результирующую
   //строку (участок минут), добавляя '0'спереди
   if sec < 10 then strTime:= strTime + ':0'+ IntToStr(sec)
   else strTime:= strTime + ':'+ IntToStr(sec);
   FileLangToStr:= strTime;
   end;

   Далее нужно определить количество секунд, затем минут и преобразовать эти данные в строковый вид (для вывода на индикатор времени). Если после расчета количества минут секунд оказалось меньше десяти, то в результирующую строку добавляется 0. К примеру, получилось, что композиция имеет длительность три минуты и пять секунд. В этом случае строка должна выглядеть как 3:05, а не 3:5.
   Процедуру создания корректного формата времени мы разобрали. Теперь необходимо выяснить, как можно узнать время, которое прошло с момента начала воспроизведения файла. Для этого потребуются свойства компонента MediaPLayer, а именно свойства Length (длина загруженного файла) и Position (текущая позиция в нем). Зная позицию, можно с помощью ранее рассмотренной функции FileLangToStr найти время воспроизведения (листинг 5.7).
 //-- Листинг 5.7. Процедура отображения или обновления индикаторов --// 
   procedure TSoundPlayerForm.UpdateViewTime;
   var
   //Длина файла и позиция в файле
   leng, posit: longint;
   begin
   //Находим длину воспроизводимого файла
   leng:= mdpSoundPlayer.Length;
   //Находим позицию в воспроизводимом файле
   posit:= mdpSoundPlayer.Position;
   //Преобразум время в строку
   lbMainTime.Captionnn:= FileLangToStr(posit);
   //Устанавливаем имя файла
   lbFileName.Caption:= mdpSoundPlayer.FileName;
   end;

   Как можно заметить из кода листинга 5.7, после получения позиции в файле и его имени данные о времени воспроизведения и путь к файлу попадают на индикаторы lbMainTime и lbFileName соответственно.
   Открытие и загрузка файла в мультимедийный компонент происходит при выполнении кода листинга 5.8. Кроме того, обработчик вызывает процедуру UpdateVi ewTime и включает таймер (tmTimer. Enabled:= true).
 //-- Листинг 5.8. Открытие файла --// 
   procedure TSoundPlayerForm.bnOpenFileClick(Sender: TObject);
   begin
   if opdOpenDialog.Execute=true then
   begin
   //Открываем файл
   mdpSoundPlayer.FileName:= opdOpenDialog.FileName;
   mdpSoundPlayer.Open;
   //Устанавливаем значения в индикаторах
   UpdateViewTime;
   //Включаем таймер
   tmTimer.Enabled:= true;
   end;
   end;

   Процедура обработки срабатывания таймера заключается в вызове функции обновления значений индикаторов (UpdateViewTime) (листинги 5.9 и 5.10).
 //-- Листинг 5.9. Событие таймера --// 
   procedure TSoundPlayerForm.tmTimerTimer(Sender: TObject);
   begin
   //Обновление значений экрана
   UpdateViewTime;
   end;

 //-- Листинг 5.10. Обработчик активизации формы --// 
   procedure TSoundPlayerForm.FormActivate(Sender: TObject);
   begin
   //Временное выключение таймера
   tmTimer.Enabled:= false;
   //Задание значений
   lbMainTime.Caption:= '00:00';
   lbFileName.Caption:= 'no file...';
   //Установка фильтров для окна
   opdOpenDialog.Filter:=
   'MP3 music (*.mp3)|*.MP3|Wav files (*.wav)|*.WAV';
   end;

   Перемотка осуществляется с помощью двух кнопок. Для перемотки вперед на десять секунд необходимо нажать кнопку >>, назад – << (листинги 5.11 и 5.12).
 //-- Листинг 5.11. Перемотка вперед --// 
   procedure TSoundPlayerForm.bnNextStClick(Sender: TObject);
   begin
   if mpCanPlay in mdpSoundPlayer.Capabilities then
   begin
   if (mdpSoundPlayer.Position+perem) <= mdpSoundPlayer.Length then
   mdpSoundPlayer.Position:= mdpSoundPlayer.Position + perem
   else
   mdpSoundPlayer.Position:= mdpSoundPlayer.Length;
   mdpSoundPlayer.Play;
   end;
   end;

 //-- Листинг 5.12. Перемотка назад --// 
   procedure TSoundPlayerForm.bnPrevStClick(Sender: TObject);
   begin
   if mpCanPlay in mdpSoundPlayer.Capabilities then
   begin
   if mdpSoundPlayer.Position >= perem then
   mdpSoundPlayer.Position:= mdpSoundPlayer.Position – perem
   else
   mdpSoundPlayer.Position:= 0;
   mdpSoundPlayer.Play;
   end;
   end;

   Таким образом, разработанный аудиопроигрыватель предоставляет минимальный набор функций и возможностей, но он обладает важным преимуществом – простотой реализации. Как вы могли заметить, созданная программа может проигрывать и МР3-файлы. Это становится возможным благодаря использованию специального программного обеспечения – кодеков, установленных в операционной системе. Современная и достаточно распространенная операционная система Windows XP содержит такие кодеки в комплекте базовой поставки. При использовании созданного проигрывателя в других операционных системах типа Windows, вероятно, понадобится самостоятельная установка кодеков.
   На этом этапе принцип построения проигрывателя звуковых записей вам должен быть понятен. Что же касается просмотра видеозаписей, то благодаря универсальности компонент MediaPLayer этот процесс схож с воспроизведением звуковых файлов.


   Видеопроигрыватель

   Не менее интересной задачей, рассматриваемой в рамках этой главы, является разработка проигрывателя видеофайлов. Форматов видео существует достаточно много, но самым распространенным из них, несомненно, является AVI. Поэтому следующей, не менее интересной, задачей будет разработка проигрывателя видеофайлов в AVI-формате.
   Учитывая, что среда Delphi предоставляет высокоуровневый доступ к мультимедийным возможностям компьютера, сам принцип построения проигрывателя не меняется. Как и в случае со звуковым проигрывателем, будет использоваться уже хорошо знакомый вам компонент MediaPlayer. Особенностью воспроизведения видео является только необходимость вывода изображения на экран в дополнение к звуковому сопровождению. Таким образом, необходимо определить, какие именно компоненты могут служить в качестве контейнеров для воспроизведения в них видеопотока.
   Как и в случае со звуковым проигрывателем, понадобятся: компонент MediaPlayer, окно открытия файлов OpenDialog и компонент-контейнер для вывода изображения (будет использован компонент GroupBox). Сначала нужно настроить форму приложения. Для этого убирается кнопка максимизации (в данном случае она является лишней): присвойте свойству biMaximaze, которое находится на вкладке BorderIcons, значение False. Затем установите значение свойства BorderStyle равным bs Single – это не позволит пользователю изменять размеры формы. Для удобства использования проигрыватель лучше расположить в центре экрана – следовательно, установите свойство Position в значение poScreenCenter. В компоненте MediaPlayer оставьте видимыми только кнопки запуска, паузы и остановки воспроизведения (так же, как в проигрывателе звука). Поместите на форму компонент GroupBox и установите свойство Caption пустой строкой, так как именно в этот компонент будет выводиться видеоизображение.
   Рис. 5.4. Вид видеопроигрывателя

   Теперь рассмотрим некоторые особенности созданного видеопроигрывателя. В качестве элемента-контейнера для динамического изображения использовался компонент GroupBox, поэтому его необходимо было назначить элементом вывода видео для компонента MediaPlayer. Этот процесс сводится к присваиванию экземпляра компонента GroupBox свойству Display компонента MediaPlayer. Делать это необходимо во время активизации формы (листинг 5.13).
 //-- Листинг 5.13. Назначение элемента вывода изображения --// 
   procedure TFormVideoPlayer.FormActivate(Sender: TObject);
   begin
   //Устанавливаем область воспроизведения
   mpVideoPlayer.Display:= gbViewVideo;
   end;

   В предложенном фрагменте текста программы переменная gbViewVideo является экземпляром компонента GroupBox.
   В качестве доказательства простоты, удобства и гибкости использования компонента MediaPLayer ниже приведен весь исходный текст приложения (листинг 5.14).
 //-- Листинг 5.14. Видеопроигрыватель --// 
   unit video_player;
   interface
   uses
   //Подключаемые модули
   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
   Dialogs, StdCtrls, MPlayer;
   Type
   //Главная форма приложения
   TFormVideoPlayer = class(TForm)
   mpVideoPlayer: TMediaPlayer;
   FileOpenDlg: TOpenDialog;
   gbViewVideo: TGroupBox;
   bnOpenFile: TButton;
   //Нажатие кнопки открытия файла
   procedure bnOpenFileClick(Sender: TObject);
   //Активизация формы
   procedure FormActivate(Sender: TObject);
   private
   {Private declarations}
   public
   {Public declarations}
   end;
   var
   FormVideoPlayer: TFormVideoPlayer;
   implementation
   {$R *.dfm}
   procedure TFormVideoPlayer.bnOpenFileClick(Sender: TObject);
   begin
   if FileOpenDlg.Execute = True then
   begin
   //Загружаем файл, выбранный в окне открытия файла
   mpVideoPlayer.FileName:= FileOpenDlg.FileName;
   //Активизируем проигрыватель
   mpVideoPlayer.Open;
   end;
   end;
   procedure TFormVideoPlayer.FormActivate(Sender: TObject);
   begin
   //Устанавливаем область воспроизведения
   mpVideoPlayer.Display:= gbViewVideo;
   end;
   end.

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



   Глава 6
   Использование Windows GDI


   • Графические объекты
   • Аппаратно-независимый графический вывод
   • Контекст устройства
   • Графические режимы
   • Работа со шрифтами
   • Рисование примитивов
   • Работа с текстом
   • Работа с растровыми изображениями
   • Альфа-смешивание

   Операционная система Windows с самого начала создавалась прежде всего как графическая оболочка. Как следствие, в ней используется графическое представление информации. Вполне естественно, что почти любое приложение использует экран для отображения данных, с которыми оно работает. Сама операционная система, по крайней мере, отображает на экране визуальные элементы приложений. Windows обеспечивает универсальность представления информации как на экране, так и на других устройствах вывода, например на принтере. Стоит отметить, что для этого используются одни и те же примитивы отображения. Система самостоятельно определяет целевое устройство и активизирует соответствующий ему модуль. Операционная система Windows является многозадачной и предъявляет к приложениям ряд основных требований, исключающих возникновение конфликтов при использовании функций вывода. Однако это вовсе не означает, что Windows обеспечивает приложения только набором функций вывода на экран или печать, – система полностью управляет всем выводом.
   Наверное, более правильно будет сказать, что приложения используют в качестве первичного вывода окно, а не непосредственно экран. Каждое устройство вывода в Windows характеризуется набором текущих параметров, с использованием которых происходит собственно вывод, причем в каждый конкретный момент времени только одному приложению соответствует некоторое устройство вывода, что исключает одновременный доступ к последнему, а также изменение параметров одним приложением перед началом процесса вывода другим.


   Графические объекты

   Для управления выводом операционная система Windows предоставляет приложению набор графических объектов.
   • Битовые массивы (bitmaps) – прямоугольные массивы точек, формирующие растровые изображения.
   • Карандаши (pens) – используются для задания таких параметров рисования линий, как толщина, цвет и стиль (сплошная, прерывистая и т. п.).
   • Кисти (brushs) – применяются для задания таких параметров заливки замкнутых контуров, как цвет и стиль.
   • Шрифты (fonts) – позволяют определить параметры вывода текста, включая имя шрифта, размер символов и т. д.
   • Регионы (regions) – определяют области окна, которые могут быть ограничены прямоугольником, многоугольником, эллипсом или их произвольной комбинацией для выполнения операций заполнения, заливки, инверсии и т. д. Помимо этого, регионы служат для определения местоположения указателя.
   • Логические палитры (logical palettes) – осуществляют взаеимодействие между приложением и таким цветным устройством вывода, как дисплей, а также содержат список цветов, необходимых приложению.
   • Контуры (paths) – используются для заполнения или выделения контура различных фигур.


   Аппаратно-независимый графический вывод

   Одной из главных особенностей Windows API является независимость графического вывода от устройства. Программное обеспечение, которое поддерживает независимость, содержится в двух динамически компонуемых библиотеках. Первая – gdi.dll – обеспечивает общий графический интерфейс устройства (Graphics Device Interface, GDI), а вторая является драйвером конкретного используемого устройства. В результате приложение использует тот интерфейс, который предоставляется первой библиотекой. Перед тем как произвести какую-либо операцию вывода на некоторое устройство, приложению необходимо запросить GDI о загрузке соответствующего драйвера (обычно это осуществляется автоматически и не требует дополнительных действий со стороны программиста). После загрузки соответствующего драйвера приложение должно настроить ряд таких параметров вывода, как цвет линии и ее ширина, тип кисти и ее цвет, шрифт, область отсечения и т. д. Операционная система Windows обеспечивает хранение всех этих и других данных в специальной структуре, называемой контекстом устройства.
   Стоит заметить, что GDI реализует интерфейс, используемый для рисования двухмерной графики. Этот интерфейс обеспечивает самый медленный из существующих способ отображения графики, однако является и самым простым для понимания основ. В основном он используется для создания простых эффектов с минимальными усилиями.


   Контекст устройства


   Контекст устройства – структура, определяющая набор графических объектов и связанных с ними атрибутов и графических режимов, которые воздействуют на вывод. Графические объекты включают карандаши для рисования линий, кисти для закрашивания и заполнения, битовые образы для копирования или прокрутки части экрана, цветовые палитры для определения набора доступных цветов, области для отсечения и других операций, а также контуры для операций рисования и закрашивания.
   Приложение не имеет прямого доступа к контексту устройства, и настройка параметров осуществляется посредством вызова соответствующих функций Win32 API.
   Существует четыре типа контекстов устройств:
   • экранный – поддерживает операции рисования непосредственно на экране;
   • принтера – поддерживает операции рисования непосредственно на принтере или плоттере;
   • памяти – поддерживает операции рисования непосредственно в битовых массивах;
   • информационный – поддерживает получение данных об устройстве.
   Приложение может осуществлять следующие операции над контекстом устройства:
   • перечисление существующих графических объектов;
   • выбор новых графических объектов;
   • удаление существующих графических объектов;
   • сохранение графических объектов, их атрибутов и параметров графических режимов;
   • восстановление графических объектов, их атрибутов и параметров графических режимов.
   Помимо всего прочего, приложение может использовать контекст устройства для определения процесса графического вывода, прерывания длительных графических операций, начатых другим потоком многопоточного приложения, а также может инициализировать принтер.


   Экранный контекст устройства

   Приложение получает контекст устройства экрана посредством вызова функций BeginPaint, GetDC или GetDCEx. Полученный контекст устройства идентифицирует окно, в которое будет непосредственно осуществляться вывод. Как правило, приложение получает контекст устройства экрана непосредственно перед началом рисования в клиентской области. Когда приложение завершает вывод, оно обязано освободить контекст устройства, вызвав одну из соответствующих функций: EndPaint или ReleaseDC.
   Win32 API позволяет получать три типа контекста устройства экрана: контекст класса, общий и частный. Контекст класса и частные контексты устройства используются в приложениях, выполняющих многочисленные операции вывода: например, программах автоматизированного проектирования, настольных издательских системах, то есть таких приложениях, которые самостоятельно и постоянно осуществляют вывод и, соответственно, для которых время, затрачиваемое на эти операции, критично с точки зрения производительности. Общие контексты устройства используются в приложениях, выполняющих операции вывода лишь время от времени.
   Контекст класса поддерживается только для совместимости с предыдущими версиями Windows. При создании Win32-приложения вместо контекстов класса следует использовать частные контексты.
   Общий контекст устройства – контекст устройства экрана, который обрабатывается в специальном кэше системы. Такие контексты устройства используются в приложениях, осуществляющих операции вывода не очень часто. Система, перед тем как возвратить описатель контекста устройства, предварительно инициализирует общие контексты устройства значениями по умолчанию, которые можно менять по мере необходимости с помощью специальных функций. Любая операция вывода, выполняемая приложением, будет использовать значения по умолчанию до тех пор, пока не будет вызвана одна из функций GDI, предназначенных для выбора нового графического объекта, изменения атрибутов существующего объекта или выбора нового режима. Поскольку может быть создано лишь определенное количество общих контекстов устройства, то приложение обязано освободить его, после того как осуществит операции вывода. Когда приложение освобождает общий контекст устройства, все произведенные изменения в данных по умолчанию отменяются. В результате параметры приходится устанавливать каждый раз заново.
   Частный контекст устройства, в отличие от общего, сохраняет любые изменения для заданных по умолчанию данных. Этот контекст устройства не является частью системного кэша, и поэтому не должен освобождаться. Система автоматически освобождает его только после того, как последнее окно будет разрушено. Приложение создает частный контекст устройства при заполнении структуры (указав предварительно CS_OWNDC стиль окна), описывающей класс окна, регистрируемого функцией RegisterClass. После создания окна с указанным стилем приложение вызывает одну из функций – GetDC, GetDCEx или BeginPaint – для получения описателя, идентифицирующего частный контекст устройства. Приложение может использовать его до тех пор, пока не будет разрушено окно, созданное с этим классом. Любые изменения графических объектов и их атрибутов или графических режимов сохраняются системой до тех пор, пока окно не будет удалено.


   Контекст устройства принтера

   Контекст устройства принтера может использоваться как для матричного, струйного и лазерного принтера, так и для плоттера. Приложение создает данный контекст устройства посредством вызова функции CreateDC. При этом задаются такие необходимые параметры, как имя драйвера принтера, имя принтера, файла или имени устройства для физической среды вывода и других параметров инициализации. После завершения приложением операции печати требуется вызвать функцию DeleteDC для удаления созданного контекста. Заметьте, что созданный контекст устройства принтера должен быть удален посредством именно этой функции. Освобождение с помощью функции ReleaseDC невозможно.
   Точно так же, как приложению требуется контекст устройства экрана прежде, чем оно сможет осуществлять операции вывода в клиентскую область окна, так и контекст устройства принтера нужен прежде, чем можно будет осуществлять операции вывода на принтер. Контекст устройства принтера подобно контексту устройства экрана содержит информацию о графических объектах и их атрибутах, а также о графических режимах, которые воздействуют на операции вывода. Графические объекты включают карандаш (для рисования линий), кисть (для заливки) и шрифт (для вывода текста).
   В отличие от контекста устройства экрана, контексты устройства принтера не связаны с компонентом управления окна Win32 API и не могут быть получены посредством вызова функции GetDC. Вместо этого приложение обязано вызвать функцию CreateDC или PrintDlgEx.
   Если вы вызываете функцию CreateDC, то обязаны указать драйвер принтера и порт. Для получения этих данных можно воспользоваться функцией GetPrinter или EnumPrinters.


   Контекст устройства памяти

   Чтобы дать приложениям возможность осуществлять операции вывода в память вместо работы с фактическим устройством, используется специальный контекст устройства для растровых операций, который называется контекстом устройства памяти. Возможность осуществлять операции вывода в памяти может понадобиться для улучшения характеристик вывода изображений. Контекст устройства памяти дает возможность системе обработать часть памяти как виртуальное устройство – массив битов в памяти, которые приложение может временно использовать для сохранения данных растрового изображения, созданного на нормальной поверхности для рисования. Поскольку точечный рисунок совместим с устройством, то иногда контекст устройства памяти упоминается как совместимый.
   Контекст устройства памяти сохраняет растровые изображения для специфического устройства. Приложение может создавать данный контекст посредством вызова функции CreateCompatibleDC. Результатом ее выполнения является растровое изображение, имеющее цветной формат, совместимый с форматом первоначального устройства.
   Первоначально изображение в контексте устройства памяти имеет размер 1 х 1 пиксел. Прежде чем приложение сможет начать работать с изображением, оно должно установить битовый массив с соответствующей шириной и высотой в контекст устройства с помощью функции SelectObject.
   Когда приложение передает описатель, который получен с помощью функции CreateCompatibleDC, одной из функций рисования, то запрашиваемая операция вывода не осуществляется на поверхности рисунка устройства. Вместо этого система сохраняет цветовую информацию для результирующей линии, кривой, текста или региона в битовом массиве. Приложение может копировать изображение, хранящееся в памяти, обратно на поверхность рисунка посредством вызова функции BitBlt, указывая в качестве источника контекст устройства памяти, а в качестве приемника – контекст устройства окна или экрана.


   Информационный контекст устройства

   Win32 API поддерживает информационный контекст устройства, используемый для восстановления или получения заданных по умолчанию параметров устройства. Для создания информационного контекста приложение должно вызвать функцию CreateIC. Для получения информации об объектах, заданных по умолчанию для интересующего устройства, используются функции GetCurrentObject и GetObject. Использование информационного контекста устройства более эффективно, чем контекстов других типов, поскольку Win32 API работает с информационным контекстом на более низком уровне и не создает структуры, которые необходимы для их работы. После завершения работы приложения с информационным контекстом устройства необходимо вызвать функцию DeleteDC для удаления созданного контекста.



   Графические режимы

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


   Работа со шрифтами

   Приложение может использовать следующие технологии шрифтов для отображения и печати текста:
   • растровые;
   • векторные;
   • TrueType;
   • OpenType.
   Различие между данными видами шрифтов заключается в способе хранения параметров начертания символов в специальных шрифтовых файлах. Растровые шрифты хранят каждый символ в виде растра (битового массива). Векторные шрифты хранят относительные координаты концов отрезков, из которых состоит соответствующий символ. Шрифты TrueType и OpenType содержат информацию о линиях и командах изгиба, а также настроечную информацию для точного отображения символа, которая используется при изменении масштаба отображения. Шрифты OpenType эквивалентны шрифтам TrueType, за исключением того, что они позволяют определять дополнительную информацию о символах.
   Поскольку точечные рисунки для каждого символа в растровом шрифте предназначены для определенной разрешающей способности устройства, то, следовательно, качество их отображения зависит от устройства вывода. Векторные же шрифты, напротив, не зависят от устройства вывода, однако время, необходимое для их отображения, больше, чем у растровых шрифтов или шрифтов TrueType. Последние (TrueType) обеспечивают приемлемую скорость вывода и могут быть масштабированы с сохранением начального вида символов.
   Операционная система Windows предоставляет разработчикам широкий набор функций для использования шрифтового оформления своих приложений, начиная с предоставления каждому контексту устройства шрифта по умолчанию и заканчивая предоставлением системного окна выбора шрифтов, которое можно использовать в приложении.


   Рисование примитивов

   Теперь, после того как вы немного ознакомились с теорией, пора начинать практиковаться. Далее будет создано простое приложение, позволяющее рисовать на форме ряд примитивов. Для этого в новом приложении для формы необходимо реализовать обработку события OnPaint (листинг 6.1).
 //-- Листинг 6.1. Обработчик события формы OnPaint --// 
   procedureTfmShapes.FormPaint (Sender: TObject);
   var
   hCurDC: HDC;
   hCurPen, hOldPen: HPEN;
   hCurBrush, hOldBrush: HBRUSH;
   begin
   //получаем общий контекст устройства
   hCurDC:= GetDC(Handle);
   //создаем графический объект "Каран даш"
   hCurPen:= CreatePen(PS_SOLID, 2, RGB(255, 64, 0));
   //выбираем его для общего контекста устройства экрана
   //и запоминаем ранее выбранный
   hOldPen:= SelectObject(hCurDC, hCurPen);
   //создаем графический объект "Кисть"
   hCurBrush:= CreateSolidBrush(RGB(0, 128, 255));
   //выбираем ее для общего контекста устройства экрана
   //и запоминаем ранее выбранную
   hOldBrush:= SelectObject(hCurDC, hCurBrush);
   //рисуем эллипс
   Ellipse(hCurDC, 10, 10, 100, 70);
   //рисуем прямоугольник
   Rectangle(hCurDC, 110, 10, 210, 70);
   //прямоугольник с округленными углами
   RoundRect(hCurDC, 10, 80, 100, 140, 10, 10);
   //прямоугольник в виде "бочки"
   RoundRect(hCurDC, 110, 80, 210, 140, 10, 100);
   //рисуем прямую
   MoveToEx(hCurDC, 10, 150, nil);
   LineTo(hCurDC, 100, 220);
   //рисуем дугу
   Arc(hCurDC, 110, 150, 210, 220, 110, 150, 210, 220);
   //восстан вливаем ранее выбранную кисть
   SelectObject(hCurDC, hOldBrush);
   //удаляем созданную кисть
   DeleteObject(hCurBrush);
   //восстанавливаем ранее выбранный карандаш
   SelectObject(hCurDC, hOldPen);
   //удаляем созданный карандаш
   DeleteObject(hCurPen);
   //освобождаем общий контекст устройства
   ReleaseDC(Handle, hCurDC);
   end;

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

   hCurDC:= GetDC(Handle);

   Эта функция получает описатель контекста устройства экрана для клиентской области указанного окна или всего экрана. Функция имеет следующий формат заголовка:

   Function GetDC(hWnd: HWND): HDC;

   Здесь hWnd – дескриптор окна, для которого получается контекст устройства. Если это значение равно nil, то GetDC возвращает контекст устройства для всего экрана. В случае успешного выполнения задачи функция возвращает контекст устройства, в противном случае – nil.
   Теперь нужно изменить атрибуты контекста устройства по умолчанию на необходимые. Для этого изменим цвет карандаша и его толщину, а также цвет кисти, предварительно создав новый графический объект с помощью функции CreatePen:

   hCurPen:= CreatePen(PS_SOLID, 2, RGB(255, 64, 0));

   Формат данной функции следующий:

   Function CreatePen(fnPenStyle: Integer; nWidth: Integer; crColor: COLORREF): HPEN;

   Параметр fnPenStyle определяет стиль карандаша. Перечень возможных значений этого параметра приведен в табл. 6.1.
 //-- Таблица 6.1. Стили карандаша --// 
   Параметр nWidth определяет ширину карандаша в логических единицах. Если nWidth равен 0, то карандаш будет иметь ширину в один пиксел независимо от текущей трансформации.
   CreatePen создает карандаш с заданной шириной и стилем PS_SOLID, если вы указали ширину больше, чем 1, для одного из стилей: PS_DASH, PS_DOT, PS_ DASHDOT, PS_DASHDOTDOT.
   Параметр crColor задает цвет карандаша.
   Если выполнение функции CreatePen завершилось удачно, то она возвращает дескриптор логического карандаша. В противном случае она возвращает nil.
   После того как карандаш создан, следует его выбрать для полученного контекста с помощью функции SelectObject.

   hOldPen:= SelectObject(hCurDC, hCurPen);

   Данная функция имеет следующий формат:

   Function SelectObject(hdc: HDC; hgdiobj: HGDIOBJ): HGDIOBJ;

   Здесь hdc – дескриптор контекста устройства, а hgdiobj – дескриптор выбираемого объекта.
   Если выбранный объект не является регионом и функция выполнилась успешно, то она возвращает дескриптор на объект, который был заменен. Если же выбранный объект является регионом и функция выполнилась успешно, то она возвращает одно из приведенных в табл. 6.2 значений.
 //-- Таблица 6.2. Результат выполнения функции SelectObject для выбранного региона --// 
   Если во время выполнения функции произойдет ошибка и выбранный объект не будет являться регионом, то возвратится значение nil. В противном случае будет возвращено значение HGDI_ERROR.
   Функция SelectObject возвращает предыдущий выбранный объект указанного типа. Приложение должно всегда восстанавливать объект по умолчанию, после того как закончилось рисование с использованием нового объекта.
   Приложение не может выбрать битовый массив более чем для одного контекста устройства одновременно.
   После выбора созданного и записи предыдущего выбранного карандашей необходимо создать и выбрать кисть. Для этого используется функция CreateSolidBrush:

   hCurBrush:= CreateSolidBrush(RGB(0, 128, 255));

   Данная функция имеет следующий формат:

   Function CreateSolidBrush(crColor: COLORREF): HBRUSH;

   Параметр crColor определяет цвет кисти.
   Если функция завершилась успешно, то она возвращает дескриптор логической кисти. В противном случае она возвращает значение nil.
   После создания кисти необходимо выбрать ее с использованием функции SelectObject и запомнить ранее выбранную:

   hOldBrush:= SelectObject(hCurDC, hCurBrush);

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

   Ellipse(hCurDC, 10, 10, 100, 70);

   Эта функция имеет следующий формат:

   Function Ellipse(hdc: HDC; nLeftRect, nTopLeft, nRightRect, nBottomRect: Integer): BOOL;

   Ее параметры имеют следующие значения:
   • hdc – дескриптор контекста устройства;
   • nLeftRect – задает координату x (в логических единицах) верхнего левого угла описываемого прямоугольника;
   • nTopRect – задает координату y (в логических единицах) верхнего левого угла прямоугольника;
   • nRightRect – задает координату x (в логических единицах) правого нижнего угла прямоугольника;
   • nBottomRect – задает координату y (в логических единицах) правого нижнего угла прямоугольника.
   Если выполнение функции завершается успешно, то ее результат составляет ненулевое значение. В противном случае возвращается 0.
   Для рисования прямоугольника используется функция Rectangle:

   Rectangle(hCurDC, 110, 10, 210, 70);

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

   RoundRect(hCurDC, 10, 80, 100, 140, 10, 10);

   У данной функции первые пять параметров аналогичны параметрам предыдущей функции, а последние два задают ширину и высоту эллипса, с помощью которого происходит округление углов прямоугольника.
   Следующим создаваемым примитивом будет отрезок. Процесс его рисования осуществляется в два этапа. Сначала с помощью функции MoveToEx устанавливается начальная точка отрезка, а затем используется функция MoveTo с указанием конечной точки:

   MoveToEx(hCurDC, 10, 150, nil);
   LineTo(hCurDC, 100, 220);

   Четвертый параметр функции MoveToEx – переменная типа TPoint, в которую помещается предыдущее положение карандаша.
   Последней рисуется дуга с помощью функции Arc.

   Arc(hCurDC, 110, 150, 210, 220, 110, 150, 210, 220);

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

   SelectObject(hCurDC, hOldPen);
   DeleteObject(hCurPen);
   SelectObject(hCurDC, hOldPen);
   DeleteObject(hCurPen);
   ReleaseDC(Handle, hCurDC);

   Сначала восстанавливаются карандаш и кисть для контекста устройства и удаляются созданные, а после освобождается и сам контекст устройства. Результат выполнения приложения показан на рис. 6.1.
   Рис. 6.1. Результат работы приложения «Рисование примитивов»

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


   Работа с текстом

   Далее будет разработано простое приложение, которое будет способно выводить текст под различным углом через определенный интервал времени. Для этого необходимо опять выполнить обработку события OnPaint формы, в которой будет осуществляться вывод некоторого текста на поверхность формы. Исходный код данного обработчика приведен в листинге 6.2.
 //-- Листинг 6.2. Обработчик события формы OnPaint --// 
   procedure TfmText.FormPaint(Sender: TObject);
   var
   hCurDC: HDC;
   hCurFont, hOldFont: HFONT;
   nOldMode: Integer;
   sText: String;
   begin
   //получаем общий контекст устройства
   hCurDC:= GetDC(Handle);
   //создаем шрифт из шаблона
   hCurFont:= CreateFontIndirect(LogFontData);
   //выбираем созданный шрифт
   hOldFont:= SelectObject(hCurDC, hCurFont);
   //устанавливаем новый режим вывода
   nOldMode:= SetBkMode(hCurDC, TRANSPARENT);
   //устанавливаем цвет текста
   SetTextColor(hCurDC, RGB(0, 0, 255));
   //задаем текстовую строку
     sText:= 'Текст примера';
   //выводим текст на экран
     TextOut(hCurDC, Width div 2, Height div 2, PAnsiChar(sText), Length(sText));
    //восстанавливаем режим вывода
   SetBkMode(hCurDC, nOldMode);
   //восстанавливаем ранее выбранный шрифт
   SelectObject(hCurDC, hOldFont);
   //удаляем созданный шрифт
   DeleteObject(hCurFont);
   //освобождаем общий контекст устройства
   ReleaseDC(Handle, hCurDC);
   end;

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

   hCurFont:= CreateFontlndirect(LogFontData);

   Данная функция имеет следующий формат заголовка:

   Function CreateFontIndirect(const lf: LOGFONT): HFONT;

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

   hOldFont:= SelectObject(hCurDC, hCurFont);

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

   nOldMode:= SetBkMode(hCurDC, TRANSPARENT);

   Функция SetBkMode служит для установки режима смешивания фона определенного контекста устройства. Этот режим используется для текста, штриховых кистей, а также для карандашей, использующих стиль, отличный от сплошных линий.
   Формат заголовка данной функции следующий:

   Function SetBkMode(hdc: HDC; nBkMode: Integer): Integer;

   Здесь параметр hdc задает описатель контекста устройства, для которого устанавливается режим смешивания фона, а nBkMode определяет режим смешивания фона и может принимать одно из значений, указанных в табл. 6.3.
 //-- Таблица 6.3. Режимы смешивания фона --// 
   Если выполнение функции производится успешно, то она возвращает предыдущий установленный режим смешивания фона. В противном случае она возвращает 0.
   Стоит отметить, что данная функция оказывает эффект на стили линий, которые рисуются с использованием карандаша, созданного с помощью функции CreatePen. Если карандаш создан с помощью функции ExtCreatePen, то никакого эффекта не будет.
   Параметр nBkMode может быть установлен и в другие значения, отличные от указанных, которые специфичны для данного драйвера устройства. GDI передает драйверу устройства полученное специфическое значение.
   Теперь необходимо установить определенный цвет текста с помощью функции SetTextColor для контекста устройства:

   SetTextColor(hCurDC, RGB(0, 0, 255));

   Данная функция имеет следующий формат заголовка:

   Function SetTextColor(hdc: HDC; crColor: COLORREF): COLORREF;

   Первый параметр функции задает контекст устройства, для которого устанавливается цвет текста. Второй параметр задает сам цвет, который необходимо установить. В результате функция возвращает предыдущий установленный цвет, но если она завершается неудачно, то возвращается CLR_INVALID.
   Цвет текста используется при рисовании изображения каждого символа с помощью функций TextOut и ExtTextOut, а также для преобразования растрового изображения при конвертировании из цветного в монохромный режим.
   На этом все необходимые подготовки к выводу текста можно считать завершенными, и теперь необходимо просто вывести его с центра формы:

   TextOut(hCurDC, Width div 2, Height div 2, PAnsiChar(sText), Length(sText));

   Однако обработать лишь событие OnPaint недостаточно, поэтому на форму необходимо поместить таймер и установить интервал его срабатывания равным, например, 100, а затем в обработчике изменять атрибуты текста, которые задают угол его наклона при выводе. После этого нужно заставить срабатывать обработчик события OnPaint формы посредством вызова функции RePaint (листинг 6.3).
 //-- Листинг 6.3. Обработчик события таймера OnTimer --// 
   procedure TfmText.TurnTimerTimer(Sender: TObject);
   begin
   with LogFontData do
   begin
   lfEscapement:= lfEscapement + 60;
   lfOrientation:= lfEscapement;
   end;
   RePaint;
   end;

   Переменная LogFontData объявлена следующим образом:

   LogFontData: LOGFONT;

   На основании ее создается шрифт, которым выводится текст. Здесь изменяются только два ее поля, которые влияют на наклон текста при выводе. Все остальные параметры только единожды заполняются при создании формы. Там же активизируется и таймер (листинг 6.4).
 //-- Листинг 6.4. Обработчик события формы OnCreate --// 
   procedure TfmText.FormCreate(Sender: TObject);
   begin
   with LogFontData do
   begin
   lfHeight:= 30; //высота шрифта
   lfWidth:= 0; //средняя ширина символа
   lfEscapement:= 0; //наклон строки относительно оси oX
   lfOrient ati on:= 0; //наклон символа относительно оси oX
   lfWeight:= FW_BOLD; //вес сшрифта
   lfItalic:= 0;
   lfUnderline:= 0;
   lfStrikeOut:= 0;
   lfCharSet:= DEFAULT_CHARSET; //кодовая страница по умолчанию
   lfOut Precision:= OUT_DEFAULT_PRECIS; //точность вывода
   lfClip Precision:= CLIP_DEFAULT_PRECIS; //отсечение вывода
   lfQu alit y:= PROOF_QU ALITY; //качество вывода
   lfPit chAndFamily:= VARIABLE_PITCH or FF_DONTCARE; //семейство
   //шрифта
   lfFaceName:= 'Arial'; //название шрифта
   end;
   TurnTimer.Enabled:= True;
   end;

   Результат работы приложения показан на рис. 6.2
   Рис. 6.2. Результат работы приложения «Работа с текстом»


   Работа с растровыми изображениями

   Вы можете использовать точечный рисунок, чтобы запомнить изображение, а потом сохранить его в памяти, отобразить в другом месте окна вашего приложения или вообще в другом окне.
   В некоторых случаях вам может понадобиться, чтобы ваше приложение запоминало и хранило изображение только временно, например, когда вам необходимо масштабировать его в каком-нибудь приложении для рисования. Для этого необходимо временно запомнить нормальное представление изображения и показать измененное. После того как пользователь опять выберет нормальное представление изображения, приложение будет обязано заменить масштабированное изображение копией нормального, которое временно сохранено.
   Чтобы временно запомнить изображение и создать контекст устройства памяти, совместимый с контекстом устройства экрана текущего окна, приложению необходимо вызвать функцию CreateCompatibleDC. После этого вам нужно создать точечный рисунок с соответствующими атрибутами посредством вызова функции CreateCompatibleBitmap, а затем выбрать его в контекст устройства памяти уже известным вам образом.
   После того как будет создан совместимый контекст устройства и выбран соответствующий точечный рисунок, вы сможете запоминать изображение. Функция BitBlt получает изображение, а также копирует данные из исходного точечного рисунка и помещает их в точечный рисунок приемника. Однако два параметра функции не являются описателями точечных рисунков. Вместо этого функция получает два описателя контекстов устройств и копирует растровые данные из точечного рисунка, выбранного в исходном контексте устройства, в точечный рисунок, выбранный в целевом контексте устройства. В этом случае целевой контекст является и совместимым контекстом устройства. Когда копирование растровых данных завершается, изображение помещается в память. Чтобы восстановить изображение, нужно повторно вызвать функцию BitBlt, указав в качестве источника совместимый контекст устройства, а в качестве приемника контекст устройства экрана (принтера и т. д.).
   Следующий пример демонстрирует способ получения, а также масштабирования изображения всего Рабочего стола. В данном приложении будут обрабатываться три события формы: OnCreate, OnPaint, OnClose, а также одно событие кнопки OnClick.
   Рассмотрим исходный код обработчика события OnCreate (листинг 6.5).
 //-- Листинг 6.5. Обработчик события OnCreate --// 
   procedure TfmCaptureImage.FormCreate(Sender: TObject);
   begin
   //создаем контекст устройства экрана
   hdcScreen:= CreateDC('DISPLAY', nil, nil, nil);
   //создаем совместимый контекст устройства памяти
   hdcCompatible:= CreateCompatibleDC(hdcScreen);
   bmpWidth:= GetDeviceCaps(hdcScreen, HORZRES);
   bmpHeight:= GetDeviceCaps(hdcScreen, VERTRES);
   //создаем соместимый точечный рисунок для hdcScreen
   hbmScreen:= CreateCompatibleBitmap(hdcScreen, bmpWidth, bmpHeight);
   if hbmScreen <> 0 then
   hOldBitmap:= SelectObject(hdcCompatible, hbmScreen)
   else
   hOldBitmap:= 0;
   Captured:= False;
   end;

   Здесь происходит создание контекста устройства Рабочего стола посредством вызова функции CreateDC:

   hdcScreen:= CreateDC('DISPLAY', nil, nil, nil);

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

   bmpWidth:= GetDeviceCaps(hdcScreen, HORZRES);
   bmpHeight:= GetDeviceCaps(hdcScreen, VERTRES);
   //создаем соместимый точечный рисунок для hdcScreen
   hbmScreen:= CreateCompatibleBitmap(hdcScreen, bmpWidth, bmpHeight);

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

   hdcScreen, hdcCompatible: HDC;
   hbmScreen, hOldBitmap: HBITMAP;
   bmpWidth, bmpHeight: Integer;
   Captured: LongBool;

   Рассмотрим исходный код обработчика события OnPaint (листинг 6.6).
 //-- Листинг 6.6. Обработчик события OnPaint --// 
   procedure TfmCaptureImage.FormPaint(Sender: TObject);
   var
   hCurDC: HDC;
   begin
   if Captured then
   begin
   hCurDC:= GetDC(Handle);
   StretchBlt(hCurDC, 0, 0, Width, Height, hdcCompatible,
   0, 0, bmpWidth, bmpHeight, SRCCOPY);
   ReleaseDC(Handle, hCurDC);
   end;
   end;

   Здесь проверяется, есть ли необходимое для отображения изображение и, если есть, производится получение контекста устройства окна и масштабируется на него полученное изображение с помощью функции StretchBlt.
   Перед закрытием формы нужно освободить занятые ресурсы системы, для чего обрабатывается событие OnClose, исходный код обработчика которого приведен ниже (листинг 6.7).
 //-- Листинг 6.7. Обработчик события OnClose --// 
   procedure TfmCaptureImage.FormClose(Sender: TObject;
   var Action: TCloseAction);
   begin
   //восстанавливаем точечный рисунок по умолчанию
   if hOldBitmap <> 0 then
   SelectObject(hdcCompatible, hOldBitmap);
   //удаляем совместимый точечный рисунок
   if hbmScreen <> 0 then
   DeleteObject(hbmScreen);
   //удаляем совместимый контекст устройства памяти
   if hdcCompatible <> 0 then
   DeleteDC(hdcCompatible);
   //удаляем контекст устройства экрана
   if hdcScreen <> 0 then
   DeleteDC(hdcScreen);
   end;

   Осталось рассмотреть лишь последний обработчик события OnClick кнопки, помещенной на форму. Он реализует скрытие окна, сохранение изображения экрана и затем отображение окна (листинг 6.8).
 //-- Листинг 6.8. Сохранение захваченного изображения --// 
   procedure TfmCaptureImage.btnCaptureClick(Sender: TObject);
   var
   hdcForm: HDC;
   begin
   //прячем наше окно
   Hide;
   //сохраняем текущее изображение экрана
   Captured:= BitBlt(hdcCompatible, 0, 0, bmpWidth, bmpHeight, hdcScreen, 0, 0, SRCCOPY);
   //показываем наше окно
   Show;
   end;

   В итоге получается довольно простое приложение, которое способно получать изображение всего Рабочего стола. Результат работы приложения показан на рис. 6.3.
   Рис. 6.3. Результат работы приложения «Захват изображения»


   Альфа-смешивание

   Здесь будет рассмотрен пример, иллюстрирующий способ осуществления альфа-смешивания точечного рисунка. Будет создано приложение, в котором окно делится на три горизонтальные области, в каждой из которых затем рисуется точечный рисунок с альфа-смешиванием следующим образом:
   • в верхней области постоянная альфа равна 50 %, исходная альфа отсутствует;
   • в средней области постоянная альфа равна 100 %, исходная альфа равна 0 %;
   • в нижней области постоянная альфа равна 75 %, исходная альфа переменная.
   Для этого в описание формы сначала нужно добавить процедуру со следующим форматом заголовка:

   procedure DrawAlphaBlend(hWnd: HWND; hdcwnd: HDC);

   В самой процедуре объявляется ряд переменных, которые понадобятся в процессе работы. Код объявления приведен в листинге 6.9.
 //-- Листинг 6.9. Объявление переменных --// 
   var
   hCurDC: HDC; //описатель контекста устройства, который
   //мы создадим
   bf: BLENDFUNCTION; //запись альфа-смешивания
   hb mp: HBITMAP; //дескриптор точечного рисунка
   bmi: BITMAPINFO; //заголовок точечного рисунка
   pvBits: Pointer; //pointer to DIB section
   ulWindowWidth, ulWindowHeight: ULONG; //ширина / высота клиентской
   //области
   ulBit mapWidth, ulBit mapHeight: ULONG; //ширина / высота точечного
   //рисунка
   rt: TRect; //используется для получения размеров клиентской
   //области
   x,y: Integer; //циклические переменные
   ub Alph a: UCHAR; //используется для создания прозрачного градиента
   ubRed: UCHAR;
   ubGreen: UCHAR;
   ubBlue: UCHAR;
   fAlphaFactor: Real;
   r, g, b: UCHAR;

   В самом начале процедуры осуществляется подготовка необходимых данных для альфа-смешивания. Данные содержат информацию о требуемых размерах, а также необходимые данные точечного рисунка (листинг 6.10).
 //-- Листинг 6.10. Подготовка необходимых данных --// 
   //получаем размеры клиентской области
   Windows.GetClientRect(hWnd, rt);
   //рассчитываем ширину и высоту клиентской области
   lWindowWidth:= rt.right – rt.left;
   lWindowHeight:= rt.bott om – rt.top;
   if (ulWindowWidth = 0) or (ulWindowHeight = 0) then
   Exit;
   //делим окно на три горизонтальные области
   ulWindowHeight:= ulWindowHeight div 3;
   //создаем контекст устройства для нашего точечного рисунка
   hCurDC:= CreateCompatibleDC(hdcwnd);
   ZeroMemory(@bmi, sizeof(BITMAPINFO));
   //устанавливаем параметры точечного рисунка
   //указываем ширину и высоту точечного рисунка для каждой из трех
   //горизонтальных областей равными 60 % ширины и высоты главного окна
   //позже осуществим смешивание в центре каждой из этих трех областей
   with bmi.bmiHeader do
   begin
   biSize:= sizeof(BITMAPINFOHEADER);
   biWidth:= ulWindowWidth – (ulWindowWidth div 5) * 2;
   ulBitmapWidth:= biWidth;
   bi Height:= ulWindowHeight – (ulWindowHeight div 5) * 2;
   ulBitmapHeight:= biHeight;
   biPlanes:= 1;
   bi Bit Count:= 32; //четыре восьмибитных составляющих
   biCompression:= BI_RGB;
   biSizeImage:= ulBitmapWidth * ulBitmapHeight * 4;
   end;
   //создаем DIB секцию и выбираем точечный рисунок в контексте устройства
   hbmp:= CreateDIBSection(hCurDC, bmi, DIB_RGB_COLORS, pvBits, 0, 0);
   SelectObject(hCurDC, hbmp);

   Далее необходимо осуществить описанное раннее альфа-смешивание для каждой из областей. Для первой области в точечном рисунке устанавливается синий цвет точки. В листинге 6.11 задаются необходимые параметры и выполняется альфа-смешивание.
 //-- Листинг 6.11. Альфа-смешивание верхней области --// 
   //в верхней области окна постоянная альфа = 5 0 %,
   //но исходная альфа отсутствует
   //цветовой формат для каждого пиксела 0xaarrggbb
   //установим пикселы в синий цвет и альфа в ноль
   for y:= 0 to ulBitmapHeight – 1 do
   for x:= 0 to ulBitmapWidth – 1 do
   PULONG(Integer(pvBits) +
   (x + y * ulBitmapWidth) * sizeof(ULONG))^:= $000000ff;
   bf.BlendOp:= AC_SRC_OVER;
   bf.BlendFlags:= 0;
   bf.Alph aFormat:= 0; //игнорировать исходный альфа-канал
   bf.SourceConstant Alph a:= $7f; //половина $ff = 50 % прозрачности
   if not Windows.AlphaBlend(hdcwnd, ulWindowWidth div 5,
   ulWindowHeight div 5,
   ulBitmapWidth, ulBitmapHeight,
   hCurDC, 0, 0, ulBitmapWidth, ulBitmapHeight, bf) then
   begin
   DeleteObject(hbmp);
   DeleteDC(hCurDC);
   Exit;
   end;

   По аналогии выполняются необходимые действия и со средней областью. В центре точечного рисунка прозрачность отсутствует, поэтому там будет только указанный цвет. В центре устанавливается красный цвет, а остальная часть закрашивается синим цветом. Далее опять задаются необходимые параметры альфа-смешивания и осуществляется его выполнение (листинг 6.12).
 //-- Листинг 6.12. Альфа-смешивание средней области --// 
   //в средней области постоянная альфа = 100 %, исходная равна 0
   for y:= 0 to ulBitmapHeight – 1 do
   for x:= 0 to ulBitmapWidth – 1 do
   if (x > Integer(ulBitmapWidth div 5)) and
   (x < (ulBitmapWidth – ulBitmapWidth div 5)) and
   (y > Integer(ulBitmapHeight div 5)) and
   (y < (ulBitmapHeight – ulBitmapHeight div 5)) then
   //в середине точечного рисунка альфа равна нулю
   //это означает, что каждый цветной компонент умножается на 0
   //таким образом, после альфа-смешивания мы получим 0 * r,
   //0x00 * g, 0x00 * b ($00000000)
   //установим сейчас цвет пикселов в красный
   PULONG(Integer(pvBits) +
   (x + y * ulBitmapWidth) * sizeof(ULONG))^ := $0000ff00
   else
   //остальную часть точечного рисунка сделаем синей
   PULONG(Integer(pvBits) +
   (x + y * ulBitmapWidth) * sizeof(ULONG))^ := $00ff0000;
   bf.BlendOp:= AC_SRC_OVER;
   bf.BlendFlags:= 0;
   bf.Alph aFormat:= AC_SRC_ALPHA; //используем исходную альфа
   bf.SourceConstant Alph a:= $ff; //непрозрачный
   if not Windows.AlphaBlend(hdcwnd, ulWindowWidth div 5,
   ulWindowHeight div 5 + ulWindowHeight, ulBitmapWidth,
   ulBitmapHeight,
   hCurDC, 0, 0, ulBitmapWidth, ulBitmapHeight, bf) then
   begin
   DeleteObject(hbmp);
   DeleteDC(hCurDC);
   Exit;
   end;

   В последней области происходит градиентное альфа-смешивание. Соответствующий код приведен в листинге 6.13.
 //-- Листинг 6.13. Альфа-смешивание нижней области --// 
   //нижняя область, используем альфа = 75 % и переменную исходную альфа
   //создаем градиентный эффект, используя исходную альфа
   ubRed:= $ff;
   ubGreen:= $00;
   ubBlue:= $00;
   for y:= 0 to ulBitmapHeight – 1 do
   for x:= 0 to ulBitmapWidth – 1 do
   begin
   ubAlpha:= Trunc(x / ulBitmapWidth * 255) and $FF;
   fAlphaFactor:= ubAlpha / $ff;
   r:= (Round(ubRed * fAlphaFactor) * (1 shl 16)) and $FF;
   g:= (Round(ubGreen * fAlphaFactor) * (1 shl 8)) and $FF;
   b:= Round(ubBlue * fAlphaFactor) and $FF;
   PULONG(Integer(pvBits) +
   (x + y * ulBitmapWidth) * sizeof(ULONG))^:=
   (ubAlpha shl 24) or //0xaa000000
   r or //0x00rr0000
   g or //0x0000gg00
   b; //0x000000bb
   end;
   bf.BlendOp:= AC_SRC_OVER;
   bf.BlendFlags:= 0;
   bf.AlphaFormat:= AC_SRC_ALPHA;
   bf.SourceConstantAlpha:= $bf;
   Windows.AlphaBlend(hdcwnd, ulWindowWidth div 5,
   ulWindowHeight div 5 + 2 * ulWindowHeight, ulBitmapWidth,
   ulBitmapHeight, hCurDC, 0, 0, ulBitmapWidth,
   ulBitmapHeight, bf);
   DeleteObject(hbmp);
   DeleteDC(hCurDC);

   Обработчик события OnPaint формы использует написанную функцию при каждой необходимости обновления формы. Для этого он получает контекст устройства формы, производит заливку фона темно-синим цветом, а затем вызывает функцию альфа-смешивания трех областей. Соответствующий исходный код приведен в листинге 6.14.
 //-- Листинг 6.14. Обработчик события OnPaint --// 
   procedure TfmAlphaBlending.FormPaint(Sender: TObject);
   var
   hCurDC: HDC;
   hCurBrush, hOldBrush: HBRUSH;
   begin
   hCurDC:= GetDC(Handle);
   hCurBrush:= CreateSolidBrush(RGB(128, 128, 255));
   FillRect(hCurDC, Rect(0, 0, Width, Height), hCurBrush);
   DrawAlphaBlend(Handle, hCurDC);
   DeleteObject(hCurBrush);
    ReleaseDC(Handle, hCurDC);
   end;

   Осталось только взглянуть на результат работы, запустив созданное приложение (рис. 6.4).
   Рис. 6.4. Результат работы приложения «Alpha-смешивание точечного рисунка»

   На этом рассмотрение работы с графикой в Delphi завершено.



   Глава 7
   Системная информация и реестр Windows


   • Системная информация
   • Системное время
   • Реестр

   Возникала ли у вас необходимость программно определять текущее состояние компьютера или узнавать какие-нибудь сведения об операционной системе? Можно только удивляться, как близко – практически «под носом» у программиста – находятся средства получения системной информации. Речь идет о средствах, которые всегда доступны при программировании для Windows – функции Windows API.
   В данной главе будут рассмотрены некоторые способы, с помощью которых можно получить информацию, касающуюся операционной системы. Это может пригодиться, например, если вы используете в своих приложениях возможности, отличающиеся в различных платформах Windows, но и не только в этих случаях.
   Рассмотренные в данной главе функции Windows API являются самыми обычными во всех смыслах этого слова. Просто они часто упоминаются вскользь либо вообще не упоминаются в книгах по программированию в таких средах, как Borland Delphi.
   В примерах представленной вашему вниманию главы, кроме получения информации о самой Windows, некотором оборудовании компьютера, также рассмотрена работа с системным реестром Windows – своеобразной базой данных, в которой хранится много полезной и не очень информации: от параметров операционной системы и настроек приложений до сведений о работе компьютера, изменяющихся в реальном времени. Правда, по определенным причинам последние сведения хранятся не в реальных, а в виртуальных ключах реестра. Однако обо всем по порядку.


   Системная информация


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


   Версия операционной системы

   Информация об операционной системе хотя и не является повседневной необходимостью, но все же в некоторых случаях может понадобиться: например, когда ваша программа ведет себя по-разному при разных установленных обновлениях Windows либо когда вы самостоятельно пишете инсталлятор, который способен устанавливать версии программы, скомпилированные для Windows Me (95, 98) или Windows NT (2000, XP).
   Одним из способов получения сведений о версии Windows является использование API-функции GetVersionEx. Эта функция принимает в качестве параметра структуру OSVERSIONINFO (или OSVERSIONINFOEX, но об этом позже), заполняет ее поля и в случае успешного выполнения возвращает ненулевое значение.
   Объявление ANSI-версии структуры OSVERSIONINFO в библиотеке Delphi 7 выглядит следующим образом:

   OSVERSIONINFOA = record
   dwOSVersionlnfoSize: DWORD; //Размер структуры
   dwMajorVersion: DWORD; //Старшая часть версии Windows
   dwMinorVersion: DWORD; //Младшая часть версии
   dwBuildNumber: DWORD; //Номер сборки операционной системы
   dwPlatformId: DWORD; //Идентификатор платформы Windows
   szCSDVersion: array[0..127] of AnsiChar; //Дополнительные сведения,
   //например, установленный пакет обновлений
   end;

   Не буду вдаваться в подробное описание возможных значений полей этой структуры – практически все будет ясно из приведенного ниже примера. Не забывайте только заполнять поле dwOSVersionlnfoSize перед вызовом функции GetVersionEx.
   Пример обработки данных, помещаемых в структуру OSVERSIONINFO, приведен в листинге 7.1. При загрузке формы элемент управления ListView с именем lvwVerlnfo заполняется сведениями о версии системы, представленными в форме, доступной для чтения.
 //-- Листинг 7.1. Получение и отображение сведений о Windows --// 
   procedure TForm1.FormCreate(Sender: TObject);
   var
   info: OSVERSIONINFO;
   item: TListItem;
   begin
   //Получаем информацию о версии операционной системы
   info.dwOSVersionInfoSizeee:= SizeOf(info);
   GetVersionEx(info);
   //Заполняем список информацией об операционной системе
   //..версия операционной системы
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Версия системы';
   item.SubItems.Insert (0, IntToStr (Integer (info.dwMajorVersion)) + '.'+
   IntToStr(Integer(info.dwMinorVersion)));
   //..номер сборки
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Сборка';
   item.SubItems.Insert (0, IntToStr(Integer(info.dwBuildNumber)));
   //..платформа
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Платформа ';
   case info.dwPlatformId of
   VER_PLATFORM_WIN32s:
   //Эмуляция Win32 или Win16
   item.SubItems.Insert(0, 'Win16');
   VER_PLATFORM_WIN32_WINDOWS:
   //"Классическая" Win32: 95, 98 или Me
   item.SubItems.Insert(0, 'Win32');
   VER_PLATFORM_WIN32_NT:
   //Ядро оNT
   item.SubItems.Insert(0, 'WinNT');
   end;
   //..дополнительная информация (на пример, пакет обновлений)
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Дополнительные сведения ';
   item.SubItems.Insert(0, info.szCSDVersion);
   end;

   Возможный результат выполнения программы (для Windows XP SP1) показан на рис. 7.1.
   Рис. 7.1. Информация о версии Windows

   Теперь снова обратимся к функции GetVersionEx, а точнее, к структуре OSVERSIONINFOEX, которая может также передаваться в качестве параметра в функцию. К сожалению, в библиотеке Delphi 7 эта структура не объявлена. Однако это можно сделать самостоятельно:

   OSVERSIONINFOEX = record
   dwOSVersionInfoSize: DWORD;
   dwMajorVersion: DWORD;
   dwMinorVersion: DWORD;
   dwBuildNumber: DWORD;
   dwPlatformId: DWORD;
   szCSDVersion: array[0..127] of AnsiChar;
   //Поля, которых нет в OSVERSIONINFO
   wServicePackMajor: WORD; //Старшая цифра версии ServicePack
   wServicePackMinor: WORD; //Младшая цифра версии ServicePack
   wSuiteMask: WORD; //Комплектация системы
   wProductType: BYTE; //Дополнительная информация об операционной системе
   wReserved: BYTE;
   end;

   Дополнительные (по сравнению с OSVERSIONINFO) поля структуры OSVERSIONINFOEX может заполнить операционная система Windows NT 4.0 SP6 и более поздние версии Windows NT (в том числе 2000 и XP). Значения дополнительных полей структуры OSVERSIONINFOEX пояснены комментариями в объявлении структуры.
   Значение поля wSuiteMask (является битовой маской) может быть составлено из значений следующих констант (их объявления также пришлось добавить самостоятельно).

   VER_SUITE_BACKOFFICE = 4; //Установлена Microsoft Back Office
   VER_SUITE_DATACENTER = 128;//Установлена Microsoft Data Center
   VER_SUITE_ENTERPRISE = 2; //Установлена операционная система Windows 2000 Advanced Server
   VER_SUITE_SMALLBUSINESS = 1; //Установлена Microsoft Small Business Server
   VER_SUITE_SMALLBUSINESS_RESTRICTED = 32; //Установлена ограниченная версия Microsoft Small Business Server
   VER_SUITE_TERMINAL = 16; //Установлены терминальные службы
   VER_SUITE_PERSONAL = 512; //Персональная версия операционной системы (типичный набор функций, меньше, чем в Professional)

   Поле wProductType может иметь одно из приведенных ниже значений (тип сетевой операционной системы и, соответственно, роль, которую компьютер с данной операционной системой может исполнять при подключении в сети):

   VER_NT_WORKSTATION = 1; //Рабочая станция
   VER_NT_DOMAIN_CONTROLLER = 2; //Контроллер домена
   VER_NT_SERVER = 3; //Сервер

   Чтобы можно было передавать в функцию GetVersionEx ссылку на структуру OSVERSIONINFOEX, а не OSVERSIONINFO, следует перегрузить эту функцию следующим образом:

   function GetVersionEx(var lpVersionlnformation: OSVERSIONINFOEX): BOOL;
   stdcall; external kernel32 name 'GetVersionExA';

   Теперь определение полной информации о версии операционной системы Windows
   на платформе NT (выше NT 4.0 SP6) может выглядеть следующим образом (листинг 7.2) (часть, совпадающая с кодом листинга 7.1, опущена).
 //-- Листинг 7.2. Определение версии операционной системы (NT, 2000, ХР) --// 
   procedure TForm1.FormCreate(Sender: TObject);
   var
   info: OSVERSIONINFOEX;
   item: TListItem;
   suite, additional: String;
   begin
   //Получаем информацию о версии операционной системы
   info.dwOSVersionInfoSizeee:= SizeOf(info);
   GetVersionEx(info);
   //Заполняем список информацией об операционной системе
   //...
   //..версия ServicePack
   item:= lvwVerInfo.Items.Add();
   item.Caption:= ' Версия ServicePack';
   item.SubItems.Insert(0, IntToStr(Integer(info.wServicePackMajor)) + '.'
   + IntToStr(Integer(info.wServicePackMinor)));
   //..комплектация операционной системы
   suite:= '';
   if info.wSuiteMask and VER_SUITE_BACKOFFICE <> 0 then
   suite:= suite + '[ Установлен Back Office] ';
   if info.wSuiteMask and VER_SUITE_DATACENTER <> 0 then
   suite:= suite + '[Microsoft Data Center] ';
   if info.wSuiteMask and VER_SUITE_ENTERPRISE <> 0 then
   suite:= suite + '[Windows 2000 Advanced Server] ';
   if info.wSuiteMask and VER_SUITE_SMALLBUSINESS <> 0 then
   suite:= suite + '[Small Business Server] ';
   if info.wSuiteMask and VER_SUITE_SMALLBUSINESS_RESTRICTED <> 0 then
   suite:= suite + '[Small Business Server, ограниченная версия ] ';
   if info.wSuiteMask and VER_SUITE_TERMINAL <> 0 then
   suite:= suite + '[Terminal Service] ';
   if info.wSuiteMask and VER_SUITE_PERSONAL <> 0 then
   suite:= suite + '[Workstati on Personal (не Professional)] ';
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Комплектация ';
   item.SubItems.Add(suite);
   //..дополнительные сведения
   additional:= '';
   case info.wProductType of
   VER_NT_WORKSTATION:
   additi onal:= '[ Рабочая станция] ';
   VER_NT_DOMAIN_CONTROLLER:
   additi onal:= '[ Контроллер домена] ';
   VER_NT_SERVER:
   additi onal:= '[ Сервер ] ';
   end;
   item:= lvwVerInfo.Items.Add();
   item.Caption:= 'Дополнительно ';
   item.SubItems.Add(additional);
   end;


   Имя компьютера

   Следующий простой пример (листинг 7.3) демонстрирует способ определения сетевого имени компьютера. Функция ComputerName скрывает «прелести» работы со строковым буфером, который нужно передавать в API-функцию GetComputerName.
 //-- Листинг 7.3. Определение сетевого имени компьютера --// 
   function ComputerName(): String;
   var
   buffer: String;
   len: Cardinal;
   begin
   len:= MAX_COMPUTERNAME_LENGTH + 1;
   SetLength(buffer, len);
   if GetComputerName(PAnsiChar(buffer), len) <> False then
   ComputerName:= Copy(buffer, 1, len)
   else
   ComputerName:= '';
   end;


   Имя пользователя

   Определить имя пользователя, от имени которого запущена программа (а точнее – вызывающий функцию поток), можно с использованием функции, код которой представлен в листинге 7.4.
 //-- Листинг 7.4. Определение имени пользователя --// 
   function UserName(): String;
   var
   buffer: String;
   len: Cardinal;
   begin
   len:= 100; //Увы, но константы UNLEN в модуле Windows найти
   //не удалось
   //Буфера такой длины должно хватить
   SetLength(buffer, len);
   if GetUserName(PAnsiChar(buffer), len)
   <> False then
   UserName:= Copy(buffer, 1, len)
   else
   UserNameee:= '';
   end;

   Чаще всего приведенная в листинге 7.4 функция определяет пользователя, выполнившего вход в систему, но если приложение запущено от имени другого пользователя (например, User при вошедшем пользователе Admin), то, соответственно, определяется имя пользователя User.


   Состояние системы питания компьютера

   Следующий пример может заинтересовать обладателей компьютеров с резервным источником питания (батарея в ноутбуке или источник бесперебойного питания).
   Для определения состояния системы питания компьютера используется API-функция GetSystemPowerStatus. Она заполняет структуру TSystemPowerStatus и в случае успеха возвращает ненулевое значение. Упомянутая структура имеет следующие поля:

   TSystemPowerStatus = packed record
   ACLineStatus: Byte; //Подключение к сети переменного тока
   BatteryFlag: Byte; //Состояние батареи (уровень заряда
   //и прочее)
   BatteryLifePercent: Byte; //Оставшийся ресурс батареи (в %)
   Reserved1: Byte;
   BatteryLifeTime: DWORD; //Оставшееся время (в сек.) работы батареи
   BatteryFullLifeTime: DWORD; //Полное время (в сек.) работы батареи
   end;

   Если значения полей BatteryLifePercent, BatteryLifeTime, BatteryFullLifeTime предельно ясны, то извлечение информации из полей ACLineStatus и BatteryFlag можно посмотреть в листинге 7.5.
 //-- Листинг 7.5. Определение состояния системы питания --// 
   procedure TForm1.LoadPowerStatus();
   var
   batFlags: String;
   status: TSystemPowerStatus;
   prof_info: THWProfileInfo;
   begin
   lvwPowerStatus.Clear;
   //Получаем информацию о состоянии питания
   ZeroMemory(Addr(status), SizeOf(status));
   GetSystemPowerStatus(status);
   //Заполняем список информацией о состоянии питания
   //..подключение к сети
   case status.ACLineStatus of
   0: AddParam('Подключение к сети', 'Отключен');
   1: AddParam('Подключение к сети', 'Подключен');
   else AddParam('Подключение к сети', 'Неизвестно');
   end;
   //..заряд батареи (битовая маска)
   if statu s.BatteryFlag and 1 <> 0 then batFlags:= 'Высокий';
   if statu s.BatteryFlag and 2 <> 0 then batFlags:= batFlags + ' Низкий';
   if status.BatteryFlag and 4 <> 0 then
   batFlags:= batFlags + 'Критический';
   if status.BatteryFlag and 8 <> 0 then
   batFlags:= batFlags + '(Идет тзарядка)';
   if status.BatteryFlag and 128 <> 0 then
   batFlags:= batFlags + 'Батарея не установлена ';
   if statu s.BatteryFlag = 255 then batFlags:= batFlags + ' Неизвестно ';
   AddParam('Заряд батареи', batFlags);
   //..численные характеристики батареи
   if status.BatteryLifePercent <> 255 then
   AddParam('Остаток заряда батареи ',
   IntToStr(Integer(status.BatteryLifePercent)))
   else
   AddParam(' Остаток заряда батареи', 'Неизвестно');
   if status.BatteryLifeTime <> Cardinal(–1) then
   AddParam('Время работы батареи (остаток, сек.)',
   IntToStr(Integer(status.BatteryLifeTime)))
   else
   AddParam(' Время работы батареи (остаток, сек .)', 'Неизвестно');
   if status.BatteryFullLifeTime <> Cardinal(–1) then
   AddParam('Полное время работы батареи, сек.',
   IntToStr(Integer(status.BatteryFullLifeTime)))
   else
   AddParam(' Полное время работы батареи, сек .', 'Неизвестно');
   end;

   В листинге 7.5 для отображения каждого параметра системы питания вызывается процедура AddParam, добавляющая в элемент управления формы название параметра и его значение. Этим элементом управления может быть, например, компонент ListView. Для рассмотренного примера возможный результат работы процедуры LoadPowerStatus показан на рис. 7.2.
   Рис. 7.2. Собранная информация о системе питания

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


   Состояние памяти компьютера

   Получение информации о текущем состоянии памяти компьютера также не представляет серьезной задачи. Недаром эту информацию многие приложения, тот же Блокнот, выводят в окне О программе: заполнить форму чем-то надо, а сведения об объеме памяти кажутся достаточно актуальными.
   Получить сведения о состоянии памяти компьютера помогает API-функция GlobalMemoryStatus. Эта функция принимает в качестве параметра структуру TMemoryStatus, заполняет ее поля значениями и в случае успешного выполнения задачи возвращает отличное от нуля число. Объявление структуры TMemoryStatus с комментариями ее полей приведено ниже:

   TMemoryStatus = record
   dwLength thth: DWORD; //Размер структуры (байт)
   dwMemoryLoad: DWORD; //Процент загрузки физической памяти
   dwTotalPhys: DWORD; //Полный объем физической памяти
   dwAvailPhys: DWORD; //Объем свободной оперативной памяти
   dwTotalPageFile: DWORD; //Полный объем файла подкачки
   dwAvailPageFile: DWORD; //Объем свободного пространства
   //в файле подкачки
   dwTotalVirtu al: DWORD; //Полный объем виртуальной памяти
   dwAvailVirtu al: DWORD; //Объем свободной виртуальной памяти
   end;

   Два последних поля структуры TMemoryStatus относятся к приложению, вызывающему функцию GlobalMemoryStatus. Их смысл рассмотрен чуть ниже. Пример использования функции GlobalMemoryStatus приведен в листинге 7.6.
 //-- Листинг 7.6. Определение состояния памяти --// 
   procedure TForm1.LoadMemoryInfo();
   var
   memStat: TMemoryStatus;
   begin
   memStat.dwLength:= SizeOf(memStat);
   //Получение информации о загрузке памяти
   GlobalMemoryStatus(memStat);
   //Заполнение полей формы
   //..% использования памяти
   pbMemUsage.Position:= memStat.dwMemoryLoad;
   lblMemUsage.Caption:= IntToStr(memStat.dwMemoryLoad) + '%';
   //..использование оперативной памяти
   txtMemTotal.Text:= IntToStr(memStat.dwTotalPhys div 1024);
   txtMemAvail.Text:= InttoStr(memStat.dwAvailPhys div 1024);
   //..использование файла подкачки
   txtPageTotal.Text:= IntToStr(memStat.dwTotalPageFile div 1024);
   txtPageAvail.Text:= InttoStr(memStat.dwAvailPageFile div 1024);
   //..использование виртуальной памяти
   txtVirtualTotal.Text:= IntToStr(memStat.dwTotalVirtual div 1024);
   txtVirtualAvail.Text:= InttoStr(memStat.dwAvailVirtual div 1024);
   end;

   Внешний вид формы, элементы управления которой заполняются значениями в листинге 7.6, показан на рис. 7.3.
   Рис. 7.3. Программа для определения состояния памяти компьютера

   Напоследок рассмотрим (несколько упрощенно) результаты, выводимые в текстовых полях формы, для тех, кто немного не в курсе, как организовано управление памятью в Windows.
   Каждому процессу Windows предоставляет адресное пространство (виртуальное) размером немного меньше 2 Гбайт. В отличие от 16-битных предшественниц, в 32-битных Windows адресные пространства различных процессов являются закрытыми: приложение использует память (а точнее, младшие 2 Гбайт адресного пространства) единолично и не может без дополнительных прав манипулировать данными других процессов. Значения в двух последних полях структуры TMemoryStatus (и нижней группе текстовых полей на форме рис. 7.3) как раз и отражают использование приложением представляемого ему адресного пространства.
   Механизм предоставления виртуальной памяти является довольно удобной надстройкой, сглаживающей ограниченность аппаратных ресурсов компьютера. Ограниченный объем оперативной памяти компенсируется использованием места на диске (файла подкачки, страничного файла). В этот файл записываются для временного хранения неиспользуемые страницы памяти (блоки данных по несколько Кбайт), давая возможность помещать другие данные, более нужные приложению, в оперативную память.
   Теперь вернемся к форме, показанной на рис. 7.3. Группа текстовых полей Оперативная память показывает полный и свободный объем реально имеющейся на компьютере оперативной памяти (за вычетом памяти, используемой для системных нужд). Использование этого вида памяти иллюстрирует индикатор ProgressBar на форме. Назначение правой группы текстовых полей (Файл подкачки) должно быть также понятно.
   Из цифр, выведенных в текстовые поля на форме (рис. 7.3), можно также определить, что общий объем памяти, доступной приложениям (всего было запущено 30 процессов), на испытуемом компьютере составлял около 1,26 Гбайт. Если представить, что память распределялась между всеми процессами одинаково, то получается примерно 43 Мбайт на каждого, не считая памяти, резервируемой для самой Windows.



   Системное время


   Этот раздел посвящен отнюдь не простому получению текущего времени или даты (благо эти функции можно найти и в библиотеке Borland). Здесь будет рассмотрена несколько более интересная тема – использование системных средств измерения малых промежутков времени.
   Все рассмотренные далее способы измерения времени основаны на подсчете количества срабатываний таймера. Для сохранения показаний таймера система поддерживает соответствующие счетчики. Для определения временного интервала регистрируются показания счетчика в начале и конце измеряемого промежутка времени. Затем вычисляется разность между этими показаниями и, если период таймера не соответствует требуемой единице измерения (например, мс), полученная разность делится на частоту таймера.


   Определение времени работы операционной системы

   В момент запуска Windows запускается и специальный счетчик, отсчитывающий количество миллисекунд, прошедших с момента запуска системы.
   Этот системный счетчик можно использовать как для определения времени работы системы, так и для измерения временных интервалов. Для доступа к нему можно использовать API-функцию GetTickCount. Эта функция не имеет параметров и возвращает целочисленное 32-битное значение.
   Приведенная в листинге 7.7 функция GetSystemWorkTime демонстрирует использование описываемого счетчика для определения времени работы системы в часах, минутах и секундах.
 //-- Листинг 7.7. Определение времени работы системы --// 
   function GetSystemWorkTime(): String;
   var
   ticks: DWORD;
   hh, mm, ss: Cardinal;
   begin
   //Получаем количество миллисекунд с момента старта системы
   ticks:= GetTickCount();
   //Переводим в секунды
   ticks:= ticks div 1000;
   //Получаем количество часов, минут, секунд
   hh:= ticks div 3600;
   Dec(ticks, hh * 3600);
   mm:= ticks div 60;
   Dec(ticks, mm * 60);
   ss:= ticks;
   GetSystemWorkTime:= IntToStr(hh) + ':'+
   IntToStr(mm) + ':'+ IntToStr(ss);
   end;

   Из-за относительно малой разрядности значение счетчика обнуляется приблизительно каждые 49,7 суток, что следует учитывать, если планируется измерять длительные интервалы или если измерение времени начинается после длительной работы системы (например, начало измерения выпадает на 50-е сутки за час до обнуления счетчика).


   Аппаратный таймер

   Следующий рассматриваемый способ измерения времени основан на использовании таймера высокого разрешения (высокочастотного). Временной промежуток между срабатываниями этого таймера может быть намного меньше 1 мс, что позволяет производить достаточно точные измерения. Для сохранения количества срабатываний аппаратного таймера используется 64-битный счетчик.
   Пример получения значения счетчика аппаратного таймера приведен в листинге 7.8. Частота, возвращаемая функцией hwTimerGetCounter, измеряется в герцах (с -------
| Библиотека iknigi.net
|-------
|  
 -------


), то есть означает количество срабатываний таймера в 1 с.
 //-- Листинг 7.8. Получение значения счетчика аппаратного таймера --// 
   function hwTimerGetCounter(): Int64;
   var
   freq: Int64;
   begin
   if QueryPerformanceCounter(freq) <> False then
   hwTimerGetCounter:= freq
   else
   hwTimerGetCount er:= 0; //Ошибка
   end;

   Чтобы перевести количество срабатываний аппаратного таймера в привычные единицы измерения, нужно определить его частоту. Помочь в этом может функция, приведенная в листинге 7.9.
 //-- Листинг 7.9. Определение частоты аппаратного таймера --// 
   function hwTimerGetFreq(): Int64;
   var
   freq: Int64;
   begin
   if QueryPerformanceFrequency(freq) <> False then
   hwTimerGetFreq:= freq
   else
   hwTimerGetFreq:= 0; //Ошибка
   end;

   Пусть известна разность между значениями счетчика в начале и конце измерения. Перевести ее в секунды можно следующим образом:

   time:= counter div hwTimerGetFreq();

   Пример, точнее, результат определения характеристик аппаратного таймера показан на рис. 7.4.
   Рис. 7.4. Характеристики аппаратного таймера

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


   Мультимедиа-таймер

   Рассмотрим еще один способ измерения, основанный на использовании так называемого мультимедиа-таймера. Его использование удобно тем, что появляется возможность указывать точность измерения. Группа API-функций, предназначенная для работы с мультимедиа-таймером, позволяет не только измерять временные интервалы, но и создавать программные таймеры (см. компонент Timer), срабатывающие через гораздо меньшие промежутки времени.
   Для получения текущего значения счетчика мультимедийного таймера можно воспользоваться функцией timeGetTime. В бщем случае она возвращает значения, аналогичные значениям, возвращаемым функцией GetTickCount. Счетчик является также 32-битным, обнуляемым приблизительно каждые 49,7 суток. Прототип функции timeGetTime следующий:

   function timeGetTime: DWORD; stdcall;

   Пример использования этой функции приведен в листинге 7.12.
   Теперь несколько слов о том, как получить для рассматриваемого таймера значения минимальной и максимальной точности. Для получения этих данных можно использовать функцию timeGetDevCaps. Эта функция принимает в качестве параметра структуру TTimeCaps и заполняет два ее поля соответствующими значениями. В листинге 7.10 приведен пример возможной реализации функций определения характеристик мультимедийного таймера.
 //-- Листинг 7.10. Определение характеристик мультимедиа-таймера --// 
   //Получение максимального периода таймера (мс)
   function timeGetMaxPeriod(): Cardinal;
   var
   time: TTimeCaps;
   begin
   timeGetDevCaps(Addr(time), SizeOf(time));
   timeGetMaxPeriod:= time.wPeriodMax;
   end;
   //Получение минимального периода таймера (мс)
   function timeGetMinPeriod(): DWORD;
   var
   time: TTimeCaps;
   begin
   timeGetDevCaps(Addr(time), SizeOf(time));
   timeGetMinPeriod:= time.wPeriodMin;
   end;

   Итак, теперь вам известно, как получать параметры таймера, но выше было сказано, что его точность может быть изменена, то есть регулируется. Сделать это можно с помощью функций timeBeginPeriod и timeEndPeriod.
   • Первая функция (timeBeginPeriod) вызывается для установления минимальной устраивающей приложение точности таймера. Функция timeBeginPeriod принимает значение требуемой точности таймера в миллисекундах и возвращает TIMERR_NOERROR в случае успеха либо TIMERR_NOCANDO, если требуемая точность не может быть обеспечена.
   • Вторая функция (timeEndPeriod) восстанавливает точность таймера, установленную до вызова функции timeBeginPeriod. В функцию timeEndPeriod должно передаваться то же значение, что и в функцию timeBeginPeriod.
   В листинге 7.11 продемонстрирован пример использования функций timeBeginPeriod и timeEndPeriod (реализованы функции-оболочки). При использовании обеих функций, представленных в листинге 7.11, нужно помнить, что после вызова функции timeSetTimerPeriod и проведения измерения обязательно должна быть вызвана функция timeRestoreTimerPeriod. Функция timeSetTimerPeriod сохраняет значение установленной точности таймера в глобальной переменной lastPeriod, чтобы можно было не заботиться о сохранении этого значения в коде, использующем таймер.
 //-- Листинг 7.11. Функции изменения точности таймера --// 
   Var lastPeriod: Cardinal;
   //Установка периода таймера (мс) перед началом измерения
   function timeSetTimerPeriod(period: Cardinal): Boolean;
   begin
   if timeBeginPeriod(period) = TIMERR_NOERROR then
   begin
   //Сохраним значение для восстановления состояния таймера
   lastPeriod:= period;
   timeSetTimerPeriod:= True;
   end
   else
   //Неудача
   timeSetTimerPeriod:= False;
   end;
   //Восстановление периода таймера (обязательно)
   function timeRestoreTimerPeriod(): Boolean;
   begin
   if timeEndPeriod(lastPeriod) = TIMERR_NOERROR then
   timeRestoreTimerPeriod:= True
   else
   timeRestoreTimerPeriod:= False;
   end;

   Теперь, после долгого рассмотрения особенностей настройки мультимедиа-таймера, пора привести пример его использования для измерения времени выполнения простейшего отрезка программы (листинг 7.12).
 //-- Листинг 7.12. Измерение времени выполнения отрезка программы --// 
   procedure TForm1.cmbTimeGoClick(Sender: TObject);
   var
   summ, arg, maxVal: Int64;
   startTime, endTime: Cardinal;
   begin
   txt TimeResult.Text:= 'Измерение...';
   Refresh;
   maxVal:= StrToInt(txtTimeMaxVal.Text);
   //Устанавливаем маскимальную точность таймера
   timeSetTimerPeriod(timeGetMinPeriod());
   startTime:= ti meGetTime(); //Начальный момент времени
   //Суммируем 64-битные числа
   //(как раз и измеряем время его выполнения)
   summ:= 0;
   arg:= 1;
   while (arg <= maxVal) do
   begin
   Inc(summ, arg);
   Inc(arg);
   end;
   endTime:= ti meGetTime(); //Конечный момент времени
   //Восстанавливаем период таймера
   timeRestoreTimerPeriod();
   //Время выполнения операций (мс)
   txtTimeResult.Text:= IntToStr(endTime – startTime);
   end;


   Создание программного таймера высокой точности

   В самом начале рассмотрения возможностей мультимедиа-таймера было сказано, что в его API заложена возможность создания программных таймеров. Это действительно так. Причем максимальная точность такого таймера может получиться достаточно высокой: на современных компьютерах создание программного таймера с периодом срабатывания 1 мс – не проблема. Правда, использовать максимальную частоту таймера вряд ли стоит: слишком велика вероятность ошибки, по меньшей мере, на 1 мс.
   Стоит уяснить, что же за программный таймер должен быть создан и чем он отличается от компонента Timer, помещаемого на форму. Отличается создаваемый таймер, помимо более высокой точности, тем, что его не нужно привязывать к окну (форме): при срабатывании стандартного компонента Timer окну, за которым он закреплен, посылается сообщение WM_TIMER. Программный же таймер работает по-другому, что удобнее рассмотреть на примере.

   timerID:= timeSetEvent
   (
   StrToInt(txt TimeInt erval.Text), //Интервал между срабатываниями таймера
   ti meGetMin Period(), //Точность таймера
   TimerProc, //Адрес процедуры, вызываемой при каждом срабатывании таймера
   0, //Параметр, передаваемый в процедуру обратного вызова
   TIME_CALLBACK_FUNCTION or TIME_PERIODIC //Тип таймера
   );

   В приведенном выше отрывке программы с помощью функции timeSetEvent происходит регистрация и запоминание адреса процедуры TimerProc, вызываемой периодически при срабатываниях таймера. При успешном создании таймера функция timeSetEvent возвращает ненулевое значение – идентификатор созданного таймера. Это значение может использоваться в дальнейшем для определения сработавшего таймера. Значение, возвращенное функцией timeSetEvent, также необходимо при удалении таймера:

   timeKillEvent(timerID);

   Функция timeKillEvent возвращает целочисленное значение:
   • TIMERR_NOERROR – если вызов завершился успешно;
   • MM SYSE RR_ INVAL PARAM – если таймера, заданного параметром функции, не существует.
   Теперь о процедуре, адрес которой передается в функцию timeSetEvent. В данном примере она выглядит следующим образом (листинг 7.13).
 //-- Листинг 7.13. Процедура, вызываемая при срабатывании таймера --// 
   procedure TimerProc(uTimerID, uMessage: UINT; dwUser, dw1, dw2: DWORD) stdcall;
   begin
   //Добавляем текущее значение времени в список (чтобы была
   //видна разница между моментами вызова этой процедуры)
   Form1.lstTimes.Items.Add(IntToStr(timeGetTime()));
   end;

   Естественно, действия, выполняемые процедурой TimerProc, могут быть самыми различными. В рассматриваемом случае происходит заполнение списка (List) значениями счетчика срабатываний таймера на момент вызова процедуры (рис. 7.5).
   Рис. 7.5. Результат работы таймера

   В завершение вновь обратимся к функции timeSetEvent. Чуть ниже будут кратко перечислены предоставляемые ею возможности, которые не использовались в приведенном выше примере.
   Как вы могли заметить, последний параметр функции timeSetEvent является битовой маской. Флаги этой маски определяют два аспекта поведения таймера: количество срабатываний таймера и тип действия, которое требуется выполнять при срабатывании таймера.
   Количество срабатываний таймера определяется двумя значениями.
   • TIME_ONESHOT – таймер срабатывает один раз. Для таких таймеров вызывать функцию timeKillEvent после срабатывания не нужно.
   • TIME_PERIODIC – таймер срабатывает периодически через заданные промежутки времени.
   Тип действия, выполняемого таймером, задается с помощью следующих констант:
   • TIME_CALLBACK_FUNCTION – при срабатывании таймера вызывается процедура, адрес которой был передан третьим параметром;
   • TIME_CALLBACK_EVENT_SET – вызывает функцию SetEvent для объекта синхронизации «событие», дескриптор которого передан третьим параметром;
   • TIME_CALLBACK_EVENT_PULSE – вызывает функцию PulseEvent для объекта синхронизации «событие», дескриптор которого передан третьим параметром.
   К сожалению, использование объектов синхронизации хоть и является темой для интересного разговора, но все же выходит далеко за рамки этой главы. Потому эта тема больше рассматриваться не будет.



   Реестр


   Далее будет рассмотрено несколько примеров использования в программах на Delphi одного из важнейших хранилищ информации Windows – системного реестра.


   Краткие сведения о реестре Windows

   Что же представляет собой системный реестр и для чего он предназначен? Реестр состоит из нескольких файлов с довольно сложной организацией записей, формирующих иерархическую структуру (родитель-потомки), а точнее, несколько веток структуры. Благодаря наличию специальных функций пользователь может работать с реестром именно как с иерархической структурой, а не как с набором записей в файле.
   Реестр Windows является отличным примером организации централизованного хранения данных, в основном, настроек программ, и хорошей альтернативой большим INI-файлам, доставшимся в наследство от 16-разрядных версий Windows, главным образом, из-за обеспечения возможности лучше структурировать информацию (ведь секции разделов в реестре могут быть много раз вложенными). В реестре хранятся и данные, которые могут пригодиться сразу многим программам: например, расположения COM-серверов, пути приложений, ассоциированных с различными типами файлов.
   В реестре могут содержаться объекты двух типов: разделы (во многом аналогичны папкам файловой системы) и параметры (имеют имя, тип и значение).
   Данные реестра сгруппированы в несколько ветвей (рис. 7.6). Для запуска показанной на рис. 7.6 программы Редактор реестра достаточно набрать в командной строке Regedit либо найти файл Regedit.exe в папке Windows.
   Рис. 7.6. Корневые разделы реестра

   Информация, помещаемая в различных разделах реестра, группируется по следующим признакам.
   • HKEY_CURRENT_USER – хранит информацию, используемую для текущего пользователя, выполнившего вход в систему. Этой информацией могут быть, например, значения переменных окружения, фон Рабочего стола, вид меню "Пуск".
   • HKEY_USERS – содержит настройки системы для различных пользователей, а также настройки, используемые по умолчанию для нового пользователя.
   • HKEY_LOCAL_MACHINE – самая большая и главная ветвь реестра, содержащая параметры Windows, приложений, оборудования, ассоциации расширений файлов, расположение COM-серверов и много другой полезной информации.
   • HKEY_CURRENT_CONFIG – в этом разделе хранятся значения параметров Windows, отличающиеся от стандартных. Он является псевдонимом для ветви HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current.
   • HKEY CLASSES ROOT – в системах Windows 95/98/NT 4.0 и более ранних этот раздел является псевдонимом для ветви HKEY LOCAL MACHINE\SOFTWARE\Classes. В Windows 2000/XP содержимое этого раздела составляется из содержимого разделов HKEY_LOCAL_MACHINE\SOFTWARE\Classes и HKEY_ CURRENT_USER\Software\Classes.
   Доступ к разделам реестра происходит по дескрипторам. Дескриптор раздела можно получить при создании или открытии раздела, указав дескриптор одной из рассмотренных выше корневых ветвей, а также путь требуемого раздела. Для хранения дескрипторов корневых ветвей реестра определены одноименные константы.


   Средства работы с реестром

   Для работы с реестром предусмотрена целая группа API-функций. Однако зачем изобретать велосипед, испытывая на себе «удобство» работы с этими функциями, ведь Borland предоставляет замечательный по своей простоте класс TRegistry? Использованию этого класса как раз и посвящено несколько следующих далее абзацев.
   Класс TRegistry находится в модуле Registry. Если вам все же станет интересно использование API для работы с реестром, то можете заглянуть в этот модуль и там посмотреть, как реализованы методы класса TRegistry.

   Примечание
   Помимо TRegistry, в модуле Registry можно найти такие классы, как TRegIniFile и TRegistrylniFile, позволяющие работать с реестром, будто это INI-файл. В ряде случаев использование этих классов вместо TRegistry позволяет сокращать размер программы, да и значительно ее упрощать.

   В табл. 7.1 приведены свойства класса TRegistry.
 //-- Таблица 7.1. Свойства класса TRegistry --// 
   Константы, которые могут объединяться операцией Or для формирования значения свойства Access, следующие:
   • KEY_QUERY_VALUE – получение значений параметров раздела;
   • KEY_ENUMERATE_SUB_KEYS – составление списка подразделов;
   • KEY_SET_VALUE – задание значений, создание параметров в разделе;
   • KEY_CREATE_SUB_KEY – создание подразделов;
   • KEY_CREATE_LINK – создание символических ссылок (здесь не рассматривается);
   • KEY_NOTIFY – обеспечивает право на уведомление об изменении раздела и его подразделов (здесь не рассматривается);
   • KEY_READ – сочетание значений KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_ KEYS и KEY_NOTIFY;
   • KEY_WRITE – сочетание значений KEY_SET_VALUE и KEY_CREATE_SUB_KEY;
   • KEY_ALL_ACCESS – сочетание значений KEY_READ, KEY_WRITE и KEY_CREATE_LINK.
   Приводить список всех методов класса TRegistry в книге нерационально, да и незачем. Благо, названия методов говорят сами за себя (к тому же Delphi поставляется с неплохой справочной системой). Здесь же остановим внимание на рассмотрении некоторых особенностей работы с методами класса TRegistry.
   Работая с разделами реестра, важно (в общем случае) соблюдать следующую последовательность.
   1. Установить значение свойства RootKey, если корневой раздел отличен от HKEY_CURENT_USER. Установить значение свойства Access, если нужно ограничить доступ.
   2. Открыть методом OperKey или создать методом CreateKey раздел реестра. Если использовать OperKeyReadOnly, то задавать значение свойства Access, о котором сказано в пункте 1, не имеет смысла.
   3. Произвести нужные операции с элементами раздела.
   4. Закрыть раздел, по крайней мере, если вы собираетесь использовать один и тот же объект TRegistry для последовательной работы с несколькими разделами (метод OpenKey не закрывает ранее открытый раздел).
   Теперь несколько слов о проверке работы методов класса TRegistry. Большинство методов этого класса, организующих доступ к разделам реестра, реализованы как функции, возвращающие True в случае успеха и False при возникновении ошибки. При этом особо выделяется метод CreateKey, который в случае неудачи генерирует исключение ERegistryException.
   Для чтения и записи параметров разного типа в классе TRegistry предусмотрены пары Read– и Write-методов. Использовать их крайне просто, в чем вы убедитесь чуть позже. Главное при использовании этих методов – не забывать определять тип значений параметров, если он заранее вам точно не известен, например, с помощью функции GetDataType. Следует также помнить, что методы работы с параметрами генерируют исключение ERegistryException при возникновении ошибок.
   Напоследок о параметре (По умолчанию) – он может присутствовать в каждом разделе. Для обращения к этому параметру используется пустая строка в качестве имени раздела. Нужно только учитывать, что в отличие от более ранних версий Windows в Windows 2000/XP этот параметр автоматически не создается.


   Хранение настроек программы в реестре

   Первый простой пример демонстрирует способ использования реестра для сохранения небольшого объема данных между запусками приложения.
   Пусть нужно, чтобы формы приложения запоминали свое расположение, размер, введенные и выбранные в элементах управления данные. В таком случае необходимость в сотый раз перетаскивать часто открываемую форму на удобное место не будет раздражать пользователя. Если же форма требует постоянного ввода похожих данных, то восстановление выбранных и введенных в прошлый раз значений будет только достоинством.
   Теперь о деле: пусть есть форма для фильтрации запроса к базе данных, показанная на рис. 7.7.
   Рис. 7.7. Форма фильтра для поиска товара

   Содержимое формы не имеет особого значения – важно то, что при нажатии кнопки OK положение, размер формы, а также данные, введенные пользователем, будут сохранены в реестре с помощью процедуры SaveFilter (листинг 7.14).
 //-- Листинг 7.14. Сохранение параметров формы в реестре --// 
   procedure TForm1.OKClick(Sender: TObject);
   begin
   SaveFilter();
   //Выполняем требуемые действия...
   end;
   //Процедура сохраняет параметры в реестр
   procedure TForm1.SaveFilter();
   var
   reg: TRegistry; //По умолчанию: RootKey = HKEY_CURRENT_USER
   strShops: String;
   i: Integer;
   begin
   reg:= TRegistry.Create();
   try
   //Открываем или создаем раздел, в котором будут
   //сохранены параметры формы
   reg.OpenKey(strBaseKey + '\Form1', True);
   //Сохранение параметров
   //1. Размер и положение формы
   reg.WriteInteger('Width', Width);
   reg.WriteInteger('Height', Height);
   reg.WriteInteger('Top', Top);
   reg.WriteInteger('Left', Left);
   //2. Последнее введенное наименование
   reg.WriteString('txtName.Text', txtName.Text);
   //3. Выбранные магазины
   strShops:= '';
   for i:= 0 to lstShops.Count–1 do
   if lstShops.Selected[i] then
   strShops:= strShops + lstShops.Items[i] + ',';
   reg.WriteString('lstShops.Selection', strShops);
   //4. Применение сортировки
   reg.WriteBool('chkSort.Checked', chkSort.Checked);
   except
   on ERegistryException do
   MessageBox(Handle, 'Ошибка при сохранении фильтра ',
   'Поиск товара', MB_ICONEXCLAMATION);
   end;
   reg.CloseKey();
   reg.Free();
   end;

   В рассматриваемом примере константа strBaseKey, определяющая положение раздела для сохранения настроек, задана следующим образом:

   const strBasekey = 'Software\Delphi. Трюки и эффекты\Настройки программы';

   Открыв Редактор реестра, можно удостовериться в правильном сохранении параметров (рис. 7.8).
   Рис. 7.8. Параметры формы, записанные в реестр

   Считывание параметров формы можно выполнять, например, при ее создании. Тогда в обработчик события Create достаточно поместить вызов процедуры LoadFilter (листинг 7.15).
 //-- Листинг 7.15. Загрузка параметров формы из реестра --// 
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   LoadFilter();
   end;
   //Процедура загружает параметры из реестра
   procedure TForm1.LoadFilter();
   var
   reg: TRegistry; //По умолчанию: RootKey = HKEY_CURRENT_USER
   strShops: String;
   shopStart, shopEnd: Integer;
   begin
   reg:= TRegistry.Create();
   try
   //Открываем раздел, в котором сохранены параметры формы
   reg.OpenKey(strBaseKey + '\Form1', False);
   //Загрузка сохраненных ранее параметров
   //1. Размер и положение формы
   Width:= reg.ReadInteger('Width');
   Height:= reg.ReadInteger('Height');
   Top:= reg.ReadInteger('Top');
   Left:= reg.ReadInteger('Left');
   //2. Последнее введенное наименование
   txtName.Text:= reg.ReadString('txtName.Text');
   //3. Выбранные в прошлый раз магазины
   strShops:= reg.ReadString('lstShops.Selection');
   shopStart:= 1;
   for shopEnd:= 0 to Length(strShops) do
   if strShops[shopEnd] = ',' then
   begin
   //Получение имени магазина и выделение его в списке
   SelectShop(Copy(strShops, shopStart, shopEnd – shopStart));
   shopStart:= shopEnd + 1;
   end;
   //4. Применение сортировки
   chkSort.Checked:= reg.ReadBool('chkSort.Checked');
   except
   on ERegistryException do
   //Игнорируем ошибки (просто не
   //будут зачитаны данные из реестра,
   //например, при первом запуске программы)
   ;
   end;
   reg.CloseKey();
   reg.Free();
   end;
   //Процедура выделяет магазин с заданным названием (если он есть в списке)
   procedure TForm1.SelectShop(strShopName: String);
   var
   i: Integer;
   begin
   for i:= 0 to lstShops.Count–1 do
     if lstShops.Items[i] = strShopName then
   begin
   lstShops.Selected[i]:= True;
   Exit;
   end;
   end;

   Некоторая сложность алгоритма загрузки списка выбранных магазинов обусловлена желанием добиться того, чтобы при изменении списка не выделялись ранее не выбранные магазины (иначе можно было бы просто сохранять индексы).

   Примечание
   Чтобы при первом запуске процедуры LoadFilter не появлялись сообщения об исключениях (при работе в отладчике Delphi), снимите флажок Stop on Delphi Exceptions на вкладке Language Exceptions окна Debugger Options (меню Tools ► Debugger Options).



   Автозапуск программ

   Так уж повелось, что, рассматривая работу с реестром, редко удается удержаться от рассказа об организации автоматического запуска приложений, минуя пресловутое меню Автозагрузка. В данной книге эта тема также будет затронута: здесь будут рассмотрены наиболее простые способы автоматического запуска не сервисных (!) программ.
   В ветвях реестра HKEY_CURRENT_USER и HKEY_LOCAL_MACHINE находятся разделы Software\Microsoft\Windows\CurrentVersion\Run иSoftware\Microsoft\Windows\CurrentVersion\RunOnce. В первом (Run) сохраняются пути приложений, запускаемых при каждой загрузке Windows. В разделе же RunOnce обычно регистрируются приложения типа инсталляторов, которые запускаются при первой с момента регистрации перезагрузке Windows, но до запуска программы Проводник. При запуске приложения, зарегистрированного в ключе RunOnce, соответствующая запись из этого раздела автоматически удаляется.
   От выбора ветви реестра (HKEY_LOCAL_MACHINE или HKEY_CURRENT_USER) зависит, в сеансе всех пользователей будет запускаться приложение или только определенного.
   Рассмотрим создание простейшей программы, способной определить, запускается ли она автоматически, а если запускается, то каким образом. Программа также будет способна создавать и удалять параметры в нужных разделах реестра для задания нужного режима запуска.
   Пусть на форме приложения расположен переключатель с тремя положениями (рис. 7.9). Процедура, код которой приведен в листинге 7.16, устанавливает состояние переключателя в зависимости от того, в каком разделе ветви HKEY_LOCAL_MACHINE расположен параметр с именем, совпадающим с именем программы (это условность, которая нужна для работы данного примера).
   Рис. 7.9. Форма после определения варианта режима запуска приложения

 //-- Листинг 7.16. Определение режима запуска приложения --// 
   procedure TForm1.GetRunMode();
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_LOCAL_MACHINE;
   //Определение способа запуска программы (по наличию значений в
   //соответствующих разделах)
   if reg.OpenKey('Software\Microsoft\Windows\Current Version\Run', False)
   then
   begin
   if reg.ValueExists(Application.Title) then
   begin
   //Программа есть в разделе Run —
   //запускается при каждой загрузке Windows
   optAutoRun.Checked:= True;
   reg.CloseKey();
   Exit;
   end;
   reg.CloseKey();
   end;
   if reg.OpenK nKey('('Software\Microsoft\Windows\Current Version\Run Once', False)
   then
   begin
   if reg.ValueExists(Application.Title) then
   begin
   //Программа есть в разделе Run Once —
   //запускается один раз при старте Windows
   optRunOnce.Checked:= True;
   reg.CloseKey();
   Exit;
   end;
   reg.CloseKey();
   end;
   //Автозапуск программы (рассматриваемым способом) не включен
   optRunNone.Checked:= True;
   reg.Free();
   end;

   Параметры запуска изменяются (в рассматриваемом приложении) при нажатии кнопки Применить (листинг 7.17).
 //-- Листинг 7.17. Применение режима запуска --// 
   procedure TForm1.cmbApplyClick(Sender: TObject);
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_LOCAL_MACHINE;
   //Отмена прошлого режима
   //..удаление параметра из раздела Run
   if not optAutoRun.Checked then
   if reg.OpenKey('Software\Microsoft\Windows\Current Version\Run', False)
   then
   begin
   reg.DeleteValue(Application.Title);
   reg.CloseKey();
   end;
   //..удаление параметра из раздела Run Once
   if not optRunOnce.Checked then
   if reg.OpenKey('Software\Microsoft\Windows\Current Version\RunOnce',False)
   then
   begin
   reg.DeleteValue(Application.Title);
   reg.CloseKey();
   end;
   //Установка нового режима (создание параметра в соответствующем разделе)
   if optAutoRun.Checked then
   //..добавление параметра в раздел Run
   if reg.OpenKey('Software\Microsoft\Windows\Current Version\Run', True)
   then
   begin
   reg.WriteString(Application.Title, Application.ExeName);
   reg.CloseKey();
   end;
   if optRunOnce.Checked then
   //..добавление параметра в раздел RunOnce
   if reg.OpenKey('Software\Microsoft\Windows\Current Version\RunOnce',True)
   then
   begin
   reg.WriteString(Application.Title, Application.ExeName);
   reg.CloseKey();
   end;
   //Для верности обновим показания на форме по данным из реестра
   GetRunMode();
   reg.Free();
   end;

   При желании вы можете изменить ветвь реестра на HKEY_CURRENT_USER, если нужно, чтобы приложение (которое вы будете создавать) запускалось только для определенных пользователей.


   Запуск приложения из командной строки

   Сразу оговорюсь, что из командной строки (например, из окна Запуск программы, открываемого командой Пуск ► Выполнить) можно запустить любое приложение: достаточно только ввести его полный или относительный (относительно рабочей папки) путь. Однако, возможно, вы замечали, что некоторые приложения можно запускать, просто указав в командной строке имя приложения, например, calc (Кулькулятор), msaccess (Microsoft Access) или winword (Microsoft Word). Далее как раз будет рассмотрен способ обеспечения возможности запуска приложения таким ускоренным способом.
   Чтобы зарегистрировать приложение для быстрого запуска, можно поместить его путь в ветвь реестра SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths корневого раздела HKEY_CURRENT_USER или HKEY_LOCAL_MACHINE. Путь EXE-файла приложения должен быть записан в параметр (По умолчанию) подраздела, имеющего такое же имя, что и EXE-файл приложения (включая расширение).
   Пример процедуры, регистрирующей приложение для быстрого запуска, приведен в листинге 7.18.
 //-- Листинг 7.18. Регистрация приложения для запуска из командной строки --// 
   procedure RegisterQuickStart();
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_LOCAL_MACHINE;
   //Регистрируем программу для запуска по имени из командной строки
   if reg.OpenKey(paths + '\'+ Application.Title + '.exe', True) then
   begin
   reg.WriteString('', Application.ExeName);
   reg.CloseKey();
   end;
   reg.Free();
   end;

   Для отключения возможности быстрого запуска приложения из командной строки можно воспользоваться процедурой, приведенной в листинге 7.19.
 //-- Листинг 7.19. Отмена быстрого запуска приложения --// 
   procedure UnregisterQuickStart();
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_LOCAL_MACHINE;
   //Удаляем сведения о программе из реестра
   reg.DeleteKey(paths + '\'+ Application.Title + '.exe');
   reg.Free();
   end;

   В приведенных выше листингах значение константы paths равно:

   const paths = 'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths';


   Регистрация типов файлов

   Теперь рассмотрим вопрос, нередко интересующий программистов, приложения которых должны сохранять и загружать данные из файлов. Логично задавать всем таким файлам одно расширение: получается тип файлов приложения.
   Открытие файлов (документов) приложения из самого приложения организовать несложно – достаточно применить окно открытия файла. Однако как заставить, например, Проводник автоматически запускать нужное приложение при выборе соответствующего файла. Сделать это тоже несложно – достаточно внести небольшие изменения в раздел реестра HKEY_CLASSES_ROOT.
   Перечень операций, которые нужно выполнить для регистрации собственного типа файла (к примеру, MYDOC), следующий.
   1. Создать раздел HKEY_CLASSES_ROOT\.mydoc, в параметр (По умолчанию) которого записать имя типа файла, например, TricksDelphi.DocumentSample.
   2. Создать раздел HKEY_CLASSES_ROOT\<имя_типа>, например, HKEY_CLASSES_ ROOT\TricksDelphi.DocumentSample. Если в параметр (По умолчанию) этого раздела записать строку, то она будет отображаться в качестве описания типа файла.
   3. Если нужно, чтобы для документа использовался определенный значок, необходимо создать раздел HKEY_CLASSES_ROOT\<имя_типа>\DefaultIcon, в параметр (По умолчанию) которого записать полный путь EXE– или DLL-файла, содержащего необходимый значок, и через запятую номер значка (см. гл. 4).
   4. Наконец, для автоматического запуска приложения при выборе файла заданного типа нужно создать раздел HKEY_CLASSES_ROOT\<имя_типа>\Shell\Open\Command, в параметр (По умолчанию) которого записать строку вида <путь_приложения> %1 для передачи имени документа командной строке.
   Описание процедуры, производящей все вышеперечисленные операции, приведено в листинге 7.20.
 //-- Листинг 7.20. Регистрация типа файла --// 
   procedure RegisterAppDocuments();
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_CLASSES_ROOT;
   //Вносим информацию о типе файлов в реестр
   //..само расширение
   if reg.OpenKey('.mydoc', True) then
   begin
   reg.WriteString('', 'TricksDelphi.DocumentSample');
   reg.CloseKey();
   end;
   //..описание типа файла
   if reg.OpenKey('TricksDelphi.DocumentSample', True) then
   begin
   reg.WriteString('', ' Документ TricksDelphi.DocumentSample');
   reg.CloseKey();
   end;
   //..значок для файлов MYDOC-типа
   if reg.OpenKey('TricksDelphi .Document Sample\DefaultIcon', True) then
   begin
   reg.WriteString('', Application.ExeName + ', 1');
   reg.CloseKey();
   end;
   //..приложение, открывающее MYDOC-документ
   if reg.OpenKey('TricksDelphi .Document Sample\Shell\Open\Command', True)
   then
   begin
   reg.WriteString('', Application.ExeName + '%1');
   reg.CloseKey();
   end;
   reg.Free();
   end;

   Результат работы этой процедуры показан на рис. 7.10.
   Рис. 7.10. Результат регистрации типа файла

   Теперь при выборе в файловой оболочке MYDOC-приложение будет запускаться с путем выбранного файла (правда, в формате 8.3) в качестве аргумента командной строки. Как перевести путь из короткой формы в длинную (если это вообще надо), рассказано в подразделе «Преоразование длинных имен файлов в короткие и наоборот» раздела «Папки и пути» гл. 4 (стр. 129). Если вы не знакомы со способами получения доступа к аргументам командной строки, можете взглянуть на код, представленный в листинге 7.21 (здесь путь открываемого файла выводится в текстовое поле на форме).
 //-- Листинг 7.21. Определение имени открываемого файла --// 
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   if ParamCount() > 0 then
   begin
   //Обрабатываем данные командной строки...
   txtDoc.Text:= 'Имя открываемого файла: '+ ParamStr(1);
   end;
   end;

   Удалить сведения о типе файла можно с помощью простого удаления созданных ранее разделов, например, способом, представленным в листинге 7.22 .
 //-- Листинг 7.22. Удаление из реестра сведений о типе файла --// 
   procedure UnregisterAppDocuments();
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   reg.RootKey:= HKEY_CLASSES_ROOT;
   //Удаление из реестра информации о типе файла
   reg.DeleteKey('.mydoc');
   reg.DeleteKey('TricksDelphi.DocumentSample');
   reg.Free();
   end;


   Программа просмотра реестра

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

   Далее будут рассмотрены в порядке использования функции и процедуры, формирующие основу этого приложения. Итак, при запуске формы составляется список корневых разделов реестра (листинг 7.23).
 //-- Листинг 7.23. Первоначальная инициализация дерева разделов реестра --// 
   procedure TForm1.FormCreate(Sender: TObject);
   var
   item: TTreeNode;
   begin
   //Формирование списка корневых разделов реестра
   item:= keys.Items.AddChild(nil, 'HKEY_CLASSES_ROOT');
   item.Data:= Pointer(HKEY_CLASSES_ROOT);
   CheckSubKeys(item);
   item:= keys.Items.AddChild(nil, 'HKEY_CURRENT_USER');
   item.Data:= Pointer(HKEY_CURRENT_USER);
   CheckSubKeys(item);
   item:= keys.Items.AddChild(nil, 'HKEY_LOCAL_MACHINE');
   item.Data:= Pointer(HKEY_LOCAL_MACHINE);
   CheckSubKeys(item);
   item:= keys.Items.AddChild(nil, 'HKEY_USERS');
   item.Data:= Pointer(HKEY_USERS);
   CheckSubKeys(item);
   item:= keys.Items.AddChild(nil, 'HKEY_CURRENT_CONFIG');
   item.Data:= Pointer(HKEY_CURRENT_CONFIG);
   CheckSubKeys(item);
   end;

   Вызываемая для каждого нового элемента дерева (листинг 7.23) процедура CheckSubKeys реализована следующим образом (листинг 7.24).
 //-- Листинг 7.24. Оформление элемента дерева в зависимости от наличия вложенных разделов --// 
   procedure TForm1.CheckSubKeys(item: TTreeNode);
   var
   reg: TRegistry;
   begin
   reg:= TRegistry.Create();
   //Проверка, есть ли в разделе реестра вложенные подразделы
   reg.RootKey:= GetRootKey(item);
   if reg.OpenKeyReadOnly(GetKeyPath(item)) then
   begin
   if reg.HasSubKeys() then
   begin
   //Добавляем фиктивный элемент (чтобы показывался "+" для
   //разворачивания раздела). Одновременно помечаем фиктивный элемент
   keys.Items.AddChild(item,'').Data:= Pointer(-1);
   end;
   reg.CloseKey();
   end;
   reg.Free();
   end;

   По сравнению с примером (дерево папок), рассмотренным в подразделе «Построение дерева папок» раздела «папки и пути», гл. 4 (стр. 140), определение наличия дочерних разделов реестра – относительно легкая операция, поскольку эта проверка производится сразу при составлении списка подразделов. Как и в только что упомянутом примере из гл. 4, в дерево добавляется фиктивный дочерний элемент для тех элементов дерева, для которых соответствующие им разделы реестра содержат подразделы.
   Важно, что фиктивный элемент помечается значением -1. Именно по наличию дочернего элемента с полем Data, равным -1, можно определить, зачитывалось ли содержимое раздела, соответствующего определенному элементу дерева. Содержимое раздела зачитывается при разворачивании элемента дерева (листинг 7.25).
 //-- Листинг 7.25. Составление списка дочерних разделов --// 
   procedure TForm1.keysExpanding(Sender: TObject; Node: TTreeNode;
   var AllowExpansion: Boolean);
   var
   reg: TRegistry;
   subkeys: TStrings;
   i: Integer;
   begin
   if Integer(Node.getFirstChild.Data) <> -1 then
   //Список подразделов был зачитан ранее
   Exit;
   Node.DeleteChildren(); //Удаление фиктивного элемента дерева
   reg:= TRegistry.Create();
   //Загрузка списка подразделов выбранного раздела
   reg.RootKey:= GetRootKey(Node);
   if reg.OpenKey(GetKeyPath(Node), False) then
   begin
   //Получение списка подразделов
   subkeys:= TStringList.Create();
   reg.GetKeyNames(subkeys);
   for i:= 0 to subkeys.Count – 1 do
   begin
   //Добавление элемента для дочернего раздела (не забываем
   //проверять подразделы у каждого дочернего раздела)
    CheckSubKeys(keys.Items.AddChild(Node, subkeys[i]));
   end;
   subkeys.Free();
   reg.CloseKey();
   end;
   reg.Free();
   end;

   В листинге 7.25 используются две дополнительные функции: для определения полного пути раздела, соответствующего элементу дерева (без имени коневого раздела), и для получения дескриптора корневого раздела (хранится в поле Data корневого элемента каждой ветви дерева).
   Путь раздела определить несложно: достаточно подняться к корню соответствующей ветви дерева, собирая по ходу имена элементов дерева в одну строку (листинг 7.26).
 //-- Листинг 7.26. Определение пути раздела в дереве --// 
   function GetKeyPath(item: TTreeNode): String;
   var
   temp: TTreeNode;
   path: String;
   begin
   temp:= item;
   while temp.Parent <> nil do
   begin
   path:= temp.Text + '\'+ path;
   temp:= temp.Parent;
   end;
   GetKeyPath:= path;
   end;

   Аналогично, даже проще, определяется дескриптор корневого раздела определенной ветви реестра: для этого достаточно добраться до корня ветви дерева и прочитать значение поля Data корневого элемента (листинг 7.27).
 //-- Листинг 7.27. Определение дескриптора корневого раздела ветви --// 
   function GetRootKey(item: TTreeNode): HKEY;
   var
   temp: TTreeNode;
   begin
   temp:= item;
   while temp.Parent <> nil do
   temp:= temp.Parent;
   GetRootKey:= HKEY(temp.Data);
   end;

   При выделении элемента дерева список в правой части формы заполняется параметрами соответствующего раздела. Порядок заполнения списка продемонстрирован в листинге 7.28.
 //-- Листинг 7.28. Составление списка параметров раздела реестра --// 
   procedure TForm1.keysChange(Sender: TObject; Node: TTreeNode);
   var
   reg: TRegistry;
   valueItem: TListItem;
   item: TTreeNode;
   valueNames: TStrings;
   i: Integer;
   begin
   item:= keys.Selected;
   if item <> nil then
   begin
   //Зачитаем содержимое выбранного раздела в ListView (values)
   values.Clear;
   reg:= TRegistry.Create();
   reg.RootKey:= GetRootKey(item);
   if reg.OpenKeyReadOnly(GetKeyPath(item)) then
   begin
   valueNames:= TStringList.Create();
   //Получение списка названий параметров
   reg.GetValueNames(valueNames);
   //Добавление каждого параметра в список
   for i:= 0 to valueNames.Count – 1 do
   begin
   valueItem:= values.Items.Add();
   if valueNames[i] = '' then
   valueItem.Caption:= '< По умолчанию >'
   else
   valueItem.Caption:= valueNames[i];
   //Получение типа и значения параметра
   case reg.GetDataType(valueNames[i]) of
   rdUnknown:
   valueItem.Sub Items.Add('Неизвестно');
    rdString, rdExpandString:
   begin
   valueItem.Sub Items.Add(' Строка');
   valueItem.SubItems.Add(reg.ReadString(valueNames[i]));
   end;
   rdInteger:
   begin
   valueItem.Sub Items.Add(' Число');
   valueItem.SubItems.Add(IntToStr(
   reg.ReadInteger(valueNames[i])));
   end;
   rdBinary:
   valueItem.Sub Items.Add(' Двоичные еданные');
   end;
   end;
   valueNames.Free();
   reg.CloseKey();
   end;
   reg.Free();
   end;
   end;

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




   Глава 8
   Обмен данными между приложениями


   • Сообщение WM_COPYDATA
   • Использование буфера обмена
   • Проецируемые в память файлы

   Организация обмена данными между приложениями, а именно между процессами этих приложений, является достаточно трудоемкой задачей. Архитектура Win32 подразумевает максимальную изоляцию исполняющихся приложений друг от друга. Каждое приложение исполняется в своем виртуальном адресном пространстве, которое изолированно и не имеет доступа к памяти других процессов приложений. Однако довольно часто возникает необходимость перевать данные из одного исполняющегося процесса в другой. Она вызвана тем, что функциональные приложения и пакеты программ исполняются не в одном процессе, а в нескольких. Поэтому для нормальной работы используются основные возможности межпроцессного взаимодействия. Наиболее простой, понятной, но не всегда удобной является передача данных с использованием сообщения WM_COPYDATA. Также для передачи данных между приложениями широко используются проецируемые в память файлы (Mapping Files). Существуют и такие высокоуровневые средства, как буфер обмена или уже рассмотренная технология COM. Перечисленные способы будут подробно рассмотрены в этой главе. За рамки этой книги выходит рассмотрение способа передачи данных через каналы (трубы, или «Pipe»), который считается устаревшим и по этой причине не будет столь интересным.


   Сообщение WM_COPYDATA

   Сообщение WM_COPYDATA позволяет приложениям копировать данные между их адресными пространствами. Для передачи сообщения должна использоваться функция синхронной отправки сообщения SendMessage, а не PostMessage, которая асинхронным образом передает сообщение. Данные, предназначенные для передачи, не должны содержать указателей или других ссылок на объекты, недоступные программе, принимающей эти данные. Рассмотрим параметры, передаваемые с сообщением WM_COPYDATA:

   //дескриптор передающего окна
   wParam = (WPARAM) (HWND) hwnd;
   //указатель на структуру с данными
   lParam = (LPARAM) (PCOPYDATASTRUCT) pcds;

   На использование сообщения налагаются следующие ограничения:
   • принимаемые данные должны быть доступны только для чтения, так как изменение их структуры может привести к непредсказуемым последствиям;
   • если приложению, получающему данные, требуется использовать их после возврата из обработчика WM_COPYDATA, то это приложение должно скопировать их в локальный буфер.
   Приступим к созданию приложения, демонстрирующего работу WM_COPYDATA. Для создания примера необходимо создать два приложения. Первое будет отправлять данные, например, строку текста, другое же приложение будет их получать. На главной форме первого приложения необходимо поместить элемент управления TextBox, в который будет записываться передаваемая строка, и кнопка, нажатие которой будет инициировать передачу данных. Для второго приложения достаточно элемента, отображающего текстовую информацию типа Label. Перейдем к рассмотрению исходных текстов разрабатываемых приложений.
   В рассматриваемом примере окну будет посылаться сообщение, причем количество сообщений может быть различным, поэтому для уникальной идентификации операции необходимо ввести специальную константу:

   const
   CMD_SETLABELTEXT = 1; //Задаем ID Dкоманды

   На форме находится кнопка отправки данных другому приложению, обработчик которой выглядит следующим образом (листинг 8.1).
 //-- Листинг 8.1. Отправка данных другому приложению --// 
   procedureTDataSender.bnSendClick(Sender: TObject);
   var
   CDS: TCopyDataStruct;
   begin
   //Устанавливаем тип команды
   CDS.dwData:= CMD_SETLABELTEXT;
   //Устанавливаем длин у передаваемых данных
   CDS.cbData:= Length (StringEdit.Text) + 1;
   //Выделяем память буфера для передачи данных
   GetMem(CDS.lpData, CDS.cbData);
   try
   //Копируем данные в буфер
   StrPCopy (CDS.lpData, StringEdit.Text);
   //Отсылаем сообщение в окно с заголовком «StringReciever»
   SendMessage(FindWindow(NIL, 'StringReciever'),
   WM_COPYDATA, Handle, Integer(@CDS));
   Finally
   //Высвобождаем буфер
   FreeMem(CDS.lpData, CDS.cbData);
   end;
   end;

   Подробного комментария данный листинг не требует. Обратите внимание на вызов функции SendMessage, которая использует процедуру FindWindow для установки одного из своих параметров. Процедура FindWindow в случае успешного выполнения возвращает HWND окна, заголовок которого задается в параметре этой функции (строка StringReciever из предыдущего примера). Синхронная отправка сообщения WM_COPYDATA с набором данных, которые помещены в структуру CDS, осуществляется вызовом SendMessage.
   Теперь рассмотрим второе приложение, которое принимает строку и отображает ее в надписи (Label). Для начала в блок объявления необходимо поместить обработчик сообщения и объявить само сообщение WM_COPYDATA:

   type
   TStringReciever = class(TForm)
   LabelStr: TLabel;
   private
   //Обработчик сообщения WM_COPYDATA
   procedure WMCopyData(var MessageData: TWMCopyData);
   message WM_COPYDATA;

   Как и в случае с первым приложением, здесь также необходима константа, которая будет идентифицировать тип операции:

   const CMD_SETLABELTEXT = 1;

   Далее рассмотрим тело функции обработчика сообщения WM_COPYDATA (листинг 8.2).
 //-- Листинг 8.2. Обработка сообщения WMCOPYDATA --// 
   proceduree TStringReciever.WMCopyData (varrrMessageData: TWMCopyData);
   begin
   //Устанавливаем свойства метки, если заданная команда совпадает
   if MessageData.CopyDataStruct.dwData = CMD_SETLABELTEXT then begin
   //Устанавливаем текст из полученных данных
   LabelStr.Caption:= PChar(MessageData.CopyDataStruct.lpData);
   MessageData.Result:= 1;
   end else
   MessageData.Result:= 0;
   end;

   Если окну второго приложения, которое носит название StringReciver («получатель строки»), приходит сообщение WM_COPYDATA, происходит вызов процедуры WMCopyData. В качестве параметра эта процедура получает структуру данных MessageData типа TWMCopyData, содержащую идентификатор операции и данные (передаваемую строку). После проверки типа операции, в случае совпадения его с константой CMD_SETLABELTEXT, полученные данные преобразуются в строку. Преобразование происходит с помощью функции PChar. Полученная строка устанавливается в качестве заголовка для метки с именем LabelStr. Затем полю Result структуры MessageData присваивается значение 1 или 0 в зависимости от успешности выполненной операции (если успешно, то функция возвращает 1).
   Таким образом, для передачи данных (строки) необходимо записать передаваемую строку в текстовое поле первой формы и нажать кнопку Отправить. Результат работы приложений показан на рис. 8.1.
   Рис. 8.1. Вид приложений посылки и получения строки

   Необходимо отметить, что передача данных посредством сообщения WM_COPYDATA является удобным и простым способом, однако WM_COPYDATA можно передавать только функцией SendMessage, что представляет существенный недостаток такого метода. SendMessage «замораживает» работу приложения отправителя, поэтому такой способ применяется для передачи небольших объемов данных, которые не требуют сложной обработки на стороне программы приемника. К тому же на использование сообщения WM_CO PYDATA налагаются некоторые существенные ограничения, о которых говорилось выше.


   Использование буфера обмена

   Буфер обмена представляет собой область оперативной памяти, которая используется операционной системой для временного хранения данных. Он выступает в роли общего хранилища данных для всех приложений системы – фактически любая программа может записывать данные в буфер обмена и считывать их оттуда. Он способен хранить данные различных типов и, кроме данных, содержит сведения о типе хранимой информации. Буфер обмена является неотъемлемым компонентом операционной системы типа Windows.
   Буфер обеспечивает простейший обмен данными между приложениями. Одно приложение помещает туда данные, другое – считывает их оттуда. Как правило, эти действия (чтение и запись) выполняются при непосредственном участии пользователя. В использовании буфера может участвовать и одно приложение, в случае чего проходит обмен данными внутри самого приложения.
   Для выполнения операции обмена данными через буфер в Delphi предназначен специальный класс TClipboard. В Delphi также имеется глобальный объект Clipboard, который является экземпляром класса TClipboard и представляет буфер обмена Windows.
   С помощью свойств и методов объекта Clipboard можно осуществлять различные операции обмена или анализа хранимых данных. Для доступа к объекту, содержащемуся в буфере, в разделе uses модуля, в котором выполняются операции с объектом буфера обмена, указывается модуль Clipboard.
   Общее количество форматов, поддерживаемых буфером обмена, содержится в свойстве FormatCount типа Integer. Чтобы отобразить количество форматов, распознаваемых буфером, можно воспользоваться следующим программным кодом:

   //В разделе uses указываем модуль Clipboard
   InformationClipLabel.Caption:= IntToString(Clipboard.FormatCount);

   Буфер обмена поддерживает самые разнообразные типы данных. Список поименованных констант некоторых форматов представлен ниже.
   • CF_TEXT – обычный текст (коды ANSI). Символы окончания строки – #10 и #13, окончания текста – #0.
   • CF_BITMAP – рисунок BMP-формата.
   • CF_MetaFilePic – рисунок WMF-формата.
   • CF_TIFF – рисунок TIFF-формата.
   • CF_OEMTEXT – текст.
   • CF_DIB – рисунок DIB-формата.
   • CF_Wave – звуковые данные.
   • CF_UniCodeText – текст (коды Unicode).
   • CF_Picture – объект типа TPicture.
   При необходимости можно создавать и регистрировать свои форматы данных в дополнение к имеющимся базовым.
   При использовании нестандартных форматов данных, помещаемых в буфер обмена и извлекаемых оттуда, программы должны соблюдать устанавливаемые разработчиками соглашения об обмене такими данными.
   В листинге 8.3 приведен код обработчика нажатия кнопки, загружающего в список ListBoxInfo значения констант, идентифицирующих каждый формат данных буфера обмена.
 //-- Листинг 8.3. Отображение значений форматов буфера --// 
   procedureTFormClipboard.bnInfoCipClick(Sender: TObject);
   var i: integer;
   begin
   ListBoxInfo.Clear;
   for i:= 0 to Clipboard.FormatCount – 1 do
   ListBoxInfo.Items.Add(IntToStr(Clipboard.Formats[i]));
   end;

   Приложение может помещать информацию в буфер обмена и извлекать ее только в тех форматах, которые будет поддерживать буфер. Список поддерживаемых форматов создается при инициализации приложения.
   Перед осуществлением доступа к данным, содержащимся в объекте Clipboard, может потребоваться анализ формата данных, для чего служит метод HasFormat. Процедура HasFormat (Format: Word): Boolean используется для запроса к буферу обмена и позволяет узнать, можно ли извлечь хранимые в нем данные в формате, указанном параметром Format. При положительном результате возвращаемое значение равно True, в противном случае – False.
   Как правило, буфер обмена одновременно используется различными приложениями, однако в случае, когда необходимо получить монопольный доступ к буферу, приложение должно открыть его для себя в специальном режиме. Для этого необходимо вызвать метод Open, позволяющий программе получить полный (исключительный) доступ к общей области обмена. После вызова метода Open содержимое буфера не может быть изменено другими приложениями, поэтому, после того как необходимость в монопольном использовании буфера отпадет, приложение должно вызвать метод Close объекта Clipboard. Если открытый буфер не будет закрыт с помощью метода Close, то он будет автоматически закрыт системой после завершения программы, открывшей буфер обмена.
   Для очистки содержимого буфера обмена используется метод Clear, который вызывается автоматически при изменении содержимого буфера. Перед записью новых данных в буфер старая информация удаляется.
   Класс TClipboard используется многими классами и компонентами, которые поддерживают обмен данными через буфер обмена. К примеру, компоненты Memo и Edit содержат специальные методы для обмена текстовой информацией посредством буфера. Методы CopyToClipBoard и CutToClipBoard помещают текстовые данные в буфер обмена, копируя и вырезая их из источника (компонента) соответственно, а метод PasteFromClipBoard вставляет текстовый фрагмент из буфера в текстовое поле.
   Правда, при использовании подобных методов лучше проверять, является ли содержимое буфера обмена текстовой информацией. В листинге 8.4 показан пример копирования в буфер обмена всего текста, введенного в текстовое поле.
 //-- Листинг 8.4. Копирование текста из поля редактора Memo в буфер обмена --// 
   procedure TFormClipboard.bnCopyTextClick(Sender: TObject);
   begin
   //Выделяем весь текст в поле редактора
   MemoText.SelectAll;
   //Копируем текст
   MemoText.CopyToClipBoard;
   end;

   Буфер обмена часто используется для хранения текста, поэтому объект Clipboard имеет специальное свойство AsText типа String, предназначенное для обработки содержимого буфера как данных текстового формата. Свойство AsText предназначено как для чтения, так и для записи. При чтении свойства данные извлекаются, а при записи заносятся в буфер обмена (листинг 8.5).
 //-- Листинг 8.5. Копирование текстовой информации --// 
   procedureTFormClipboard.bnCopyTextAsTextClick(Sender: TObject);
   begin
   //Если в буфере текст, то выводим его в поле редактора
   if Clipboard.HasFormat(CF_Text)
   then MemoText:= Clipboard.AsText
   end;

   При работе с графическими компонентами для операций, связанных с обменом информацией через общую область, удобно использовать метод Assi gn. Процедура Assign (Source: TPersistent) присваивает буферу обмена объект, указанный параметром Source. Если объект является изображением и принадлежит к таким графическим классам, как TBitmap, TPicture или TMetafile, то в буфер обмена копируется изображение установленного формата. Для извлечения изображения из буфера также может использоваться метод Assign.
   Пример использования буфера обмена для копирования изображений проведен в листинге 8.6.
 //-- Листинг 8.6. Обмен изображением через буфер обмена --// 
   procedure TFormClipboard.bnCopyImageClick(Sender: TObject);
   begin
   //Открываем монопольный доступ
   Clipboard.Open;
   //Заносим изображение в буфер
   Clipboard.Assign(ImageMyPic1.Picture);
   //Проверям формат находящихся в буфере данных
   if Clipboard.HasFormat(CF_Picture)
   then ImageMyPic2.Picture.Assign(Clipboard);
   //Закрываем монопольный доступ к буферу
   Clipboard.Close;
   end;

   Изображение, находящееся в образе ImageMyPic1, помещается в буфер обмена, откуда затем копируется в образ ImageMyPic2. Для выполнения этих операций устанавливается монопольный доступ к объекту Clipboard.
   Таким образом, использование объекта Clipboard широко примененяется при разработке приложений, которым необходим обмен данными с другими программами. Необходимо отметить, что буфер обмена ориентирован на работу с пользователем (пользователь инициирует обмен данными между приложениями), поэтому такой способ обмена данными наиболее удобен с точки зрения пользователя. К тому же, буфер обмена поддерживает множество форматов представления информации, что позволяет сделать обмен данными более гибким и эффективным.


   Проецируемые в память файлы

   Не менее мощным и гибким методом организации обмена данными между приложениями является метод, который основывается на проецируемых в память файлах (Files Mapping). Главная идея этого механизма состоит в использовании динамической разделяемой памяти системы для хранения в ней данных. Как известно, каждому процессу выделяется отдельный участок памяти, называемый виртуальным адресным пространством. При использовании механизма проецируемых в память файлов данные становятся доступны из любого процесса, который использует этот файл. В этом случае говорят, что файл отображается в виртуальное адресное пространство процесса, поэтому данные, хранимые в файле, доступны процессу, который этот файл открыл. Механизм проецирования файлов в память используется, например, для исполняемых файлов приложений (EXE), а также для DLL.
   Для работы с проецируемыми в память файлами предусмотрен целый ряд API-функций. Но прежде чем приступить к их рассмотрению, нужно разобраться в процессе организации обмена данными через проецируемые файлы. На первом этапе необходимо создать объект (файл, отображаемый в память), затем «отобразить» созданный объект в адресное пространство процесса приложения. После этого появится возможность записи и чтения данных из спроецированного файла. При отображении файла на определенный участок памяти (адресного пространства процесса) манипуляции с данными этого участка отражаются на содержимом файла. После завершения манипуляций над объектом необходимо закрыть доступ к данным файла (удалить проекцию и закрыть файл).
   Рассмотрим некоторые функции, предназначенные для работы с проецируемым в память файлом. Для создания объекта проецируемого файла служит функция CreateFileMapping. Ее синтаксис выглядит следующим образом:

   function CreateFileMapping(hFile: THandle;
   lpFileMappingAttributes: PSecurityAttributes;
   flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD;
   lpName: PChar): THandle;

   Параметры этой функции имеют следующие значения.
   • hFile – идентификатор файла. Посредством присвоения этому аргументу значения константы INVALI D_HAN D L E_VALU E создаваемый объект файлового отображения связывается со страничным swap-файлом (системным файлом подкачки).
   • lpFileMappingAttributes – указатель на структуру типа TSecurityAttributes. Структура содержит параметры безопасности создаваемого файла.
   • flProtect – параметр, задающий способ совместного использования создаваемого объекта, который в случае разрешенного доступа на чтение и запись принимает значение PAGE_READWRITE.
   • dwMaximumSizeHigh – старший разряд 64-битного значения выделяемого объема памяти.
   • dwMaximumSizeLow – младший разряд 64-битного значения выделяемого объема памяти.
   • lpName – имя объекта проецируемого файла (для создания безымянной проекции файла может иметь значение nil).
   Функция CreateFileMapping возвращает глобальный дескриптор (THandle). Если проецируемый файл не создан, то функция возвращает нулевое значение.
   После того как проецируемый файл создан, необходимо отобразить его в адресное пространство процесса создаваемой программы. Для этого предназначена функция MapViewOfFile, имеющая следующий синтаксис:

   function MapViewOfFile (hFileMappingObject: THandle;
   dwDesiredAccess: DWORD;
   dwFileOffsetHigh, dwFileOffsetLow,
   dwNumberOfBytesToMap: DWORD): Pointer;

   Значения параметров этой функции следующие.
   • hFileMappingObject – описатель созданного объекта файлового отображения.
   • dwDesiredAccess – параметр доступа к полученным данным, который в случае разрешенного доступа на чтение и запись принимает значение FILE_MAP_WRITE.
   • dwFileOffsetHigh, dwFileOffsetLow – 64-битное смещение от начала файла.
   • dwNumberOfBytesToMap – указывает количество отображаемых байт. Если этот аргумент имеет значение 0, то на область памяти будет отображен весь файл.
   При успешном выполнении функция MapViewOfFile возвращает указатель (тип Pointer) на начальный адрес данных объекта. Этот указатель может использоваться в дальнейшем для записи или чтения файла.
   Следующей функцией, противоположной по производимым действиям функции MapViewOfFile, является UnMapViewOfFile. Эта функция отключает проецируемый файл от текущего процесса:

   function UnMapViewOfFile(lpBaseAddress: Pointer): Boolean;

   Функция принимает указатель, возвращаемый MapViewOfFile, и использует его для отмены проекции файла на адресное пространство процесса. В случае успешной выгрузки функция возвращает значение True, в противном случае – False.
   Последняя функция, которую необходимо рассмотреть, – CloseHandle. Эта функция используется для закрытия дескриптора (многих системных объектов, а не только проекции файла). Синтаксис функции следующий:

   function CloseHandle(hFileMapObj: THandle):Boolean;

   Как видно из синтаксиса функции, она принимает описатель объекта файлового отображения, полученный в результате выполнения функции CreateFileMapping, и освобождает его. Для правильного завершения работы с объектом файлового отображения сначала следует применить функцию UnMapViewOfFile, а затем CloseHandle.
   Сама проекция файла будет удалена только после закрытия всех дескрипторов во всех использующих эту проекцию процессах.
   Далее для демонстрации работы проецируемых в память файлов будет создано приложение, записывающее в такой файл строку и спустя некоторое время считывающее ее оттуда. Для этого понадобится стандартный TextBox, кнопка, метка и таймер. Программа будет работать следующим образом: строка, записанная в поле редактора, после нажатия кнопки будет помещаться в проецируемый файл. Далее, спустя некоторое время (определяемое временным интервалом срабатывания таймера), содержимое файла будет считываться и задаваться в качестве заголовка метки (рис. 8.2).
   Рис. 8.2. Вид приложения, использующего проецируемый файл

   В секцию описания переменных программы необходимо поместить следующие переменные:

   var
   Form MappingFile: TFormMappingFile;
   //Глобальные переменные
   //Описатель объекта проецируемого файла
   hFileMapObj:THandle;
   //Указатель на начальный адрес данных
   lpBaseAddress:PChar;

   Далее рассмотрим действия, выполняемые при загрузке формы. Создание проецируемого файла и его отображение в адресное пространство процесса выполняется в момент создания формы (листинг 8.7).
 //-- Листинг 8.7. Создание формы приложения --// 
   procedureTMappingFile.FormCreate (Sender: TObject);
   begin
   //Создаем проецируемый файл с именем FileMemory
   //и передаем полученный в результат е описатель
   //в глобальную переменн ую hFileMapObj
   hFileMapObjj:=
   CreateFileMapping(MAXDWORD,Nil,PAGE_READWRITE,0,4,'FileMemory');
   Ifffff(hFileMapObj= 0) Then
   ShowMessage('Не могу создать проецируемый файл!')
   Else
   //Подключаем файл к адресному пространству
   //и получаем начальный адрес данных
   lpBaseAddress:= MapViewOfFile(hFileMapObj,FILE_MAP_WRITE,0,0,0);
   If lpBaseAddress = Nil Then
   ShowMessage('Не могу подключить проецируемый файл!');
   end;

   После инициализации файла его можно использовать. Листинг обработчика, копирующего данные в проецируемый файл, приведен ниже (листинг 8.8).
 //-- Листинг 8.8. Копирование данных в проецируемый файл --// 
   procedure TMappingFile.bnOKClick(Sender: TObject);
   begin
   //Считываем данные в проецируемый файл
   StrPCopy(lpBaseAddress,edVariable.Text);
   end;

   После того как будет нажата кнопка, данные поместятся в проецируемый файл. По истечении некоторого времени, заданного таймером, строка установится в качестве текста метки (листинг 8.9).
 //-- Листинг 8.9. Считывание данных из проекции файла --// 
   procedure TMappingFile.TimerMFTimer(Sender: TObject);
   begin
   lbVariable.Caption:= PChar(lpBaseAddress);
   end

   В момент завершения приложения необходимо отключить проецируемый файл от адресного пространства процесса и закрыть объект файла. Эти действия можно выполнять в момент уничтожения формы (листинг 8.10).
 //-- Листинг 8.10. Уничтожение формы приложения --// 
   procedure TMappingFile.FormClose(Sender: TObject; var Action: TCloseAction);
   begin
   //Отключим файл от адресного пространства
   UnMapViewOfFile(lpBaseAddress);
   //Освобождаем объект файла
   CloseHandle(hFileMapObj);
   //Закрываем форму
   Action:= caFree;
   end;

   Данный пример работы с проекцией файла в рамках одного приложения является достаточно простым. Более же интересный и реальный пример будет рассмотрен в гл. 10 при рассмотрении программы «Оконный шпион», в которой проекция файла в память используется для передачи данных из функции DLL, работающей в памяти другого процесса.



   Глава 9
   Возможности COM в Microsoft Word и Microsoft Excel


   • Технология OLE
   • Технология COM
   • Использование технологии OLE в Delphi
   • Управление Microsoft Word и Microsoft Excel

   Технология COM/DCOM является одной из наиболее важных и широко используемых современных технологий. Охватить все аспекты этой технологии очень сложно, и пытаться сделать это в рамках данной книги нет необходимости. В этой главе будут рассмотрены основные возможности COM и их практическое применение. Примеры, разобранные в главе, демонстрируют основы управления приложениями, снабжаемыми COM-объектами. К таким приложениям можно отнести все программы из пакета Microsoft Office (Microsoft Word, Microsoft Excel и т. д.).


   Технология OLE

   В Windows 3.1 и более ранних версиях основным средством обмена данными между программами была технология DDE – Dynamic Data Exchange («динамический обмен данными»). На этой же технологии основывалась и технология OLE – Object Linking and Embedding («связывание и внедрение объектов»). OLE позволяет внедрять документы одного приложения в другое. Это позволило использовать функции различных программ для редактирования одного документа.
   В основе технологии DDE лежит обмен сообщениями между окнами операционной системы. Подобный механизм затрудняет распараллеливание процессов и обмен данными через сеть между работающими на разных компьютерах приложениями. Это привело к созданию расширения DDE – NetDDE, однако эта технология работает медленно и неустойчиво.
   С момента выпуска Windows NT 3.51 начала внедряться технология OLE 2 – дальнейшее развитие технологии OLE. OLE 2 дополнительно включает в себя технологию ActiveX. Позже термин «OLE 2» изменили на «OLE».
   Технология DDE была не в состоянии поддерживать OLE 2, поэтому специально для нее была создана технология взаимодействия между программами – COM (Component Object Model, «модель компонентных объектов»), которая оказалась весьма удачной. Начиная с Windows 95, DDE была объявлена устаревшей, а основной технологией обмена данными в системе стала технология COM.


   Технология COM

   Модель COM построена по принципу архитектуры «клиент-сервер». Сервер предоставляет список возможных действий (функций), которые могут использоваться клиентским процессом. Таким образом, серверный процесс позволяет обрабатывать запросы клиента, выполняя некоторые действия, возвращать данные и т. п. Когда взаимодействие между клиентом и сервером подразумевает обмен данными, эти данные передаются в качестве параметров функций. При необходимости клиент также может экспортировать функции, которые могут быть вызваны сервером. В этом случае сервер получает возможность вызывать некоторые функции, предоставляемые клиентом.
   В основе технологии COM лежат понятия, которые характерны и для объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм. Рассмотрим их применительно к объектам COM.
   Инкапсуляция позволяет скрывать методы (функции) и данные от использования другими объектами. Этот механизм необходим для обеспечения безопасности и надежности конечной системы. Термин «метод» использован не случайно – объекты COM строятся по принципу классов, используемых в программировании (класс имеет название CoClass, где приставка «Со» говорит о том, что это класс COM).
   Наследование позволяет многократно использовать готовые решения. Создав объект, в дальнейшем вы можете многократно использовать некоторые его свойства (данные) и методы (функции), наследуя их. Механизм наследования в связке с принципом полиморфизма позволяет создавать иерархии COM-классов для эффективного решения любых задач. Кроме наследования, часто используется и агрегация – внедрение ранее реализованных объектов внутрь вновь разрабатываемых.
   Полиморфизм позволяет переопределять реализацию (поведение) унаследованных функций и данных. Это дает возможность более гибко строить иерархию классов, снижая тем самым сложность реализации программ.
   Изначально технология COM обеспечивала межпроцессное взаимодействие только на локальном компьютере. Эволюция COM привела к созданию технологии DCOM (Distributed COM, «распределенная COM»), позволяющей работать с объектами, которые расположены на различных и удаленных друг от друга компьютерах.
   На данный момент технология DCOM является межплатформенной. Средства для поддержки DCOM существуют в различных UNIX-системах (в том числе Linux), Solaris, MacOS, VxWorks.


   Использование технологии OLE в Delphi


   Как и многие современные среды программирования, Delphi поддерживает возможность автоматизированной разработки приложений, работающих с различными COM-сервисами или серверами. Для более глубокого понимания принципов работы приложений, создаваемых на основе COM-технологии, ниже приведен краткий обзор наиболее доступных COM-серверов – приложений пакета Microsoft Office.


   Microsoft Office с точки зрения COM

   Microsoft Office является средой, в которой большая часть задач решается без использования программирования. Однако ценность приложений Microsoft Office заключается в том, что все задачи могут решаться как традиционным способом (ручным редактированием), так и посредством применения программирования на распространенном языке VBA (Visual Basic for Application). Кроме того, приложения данного пакета снабжаются серверами COM, которые предоставляют интерфейс доступа к приложению и его объектам. Благодаря этому разработчик в среде Delphi имеет возможность, создав контроллер автоматизации, управлять сервером. Приложение Microsoft Office можно рассматривать как совокупность объектов с их методами и свойствами (они организуют основу программы). Как правило, в каждом приложении существует так называемый корневой объект, который носит название Application. Каждое приложение Microsoft Office имеет собственный корневой объект – Word.Application, Excel.Application и т. д. Приложение само является корневым объектом, но, несмотря на это, в объекты Application встраиваются все остальные объекты (участники), являющиеся свойствами главного объекта. Документ, созданный на базе технологии COM, предоставляет большое количество разнообразных методов, но в различных приложениях Microsoft Office имеются и одинаковые методы, например Run, Quit, Activate.
   При открытии любого приложения из пакета автоматически создается каркас нового документа, который представляет собой набор библиотек с классами. Объекты этих классов будут доступны в открытом документе. Задачей разработчика клиента (контроллера автоматизации) является получить доступ к корневому объекту сервера, выстроить цепочку доступа к объектам-участникам (встроенным объектам) и правильно передать параметры. Таким образом, получив доступ к объектам документа, можно проводить с ним различные манипуляции, редактировать его и т. д.


   Объект Application

   Безусловно, самым важным объектом в приложениях Microsoft Office является объект Application. Чуть позже небольшой пример использования данного объекта продемонстрирует простоту программирования с применением COM. Для решения часто встречающихся задач используются уже хорошо известные вам компоненты среды Delphi. В случае с использованием технологии COM это не является исключением. Для запуска сервера приложения Microsoft Word необходимо выполнить следующее.
   1. Создать новый проект.
   2. На главную форму приложения поместить компонент WordApplication вкладки Servers.
   3. Задать свойства компонента AutoConnect и AutoQuit со значениями True.
   4. Запустить созданное приложение.
   На первый взгляд, ничего существенного не происходит, но результат работы программы можно заметить, если просмотреть список запущенных процессов (не путайте с задачами). В приложении Диспетчер задач среди процессов различных приложений можно увидеть процесс WordCOM.exe. Этот говорит о том, что созданное приложение подключилось к серверу Microsoft Word и запустило его.
   Фактически в системе произошло следующее. В реестре был найден зарегистрированный ранее COM-сервер приложения Microsoft Word. Используя все тот же реестр, был найден путь к программе и произведен ее запуск. Вследствие этого в списке процессов появился процесс, отвечающий за работу редактора Microsoft Word.
   Но чтобы лучше понять запуск приложения Microsoft Word, стоит привести фрагмент исходного текста, результат работы которого аналогичен описанному выше (листинг 9.1).
 //-- Листинг 9.1. Запуск Microsoft Word --// 
   procedureTFormStartWord.ButtonStartClick(Sender: TObject);
   var
   //Переменная, интерфейс к объекту
   Wordvarrr: OleVariant;
   file_Nameee: string;
   begin
   //Начало блока перехвата исключения
   try
   file_Name:= ExtractFilePath(Application.EXEName) + 'worddoc.DOC';
   //Инициализируем объект интерфейса
   //для доступа к серверу COM Microsoft Word
   Wordvar:= CreateOleObject('Word.Application');
   //Добавление документа
   wordvar.application.documents.add;
   wordvar.application.activedocument.range.insertAfter(now);
   //Сохранение документа (аналог действиям: “Сохранить как...”,
   //с указанием имени файла)
   wordvar.application.activedocument.saveas(fileName);
   //Завершение работы с приложением и выгрузка COM-сервера
   wordvar.application.quit(true,0);
   ...
   end;

   Предложенный исходный текст демонстрирует подключение к серверу без помощи компонента среды разработки (который использовался в предыдущем примере). Для корректной работы необходимо в раздел uses включить COMOBJ – модуль работы с объектами COM. Важно отметить, что наличие функций, вызываемых для объекта wo rdva r, определяется в период выполнения. Это значит, что ошибка может обнаружиться только в период выполнения программы, поэтому весь код работы с объектом помещен в блок try.


   Класс TOLEServer

   На вкладке Servers находится набор компонентов для доступа к серверам автоматизации. Не все компоненты возвращают ссылку на объект Application, то есть не всегда могут быть получены интерфейсы для доступа к таким вложенным объектам, как документ Microsoft Word или рабочая книга Microsoft Excel. Все компоненты унаследованы от класса TOLEServer, который наследует свойства класса Tcomponent. Класс TOLEServer является базовым для всех COM-серверов. Кроме этого, данный класс имеет еще несколько свойств и методов для управления связью с COM-сервером. Среди таковых уже знакомое вам свойство AutoConnect, которое автоматически запускает COM-сервер и извлекает из него интерфейс, обеспечивающий связь с контроллером. Еще одно важное свойство класса TOLEServer – ConnectKind, указывающее тип процесса, с которым устанавливается связь. Это свойство используется методом Connect, который вызывается автоматически, если свойство AutoConnect истинно. В табл. 9.1 представлены описания значений, которые может принимать свойство ConnectKind.
 //-- Таблица 9.1. Значения свойства ConnectKind --// 
   Более подробно следует рассмотреть значение свойства ConnectKind, равное ckAttachToInterface. Соединение с сервером производится посредством использования главного интерфейса Application, но если, например, необходимо подключить к проекту такие компоненты, как WordDocument или WordParagraphFormat, то достаточно просто подключиться к уже существующему интерфейсу, а не создавать его заново. Также это может быть необходимо, когда контроллер должен отслеживать события, происходящие в COM-сервере.



   Управление Microsoft Word и Microsoft Excel


   Трюки в Microsoft Word

   В этом разделе будут более подробно рассмотрены практические примеры использования COM-сервера редактора Microsoft Word. Популярный редактор обладает обширным набором возможностей, которые можно использовать как вручную (посредством традиционного создания и редактирования документов), так и с применением технологии COM. Основное удобство последнего метода заключается в автоматизации рутинной работы, например составления отчетов. Следующий пример поможет разобраться в принципах построения контроллеров автоматизации, которые ранее уже упоминались. Контроллер автоматизации с точки зрения COM представляет собой приложение, которое посредством вызова процедур сервера проводит различные манипуляции над документом. В Microsoft Word это может быть написание текста в установленном формате, вставка таблиц в документ и т. д.
   Рассмотрим пример приложения, которое будет создавать новый документ Microsoft Word, записывать в него некоторый текст, добавлять таблицу и сохранять полученный документ в файл. Чтобы лучше понять принципы использования объектов COM, первый пример не будет использовать компонент среды разработки. Итак, для начала необходимо создать новый проект и поместить на форму следующие кнопки:
   • открытия приложения Microsoft Word;
   • вывода текста;
   • добавления таблицы;
   • сохранения документа;
   • завершения работы Microsoft Word.
   Поскольку компоненты использоваться не будут, в секцию uses следует добавить модуль ComObj. Для работы с COM-сервером редактора понадобится объект OLE. Переменную типа OleVariant можно добавить следующим образом:

   var
   //Объект OLE
   Wrd: OleVariant;

   Обработчик кнопки запуска редактора имеет следующий вид (листинг 9.2).
 //-- Листинг 9.2. Запуск редактора Microsoft Word --// 
   procedureTFormWord.bnOpenWordClick(Sender: TObject);
   begin
   //Создаем объект
   Wrd:= CreateOleObject('Word.Application');
   //Делаем видимым приложение
   Wrd.Visible:= true;
   //Добавляем новый документ
   Wrd.Documents.Add;
   end;

   После инициализации объекта необходимо создать новый документ, предварительно активизировав (отобразив на экране) приложение. После того как Microsoft Word будет запущен и в нем будет создан новый документ, можно будет записывать текст. Для этого нужно определить обработчик кнопки вывода текста (листинг 9.3).
 //-- Листинг 9.3. Вывод текста в Microsoft Word --// 
   procedure TFormWord.bnSetTextClick(Sender: TObject);
   begin
   //Процедура записи текста
   //Устанавливаем шрифт
   Wrd.Selection.Font.Size:= 20;
   Wrd.Selection.Font.Bold:= true;
   //Пишем текст
   Wrd.Selecti on.TypeText('Технология COM является одной из современных');
   Wrd.Selecti on.TypeText('технологий организации межпроцессного взаимодействия'#13#10#13#10);
   //Задаем новые параметры шрифта
   Wrd.Selection.Font.Size:= 12;
   Wrd.Selection.Font.Bold:= false;
   Wrd.Selection.Font.Italic:= true;
   Wrd.Selecti on.TypeText('Подпись:');
   Wrd.Selection.Font.Bold:= true;
   Wrd.Selecti on.TypeText('Delphi'#13#10#13#10);
   end;

   Особых вопросов данный фрагмент вызывать не должен, так как настройка шрифта и вывод теста производятся посредством вызова простых функций (TypeText печатает передаваемый в качестве параметра текст) и заданием соответствующих свойств. Однако стоит пояснить, что последовательность символов #13#10 эквивалентна переходу на новую строку.
   Процедура добавления таблицы в документ Word выглядит следующим образом (листинг 9.4).
 //-- Листинг 9.4. Добавление таблицы --// 
   procedureTFormWord.bnAddTableClick(Sender: TObject);
   begin
   //Процедура добавления новой таблицы
   Wrd.ActiveDocument.Tables.Add(Wrd.Selection.Range,3,3);
   end;

   Таблица содержит три столбца и столько же строк. Далее следует пояснить обработчик нажатия кнопки сохранения документа (листинг 9.5).
 //-- Листинг 9.5. Сохранение документа Microsoft Word --// 
   procedureTFormWord.bnSaveClick(Sender: TObject);
   begin
   //Сохранение документа
   Wrd.ActiveDocument.SaveAs (ExtractFilePath (Application.EXEName) +
   '_result.DOC');
   end;

   Сохранение осуществляется путем вызова метода SaveAs объекта ActiveDocument, который в качестве параметра принимает путь к файлу. После нажатия кнопки сохранения документ с текстом записывается в файл (_result. doc) папки, из которой была запущена программа.
   Процедура завершения работы основана на вызове метода Quit (листинг 9.6).
 //-- Листинг 9.6. Завершение работы с Microsoft Word --// 
   procedure TFormWord.bnExitWordClick(Sender: TObject);
   begin
   //Завершение приложения
   Wrd.Quit;
   end;

   Рассмотренное приложение является примитивным контроллером автоматизации и может служить отправной точкой для создания более сложных и функциональных программ автоматического составления отчетов и т. п.
   Далее будет создано приложение, которое будет подключаться к серверу COM Microsoft Word и отображать текст, дату и время вывода этого текста в активный документ при его смене (переключении между документами). На этот раз будут использованы компоненты WordDocument и WordApplication вкладки Servers.
   Создайте новый проект и на главную форму приложения поместите компоненты WordDocument и WordApplication. Далее укажите свойству ConnectKind компонента WordApplication значение ckRunningInstance, а также свойству AutoConnect значение True. В данном случае приложение Microsoft Word создаваться не будет, а программа подключится к уже существующему серверу. Основную практическую ценность здесь представляет механизм определения активного документа и добавление в него текста, даты и времени (листинг 9.7).
 //-- Листинг 9.7. Реакция на смену активного документа --// 
   procedure TFormActiveWord.WordApplicationActiveDocumentChange(Sender: TObject);
   begin
   //Подключаемся к текущему документу
   WordDocumentNew.ConnectTo(WordApplicationActive.ActiveDocument);
   //Контроллер добавляет новую строку в текущий документ
   WordDocument New.Range.InsertAfter(#13#10+'Переход к документу'+ #13#10+
   WordApplicationActi ve.Acti veDocument .Get_FullName+'произведен:'+
   DateTimeToStr(Now));
   end;

   Как вы, наверное, заметили, подключение к уже существующему серверу происходит каждый раз после смены активного документа. В этот момент в содержимое документа записывается текстовая строка, дата и время перехода к этому документу.
   Чтобы увидеть работу этого приложения, запустите Microsoft Word и создайте в нем два документа. Запустите созданный пример и поочередно активизируйте документы (щелчком кнопкой мыши на Панели задач).


   Трюки в Microsoft Excel

   Не менее популярным и функциональным приложением из пакета Microsoft Office является Microsoft Excel. Это программа предназначена для работы с электронными таблицами. Как и Microsoft Word, Microsoft Excel также обладает возможностью создания и редактирования документов (в данном случае таблиц) посредством использования технологии COM. Преимущества использования Microsoft Excel очевидны, так как она предоставляет широкий спектр возможностей по построению диаграмм, графиков, произведению различных расчетов и пр. Поэтому в качестве примера будет создано приложение, выполняющее запуск Microsoft Excel, добавление новой книги, создание листа и помещение в его ячейки текста и формул.
   Как и в случае с Microsoft Word, будет использоваться объект типа OleVariant. Однако методы и свойства COM-сервера поменяются. Рассмотрим исходный текст приложения, предназначенного для выполнения несложных операций с сервером Microsoft Excel (листинг 9.8).
 //-- Листинг 9.8. Работа с Microsoft Excel --// 
   unit COMinExcel;
   interface
   uses
   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
   Dialogs,
   //Включаем модуль работы с COM-объектами
   ComObj;
   type
   TFormCOMExcel = class(TForm)
   //Процедура, вызываемая при создании формы
   procedureeeFormCreate (Sender: TObject);
   //Процедура, вызываемая при завершении работы приложения
   procedure FormDestroy(Sender: TObject);
   private
   {Private declarations}
   public
   {Public declarations}
   end;
   var
   FormCOMExcel: TFormCOMExcel;
   //Объявление объекта OleVariant с именем Microsoft Excel
   Excel: OleVariant;
   implementation
   {$R *.dfm}
   procedure TFormCOMExcel.FormCreate(Sender: TObject);
   begin
   //Инициализируем объект
   Excel:= CreateOleObject('Excel.Application');
   //Устанавливаем видимым окно приложения Microsoft Excel
   Excel.Visible:= true;
   //Добавляем новую книгу
   Excel.Application.Workbooks.Add;
   //Вводим текст в ячейку с индексом E5
   Excel.Application.Worksheets.Item['Лист 1'].
   Cells.Item[5,5].FormulaR1C1:= '! ТЕКСТ !';
   //Задаем характеристики ишрифта
   Excel.Application.Worksheets.Item[' Лист 1'].
   Cells.Item[1,1].Font.Bold:= true;
   //В ячейку с индексом A1 записываем формулу
   Excel.Application.Worksheets.Item['Лист 1'].
   Cells.Item[1,1].FormulaR1C1:= '=18*2';
   end;
   procedure TFormCOMExcel.FormDestroy(Sender: TObject);
   begin
   //Закрываем приложение Microsoft Excel
   Excel.Quit;
   end;
   end.

   Программа, представленная в данном листинге, демонстрирует основы удаленного управления приложением Microsoft Excel. Запуск Microsoft Excel и заполнение ячеек новой таблицы происходит в функции FormCreate. Во время создания главной формы приложения-примера на экране появляется окно программы Microsoft Excel с числом в ячейке А1 и текстом в ячейке Е5. Хотя в ячейку А1 было записано выражение =18*2, на экране в этой ячейке отображается число 3 6, так как Microsoft Excel автоматически преобразует выражения в ячейках.




   Глава 10
   Окна других приложений


   • Ловушки Windows
   • Программа «Оконный шпион»

   В данной главе будут использованы сведения, приведенные в предыдущих главах (а точнее, в главах 1, 2 и 8), для построения программы, позволяющей проводить различные операции с окнами приложений. Вы также дополнительно познакомитесь с техникой применения ловушек (hook) в Windows и увидите пример реального использования проецирования файла в память для обмена данными между несколькими приложениями. Причем второе в примере обусловлено особенностью работы ловушек, следящих за работой других приложений. Вы также узнаете, как перечислять все открытые окна и, соответственно, получать к ним доступ. Но обо всем по порядку.


   Ловушки Windows


   Из материала предыдущих глав вам должен быть понятен или, по крайней мере, известен механизм, используемый Windows для управления окнами приложений, – сообщения. Вероятно, большая мощь этого механизма и, в то же время, его уязвимость заключаются в возможности отсылать сообщения любым окнам (окнам одного процесса или окнам других процессов).
   В Windows также предусмотрен мощный механизм, позволяющий следить за некоторыми важными событиями в системе и, конечно, проводить мониторинг сообщений, получаемых различными окнами. Речь идет об установке так называемых ловушек. Ловушка представляет собой функцию, вызываемую при возникновении определенного события, например, перед получением каким-либо окном нового сообщения, при нажатии клавиши, записи события в системный журнал и т. д.: все зависит от того, для каких событий разработчики предусмотрели ловушки. Интересен тот факт, что в Windows предусмотрены даже ловушки для отладки других ловушек.
   Здесь будут рассмотрены некоторые наиболее простые виды ловушек, перехватывающих сообщения окон. По глобальности действий рассмотренные ловушки являются устанавливаемыми на отдельный поток – такой подход безопаснее для системы, если в функции-ловушке возникнет ошибка.
   Начинается создание ловушки с написания собственно функции-ловушки, имеющей следующий прототип:

   function HookProc(code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT stdcall;

   О параметре code можно сказать, что он используется для обозначения тех случаев, когда функция ловушки должна вызвать специальную API-функцию CallNextHookEx и вернуть значение, возвращенное этой API-функцией. Назначения параметров wParam и lParam этой функции сильно зависят от того, для реакции на какое именно событие предназначена ловушка.
   Для регистрации ловушки используется API-функция SetWindowsHookEx, имеющая следующий прототип:

   function SetWindowsHookEx(idHook: Integer; //Тип ловушки
   lpfn: TFNHookProc; //Адрес функции-ловушки
   hmod: HINST; //Используемый модуль, в котором
   //расположена функция ловушки
   dwThreadId: DWORD //Идентификатор потока, для
   //которого создается ловушка
   ): HHOOK; stdcall;

   Если создание ловушки завершено успешно, функция SetWindowsHookEx возвращает дескриптор новой ловушки (ненулевое значение).
   Для удаления ловушки используется функция UnhookWindowsHookEx, принимающая единственный параметр – дескриптор ловушки, возвращенный функцией SetWindowsHookEx. Удалять ловушку нужно обязательно, поэтому, по крайней мере при закрытии приложения, не следует забывать вызывать функцию UnhookWindowsHookEx.
   Теперь несколько слов о функции CallNextHookEx. Ее объявление выглядит следующим образом:

   function CallNextHookEx(hhk: HHOOK; nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;

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


   Виды ловушек

   Ниже представлен список некоторых простых типов ловушек, а именно констант из модуля Windows, обозначающих эти типы и передаваемых в функцию SetWindowsHookEx:
   • WH_CALLWNDPROC – функция ловушки вызывается каждый раз до вызова функции обработки сообщений окон, созданных наблюдаемым потоком;
   • WH_CALLWNDPROCRET – вызывается каждый раз при возврате из функции обработки сообщений окон наблюдаемого потока;
   • WH_KEYBOARD – функция ловушки вызывается перед обработкой сообщений WM_KEYDOWN и WM_KEYUP оконной функцией наблюдаемого потока;
   • WH_MOUSE – вызывается перед обработкой оконной функцией наблюдаемого потока сообщений от манипулятора «мышь».
   Значения параметров lParam и wParam функции-ловушки в каждом из перечисленных случаев подробно рассмотрены ниже.
 //-- Перехват вызова оконной функции --// 
   Для WH_CALLWNDPROC, которая, кстати, используется в рассматриваемом далее приложении, два последних параметра функции-ловушки имеют следующие значения:
   • wParam – равен нулю, если сообщение послано в окно тем же потоком, в котором исполняется функция ловушки, и не равен нулю, если сообщение послано другим потоком;
   • lParam – указатель на структуру TCWPStruct, содержащую информацию о сообщении, которое передано окну (и будет передано в оконную функцию после возврата из функции-ловушки).
   Объявление структуры TCWPStruct с описанием полей выглядит следующим образом:

   type TCWPStruct = packed record
   lParam: LPARAM; //Параметр трсообщения
   wParam: WPARAM; //Параметр сообщения
   message: UINT; //Код сообщения
   hwnd: HWND; //Окно, которому адресовано сообщение
   end;

   Ниже приведен пример преобразования параметра lParam функции-ловушки к указателю на структуру с последующей проверкой кода сообщения (фрагмент программы):

   var hook_data: hook_data: ^TCWPStruct;
   begin
   hook_data:= Pointer(lParam);
   if hook_data^.message = WM_SIZE then
   begin
   //Реагируем на изменение размера окна
   end;
   end

   Получение доступа к данным, передаваемым в остальные функции-ловушки (а именно несложное преобразование типов у операции с указателем), выполняется аналогичным образом, поэтому более демонстрироваться не будет.
 //-- Перехват возврата из оконной процедуры --// 
   Для WH_CALLWNDPROCRET значения параметров wParam и lParam функции-ловушки следующие:
   • wParam – равен нулю, если сообщение послано другим процессом, и не равен нулю в противном случае;
   • lParam – указатель на структуру TCWPRetStruct, содержащую информацию о сообщении, которое передано окну (и было передано в оконную функцию до вызова функии-ловушки).
   Объявление структуры TCWPRetStruct выглядит следующим образом:

   type TCWPRetStruct = packed record
   lResult: LRESULT; //Значение, возвращенное оконной функцией
   lParam: LPARAM; //Параметр сообщения
   wParam: WPARAM; //Параметр сообщения
   message: UINT; //Код сообщения
   hwnd: HWND; //Дескриптор окна-получателя
   end;

 //-- Перехват сообщений клавиатурного ввода --// 
   Для WH_KEYBOARD значения параметров wParam и lParam функции-ловушки следующие:
   • wParam – код нажатой клавиши;
   • lParam – первые 16 бит этого параметра означают количество повторений нажатия, старшие же 16 бит используются для дополнительного описания состояния клавиатуры в момент нажатия клавиши.
   Параметры wParam и lParam полностью аналогичны параметрам сообщений WM_ KEYDOWN и WM_KEYUP.
 //-- Перехват сообщений от мыши --// 
   В ловушку WH_MOUSE в параметрах wParam и lParam передаются следующие значения:
   • wParam – код сообщения мыши;
   • lParam – указатель на структуру TMouseHookStruct.
   Объявление структуры TMouseHookStruct выглядит следующим образом:

   type TMouseHookStructttt= packeddrecord
   pt: TPoint int; //Экранные координаты указателя мыши
   hwnd: HWND; //Дескриптор окна-получателя сообщения
   wHitTestCode: UINT; //Код, возвращенный оконной функцией в ответ
   //на сообщение WM_NCHITTEST
   dwExtraInfo: DWORD; //Дополнительные данные
   end;

   Если вы забыли, какое значение для окна имеет сообщение WM_NCHITTEST, то можете вновь обратиться к гл. 1.


   Расположение функции-ловушки и DLL

   Теперь немного о расположении функции-ловушки.
   Казалось бы, что здесь может быть особенного: написал функцию в модуле, строго соответствующую приведенному ранее прототипу, передал ее адрес в функцию SetWindowsHookEx и используй ловушку. Но не так все просто. Функция ловушки может находиться в исполняемом файле только в том случае, если предполагается использовать ее для перехвата сообщений потока (потоков) того же процесса. Тогда в функцию создания ловушки в качестве параметра HMOD следует передавать нулевое значение.
   Если же предполагается слежение за другими приложениями (за потоками других процессов), то функция ловушки должна быть экспортируемой функцией DLL. Тогда в функцию SetWindows HookEx передается дескриптор модуля DLL (похоже, что это адрес в адресном пространстве процесса, куда спроецирован файл DLL). Библиотека (DLL) может загружаться как при запуске приложения (если используется так называемое load-time-связывание), так и динамически с помощью API-функции LoadLibrary:

   function LoadLibrary(lpLibFileName: PChar): HMODULE; stdcall;

   Функция принимает в качестве параметра путь DLL и возвращает дескриптор загруженного модуля (или 0 в случае ошибки). Если библиотека больше не нужна, то можно вызвать функцию FreeLibrary, передав в качестве единственного параметра возращенный ранее функцией LoadLibrary дескриптор модуля DLL.
   Возвращаясь к теме расположения ловушки, следует задасться вопросом, почему используется именно DLL. Чем плохо расположение ловушки в EXE-модуле приложения? Самое время вспомнить о том, что каждый процесс в Windows выполняется в своем собственном адресном пространстве. Поэтому адрес функции в исполняемом файле одного процесса вполне может быть адресом структуры данных где-то внутри другого процесса (рис. 10.1).
   Рис. 10.1. Пример адресного пространства разных процессов

   В отличие от EXE-файлов, файлы библиотек легко проецируются в адресное пространство использующего их процесса. Разместив функцию ловушки в DLL и указав дескриптор модуля этой DLL, мы предоставляем системе полную информацию для того, чтобы она могла:
   • проецировать библиотеку с ловушкой в адресное пространство исследуемого процесса;
   • однозначно определять положение (адрес) функции-ловушки в адресном пространстве исследуемого процесса.
   Описанные выше манипуляции с DLL проиллюстрированы на рис. 10.2 (Процесс 2 на рисунке – процесс, в который внедряется ловушка).
   Рис. 10.2. Загрузка DLL с ловушкой в адресное пространство исследуемого процесса

   Теперь нет никаких препятствий для вызова функции-ловушки при наблюдении за другим процессом.

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




   Программа «Оконный шпион»


   Наконец, можно переходить к практической части главы: рассмотрим создание программы, позволяющей составлять дерево всех окон, просматривать и изменять их свойства, а также перехватывать сообщения выбранного окна (недаром столько времени было потрачено на рассмотрение ловушек Windows).
   Несмотря на свое «противозаконное» название, рассматриваемая программа может быть весьма полезна при отладке приложений, а не при их взломе и шпионаже (хотя многое зависит от добросовестности лица, использующего программу). В частности, с помощью этой программы была найдена ошибка, закравшаяся в один из примеров, приведенных в гл. 2: из-за неправильной установки стилей при ручном создании главного окна программы не удавалось добиться правильной перерисовки элемента управления «рамка».


   Составление списка открытых окон

   Список (а точнее, дерево) окон, открытых в момент запуска программы, показан на рис. 10.3.
   Рис. 10.3. Дерево открытых окон

   Форма, показанная на рис. 10.3, имеет имя frmMain. Элемент управления TreeView имеет имя tree. Часть программы, отвечающая за построение дерева, относительно проста. Она использует вскользь рассмотренный в гл. 2 механизм перечисления окон.
   Составление дерева окон начинается с процедуры LoadWindowsTree, которая и запускает перечисление окон верхнего уровня, то есть окон, родителем которых является окно Рабочий стол (листинг 10.1).
 //-- Листинг 10.1. Начало составления дерева окон --// 
   procedure TfrmMain.LoadWindowsTree();
   var
   desktop: TTreeNode;
   //enInfo: TEnumInfo;
   begin
   tree.Items.Clear;
   //Добавление узла для Рабочего стола
   desktop:= tree.Items.Add(t ree.Items.GetFirstNode, 'Рабочий стол');
   //Перечисление окон
   enInfo.tree:= tree;
   enInfo.parent:= desktop;
   EnumWindows(Addr(NewWindow), Integer(Addr(enInfo)));
   end;

   Сразу следует привести объявление структуры, интенсивно используемой (далее это будет видно) при составлении дерева:

   Type
   TEnumInfo = Record
   tree: TTreeView; //Компонент TreeView
   parent ntnt: TTreeNode; //Элемент дерева, соответствующий текущему
   //окну, дочерние окна которого перечисляются
   end;

   При нахождении каждого нового окна вызывается функция NewWindow (ее адрес передан в API-функцию EnumWindows). Функция NewWindow (листинг 10.2) решает две задачи. Во-первых, она добавляет в дерево элемент, соответствующий найденному окну. Во-вторых, она запускает поиск дочерних окон относительно найденного окна, что позволяет перечислить все окна (от главной формы приложения до кнопок, надписей и т. д.).
 //-- Листинг 10.2. Добавление в дерево элемента и поиск дочерних окон --// 
   function NewWindow(wnd: HWND; param: LPARAM):BOOL; stdcall;
   var
   wndNode, parentNode: TTreeNode;
   begin
   wndNode:= AddWindowToTree(wnd); //Добавление информации об окне в дерево
   //Перечисление дочерних окон
   parentNode:= enInfo.parent;
   enInfo.parent:= wndNode;
   EnumChildWindows(wnd, Addr(NewWindow), param);
   enInfo.parent:= parentNode;
   //Продолжать перечисление (после перечисления всех дочерних окон)
   NewWindow:= True;
   end;

   Используемая в листинге 10.3 функция AddWindowToTree добавляет элемент, соответствующий найденному окну, в дерево (определяет текст заголовка окна и имя оконного класса).
 //-- Листинг 10.3. Добавление элемента, соответствующего окну, в дерево --// 
   function AddWindowToTree(wnd: HWND): TTreeNode;
   var
   caption, classname: String;
   text: String;
    node: TTreeNode;
   begin
   //Получение текста окна
   SetLength(caption, SendMessage(wnd, WM_GETTEXTLENGTH, 0, 0) + 1);
   SetLength(caption, SendMessage(wnd, WM_GETTEXT, Length(caption),
   Integer(PAnsiChar(caption))));
   //Имя класса окна
   SetLength(classname, 1024);
   SetLength(classname, GetClassName(wnd, PAnsiChar(classname), 100));
   //Формирование текста для элемента и добавление его в дерево
   text:= '"'+ caption + '" '+ classname;
   node:= enInfo.tree.Items.AddChild(enInfo.parent, text);
   node.Data:= Pointer(wnd); //Не забываем запомнить дексриптор окна
   AddWindowToTree:= node;
   end;

   Вот, собственно, и все, что требуется для построения полного дерева окон, показанного на рис. 10.3.


   Получение информации об окне

   Следующей функцией оконного шпиона является получение более или менее полной информации об окне, выбранном в дереве. Форма с информацией о выделенном в дереве окне (в данном случае выбрано меню Пуск) показана на рис. 10.4.
   Рис. 10.4. Форма свойств окна

   По команде меню Правка ► Свойства вызывается метод ShowWindowProp созданного при запуске программы объекта frmWindowProp. Этот метод принимает в качестве параметра дескриптор окна, информацию о котором нужно отобразить (дескриптор был сохранен в поле Data каждого элемента при построении дерева) (листинг 10.4).
 //-- Листинг 10.4. Подготовка формы свойств выбранного окна --// 
   procedure TfrmWindowProp.ShowWindowProp(window: HWND);
   begin
   wnd:= window;
   LoadWindowInfo();
   ShowModal(); //Не забываем показать сами себя
   end;

   Переменная wnd, в которой сохраняется переданный в ShowWindowProp дескриптор окна, является членом класса TfrmWindowProp. Она нужна для того, чтобы другие методы формы TfrmWindowProp могли получать доступ к дескриптору окна.
   Определение заголовка, имени класса, идентификатора окна, а также области экрана, занимаемой окном, выполняется в процедуре LoadWindowlnfo (листинг 10.5).
 //-- Листинг 10.5. Определение общей информации об окне --// 
   procedure TfrmWindowProp.LoadWindowInfo();
   var
   rect: TRect;
   buffer: String;
   begin
   //Сбор сведений об окне
   //..имя класса
   SetLength(buffer, 1024);
   SetLength(buffer, GetClassName(wnd, PAnsiChar(buffer), 1024));
   txtClassName.Text:= buffer;
   //..имя (заголовок) окна
   SetLength(buffer, SendMessage(wnd, WM_GETTEXTLENGTH, 0, 0) + 1);
   SendMessage(wnd, WM_GETTEXT, Length(buffer),
   Integer(PAnsiChar(buffer)));
   txtWindowName.Text:= buffer;
   //..идентификатор (или дескриптор меню) окна
   txtId.Text:= IntToStr(GetWindowLong(wnd, GWL_ID));
   //..оконный прямоугольник
   GetWindowRect(wnd, rect);
   txtWindowRect.Text:=
   '('+ IntToStr(rect.Left) + ','+ IntToStr(rect.Top) +')'+
   '– '+
   '('+ IntToStr(rect.Right) + ','+ IntToStr(rect.Bottom) +') '+
   IntToStr(rect.Right
   – rect.Left) + 'x'+ IntToStr(rect.Bottom –rect.Top);
   //Определение стиля окна
   LoadWindowStyle();
   LoadWindowExStyle();
   end;

   Если вы внимательно просмотрели листинг 10.5, то наверняка заметили вызовы двух процедур в двух последних строках. Процедура LoadWindowStyle заполняет списки используемых и доступных оконных стилей (рис. 10.4), а процедура LoadWindowExStyle, соответственно, заполняет списки используемых и доступных дополнительных (или расширенных) стилей окна.
   Реализация процедуры LoadWindowStyle приведена в листинге 10.6.
 //-- Листинг 10.6. Заполнение списков оконных стилей --// 
   procedure TfrmWindowProp.LoadWindowStyle();
   var
   i: Integer;
   style: DWORD;
   begin
   style:= GetWindowLong(wnd, GWL_STYLE);
   lstStyle.Clear();
   lstAvailStyle.Clear();
   //Выделение из 32-битного значения составляющих стиля окна
   for i:= 0 to 177do
   if styles[i].value and style <> 0 then
   begin
   //Стиль используется
   lstStyle.Items.Add(styles[i].name);
   styles[i].used:= True;
   end
   else
   begin
   //Стиль не используется
   lstAvailStyle.Items.Add(styles[i].name);
   styles[i].used:= False;
   end;
   end;

   Вместо громоздкой проверки наличия в значении, возвращенном API-функцией GetWindowLong, битов каждого возможного стиля с помощью, например, функции case, здесь используется глобальный массив styles структур Stylelnfo. Объявление типа структуры (записи) StyleInfo осуществляется следующим образом:

   type
   StyleInfooo= record
   value: DWORD; //Код стиля
   name: String; //Текстовое обозначение стиля
   used: Boolean; //Служебное поле
   end;

   Каждый элемент массива styles хранит информацию об определенном оконном стиле. Объявление этого массива, как и структуры StyleInfo и прочих рассмотренных в этом разделе типов данных, находится в модуле WindowData, расположенном на диске в папке с номером главы.
   Ниже приведено объявление массива styles (флаги стиля, являющиеся комбинацией других флагов, в массив не попали) (листинг 10.7).
 //-- Листинг 10.7. Массив с информацией об оконных стилях --// 
   styles: array [0..17] of StyleInfo =
   (
   (value: WS_BORDER; name: 'WS_BORDER'),
   (value: WS_CAPTION; name: 'WS_CAPTION'),
   (value: WS_CHILD; name: 'WS_CHILD'),
   (value: WS_CLIPCHILDREN; name: 'WS_CLIPCHILDREN'),
   (value: WS_DISABLED; name: 'WS_DISABLED'),
   (value: WS_DLGFRAME; name: 'WS_DLGFRAME'),
   (value: WS_HSCROLL; name: 'WS_HSCROLL'),
   (value: WS_MAXIMIZE; name: 'WS_MAXIMIZE'),
   (value: WS_MAXIMIZEBOX; name: 'WS_MAXIMIZEBOX'),
   (value: WS_MINIMIZE; name: 'WS_MINIMIZE'),
   (value: WS_MINIMIZEBOX; name: 'WS_MINIMIZEBOX'),
   (value: WS_OVERLAPPED; name: 'WS_OVERLAPPED'),
   (value: WS_POPUP; name: 'WS_POPUP'),
   (value: WS_SYSMENU; name: 'WS_SYSMENU'),
   (value: WS_TABSTOP; name: 'WS_TABSTOP'),
   (value: WS_THICKFRAME; name: 'WS_THICKFRAME'),
   (value: WS_VISIBLE; name: 'WS_VISIBLE'),
   (value: WS_VSCROLL; name: 'WS_VSCROLL')
   );

   Процедура LoadWindowExStyle реализована практически так же, как и LoadWindowStyle, только она заполняет списки lstExStyle и lstAvailExStyle и обращается к массиву exstyles, а не styles. Поэтому достаточно привести объявление только массива exstyles (листинг 10.8).
 //-- Листинг 10.8. Массив с информацией о дополнительных оконных стилях --// 
   exstyles: array [0..18] of StyleInfo =
   (
   (value: WS_EX_ACCEPTFILES; name: 'WS_EX_ACCEPTFILES'),
   (value: WS_EX_APPWINDOW; name: 'WS_EX_APPWINDOW'),
   (value: WS_EX_CLIENTEDGE; name: 'WS_EX_CLIENTEDGE'),
   (value: WS_EX_CONTEXTHELP; name: 'WS_EX_CONTEXTHELP'),
   (value: WS_EX_CONTROLPARENT; name: 'WS_EX_CONTROLPARENT'),
   (value: WS_EX_DLGMODALFRAME; name: 'WS_EX_DLGMODALFRAME'),
   (value: WS_EX_LAYERED; name: 'WS_EX_LAYERED'),
   (value: WS_EX_LEFT; name: 'WS_EX_LEFT'),
   (value: WS_EX_LEFTSCROLLBAR; name: 'WS_EX_LEFTSCROLLBAR'),
   (value: WS_EX_MDICHILD; name: 'WS_EX_MDICHILD'),
   (value: WS_EX_NOACTIVATE; name: 'WS_EX_NOACTIVATE'),
   (value: WS_EX_NOINHERITLAYOUT; name: 'WS_EX_NOINHERITLAYOUT'),
   (value: WS_EX_NOPARENTNOTIFY; name: 'WS_EX_NOPARENTNOTIFY'),
   (value: WS_EX_RIGHTSCROLLBAR; name: 'WS_EX_RIGHTSCROLLBAR'),
   (value: WS_EX_STATICEDGE; name: 'WS_EX_STATICEDGE'),
   (value: WS_EX_TOOLWINDOW; name: 'WS_EX_TOOLWINDOW'),
   (value: WS_EX_TOPMOST; name: 'WS_EX_TOPMOST'),
   (value: WS_EX_TRANSPARENT; name: 'WS_EX_TRANSPARENT'),
   (value: WS_EX_WINDOWEDGE; name: 'WS_EX_WINDOWEDGE')
   );


   Изменение оконных стилей

   Изменять стили окна «на лету» ничуть не сложнее, чем их определять: с помощью API-функций GetWindowLong и SetWindowsLong. Пример кода добавления флага, обозначение которого выбрано в списке доступных стилей, приведен в листинге 10.9.
 //-- Листинг 10.9. Добавление оконного стиля --// 
   procedure TfrmWindowProp.cmbAddStyleClick(Sender: TObject);
   var
   style: DWORD;
   addstyle: DWORD;
   begin
   if lstAvailStyle.ItemIndex = –1 then Exit;
   //Удаление выбранного стиля окна
   //..определяем, какой стиль удалить
   addstyle:=
   styles[GetStyleIndex(lstAvailStyle.ItemIndex, False)].value;
   //..вычисляем и устанавливаем новое значение стиля окна
   style:= GetWindowLong(wnd, GWL_STYLE);
   style:= style or addstyle;
   SetWindowLong(wnd, GWL_STYLE, style);
   //..перерисуем все окна
   InvalidateRect(0, nil, True);
   //Обновим список стилей окна
   LoadWindowStyle();
   end;

   Удаление флага стиля производится аналогично добавлению – просто к битам стиля окна применяется другая операция (листинг 10.10).
 //-- Листинг 10.10. Удаление оконного стиля --// 
   procedure TfrmWindowProp.cmbDelStyleClick(Sender: TObject);
   var
   style: DWORD;
   delstyle: DWORD;
   begin
   if lstStyle.ItemIndex = –1 then Exit;
   //Удаление выбранного стиля окна
   //..определяем, какой стиль удалить
   delstyle:= styles[GetStyleIndex(lstStyle.ItemIndex, True)].value;
   //..вычисляем и устанавливаем новое значение стиля окна
   style:= GetWindowLong(wnd, GWL_STYLE);
   style:= style and not delstyle;
   SetWindowLong(wnd, GWL_STYLE, style);
   //..перерисуем все окна
   InvalidateRect(0, nil, True);
   //Обновим список стилей окна
   LoadWindowStyle();
   end;

   После удаления или добавления оконного стиля инициируется перерисовка всех окон, чтобы проявился результат проведенной операции.
   Удаление и добавление дополнительных (расширенных) оконных стилей выполняется аналогично, только при этом используется массив exstyles, функция GetExStylelndex и константа GWL_EXSTYLE, передаваемая в функции GetWindowLong и SetWindowLong.
   Что же за функция GetStylelndex, используемая в листинге 10.10? Эта функция позволяет определять положение в массиве styles стиля, выбранного в списке доступных или используемых стилей (верхний список) (листинг 10.11).
 //-- Листинг 10.11. Определение положения записи о нужном стиле --// 
   function TfrmWindowProp.GetStyleIndex(listIndex: Integer; used: Boolean)
   :Integer;
   var
   i, count: Integer;
   begin
   count:= 0;
   for i:= 0 to 17 do
   if styles[i].used = used then
   begin
   if count = listIndex then
   begin
   //Нашли
   GetStyleIndex:= i;
   Exit;
   end;
   Inc(count);
   end;
   GetStyleIndex:= 0;
   end;

   Функция GetStyleIndex принимает в качестве своих параметров номер строки в соответствующем списке и логическое значение, от истинности или ложности которого зависит, используемые или неиспользуемые стили будут подсчитываться внутри функции.
   Применение функции GetStyleIndex и введение в структуру StyleInfo поля used несколько усложняет алгоритм работы с массивом стилей, однако позволяет избавиться от постоянного перемещения данных, например, из массива доступных стилей в массив используемых. К тому же, в противном случае пришлось бы использовать по два массива для обычных и дополнительных оконных стилей.


   Перехват сообщений

   Далее рассмотрим самую сложную часть программы, отвечающую за перехват сообщений выбранного окна. Форма, ведущая статистику перехваченных сообщений, показана на рис. 10.5.
   Рис. 10.5. Форма перехвата сообщений

   Показанная на рис. 10.5 форма носит имя frmMessages.
   Перехватчик сообщений состоит из двух частей: части программы (EXE), отвечающей за построение фильтра сообщений и обработку перехваченных сообщений, и ловушки, заключенной в DLL (hook\hook.dll).
   Взаимодействие ловушки и EXE-файла построено по следующей схеме.
   1. Из приложения вызываются функции создания и удаления ловушки (расположенные в DLL).
   2. При перехвате каждого сообщения функция-ловушка посылает окну (форме) frmMessages сообщение WM_SPY_NOTIFY (определенное пользователем, или, точнее, программистом, сообщение) (листинг 10.12).
   Однако ловушка ведь предназначена для работы в другом процессе, а если так, то как ей дать знать, какому именно окну посылать сообщения? Для этого и используется именованная проекция файла в память, в которой сохраняются данные, необходимые для ловушки. В проекции же файла ловушка также сохраняет информацию о перехваченном сообщении (код и параметры сообщения). Эта информация используется приложением, ведущим слежение.
   Данные в проекции файла хранятся в виде записи THookInfo, объявленной в модуле HookData. В этом же модуле объявлена константа с именем проекции файла, код сообщения WM_SPY_NOTIFY (листинг 10.12) и две служебные переменные, использование которых будет пояснено позже.
 //-- Листинг 10.12. Содержимое файла HookData.pas --// 
   type
   //Структура (запись), которая хранится в разделяемом файле и
   //используется для передачи данных между процессами
   THookInfo = record
   wnd: HWND; //Окно, за которым ведется наблюдение
   hook_handle: HHOOK; //Дескриптор ловушки
   spy_wnd: HWND; //Окно, уведомляемое о перехвате сообщения
   //Следующие поля заполняются при перехвате сообщения
   mess: UINT;
   wParam: WPARAM;
   lParam: LPARAM;
   end;
   var
   //Указатель на разделяемую область памяти
   hook_info: ^THookInfo;
   //Дескриптор проекции файла в память
   hFile: THandle;
   const
   //Имя япроекции иифайла
   strFileMapName = 'TricksDelphi_WinSpy_Mapping';
   //Сообщение для уведомления окна-шпиона
   WM_SPY_NOTIFY = WM_USER + 1;

 //-- Построение фильтра и обработка перехваченных сообщений --// 
   Теперь вернемся к приложению-шпиону, а точнее, к той его части, которая отвечает за работу формы, показанной на рис. 10.5.
   Начнем с самого простого – управления фильтром сообщений. Оно построено по тому же принципу, что и управление списками оконных стилей (форма свойств окна, рассмотренная ранее).
   Структура, хранящая информацию о сообщении, выглядит следующим образом:

   type MessageInfo = record
   value: DWORD; //Код сообщения
   name: String; //Название сообщения
   used: Boolean; //Служебное поле
   end;

   При написании программы не ставилась цель поместить в фильтр все возможные сообщения, поэтому массив messages_list (листинг 10.13) содержит только 16 элементов. При необходимости вы можете добавить нужные сообщения самостоятельно, взяв их обозначения из модуля Windows.
 //-- Листинг 10.13. Сообщения, поддерживаемые программой --// 
   const
   mess_firsttt= 0;
   mess_lasttt= 15;
   var
   messages_list: array[ mess_first .. mess_last] ofMessageInfoo=
   (
   (value: WM_DESTROY; name: 'WM_DESTROY'; used: False),
   (value: WM_MOVE; name: 'WM_MOVE'; used: False),
   (value: WM_SIZE; name: 'WM_SIZE'; used: False),
   (value: WM_ACTIVATE; name: 'WM_ACTIVATE'; used: False),
   (value: WM_SETFOCUS; name: 'WM_SETFOCUS'; used: False),
   (value: WM_KILLFOCUS; name: 'WM_KILLFOCUS'; used: False),
   (value: WM_ENABLE; name: 'WM_ENABLE'; used: False),
   (value: WM_SETTEXT; name: 'WM_SETTEXT'; used: False),
   (value: WM_GETTEXT; name: 'WM_GETTEXT'; used: False),
   (value: WM_PAINT; name: 'WM_PAINT'; used: False),
   (value: WM_CLOSE; name: 'WM_CLOSE'; used: False),
   (value: WM_QUIT; name: 'WM_QUIT'; used: False),
   (value: WM_SIZING; name: 'WM_SIZING'; used: False),
   (value: WM_MOVING; name: 'WM_MOVING'; used: False),
   (value: WM_NOTIFY; name: 'WM_NOTIFY'; used: False),
   (value: WM_NCHITTEST; name: 'WM_NCHITTEST'; used: False)
   );

   Загрузка фильтра (выбранных и невыбранных сообщений в соответствующие списки) выполняется достаточно просто (листинг 10.14).
 //-- Листинг 10.14. Загрузка фильтра сообщений --// 
   proceduree TfrmMessages.LoadFilter();
   var
   i: Integer;
   begin
   //Загрузка фильтра сообщений
   lstAvailMessages.Clear();
   lstSelMessages.Clear();
   for i:= mess_first to mess_last do
   if messages_list[i].used then
   //Сообщение перехватывается
   lstSelMessages.Items.Add(messages_list[i].name)
   else
   lstAvailMessages.Items.Add(messages_list[i].name);
   end;

   При обращении к форме frmMessages, кроме загрузки фильтра, нужно выполнить некоторые дополнительные действия. Поэтому работа с этой формой начинается так же, как и с формой свойств окна – с вызова ее специального метода (листинг 10.15).
 //-- Листинг 10.15. Инициализация формы --// 
   procedureTfrmMessages.ShowMessages (wnd: HWND);
   begin
   self.wnd:= wnd;
   LoadFilter();
   ShowModal();
   end;

   При нажатии кнопок > (выбрать) и < (отменить выбор) происходит перемещение сообщений между списками фильтра (листинг 10.16).
 //-- Листинг 10.16. Перемещение сообщений между списками выбранных и доступных сообщений --// 
   procedure TfrmMessages.cmbAddMessageClick(Sender: TObject);
   var
   i: Integer;
   begin
   if lstAvailMessages.SelCount = 0 then Exit;
   //Включение выбранных сообщений в список перехватываемых
   for i:= lstAvailMessages.Count – 1 downto 0 do
   if lstAvailMessages.Selected[i] then
   messages_list[GetMessageIndex(i, False)].used:= True;
   //Отобразим изменения в списках
   LoadFilter();
   end;
   procedure TfrmMessages.cmDelMessageClick(Sender: TObject);
   var
   i: Integer;
   begin
   if lstSelMessages.SelCount = 0 then Exit;
   //Исключение выбранных сообщений из списка перехватываемых
   for i:= lstSelMessages.Count – 1 downto 0 do
   if lstSelMessages.Selected[i] then
   messages_list[GetMessageIndex(i, True)].used:= False;
   //Отобразим изменения в списках
   LoadFilter();
   end;

   Реализация функции GetMessageIndex, используемой в листинге 10.16, приведена в листинге 10.17.
 //-- Листинг 10.17. Функция GetMessageIndex --// 
   function TfrmMessages.GetMessageIndex(listIndex: Integer; used: Boolean)
   :Integer;
   var
   i, count: Integer;
   begin
   count:= 0;
   for i:= mess_first to mess_last do
   if messages_list[i].used = used then
   begin
   if count = listIndex then
   begin
   //Нашли
   GetMessageIndex:= i;
   Exit;
   end;
   Inc(count);
   end;
   GetMessageIndex:= 0;
   end;

   Теперь обратимся к реализации главной обязанности, выполняемой формой, – использованию ловушки.
   Слежение за выбранным в дереве окном (его дескриптор сохранен в поле wnd при инициализации формы) начинается и заканчивается при нажатии кнопки cmbStart. Код обработчика нажатия этой кнопки приведен в листинге 10.18.
 //-- Листинг 10.18. Запуск и остановка перехвата сообщений --// 
   procedureTfrmMessages.cmbStartClick(Sender: TObject);
   begin
   if cmbStart.Caption <> 'Остано вить' then
   begin
   //Начинаем слежение
   lvwMessages.Clear;
   //Создаем проекцию файла
   hFile:= CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE,
   0, SizeOf(THookInfo), strFileMapName);
   hook_info:= MapViewOfFile(hFile, FILE_MAP_WRITE, 0, 0,
   SizeOf(THookInfo));
   //Создание ловушки
   if InstallHook(wnd, frmMessages.Handle) then
   cmbStart.Caption:= 'Остановить'
   else
   begin
   //При ошибке удалим проекцию файла
   UnmapViewOfFile(hook_info);
   hook_info:= nil;
   CloseHandle(hFile);
   hFile:= 0;
   MessageBox(Handle, 'Ошибка при создании ловушки ',
   PAnsiChar(Application.Title), MB_ICONEXCLAMATION);
   end;
   end
   else
   begin
   //Заканчиваем слежение (удаляем ловушку и проекцию файла)
   RemoveHook();
   UnmapViewOfFile(hook_info);
   hook_info:= nil;
   CloseHandle(hFile);
   hFile:= 0;
   cmbStart.Caption:= ' Начать слежение';
   end;
   end;

   Как можно увидеть из приведенного кода, вся сложность на стороне приложения-шпиона состоит в создании и удалении проекции файла, а также в вызове двух экспортируемых из библиотеки hook.dll функций. Эти функции подключаются следующим объявлением:

   function InstallHook(wnd: HWND; spy: HWND): Boolean stdcall;
   external 'hook\hook.dll'name 'InstallHook';
   function RemoveHook(): Boolean stdcall;
   external 'hook\hook.dll'name 'RemoveHook';

   Для обработки сообщения WM_SPY_NOTIFY, посылаемого ловушкой, переопределена оконная процедура формы frmMessages (листинг 10.19).
 //-- Листинг 10.19. Обработка сообщения WMSPYNOTIFY --// 
   procedure TfrmMessages.WndProc(var Message: TMessage);
   var
   item: TListItem;
   i: Integer;
   begin
   if (Message.Msg = WM_SPY_NOTIFY) and (hook_info <> nil) then
   begin
   //Обрабатываем уведомление о приходе сообщения в наблюдаемое окно
   for i:= mess_first to mess_last do
   if (messages_list[i].value = hook_info^.mess) and
   messages_list[i].used then
   begin
   //Сообщение выбрано в фильтре – добавим запись в список
   item:= lvwMessages.Items.Add();
   item.Caption:= messages_list[i].name;
   item.SubItems.Add(IntToStr(hook_info^.wParam));
   item.SubItems.Add(IntToStr(hook_info^.lParam));
   end;
   end
   else
   inherited WndProc(Message);
   end;

 //-- Ловушка --// 
   Теперь обратимся к реализации самой ловушки. По рассмотренным ранее причинам ловушка размещена в отдельной библиотеке (hook\hook. dll на прилагаемом к книге диске в папке с номером главы). Если вы не знакомы с созданием DLL средствами Delphi, ознакомьтесь с приведенными далее краткими сведениями.
   Среда программирования Delphi замечательна тем, что позволяет простым образом выполнять довольно сложные вещи (правда, и при использовании сред разработки, скрывающих меньшее количество сложных деталей, например, Visual C++, создание DLL не является особенно сложной задачей).
   Итак, для создания DLL в простейшем, то есть данном случае, достаточно выполнить следующие действия.
   1. Создать соответствующий проект (откройте меню File ► New ► Other и на вкладке New выберите тип проекта DLL Wizard) (рис. 10.6).
   Рис. 10.6. Создание проекта DLL

   2. В DPR-файле получившегося проекта реализовать функции, которые предполагается экспортировать.
   3. Объявить функции, которые нужно экспортировать с помощью ключевого слова exports (листинг 10.20).
   Структура DLL реализованной в данном примере ловушки приведена в листинге 10.20.
 //-- Листинг 10.20. DLL ловушки без реализации функций --// 
   library hook;
   uses
   Windows,
   HookData;
   //
   ****************************************************
   //Экспортируемые функции
   function InstallHook(wnd: HWND; spy: HWND): Boolean stdcall; forward;
   function RemoveHook(): Boolean stdcall; forward;
   exports
   InstallHook,
   RemoveHook;
   //
   ****************************************************
   ...
   begin
   hook_info:= nil;
   hFilee e:= 0;
   end.

   Код после слова begin является кодом инициализации библиотеки (выполняется при загрузке DLL в память процесса), правда, как показали многочисленные эксперименты, проведенные во время написания и отладки ловушки, этот код не выполняется при загрузке DLL ловушки в адресное пространство другого процесса.
   Теперь обратимся к реализации экспортируемых функций InstallHook и RemoveHook. Как вы помните, только эти две функции вызываются из программы-шпиона. Функция установки ловушки представлена в листинге 10.21.
 //-- Листинг 10.21. Установка (создание) ловушки --// 
   function InstallHook (wnd: HWND; spy: HWND): Booleanstdcall;
   begin
   //Открываем проекцию файла (области файла подкачки)
   if not GetFileMapping() then
   begin
   //Не удалось спроецировать файл в память
   InstallHook:= False;
   Exit;
   end;
   //Сохраняем данные, необходимые для работы ловушки
   hook_info^.wnd:= wnd;
   hook_info^.spy_wnd:= spy;
   //Создаем ловушку
   if (GetWindowThreadProcessId(wnd) <> 0)
   then
   hook_info^.hook_handle:=
   SetWindowsHookEx(WH_CALLWNDPROC, WndProcHook,
   hInstance,, GetWindowThreadProcessId(wnd))
   else
   //Создание ловушки для потоков данного приложения было бы фатальным
   hook_info^.hook_handle:= 0;
   InstallHook:= hook_info^.hook_handle <> 0;
   //Освободим проекцию файла
   ReleaseFileMapping();
   end;

   Функция InstallHook использует глобальную переменную-указатель hook_info, которая объявлена в модуле HookData. Функция GetFileMapping, также используемая в листинге 10.21, связывает указатель hook_info с областью памяти, на которую проецируется файл. Соответственно, процедура ReleaseFileMapping отменяет проецирование файла в память (после этого использовать указатель hook_info нельзя).
   API-функция GetWindowThreadProcessId используется для определения идентификатора потока, создавшего наблюдаемое окно. Проверка неравенства значения, возвращенного этой функцией, нулю используется для того, чтобы в случае закрытия интересующего вас окна до запуска ловушки вы не начали следить за окнами приложения-шпиона.
   Работа с проецируемым файлом в ловушке будет рассмотрена чуть позже. Сейчас же обратимся к функции удаления ловушки, реализация которой приведена в листинге 10.22.
 //-- Листинг 10.22. Удаление ловушки --// 
   function RemoveHook(): Boolean stdcall;
   begin
   if GetFileMapping() then
   begin
   if hook_info^.hook_handle <> 0 then
   //Удаляем ловушку
   UnhookWindowsHookEx(hook_info^.hook_handle);
   //Закрываем проекцию файла
   ReleaseFileMapping();
   RemoveHook:= True;
   end
   else
   RemoveHook:= False;
   end;

   Здесь все просто и не требует подробного пояснения. Далее рассмотрим так часто используемые функцию GetFileMapping и процедуру ReleaseFileMapping, работающие с проекцией файла в память. Данная функция, код которой приведен в листинге 10.23, открывает проекцию файла в память и связывает указатель hook_ info с областью памяти, отведенной для проекции файла.
 //-- Листинг 10.23. Открытие проекции файла --// 
   function GetFileMapping(): Boolean;
   begin
   //Пытаемся открыть проекцию файла
   hFile:= OpenFileMapping(FILE_MAP_WRITE, False,
   PAnsiChar(strFileMapName));
   //Получаем адрес разделяемой памяти
   hook_info:= MapViewOfFile(hFile, FILE_MAP_WRITE, 0, 0,
   SizeOf(THookInfo));
   GetFileMapping:= hook_info <> nil;
   end;

   Способ реализации процедуры ReleaseFileMapping, обратной по своему назначению функции GetFileMapping, показан в листинге 10.24.
 //-- Листинг 10.24. Освобождение проекции файла --// 
   procedure ReleaseFileMapping();
   begin
   UnmapViewOfFile(hook_info);
   hook_info:= nil;
   CloseHandle(hFile);
   hFile:= 0;
   end;

   Функция GetFileMapping и процедура ReleaseFileMapping используют дополнительно глобальную переменную hFile (тип THandle), объявленную в модуле HookData.
   Наконец, пришла очередь рассмотреть функцию-ловушку. Код ее реализации приведен в листинге 10.25.
 //-- Листинг 10.25. Функция-ловушка --// 
   function WndProcHook(code: Integer; wparam: WPARAM; lparam: LPARAM): LRESULT stdcall;
   var
   hook_data: ^TCWPStruct;
   begin
   //Получим доступ к проекции файла
   if not GetFileMapping() then
   begin
   //Не удалось получить доступ к проекции файла. Ценой потери
   //сообщений не дадим возникнуть ошибкам доступа к памяти
   WndProcHook:= 0;
   Exit;
   end;
   if code < 0 then
   begin
   WndProcHook:= CallNextHookEx(hook_info^.hook_handle, code, wParam,
   lParam);
   //Освободим проекцию файла
   ReleaseFileMapping();
   Exit;
   end;
   //Можно обрабатывать сообщение
   hook_data:= Pointer(lParam);
   //Обрабатываем только сообщения нужного окна
   if hook_data^.hwnd = hook_info^.wnd then
   begin
   //Заполняем поля структуры в общей области памяти и посылаем
   //сообщение окну-шпиону
   hook_info^.mess:= hook_data^.message;
   hook_info^.wParam:= hook_data^.wParam;
   hook_info^.lParam:= hook_data^.lParam;
   PostMessage(hook_info^.spy_wnd, WM_SPY_NOTIFY, 0, 0);
   end;
   //Передаем сообщение для дальнейшей обработки
   WndProcHook:= CallNextHookEx(hook_info^.hook_handle, code, wParam,
   lParam);
   //Освободим проекцию файла
   ReleaseFileMapping();
   end;

   Код функции WndProc достаточно прост, поэтому подробно его работа рассматриваться не будет. Вместо этого поясню, для чего все же GetFileMapping и ReleaseFileMapping вызываются при обработке каждого перехваченного сообщения.
   Дело в том, что загрузка DLL в адресное пространство другого процесса отличается от штатной загрузки библиотеки, например, с помощью функции LoadLibrary: не вызывается код инициализации. Следовательно, невозможно, например, обнулить указатель hook_info или установить какой-либо другой признак того, была ли открыта проекция файла. Велика вероятность того, что без отсутствия ручной инициализации указатель hook_info не будет равен нулю. Как тогда определить, связан ли этот указатель с областью памяти, в которую спроецирован файл?
   Можно было бы, конечно, завести 64– или более битную переменную, которой присваивалось бы «магическое» число при первой инициализации указателя hook_ info. Однако в таком случае работоспособность данной программы носила бы вероятностный характер. Речь не идет о том, что в приведенном примере ловушка реализована оптимальным образом, просто альтернатива с использованием GetFileMapping и ReleaseFileMapping при написании программы показалась наиболее простой и легко поддающейся объяснению.




   Глава 11
   Сетевое взаимодействие


   • Краткое описание сетевых компонентов
   • Простой обмен данными
   • Слежение за компьютером по сети
   • Многопользовательский разговорник

   Организация надежного сетевого взаимодействия между приложениями или компонентами одного приложения зачастую является задачей довольно сложной даже для программиста со значительным опытом работы. Это правда, если пытаться самостоятельно использовать API сетевого взаимодействия, предоставляемый операционной системой (в данном случае – Windows). Однако при использовании компонентов Delphi, в которых уже реализованы рутинные операции по созданию соединений, пересылке данных, контролю ошибок и т. д., программирование сетевых приложений становится не только простым, но и увлекательным занятием.
   В данной главе будет рассмотрено несколько примеров создания несложных сетевых приложений, построенных с использованием архитектуры «клиент-сервер».


   Краткое описание сетевых компонентов

   В Delphi 7 количество компонентов, предусмотренных для программирования самых различных сетевых приложений, просто радует глаз (см. вкладки IndyClients и IndyServers). Правда, в книге будет рассмотрено построение приложения на базе только компонентов IdTCPServer и IdTCPClient (написание клиент-серверных приложений с использованием всех сетевых компонентов могло бы занять всю книгу).
   Начнем с компонента сервера IdTCPServer. Для использования возможностей сервера этот компонент нужно поместить на форму (компонент неотображаемый). При настройке компонента наиболее полезны следующие его свойства:
   • Active – активизирует или деактивизирует сервер (по умолчанию имеет значение False);
   • Bindings – настраивает серверные сокеты (присоединяет их к определенному порту компьютера, позволяет определять диапазон IP-адресов и портов клиентов) с помощью окна Binding Editor;
   • ListenQueue – ограничивает максимальное количество запросов на установление соединения от клиентов в очереди;
   • MaxConnections – позволяет ограничить максимальное количество клиентов, присоединяемых к серверу;
   • MaxConnectionReply – позволяет настроить сообщение, посылаемое сервером новым клиентам, когда их количество достигает значения, установленного в свойстве MaxConnections.
   Рассмотрим несколько подробнее настройку серверных сокетов с использованием свойства Bindings. На рис. 11.1 показано, как с помощью окна Binding Editor настроить сервер на обслуживание клиентов с любыми IP-адресами, при этом в данном случае серверный сокет присоединяется к порту 12340.
   Рис. 11.1. Использование окна Binding Editor

   Для более детальной настройки каждого серверного сокета можно использовать окна Object TreeView и Object Inspector так, как показано на рис. 11.2.
   Рис. 11.2. Настройка серверного сокета

   На этом настройку сервера можно завершить (хотя здесь использованы далеко не все возможности компонента IdTCPServer). Основная же работа сервера при обработке запросов клиентов может реализовываться в обработчике события OnExecute. В этот обработчик передается ссылка на объект TIdPeerThread – поток, ассоциированный с клиентом, присоединенным к серверу. Посредством этого объекта (а точнее, его свойства Connection) можно получать и отправлять данные, а также получать и устанавливать множество полезных параметров соединения. Первый пример использования объекта TIdPeerThread при обработке запроса клиента приведен немного ниже в листинге 11.1.
   Теперь рассмотрим порядок конфигурирования клиента (IdTCPClient), чтобы он был способен взаимодействовать с сервером. Чтобы использовать компонент TCP-клиента, достаточно поместить этот компонент на форму (компонент неотображаемый). После этого нужно указать значения, по меньшей мере, следующих его свойств (остальные упоминаются по мере необходимости в приведенных далее примерах):
   • Host – имя или IP-адрес компьютера, на котором запущен сервер;
   • Port – номер порта, к которому присоединен серверный сокет.
   По сути, даже эти свойства на этапе разработки формы настраивать необязательно. Приложение получается гораздо более гибким, если предоставлять, например, пользователю возможность выбирать (или вводить) имя или адрес сервера.


   Простой обмен данными

   Прежде чем приступить к работе с описанными в предыдущем разделе компонентами IdTCPServer и IdTCPClient, ознакомьтесь с созданием несложного клиент-серверного приложения, клиентская и серверная части которого выполняют следующие функции.
   • Клиентское приложение соединяется с сервером и отправляет ему введенную пользователем строку, ждет ответа, выводит полученный от сервера текст и затем отсоединяется от сервера.
   • Серверное приложение принимает строку от клиентского приложения и посылает ответ (также текстовый), после чего разрывает соединение. Кроме этого, приложение ведет подсчет количества обслуженных клиентов и запоминает IP-адрес компьютера, с которого пришел последний запрос.
   Реализация как серверного, так и клиентского приложения в данном случае предельно проста. Проект серверного приложения называется SimpleServer. Внешний вид формы сервера (во время работы приложения) представлен на рис. 11.3.
   Рис. 11.3. Внешний вид простого сервера

   Текстовое поле (Edit) с количеством обработанных запросов имеет имя txtCount, а текстовое поле с адресом последнего обслуженного компьютера названо txtFrom. Вся работа сервера заключается в обработке события Execute для компонента IdTCPServer, помещенного на форму (присоедините этот компонент к порту 12340 и установите значение свойства Active = True) (листинг 11.1).
 //-- Листинг 11.1. Реализация простого сервера --// 
   procedureTForm1. IdTCPServer 1 Execute (AThread: TIdPeerThread);
   var
   strText: string;
   begin
   //Принимаем от клиента строку
   strText:= AThread.Connection.ReadLn;
   //Отвечаем
   AThread.Connection.WriteLn('Принял строку:'+ strText);
   //Обно вим сведения на форме сервера (сервер многопоточный,
   //поэтому используем синхронизацию)
   section.Enter;
   Inc(processed,1);
   txtCount.Text:= IntToStr(processed);
   txtFrom.Text:= AThread.Connection.Socket.Binding.PeerIP;
   section.Leave;
   //Закрываем соединение с пользователем
   AThread.Connection.Disconnect;
   end;
   procedure TForm1.FormCreate(Sender: TObject);
   begin
   section:= TCriticalSection.Create;
   end;

   При ответе клиенту сервер повторяет принятую от него строку и добавляет текст 'Принял: в начало строки.
   Анализируя листинг 11.1, можно заметить, что даже в рассматриваемом простейшем сервере пришлось с помощью критической секции применить синхронизацию при обновлении внешнего вида формы (в секцию uses необходимо дополнительно добавить имя модуля SyncObjs).
   Теперь рассмотрим реализацию клиентской части (проект SimpleClient). Внешний вид клиентского приложения показан на рис. 11.4.
   Рис. 11.4. Внешний вид клиента

   Естественно, что для работы клиентского приложения на форму необходимо поместить экземпляр компонента IdTCPClient (его имя – IdTCPClient1). Свойству же Port этого компонента нужно присвоить значение 12 34 0. Текстовое поле (Edit) для ввода строки, подлежащей отправке не сервер, в рассматриваемом случае имеет имя txtMessage; текстовое поле (Edit), в которое вводится имя или адрес сервера, названо txtServer; поле со строками ответов (Memo) имеет имя txtResults.
   Вся работа клиентского приложения выполняется при нажатии кнопки Обработать. Текст соответствующего обработчика приведен в листинге 11.2.
 //-- Листинг 11.2. Реализация простого клиента --// 
   procedureTForm1. Button 1 Click(Sender: TObject);
   begin
   //Соединяемся с сервером и посылаем ему введенную строку
   IdTCPClient1.Host:= txtServer.Text;
   IdTCPClient1.Connect;
   IdTCPClient1.WriteLn(txtMessage.Text);
   txtMessage.Text:= '';
   //Ожидаем ответ и закрываем соединение
   txtResults.Lines.Append(IdTCPClient1.ReadLn);
   IdTCPClient1.Disconnect;
   end;


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

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


   Слежение за компьютером по сети

   Теперь рассмотрим более интересный пример использования сетевых компонентов IdTCPServer и IdTCPClient, который может быть полезен для людей, имеющих отношение к администрированию компьютеров сети.
   Серверная программа предварительно запускается на наблюдаемом компьютере. В этом примере программа-сервер позволяет клиентской программе получать следующие сведения о компьютере, на котором она (программа-сервер) запущена:
   • разрешение монитора;
   • глубину цвета для монитора;
   • полноразмерную копию экрана;
   • копию экрана, уменьшенную (или увеличенную) до заданных размеров.
   Для получения указанных сведений программа-клиент должна посылать серверу следующие строковые значения:
   • get_screen_width и get_screen_height– для получения ширины и высоты экрана в пикселах соответственно;
   • get_screen_colors – для получения значения установленной для монитора глубины цвета (бит на точку);
   • get_screen – для получения полноразмерной копии экрана;
   • get_screen: X,Y – для получения копии экрана, приведенной к размеру X х Y.
   Сначала рассмотрим реализацию сервера (проект SpyServer). Весь код, обеспечивающий работу сервера, помещен в модуль Unit1.pas формы Form1. Код обработчика запросов клиентов – главной процедуры для сервера – приведен в листинге 11.3.
 //-- Листинг 11.3. Обработчик клиентских запросов --// 
   procedureTForm1. IdTCPServer 1 Execute (AThread: TIdPeerThread);
   var
   strText: string;
   width, height, i: Integer;
   dc: HDC;
   begin
   //Принимаем от клиента строку
   strText:= AThread.Connection.ReadLn;
   //Определяем, что нужно выполнить
   if (strText = 'get_screen_height') then
   //Возвратим высоту экрана
   Athread.Connection.WriteInteger(Screen.Height)
   else if (strText = 'get_screen_width') then
   //Возвратим ширину экрана
   Athread.Connection.WriteInteger(Screen.Width)
   else if (strText = 'get_screen_colors') then
   begin
   //Возвратим количество бит на точку
   dc:= GetDC(0);
   Athread.Connection.WriteInteger(GetDeviceCaps(dc, BITSPIXEL));
   ReleaseDC(0, dc);
   end
   else if (strText = 'get_screen') then
   //Возвратим полноразмерную копию экрана
   SendScreen(Screen.Width, Screen.Height, AThread.Connection)
   else begin //строка вида 'get_screen:x,y'
   //Определим значения высоты и ширины, переданные пользователем
   strText:= Copy(strText, 12,Length(strText) -11);
   i:= Pos(',', strText); //Положение запятой
   width:= StrToInt(Copy(strText, 1, i-1));
   height:= StrToInt(Copy(strText, i+ 1, Length(strText) -i));
   //Возвратим копию экрана
   SendScreen(width, height, AThread.Connection);
   end;
   end;

   Используемая в листинге 11.3 процедура SendScreen, отправляющая клиенту копию экрана, приведена в листинге 11.4.
 //-- Листинг 11.4. Снятие копии экрана --// 
   //Процедура снимает копию экрана, преобразует полученное изображение к
   //заданному размеру и отправляет преобразованное изображение клиентской
   //программе
   procedure SendScreen(width: Integer; height: Integer;
   Connection: TIdTCPServerConnection);
   var
   ScreenCopy: TCanvas;
   gr: TBitmap;
   stream: TMemoryStream;
   rcDest, rcSource: TRect;
   begin
   rcDest:= Rect(0, 0, width, height); //Конечный размер изображения
   rcSource:= Screen.Deskt opRect; //Исходный размер изображения
   //Создаем канву и присоединяем ее к контексту Рабочего стола
   ScreenCopy:= TCanvas.Create;
   ScreenCopy.Handle:= GetDC(0);
   //Создаем объект для хранения копии экрана и копируем изображение
   gr:= TBitmap.Create;
   gr.Height:= height;
   gr.Width:= width;
   gr.Canvas.CopyRect(rcDest, ScreenCopy,rcSource);
   ReleaseDC(0, ScreenCopy.Handle);
   //Сохраняем изображение в поток данных
   stream:= TMemoryStream.Create;
   gr.SaveToStream(stream);
   //Отправляем изображение клиенту
   Connection.WriteStream(stream,True,True);
   stream.Clear;
   stream.Free;
   gr.Free;
   end;

   Как можно видеть, даже самая сложная операция рассматриваемого сервера – копирование изображения – реализуется довольно просто благодаря наличию такого стандартного класса, как TMemoryStream.
   При реализации сервера использован таймер. Он применен для скрытия формы сервера при запуске приложения (не забудьте установить его свойство Enabled равным True и свойство Interval равным 5 0). Компонент IdTCPServer (с именем IdTCPServerl) в этом примере присоединен к порту 12341 (не забудьте также установить свойство Active равным True).
   Теперь перейдем к реализации клиентского приложения (проект SpyClient). Внешний вид формы (Form1) клиента во время работы показан на рис. 11.5 (видно, что пользователь наблюдаемого компьютера только что проиграл в игру Сапер).
   Рис. 11.5. Внешний вид клиента слежения

   Описания, имена и значения настроенных вручную свойств самых важных компонентов формы клиента приведены в табл. 11.1.
 //-- Таблица 11.1. Основные компоненты формы клиента слежения и их свойства --// 
   Работа клиентского приложения начинается с соединения с сервером. Код, отвечающий за реализацию этой операции, приведен в листинге 11.5.
 //-- Листинг 11.5. Соединение с сервером --// 
   procedure TForm1.cmbConnectClick(Sender: TObject);
   begin
   if (cmbConn ect.Caption = 'Подключиться') then
   begin
   if (txtServer.Text = '') then
   //Не введено имя сервера
   MessageDlg('Введите имя машины-сервера в текстовое поле',
   mtInformation, [mbOK], 0)
   else begin
   //Подключаемся к серверу
   IdTCPClient1.Host:= txtServer.Text;
   try
   IdTCPClient1.Connect;
   except
   MessageDlg('Не удается соединиться с указанным сервером',
   mtError, [mbOK], 0);
   Exit;
   end;
   end
   end
   else begin
   //Отключаемся от сервера
   IdTCPClient1.Disconnect;
   end;
   end;

   Если соединение с сервером выполнено успешно, то выполняется обработчик TForm1. IdTCPClient1Connected, подготавливающий приложение-клиент к периодическим запросам данных с сервера (листинг 11.6).
 //-- Листинг 11.6. Действия, выполняемые при соединении с сервером --// 
   procedure TForm1.IdTCPClient1Connected(Sender: TObject);
   begin
   txtServer.Enabled:= False;
   cmbConn ect.Caption:= ' Отключиться ';
   //Начинаем периодически запрашивать данные с сервера
   Timer1.Enabled:= True;
   //Выполним первый запрос, не дожидаясь срабатывания таймера
   Timer 1 Timerr(Nil);
   end;

   При отсоединении от сервера также выполняются действия, прекращающие периодические запросы данных и переводящие клиента в состояние ожидания подключения (первоначальное состояние программы) (листинг 11.17).
 //-- Листинг 11.7. Действия при отсоединении от сервера --// 
   procedure TForm1.IdTCPClient1Disconnected(Sender: TObject);
   begin
   txtServer.Enabled:= True;
   cmbConn ect.Caption:= ' Подключиться ';
   Timer 1. Enableddd:= False;
   end;

   Самой сложной частью работы клиентского приложения является обработка данных, получаемых от сервера. Клиентское приложение запрашивает данные по таймеру и обрабатывает полученные данные. Код реализации этих действий приведен в листинге 11.8.
 //-- Листинг 11.8. Запрос и обработка данных, полученных с сервера --// 
   procedure TForm1.Timer1Timer(Sender: TObject);
   var
   stream: TMemoryStream;
   begin
   //Запрашиваем у сервера данные о наблюдаемом компьютере
   with (IdTCPClient1) do
   begin
   //... разрешение
   WriteLn('get_screen_width');
   WriteLn('get_screen_height');
   lblResolution.Caption:= IntToStr(ReadInteger) + 'x'
   + IntToStr(ReadInteger);
   //... глубина цвета
   WriteLn('get_screen_colors');
   lblColors.Caption:= IntToStr(ReadInteger);
   //... копия экрана
   //.....первый вариант – копирование экрана без сжатия
   //
   WriteLn('get_screen');
   //.....второй вариант – сжатие на стороне сервера
   WriteLn('get_screen:'+ IntToStr(imgScreen.Width) + ','+
   IntToStr(imgScreen.Height));
   //.... получаем данные
   stream:= TMemoryStream.Create;
   ReadStream(stream);
   stream.Position:= 0;
   //.... формируем изображение
   imgScreen.Picture.Bitmap.LoadFromStream(stream);
   stream.Clear;
   stream.Free;
   end;
   end;

   Текст листинга 11.8 содержит достаточное количество комментариев, поэтому дополнительно пояснять его код нет смысла. Остановимся пояснить лишь, зачем в процедуре TForm1. Timer1Timer предусмотрено два варианта получения изображения с сервера.
   Дело в том, что сжатие (в данном примере разрешение экрана наблюдаемого компьютера больше размера компонента imgScreen) на стороне сервера требует от компьютера, на котором запущено серверное приложение, большего процессорного времени на снятие копии экрана. Это снижает нагрузку на сеть при передаче изображения, а также экономит ресурсы компьютера-клиента, однако качество сжатого изображения получается несколько хуже, чем при предоставлении компоненту Image возможности масштабировать изображение самостоятельно.
   Если же не использовать сжатие изображения на сервере, возрастает нагрузка на сеть при передаче полноразмерной копии экрана, а вся работа по сжатию изображения возлагается на компонент imgScreen (то есть на компьютере клиента дополнительно тратится процессорное время). При большом разрешении экрана наблюдаемого компьютера (или при наблюдении сразу за несколькими компьютерами) машина клиента, если она недостаточно мощная, может начать весьма ощутимо «тормозить». Качество сжатого изображения при этом получается более высоким.
   В качестве относительно эффективного решения можно предложить использовать большие промежутки времени между запросами данных с сервера слежения с масштабированием изображения на серверной стороне (если, конечно, машина сервера не является очень маломощной).


   Многопользовательский разговорник


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


   Требования к клиентскому и серверному приложениям

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


   Формат сообщений клиента и сервера

   Клиент и сервер обмениваются только текстовыми сообщениями (не путайте с сообщениями, которыми обмениваются пользователи в ходе разговора). Строка любого сообщения состоит из двух частей: префикса и текста сообщения. Префикс отделяется от текста сообщения символом: (двоеточие). Префикс определяет действия, которые должны быть выполнены с полученным сообщением.
   Возможны следующие сообщения от клиента серверу:
   • name: имя_пользователя – с помощью этого сообщения клиентская программа сообщает серверу имя, под которым должен быть зарегистрирован пользователь (это имя будет отображаться и другим пользователям);
   • text: текст – при получении этого сообщения сервер должен разослать текст всем участникам разговора (включая отправителя);
   • имя_адресата: текст – при получении этого сообщения сервер должен отправить текст только заданному префиксом пользователю имя_адресата, а также должен отправить копию автору сообщения.
   К сообщениям третьего типа относятся все сообщения, принимаемые сервером и не начинающиеся со слов text: или name:.
   В свою очередь, сервер может посылать клиентской программе сообщения следующего вида.
   • ok: – указывает, что пользователь зарегистрирован и может вступать в разговор.
   • error: сообщение_об_ошибке – указывает, что пользователь по каким-то причинам не может участвовать в разговоре. При получении этого сообщения клиентская программа должна показать окно с текстом сообщение_об_ошибке и разорвать соединение с сервером.
   • adduser: имя_пользователя – при получении такого сообщения клиентская программа должна добавить строку имя_пользователя в список участников разговора.
   • deluser: имя_пользователя – при получении такого сообщения клиентская программа должна удалить строку имя_пользователя из списка участников разговора.
   • text: текст – при получении такого сообщения клиентская программа должна добавить текст к тексту разговора.
   Перед рассмотрением реализации клиентской и серверной частей нужно сказать несколько слов об использовании специальных сообщений клиента (name: имя_ пользователя) и сервера (ok: и error: сообщение об ошибке). Дело в том, что в предлагаемой реализации сервера присоединение нового пользователя к разговору происходит следующим образом.
   1. Клиентское приложение присоединяется к серверу (количество пользователей ограничено, поэтому сервер может послать лишнему пользователю сообщение error: с соответствующим текстом, описывающим ошибку, и тут же разорвать установленное соединение).
   2. Клиентское приложение посылает серверу сообщение с именем пользователя (префикс name:).
   3. Если имя, под которым хочет зарегистрироваться новый пользователь, уже используется, то клиентскому приложению отправляется сообщение error: с пояснением ошибки.
   4. Если имя, под которым хочет зарегистрироваться новый пользователь, свободно, то сервер сохраняет его (и рассылает всем остальным клиентским приложениям), а также посылает приложению присоединенного пользователя список всех остальных пользователей. Только после этого сервер дает новому пользователю возможность участвовать в разговоре (сообщение ok:).
   Остальные нюансы будут рассмотрены при описании исходного кода клиентского и серверного приложений.


   Реализация сервера

   Серверное приложение реализовано с использованием оконного интерфейса. Форма frmServer приложения во время работы показана на рис. 11.6.
   Рис. 11.6. Форма сервера сообщений

   Элемент управления ListBox (имя lstEvents), который представлен на форме, предназначен для вывода списка событий (присоединение, отсоединение клиентов, передача сообщений). Список помещается в рамку GroupBox1. Для списка и рамки задано значение client свойства align.
   Кроме перечисленных элементов управления, на форму также помещены компоненты IdTCPServer (имя TCPServer) и Timer (имя Timer1). Для таймера задано значение True свойства Enabled и 50 свойства Intervel. Компонент TCPServer настроен на прослушивание порта 12345, а свойству Active присвоено значение True.
   При реализации сервера основной программный код помещен в файле формы (Unit1.pas). Этот модуль условно можно разделить на две части: в первой реализованы специальные функции и процедуры (регистрации пользователей, пересылки текстовых сообщений между пользователями и т. д.), во второй части следуют процедуры-обработчики событий (методы класса TfrmServer).
   Сначала рассмотрим процедуры обработки событий, так как они значительно проще, чем остальные функции и процедуры, и их рассмотрение вначале позволит лучше представить порядок функционирования приложения (листинг 11.9).
 //-- Листинг 11.9. Процедуры обработки событий серверного приложения (Unitl.pas) --// 
   procedure TfrmServer.Timer1Timer(Sender: TObject);
   begin
   //Если нужно, то скроем окно сервера
   if (not SERVERVISIBLE) then
   begin
   frmServer.Visible:= False;
   ShowWindow(Application.Handle,SW_HIDE);
   end;
   //Таймер больше не нужен
   Timer1.Enabled:= False;
   end;
   procedure TfrmServer.TCPServerExecute(AThread: TIdPeerThread);
   begin
   //Обработаем сообщение, пришедшее от клиента
   ProcessMessage(AThread.Connection, AThread.Connection.ReadLn);
   end;
   procedure TfrmServer.TCPServerConnect(AThread: TIdPeerThread);
   begin
   //Попытаемся добавить нового пользователя
   if (AddClient(AThread.Connection)) then
   //Пользователь должен прислать свое имя
   ProcessMessage(AThread.Connection, AThread.Connection.ReadLn)
   else
   begin
   //Нет места для нового пользователя
   AThread.Connection.WriteLn('error:Достигнуто максимальное количество '+
   'пользователей. Извините, невозможно принять вас в разговор.');
   AThread.Connection.Socket.Close;
   end;
   end;
   procedure TfrmServer.TCPServerDisconnect(AThread: TIdPeerThread);
   var clDisconnected: client; //Структура с информацией об
   //отсоединенном клиенте (заполнены только поля strName и strIP)
   begin
   //Удалим информацию об отсоединенном клиенте
   clDisconnected:= DeleteClient(AThread.Connection);
   if (clDisconnected.strName <> '')then
   begin
   //Сообщим о событии остальным клиентам
    SendAll('deluser:'+ clDisconnected.strName);
   SendAll('Нас покинул "'+ clDisconn ected.strName + '".');
   //Добавим событие в журнал
   if (REPORT) then nAddEvent ('Отсоединился клиент "'+
   clDisconn ected .strName + '" на компьютере "'+
   clDisconnected.strIP + '"');
   end;
   end;
   procedure TfrmServer.FormCreate(Sender: TObject);
   begin
   //Создаем критическую секцию
   section:= TCriticalSection.Create;
   end;

   Первая и последняя из приведенных в листинге 11.9 процедур не имеют непосредственного отношения к работе TCP-сервера. Процедура TfrmServer. Timer1Timer вызывается только один раз при первом срабатывании таймера Timer1. В ней, исходя из заданного значения глобальной переменной SERVERVISIBLE, происходит (или не происходит) скрытие окна сервера. Значение глобальной переменной SERVERVISIBLE (а также переменной REPORT) определяется в момент запуска сервера. Процедура TfrmServer. FormCreate создает объект синхронизации, используемый остальными функциями и процедурами для предотвращения одновременного доступа к общим данным нескольких потоков (сервер же многопоточный).
   Остальные три процедуры используются непосредственно для организации взаимодействия сервера с клиентами. Как было сказано ранее, сервер хранит информацию о присоединенных к нему клиентах. Хранилищем этой информации является массив структур (подробно он будет рассмотрен позже). Здесь же нужно сказать, что при присоединении к серверу нового клиента (процедура TfrmServer. TCPServerConnect) предпринимается попытка найти для информации о новом пользователе место в указанном массиве (вызов функции AddClient). Если место нашлось, то функция AddClient возвращает значение True, и сервер переходит в режим регистрации пользователя. Для регистрации клиентская программа должна передать серверу имя пользователя (сообщение с префиксом name:).
   Особенностью реакции сервера на отключение клиентской программы (процедура TfrmServer. TCPServerDisconnect) является то, что, помимо удаления информации об отсоединившемся клиенте (вызов функции DeleteClient), все остальные пользователи уведомляются об отсоединении собеседника (вызовы функции SendAll).
   При получении сообщения от клиента (процедура TfrmServer.Execute) происходит лишь передача полученной строки функции ProcessMessage, которая и выполняет анализ текста сообщения и определяет действия, которые сервер должен выполнять.
   Теперь рассмотрим функции и процедуры, которые прямо или косвенно используются описанными выше обработчиками событий и на которых по большей части и основывается работа серверного приложения. Часть файла Unitl.pas, содержащая объявление типов данных, переменных и подключения модулей (добавленные вручную), которые нужны для работы сервера, приведена в листинге 11.10.
 //-- Листинг 11.10. Типы данных и переменные серверного приложения (Unitl.pas) --// 
   unit Unit1;
   interface
   uses
    .. ., SyncObjs;
   type
   TfrmServer = class(TForm)
   lstEvent s: TListBox; //Список событий
   ...
   end;
   var
   frmServer: TfrmServer;
   REPORT: Boolean; //Если = True, то все события записываются
   //в ListBox окна сервера
   SERVERVISIBLE: Boolean; //Если = True, то окно показывается на экране
   //и приложение есть на Панели задач
   implementation
   //Следующая структура используется для хранения информации о
   //пользователе, подключившемся к серверу
   type
   client = record
   fUsed: Boolean; {Ячейка занята}
   fNamed: Boolean; {Клиент сообщил свое имя}
   strName: string; {Имя пользователя}
   strIP: string; {IP-адрес клиента}
   Connection: TIdTCPServerConnection; {Соединение клиента с сервером}
   end;
   const
   MAX_CLIENT = 100;//Максимальное количество клиентов
   var
   clients: array [1..MAX_CLIENT] of client;//Массив со сведениями
   //о клиентах
   section: TCriticalSection; //Критическая секция для синхронизации
   //потоков

   Код процедуры, записывающей событие в журнал (ListBox на форме сервера), приведен в листинге 11.11.
 //-- Листинг 11.11. Добавление события в журнал сервера --// 
   procedure AddEvent(strEvent: string);
   begin
   section.Enter;
   frmServer.lstEvents.Items.Append(strEvent);
   section.Leave;
   end;

   В листинге 11.12 приведен код процедуры, рассылающей текстовое сообщение всем присоединенным к серверу клиентам.
 //-- Листинг 11.12. Рассылка сообщения всем клиентам --// 
   procedure SendAll(strMessage: string);
   var
   i: Integer;
   begin
   for i:=1 to MAX_CLIENT do
   if (clients[i].fNamed)then
   begin
   try
   clients[i].Connection.WriteLn(strMessage);
   except
   //При возникновении ошибки отключим клиента и продолжим рассылку
   ErrorCloseConnection(clients[i].Connection);
   end;
   end;
   end;

   В листинге 11.13 приведен код процедуры, отсылающей текстовое сообщение strMessage клиенту с заданным именем strName.
 //-- Листинг 11.13. Отправление сообщения клиенту с заданным именем --// 
   procedure SendTo(strMessage: string; strName: string);
   var
   i: Integer;
   begin
   for i:=1 to MAX_CLIENT do
   if (clients[i].fNamed)then
   if (clients[i].strName = strName) then
   //Нашли клиента с заданным именем
   try
   clients[i].Connection.WriteLn(strMessage);
   except
   //При возникновении ошибки отключим клиента и продолжим рассылку
   ErrorCloseConnection(clients[i].Connection);
   end;
   end;

   Процедура, приведенная в листинге 11.14, находит и помечает как занятую для нового пользователя запись в массиве clients. Если свободных записей в массиве не осталось, значит, достигнуто максимальное количество пользователей.
 //-- Листинг 11.14. Добавление информации о новом клиенте --// 
   function AddClient(Connection: TIdTCPServerConnection): Boolean;
   var
   i: Integer;
   begin
   section.Enter;
   for i:=1 to MAX_CLIENT do
   begin
   if (not clients[i].fUsed) then
   begin
   //Нашли свободную запись – заполним ее (клиент пока безымянный)
   clients[i].fUsed:= True;
   clients[i].Connection:= Connection;
   clients[i].strIP:= Connection.Socket.Binding.PeerIP;
   AddClient:= True;
   section.Leave;
   Exit;
   end;
   end;
   section.Leave;
   AddClient:= False;
   end;

   Процедура DeleteClient, приведенная в листинге 11.15, освобождает запись заданного пользователя в массиве clients.
 //-- Листинг 11.15. Удаление информации о клиенте --// 
   function DeleteClient(Connection: TIdTCPServerConnection):client;
   var
   i: Integer;
   begin
   section.Enter;
   for i:=1 to MAX_CLIENT do
   if (clients[i].fUsed) then
   if (clients[i].Connection = Connection) then
   begin
   //Вот она – запись о нужном клиенте
   clients[i].fUsed:= False;
   clients[i].fNamed:= False;
   clients[i].Connection:= Nil;
   DeleteClient:= clients[i];
   clients[i].strName:= '';
   clients[i].strIP:= '';
   section.Leave;
   Exit;
   end;
   end;

   Процедура SendClientList, приведенная в листинге 11.16, отправляет клиентской программе заданного пользователя (только что зарегистрировавшегося) сообщения addclient: с именем каждого зарегистрированного ранее пользователя.
 //-- Листинг 11.16. Отправление списка всех присоединенных клиентов --// 
   procedure SendClientList(Connection: TIdTCPServerConnection);
   var
   i: Integer;
   begin
   for i:= 1 to MAX_CLIENT do
   if (clients[i].fNamed) then
   if (clients[i].Connection <> Connection) then
   try
   //Сообщим имя очередного найденного пользователя
    Connection.WriteLn('adduser:'+ clients[i].strName);
   except
   //При возникновении ошибки отключим клиента и продолжим
   //рассылку
   ErrorCloseConnection(clients[i].Connection);
   end;
   end;

   Процедура ErrorCloseConnection (листинг 11.17) вызывается при ошибке отправки сообщений пользователям (например, при нарушении сетевого соединения). Эта процедура отключает пользователя, соединение с которым работает с ошибками, и сообщает об этом другим пользователям.
 //-- Листинг 11.17. Закрытие соединения с клиентом (при возникновении ошибки) --// 
   procedure ErrorCloseConnection(Connection: TIdTCPServerConnection);
   var
   clError: client; //Информация о пользователе, соединение
   //с которым прервалось (только имя и IP-адрес)
   begin
   //Отключим соединение, работающее с ошибками
   clError:= DeleteClient(Connection);
   //Сообщим об отключении остальным пользователям
   SendAll('deluser:'+ clError.strName);
   SendAll(' Нас покинул "'+ clError.strName + '".');
   //Добавим событи е в журнал
   if (REPORT) then AddEvent('Из-за ошибки отсоединен клиент "'+
     clError.strName + '" на компьютере "'+ clError.strIP + '"');
   end;

   Процедура RegisterClient, код которой приведен в листинге 11.18, регистрирует пользователя под указанным в сообщении name: именем (ранее уже выполнялась функция AddClient, которая нашла для записи этого пользователя место в массиве clients). Если имя, под которым хочет зарегистрироваться пользователь, используется, то клиентской программе посылается соответствующее уведомление, после чего соединение разрывается.
 //-- Листинг 11.18. Регистрация нового клиента --// 
   procedure RegisterClient(Connection: TIdTCPServerConnection;
   strName: string);
   var
   i: Integer;
   begin
   //Проверим, что имя клиента еще не использовалось
   for i:=1 to MAX_CLIENT do
   begin
   if (clients[i].fNamed) then
   if (clients[i].strName = strName) then
   begin
   //Дублирование имени – придется разрывать соединение
   Connection.WriteLn('error:Пользователь с именем "'+ strName +
   '" уже участвует в разговоре.');
   DeleteClient(Connection);
   Connection.Socket.Close;
   Exit;
   end;
   end;
   //Поиск записи о нужном клиенте и присвоение ему имени
   for i:=1 to MAX_CLIENT do
   begin
   if (not clients[i].fNamed and clients[i].fUsed) then
   if (clients[i].Connection = Connection) then
   begin
   //Вот он, наш клиент ...
   clients[i].fNamed:= True;
   clients[i].strName:= strName;
   //Сообщим другим о появлении нового участника
    SendAll('adduser:'+ strName);
   SendAll('text: К нам присоединился "'+ strName +
   '". Поприветствуем!');
   //Отсылаем новому клиенту список остальных участников разговора
   SendClientList(Connection);
   //Разрешим новому клиенту отсылать сообщения
   Connection.WriteLn('ok:');
   //Если нужно, то добавим событие в список
   if (REPORT) then AddEvent('Присоединен клиент "'+ strName +
   '" на компьютере "'+ Connection.Socket.Binding.PeerIP + '"');
   end;
   end;
   end;

   В листинге 11.19 приведена служебная функция, возвращающая имя пользователя по ссылке на объект TIdTCPServerConnection, соответствующий этому клиенту.
 //-- Листинг 11.19. Определение имени клиента по его соединению с сервером --// 
   function GetClientName(Connection: TIdTCPServerConnection):string;
   var
   i: Integer;
   begin
   for i:=1 to MAX_CLIENT do
   if (clients[i].fNamed) then
   if (clients[i].Connection.Socket.Binding.Handle =
   Connection.Socket.Binding.Handle) then
   begin
   GetClientName:= clients[i].strName;
   Exit;
   end;
   end;

   Наконец, в листинге 11.20 приведена главная процедура серверного приложения, обрабатывающая сообщения, полученные от клиентов.
 //-- Листинг 11.20. Обработка сообщения от клиента --// 
   procedure ProcessMessage(Connection: TIdTCPServerConnection;
   strMessage: string);
   var
   strName: string; //Имя отправителя сообщения
   strAction: string; //Строка с обозначением действия (префикс)
   len: Integer; //Длина строки strAction
   begin
   //Определим действие, которое хочет выполнить клиент
   len:= Pos(':', strMessage);
   strAction:= Copy(strMessage,1,len-1);
   Delete(strMessage,1,len);
   if (strAction= 'name') then
   begin
   //Клиент сообщает свое имя – пытаемся его зарегистрировать
   RegisterClient(Connection, strMessage);
   end
   else if (strAction = 'text') then
   begin
   //Клиент передает сообщение всем – подпишем сообщение и отошлем
   strMessage:= GetClientName(Connection) + ': '+ strMessage;
   SendAll('text:'+ strMessage);
   //Если надо, то сохраняем сообщение в списке событий
   if (REPORT) then AddEvent('Сообщение от '+ strMessage);
   end
   else
   begin
   //Клиент передает сообщение определенному собеседнику
   //(строка strAction содержит имя собеседника)
   strName:= GetClientName(Connection);
   SendTo('text:'+ strName + ': '+ strMessage, strAction);
   if (strName <> strAction) then
   //Передадим копию сообщения отправителю
   Connection.WriteLn('text:'+ strName + 'для '+ strAction + ': '
   + strMessage);
   //Если надо, то сохраним сообщение в списке событий
   if (REPORT) then AddEvent('Сообщение для '+ strAction +
   'от '+ strName + ': '+ strMessage);
   end;
   end;

   Информация о каждом пользователе (участнике разговора) хранится в отдельной структуре client:

   type
   client = record
   fUsed: Boolean; {Ячейка занята}
   fNamed: Boolean; {Клиент сообщил свое имя}
   strName: string; {Имя пользователя}
   strIP: string; {IP-адрес клиента}
   Connection: TIdTCPServerConnection; {Соединение клиента с сервером}
   end;

   Непосредственно к пользователю имеют отношение только три последних поля структуры. Самым полезным из них является ссылка на объект TIdTCPSe rve rConnection, с помощью которой сервер может в любое время отправить данные определенному пользователю.
   Информация обо всех пользователях хранится в массиве clients. Его размер ограничен (константой MAXCLIENT) и определяет максимальное количество пользователей-участников разговора. Так как используется массив с постоянным количеством элементов, то для индикации того, что ячейка массива занята (значение True) или свободна (значение False) можно использовать специальный флаг (поле fUsed). Поле fName структуры client используется для фиксации факта отправления клиентской программой имени пользователя (клиентские программы незарегистрированных пользователей сообщения не получают). Изначально значение поля fNamed равно False и, только если имя пользователя сообщено серверу и не используется одним из участников разговора, устанавливается в значение True.
   Одним из самых сложных моментов работы рассматриваемого сервера является обеспечение синхронизации доступа к массиву clients. Для этого используется критическая секция. Она же используется и для синхронизации добавления событий в список lstEvents сервера.
   Наконец, чтобы сервер можно было запускать с отключенным протоколированием событий, а также чтобы окно сервера не мешало пользователю, можно хранить значения переменных REPORT и SERVERVISIBLE в INI-файле, что, собственно, и сделано: значения этих переменных хранятся в секции [Common] файла Server.ini. Для реализации считывания при запуске сервера соответствующих значений из INI-файла код в модуле Server (файл Server.dpr) изменен следующим образом (листинг 11.21).
 //-- Листинг 11.21. Изменения в модуле Server --// 
   program Server;
   uses
   Forms,
   Unit1 in 'Unit1.pas' {frmServer},
   IniFiles, Dialogs;
   {$R *.res}
   var
   {Переменные из из INI-файла}
   config: TIniFile;
   strPath: string;
   begin
   //Грузим информацию из INI-файла
   strPath:=
   Copy(Application.ExeName,1,Length(Application.ExeName) – 3) + 'ini';
   config:= TIniFile.Create(strPath);
   SERVERVISIBLE:= config.ReadBool('Common', 'ServerVisible', False);
   REPORT:= config.ReadBool('Common','EventReport', False);
   config.Free;
   try
   //Запуск ксервера
   Application.Initialize;
   Application.CreateForm(TfrmServer, frmServer);
   Application.Run;
   except
   MessageDlg('Не удается запустить сервер сообщений. '+
   'Возможно, он был запущен ранее.', mtError, [mbOK], 0);
   end;
   end.

   В приведенном листинге код создания формы помещен в блок try. Это сделано только для того, чтобы сервер не «падал», отображая всем прекрасно знакомое окно с сообщением о критической ошибке, возникающей при попытке ошибочного запуска собственной копии.
   Соответственно, INI-файл для запуска сервера с видимым окном и включенным протоколированием имеет следующий вид:

   [Common]
   ServerVisible=1
   EventReport=1


   Реализация клиентского приложения

   Проект клиентской программы имеет имя Client. Внешний вид формы клиентского приложения во время его работы представлен на рис. 11.7.
   Рис. 11.7. Форма клиента при ведении разговора

   Приведенная на рис. 11.7 форма имеет имя frmClient. Свойства основных элементов управления (существенные для работы приложения), помещенных на форму, приведены в табл. 11.2.
 //-- Таблица 11.2. Свойства элементов управления формы frmClient --// 
   Ниже приведены функции и процедуры, не являющиеся обработчиками событий, но имеющие большое значение для работы клиентского приложения.
   Приведенная в листинге 11.22 процедура обновляет форму при удачном подключении к серверу.
 //-- Листинг 11.22. Обновление формы при присоединении к серверу --// 
   proceduree Connect();
   begin
   with frmClienttdo
   begin
   cmbConn ect.Caption := 'Отключиться ';
   txtUser.Enabled:= False;
   txtServer.Enabled:= False;
   Caption:= ' Разговорник ['+ txtUser.Text + 'подключен к'
   + txtServer.Text + ']';
   lstUsers.Enabled:= True;
   cmbSend.Enabled:= True;
   txtMessage.Enabled:= True;
   txtChat.Enabled:= True;
   end;
   end;

   Процедура Disconnect, приведенная в листинге 11.23, обновляет форму при отключении от сервера (в таком виде форма frmClient предстает первоначально).
 //-- Листинг 11.23. Обновление формы при отсоединении от сервера --// 
   procedure Disconnect();
   begin
   with frmClient do
   begin
   cmbConn ect.Caption:= ' Подключиться ';
   txtUser.Enabled:= True;
   txtServer.Enabled:= True;
   Caption:= ' Разго ворник';
   lstUsers.Enabled:= False;
   lstUsers.Clear;
   cmbSend.Enabled:= False;
   txtMessage.Enabled:= False;
   txtChat.Enabled:= False;
   end;
   end;

   Процедура ProcessMessage (листинг 11.24) обрабатывает сообщение, полученное от сервера, аналогично той же процедуре в серверном приложении (естественно, сообщения и реакция на них отличны от серверных).
 //-- Листинг 11.24. Обработка строки, полученной от сервера --// 
   procedure ProcessMessage(strMessage: string);
   var
   strAction: string; //Тип сообщения (префикс сообщения)
   len: Integer; //Длина строки strAction
   begin
   //Определим тип сообщения и выполним соответствующие действия
   len:= Pos(':', strMessage);
   strAction:= Copy(strMessage,1,len-1);
   Delete(strMessage,1,len);
   if (strAction = 'ok') then
   begin
   //Регистрация пользователя завершена – можно отправлять сообщения
   Connect;
   end
   else if (strAction = 'error') then
   begin
   //Ошибка!!!
   frmClient.TCPClient.Disconnect;
   Disconnect;
   MessageDlg(strMessage, mtError, [mbOK], 0);
   end
   else if (strAction = 'adduser') then
   begin
   //К разговору присоединился новый пользователь
   frmClient.lstUsers.Items.Add(strMessage);
   end
   else if (strAction = 'deluser') then
   begin
   //Какой-то пользователь отсоединился
   frmClient.lstUsers.Items.Delete(
   frmClient.lstUsers.Items.IndexOf(strMessage));
   end
   else begin
   //Покажем принятое сообщение
   frmClient.txtChat.Lines.Add(strMessage);
   end;
   end;

   Далее приведены обработчики событий, на которых, собственно, и основана работа клиентской программы. Обработчик нажатия кнопки cmbConnect, приведенный в листинге 11.25, пытается соединиться с сервером. Если клиент присоединен к серверу, то эта же кнопка используется для его отсоединения.
 //-- Листинг 11.25. Соединение с сервером и отсоединение от него --// 
   procedure TfrmClient.cmbConnectClick(Sender: TObject);
   begin
   if (cmbConn ect.Caption = ' Подключиться') then
   begin
   //Проверим, чтобы были введены имена сервера и пользователя
   if (txtServer.Text = '')then
   begin
   MessageDlg('Введите имя сервера в текстовое поле.',
   mtInformation, [mbOK], 0);
   Exit;
   end
   else if (txtUser.Text = '')then
   begin
   MessageDlg('Введите имя пользователя в текстовое поле.',
   mtInformation, [mbOK], 0);
   Exit;
   end;
   //Пытаемся подключиться к серверу
   try
   TCPClient.Host:= txtServer.Text;
   TCPClient.Connect;
   except
   MessageDlg(' Не удается соединиться с сервером ',mtError, [mbOK], 0);
   end;
   end
   else
   //Отключаемся от сервера
   TCPClient.Disconnect;
   end;

   Обработчик нажатия кнопки cmbSend (листинг 11.26) отправляет сообщение, которое могут прочесть все пользователи, присоединенные к серверу.
 //-- Листинг 11.26. Отправка сообщения всем собеседникам --// 
   procedureTfrmClient.cmbSendClick(Sender: TObject);
   begin
   if (txtMessage.Text <> '') then
   begin
   //Отправка сообщения всем собеседникам
   TCPClient.WriteLn('text:'+ txtMessage.Text);
   txtMessage.Text:= '';
   txtMessage.SetFocus;
   end;
   end;

   При двойном щелчке кнопкой мыши на имени в списке пользователей отправляется сообщение, которое получает только выделенный в списке пользователь (листинг 11.27).
 //-- Листинг 11.27. Отправка сообщения заданному собеседнику --// 
   procedure  TfrmClient.lstUsersDblClick(Sender: TObject);
   begin
   if ((lstUsers.ItemIndex >= 0) and (txtMessage.Text <> '')) then
   begin
   //Отправим сообщение только для выбранного собеседника
   //(сообщение вида "имя_собеседника:текст_сообщения ")
   TCPClient.WriteLn(lstUsers.Items.Strings[lstUsers.ItemIndex] +
   ':'+ txtMessage.Text);
   txtMessage.SetFocus;
   end;
   end;

   После соединения с сервером, то есть в обработчике TfrmClient. TCPClientConnected, приведенном в листинге 11.28, клиентская программа отправляет имя пользователя серверу. При отсоединении от сервера (тот же листинг 11.28) происходит соответствующее оформление внешнего вида формы frmClient.
 //-- Листинг 11.28. Обработка соединения с сервером и отсоединения от него --// 
   procedure TfrmClient.TCPClientConnected(Sender: TObject);
   begin
   //Отправляем на сервер имя пользователя
   TCPClient.WriteLn('name:'+ txtUser.Text);
   end;
   procedure TfrmClient.TCPClientDisconnected(Sender: TObject);
   begin
   //Оформим форму для отсоединенного от сервера состояния
   Disconnect;
   end;

   Ключевой обработчик приведен в листинге 11.29 (именно по таймеру проверяется факт получения сообщения от сервера). Для элемента управления TCPClient значение таймаута установлено для того, чтобы при отсутствии принятых данных клиентская программа не переходила надолго в состояние ожидания, а генерировалось исключение, по которому можно было бы судить, что данных нет (см. блок try в этом обработчике).
 //-- Листинг 11.29. Проверка наличия данных от сервера --// 
   procedure TfrmClient.Timer1Timer(Sender: TObject);
   var strMessage: string;
   begin
   //Проверим, нет ли для нас сообщения
   if (TCPClient.Connected)then
   begin
   try
   strMessage:= TCPClient.ReadLn;
   if (strMessage <> '')then
   ProcessMessage(strMessage);
   except
   on EIdReadTimeout do; //Ошибки тайм-аута игнорируем
   else
   //При остальных ошибках от соединяемся от сервера
   TCPClient.Disconnect;
   end;
   end;
   end;
   end.


   Примечание
   Чтобы при запуске клиентского приложения из среды Delphi постоянно не появлялись сообщения об исключениях (возникают при истечении таймаута в TfrmClient. Timer1Timer), снимите флажок Stop on Delphi Exceptions на вкладке Language Exceptions окна Debugger Options (меню Tools ► Debugger Options).

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




   Глава 12
   Шифрование


   • Основы криптографии
   • Шифр простой подстановки
   • Транспозиция
   • Шифр Виженера и его варианты
   • Шифр с автоключом
   • Взлом

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


   Основы криптографии

   Американский математик Клод Шеннон (Claude Elwood Shannon) написал работу «Теория связи в секретных системах», в которой обобщил накопленный до него опыт разработки шифров. В этой работе указано, что даже в самых очень сложных шифрах в качестве типичных компонентов можно выделить шифры замены и шифры перестановки или их сочетание.
   Для начала рассмотрим эти шифры, а позже реализуем их. Начать стоит, пожалуй, с шифра замены, как с самого простого и наиболее популярного. Примерами самых распространенных из известных шифров замены могут служить шифр Цезаря, «цифирная азбука» Петра Великого и «пляшущие человечки» А. Конана Дойла. Из самого названия видно, что шифр замены осуществляет преобразование замены букв или других «частей» открытого текста на аналогичные «части» шифрованного текста. Математическое описание шифра замены можно дать следующим образом. Пусть X и Y – два алфавита (открытого и шифрованного текстов соответственно), состоящие из одинакового количества символов. Пусть также g:X→Y – взаимнооднозначное отображение X в Y. Тогда шифр замены действует так: открытый текст x -------
| Библиотека iknigi.net
|-------
|  
 -------


x -------
| Библиотека iknigi.net
|-------
|  
 -------


…x -------
| Библиотека iknigi.net
|-------
|  
 -------


преобразуется в шифрованный текст g(x -------
| Библиотека iknigi.net
|-------
|  
 -------


)g(x -------
| Библиотека iknigi.net
|-------
|  
 -------


)…g(x -------
| Библиотека iknigi.net
|-------
|  
 -------


).
   Шифр перестановки, как видно из названия, осуществляет преобразование перестановки букв в открытом тексте. Примером одного из известных шифров перестановки может служить шифр «Сцитала». Обычно открытый текст разбивается на отрезки равной длины, и каждый отрезок шифруется независимо. Например, длина отрезков равна n и g — взаимнооднозначное отображение множества {1, 2, …, n} в себя. Тогда шифр перестановки действует следующим образом: отрезок открытого текста x -------
| Библиотека iknigi.net
|-------
|  
 -------


x -------
| Библиотека iknigi.net
|-------
|  
 -------


…x -------
| Библиотека iknigi.net
|-------
|  
 -------


преобразуется в отрезок шифрованного текста. x -------
| Библиотека iknigi.net
|-------
|  
 -------


x -------
| Библиотека iknigi.net
|-------
|  
 -------


…x -------
| Библиотека iknigi.net
|-------
|  
 -------


.
   Наиболее важным для развития криптографии является вывод К. Шеннона о существовании и единственности абсолютно стойкого шифра. Единственным таким шифром является форма так называемой ленты однократного использования, в которой открытый текст «объединяется» с полностью случайным ключом такой же длины.
   Этот результат был доказан К. Шенноном с помощью разработанного им теоретико-информационного метода исследования шифров. Подробно эта тема здесь рассматриваться не будет: если вы заинтересовались, можете обратиться к работе К. Шеннона.
   Стоит пояснить один очень важный момент по поводу единственного абсолютно стойкого шифра. Чтобы шифр являлся таковым, должны выполняться три условия:
   • полная случайность (равновероятность) ключа (это, в частности, означает, что ключ нельзя выработать с помощью какого-либо детерминированного устройства);
   • равенство длины ключа и длины открытого текста;
   • однократность использования ключа.
   В случае нарушения хотя бы одного из этих условий шифр перестает быть абсолютно стойким и появляются принципиальные возможности для его вскрытия (хотя реализовать их может быть чрезвычайно сложно).
   Однако, оказывается, именно эти условия и делают абсолютно стойкий шифр очень дорогим и непрактичным: прежде чем пользоваться таким шифром, необходимо обеспечить всех законных пользователей достаточным запасом случайных ключей и исключить возможность их повторного применения, что сделать очень трудно и дорого.
   В силу указанных причин абсолютно стойкие шифры применяются только в сетях связи с небольшим объемом передаваемых данных (обычно в сетях для передачи особо важной государственной информации).
   Теперь вам должно быть понятно, что чаще всего для защиты своей информации законные пользователи вынуждены применять не абсолютно стойкие шифры. Такие шифры могут быть вскрыты (по крайней мере, теоретически). Вопрос заключается только в том, хватит ли у взломщика сил, средств и времени для разработки и реализации соответствующих алгоритмов. Обычно эту мысль выражают так: противник с неограниченными ресурсами может вскрыть любой не абсолютно стойкий шифр.
   Как же должен действовать законный пользователь, выбирая для себя шифр? Лучше всего, конечно, было бы доказать, что никакой противник не может вскрыть выбранный шифр, скажем, за десять лет и, тем самым, получить теоретическую оценку стойкости, но, к сожалению, математическая теория еще не предлагает необходимых теорем – они относятся к нерешенной проблеме нижних оценок вычислительной сложности задач.
   У пользователя остается единственный путь – получение практических оценок стойкости. Этот путь включает следующие этапы.
   1. Понимание и четкое формулирование, от какого противника требуется защищать информацию. Необходимо уяснить, что именно противник знает или может узнать о системе шифра, а также какие силы и средства он сможет применить для его вскрытия.
   2. Мысленное перемещение в положение противника и атака шифра с его позиций, то есть разработка различных алгоритмов вскрытия шифра. При этом необходимо в максимальной мере обеспечить моделирование сил, средств и возможностей противника.
   3. Использование наилучшего из разработанных алгоритмов для практической оценки стойкости шифра.
   Полезно упомянуть о двух простейших методах вскрытия шифра: случайное угадывание ключа (срабатывает с малой вероятностью, зато имеет наименьшую сложность) и перебор ключей вплоть до нахождения истинного (срабатывает всегда, зато имеет очень высокую сложность). Отмечу также, что не всегда нужна атака на ключ: для некоторых шифров можно сразу, даже не зная ключа, восстанавливать открытый текст по шифрованному.
   Теперь можно переходить не только к изучению столь необходимой теоретической части, но и к рассмотрению практической реализации различных криптосистем.
   Существует много их классификаций. Принципы классификации относятся не к качеству рассматриваемых криптосистем, а к присущим им свойствам.


   Шифр простой подстановки

   В шифре простой подстановки производится замена каждой буквы сообщения некоторым заранее определенным символом (обычно также буквой). В результате сообщение, имеющее вид M = m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


…, где m -------
| Библиотека iknigi.net
|-------
|  
 -------


, m -------
| Библиотека iknigi.net
|-------
|  
 -------


, … – последовательность букв, преобразуется в сообщение вида E = e -------
| Библиотека iknigi.net
|-------
|  
 -------


e -------
| Библиотека iknigi.net
|-------
|  
 -------


e -------
| Библиотека iknigi.net
|-------
|  
 -------


e -------
| Библиотека iknigi.net
|-------
|  
 -------


= f(m -------
| Библиотека iknigi.net
|-------
|  
 -------


)f(m -------
| Библиотека iknigi.net
|-------
|  
 -------


)f(m -------
| Библиотека iknigi.net
|-------
|  
 -------


)f(m -------
| Библиотека iknigi.net
|-------
|  
 -------


)…, причем функция f(m) имеет обратную функцию g, для которой верно g(f(m)) = m для всех возможных значений m. В данном шифре в качестве ключа используется простая перестановка алфавита (верно в том случае, если буквы заменяются буквами), например, перестановка ЛРЭИБПВЪДЁЗЩЙГХМЦАУОСЖТЯФКЕШНЫЬЧЮ. Она используется следующим образом:
   • буква «А» открытого текста заменяется буквой «Л»;
   • буква «Б» заменяется буквой «Р»;
   • буква «В» заменяется буквой «Э» и т. д.
   Как можно понять из определения, данный шифр является довольно простым.
   Рассмотрим пример, демонстрирующий одну из возможных реализаций этого шифра. Для этого понадобится создать новое приложение, а на форму поместить следующие компоненты: по два компонента классов TMemo и TLabel с соответствующими именами(mmDecryptMessage, mmEncryptMessage, lbDecryptMessage, lbEncryptMessage), три компонента класса TButton (btnEncryptMessage, btnDecpyptMessage, btnGenRearrangement), а также один компонент класса TValueListEditor (vleSubst). По умолчанию все перечисленные компоненты находятся на вкладке Standard, за исключением компонента класса TValueListEditor, который расположен на вкладке Additional. Когда вы закончите создание интерфейса программы, у вас должно будет получиться нечто подобное тому, что изображено на рис. 12.1.
   Рис. 12.1. Интерфейс программы «Шифр простой подстановки»

   Текстовый редактор mmDecryptMessage будет служить для ввода и отображения открытого текста сообщения, mmEncryptMessage – для текста, преобразованного с помощью шифра. Редактор значений vleSubst будет использоваться для задания перестановки алфавита, с помощью которой будет шифроваться и дешифроваться текст сообщения. Кнопка btnEncryptMessage будет отвечать за шифрование сообщения из текстового редактора mmDecryptMessage и помещение результата в mmEncryptMessage. Кнопка же btnDecpyptMessage будет выполнять противоположные действия. Последняя кнопка btnGenRearrangement будет служить для генерации случайной перестановки алфавита, чтобы не утруждать себя ее указанием вручную. Для каждой из кнопок необходимо будет добавить обработчики событий OnClick, а для формы – обработчик события OnCreate (он нужен для инициализации редактора значений vleSubst).
   Следует оговориться, что программа будет шифровать и дешифровать только русский текст, оставляя неизменным все остальное. Исходный код данной программы рассмотрен ниже.
   Первым делом нужно указать необходимые типы для лучшего понимания написанного кода, а также соответствующим образом объявить класс формы (листинг 12.1).
 //-- Листинг 12.1. Объявление типов и класса формы --// 
   type
   TRusDstAlphabet = array [Char] of Char;
   TfmSubstitution = class(TForm)
   mmDecryptMessage: TMemo;
   mmEncryptMessage: TMemo;
   lbDecryptMessage: TLabel;
   lbEncryptMessage: TLabel;
   btnEncryptMessage: TButton;
   btnDecpyptMessage: TButton;
   btnGenRearrangement: TButton;
   vleSubst: TValueListEditor;
   procedure FormCreate(Sender: TObject);
   procedure btnGenRearrangementClick(Sender: TObject);
   procedure btnEncryptMessageClick(Sender: TObject);
   procedure btnDecpyptMessageClick(Sender: TObject);
   private
   {Private declarations}
   RusDstAlphabet: TRusDstAlphabet;
   procedure GenRearrangment;
   function ValidateRearrangement: Boolean;
   function UpCaseRus(Ch: Char): Char;
   function LowCaseRus(Ch: Char): Char;
   procedure RecalcAlphabet(nKey: Integer);
   function EncryptDecryptString(strMsg: String): String;
   public
   {Publicccdeclarationss}
   end;

   Каждую задачу следует рассматривать детально и выделять необходимые подзадачи, решение которых позволит облегчить и упростить общее решение. Помимо ряда стандартных обработчиков событий, для лучшей структуризации кода и повышения его читабельности сюда добавлено несколько собственных методов, что является немаловажным фактором при разработке приложений. Ниже приведено описание работы каждого метода.
   В данном приложении для удобства и простоты работы будет реализована возможность задания случайной автоматической перестановки. Первым рассматриваемым методом является функция, реализующая алгоритм генерации случайной перестановки заданной длины из букв русского алфавита. Принцип ее работы заключается в следующем. Сначала считается, что в перестановке нет ни единого символа, о чем свидетельствует установка всех элементов массива WasGen в значение False. Далее в цикле случайным образом генерируются буквы русского алфавита. На очередном шаге цикла буква генерируется до тех пор, пока не будет присутствовать среди уже сгенерированных. Как только такая буква будет получена, соответствующий элемент массива WasGen установится в значение True, которое свидетельствует о том, что буква больше не может быть сгенерирована. Буква добавляется в перестановку. Код, соответствующий данному описанию, приведен в листинге 12.2.
 //-- Листинг 12.2. Реализация метода генерации случайной перестановки --// 
   procedure TfmSubstitution.GenRearrangment;
   var
   Ch, c: char;
   //нужен для определения, встречался ли символ ранее
   WasGen: array [Char] of Boolean;
   begin
   //заполняем массив значением False
   FillChar(WasGen, SizeOf(WasGen), False);
   for Ch:= ' А'to 'Я'do
   begin
   //генерируем случайный символ до тех пор, пока
   //не будет получен еще не сгенерированный
   repeat
   c:= Chr(Ord('А') + random(32));
   until not WasGen[c];
   //помечаем, что символ сгенерирован
   WasGen[c]:= True;
   vleSubst.Values[Ch]:= c;
   end;
   end;

   В данном приложении пользователь может сам задавать необходимую перестановку букв алфавита, поэтому стоит учесть тот факт, что он может ошибиться при ее вводе. Для решения данной проблемы необходимо реализовать функцию, которая будет отвечать на вопрос о том, является ли введенная перестановка корректной. Чтобы перестановка считалась допустимой, она должна отвечать следующим критериям. Во-первых, в каждой ячейке ввода должна присутствовать лишь одна буква – ни больше, ни меньше. Во-вторых, каждая введенная буква обязана принадлежать множеству букв русского алфавита. И в-третьих, ни одна введенная буква не должна повторяться. Проверка первого критерия довольна проста – для этого достаточно лишь проверить длину строки, введенной в каждой ячейке. Второй критерий также проверяется довольно простой конструкцией принадлежности заданному множеству. Третий критерий проверяется подобно тому, как в предыдущем реализованном методе проверялось, сгенерирована ли данная буква (листинг 12.3).
 //-- Листинг 12.3. Реализация метода проверки допустимости перестановки --// 
   function TfmSubstitution.ValidateRearrangement: Boolean;
   var
   i: Integer;
   s: String;
   Used: array [Char] of Boolean;
   begin
   Result:= False;
   FillChar(Used, SizeOf(Used), False);
   for i:= 1 to vleSubst.RowCount – 1 do
   Begin
   //символ единственный в строке?
   s:= vleSubst.Cells[1, i];
   if (Length(s) <> 1) then
   Exit;
   //символ – буква русского языка?
   s[1]:= UpCaseRus(s[1]);
   if not (s[1] in [' А '..'Я ']) then
   Exit;
   //уже встречался ранее?
   if Used[s[1]] then Exit;
   Used[s[1]]:= True;
   End;
   Resulttt:= True;
   end;

   Далее будут реализованы две вспомогательные функции, которые позволят преобразовывать буквы верхнего регистра к нижнему и наоборот. Их реализация немного специфична и основывается на используемой кодировке. Отдельная проверка буквы «Ё» производится на основании иного расположения в таблице кодировки, чем у остальных букв. Буквы русского алфавита верхнего регистра расположены, начиная с буквы «А», по порядку следования в алфавите, а сразу после них аналогично расположены буквы нижнего регистра. Этим объясняется увеличение кода буквы на фиксированное число. Код реализации данных вспомогательных функций приведен в листинге 12.4.
 //-- Листинг 12.4. Вспомогательные функции преобразования регистра букв --// 
   function TfmSubstitution.UpCaseRus(Ch: Char): Char;
   begin
   if Ch = 'ё' then Ch:= 'Е';
   if Ch in ['а'..'я'] then Dec(Ch, 32);
   Result:= Ch;
   end;
   function TfmSubstitution.LowCaseRus(Ch: Char): Char;
   begin
   if Ch = 'Ё' then Ch:= 'е';
   if Ch in ['А'..'Я'] then Inc(Ch, 32);
   Result:= Ch;
   end;

   Теперь рассмотрим работу обработчика события формы OnCreate и обработчика события кнопки OnClick. Первый сначала инициализирует редактор значений полями, для которых будут задаваться данные, затем, после того как все поля созданы, он вызывает функцию генерации случайной перестановки, заполняющую, в свою очередь, все поля редактора значений необходимыми данными. Второй же обработчик только вызывает функцию генерации случайной перестановки. В листинге 12.5 приведен исходный код данных методов.
 //-- Листинг 12.5. Использование генерации случайной перестановки --// 
   procedureTfmSubstitution.FormCreate (Sender: TObject);
   var
   Ch: char;
   begin
   Randomize;
   //инициализация редактора значений
   for Ch:= 'А'to 'Я'do
   vleSubst.InsertRow(Ch, '', True);
   //генерация случайной перестановки
   GenRearrangment;
   end;
   procedure TfmSubstitution.btnGenRearrangementClick(Sender: TObject);
   begin
   GenRearrangment;
   end;

   Следующим объектом рассмотрения является функция предварительной подготовки алфавита преобразования для шифрования либо дешифрования сообщения. У метода RecalcAlphabet есть параметр nKey, который в зависимости от своего значения показывает, что является ключом. Возможными значениями параметра nKey являются 0 и 1. Значение 0 указывает, что будет производиться шифрование сообщения и требуется сопоставить в соответствие буквам открытого текста буквы перестановки. Значение 1, напротив, указывает на то, что будет производиться дешифрование сообщения и требуется сопоставить в соответствие буквам перестановки буквы открытого текста. Для этого массив сопоставления символов изначально заполняется таким образом, чтобы каждый символ соответствовал самому себе. Это происходит в следующих строках метода:

   for Ch:= Low(RusDstAlphabet) to High(RusDstAlphabet) do RusDstAlphabet[Ch]:= Ch;

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

   for i:= 1 to vleSubst.RowCount – 1 do
   RusDstAlphabet[vleSubst.Cells[nKey, i][1]]:= vleSubst.Cells[1 – nKey, i][1];

   Редактор значений vleSubst предназначен для сопоставления букв верхнего регистра. Нам же требуется избавиться от различия между буквами верхнего и нижнего регистров. Для этого необходимо дополнительно произвести следующие действия:

   for i:= 1 to vleSubst.RowCount – 1 do
   RusDstAlphabet[LowCaseRus(vleSubst.Cells[nKey, i][1])]:=
   LowCaseRus(vleSubst.Cells[1 – nKey, i][1]);

   Работа данного метода по частям рассмотрена. Полный же его код приведен в листинге 12.6. Как видите, все относительно просто (здесь используется вспомогательная функция LowCaseRus).
 //-- Листинг 12.6. Функция предварительной подготовки алфавита преобразования --// 
   procedure TfmSubstitution.RecalcAlphabet(nKey: Integer);
   var
   Ch: Char;
   i: Integer;
   begin
   //предварительно все символы в алфавите шифрования
   //соответствуют символам из незашифрованного алфавита
   for Ch:= Low(RusDstAlphabet) to High(RusDstAlphabet) do
   RusDstAlphabet[Ch]:= Ch;
   //формируем алфавит от дельно для каждого из регистров букв
   //здесь для верхнего
   for i:= 1 to vleSubst.RowCount – 1 do
   RusDstAlphabet[vleSubst.Cells[nKey, i][1]]:= vleSubst.Cells[1
   –nKey, i][1];
   //здесь для нижнего
   for i:= 1 to vleSubst.RowCount – 1 do
   RusDstAlphabet[LowCaseRus(vleSubst.Cells[nKey, i][1])]:=
   LowCaseRus(vleSubst.Cells[1 – nKey, i][1]);
   end;

   Еще одной вспомогательной функцией является функция преобразования строки символов с помощью алфавита преобразования в соответствии с указанной операцией. Работа ее довольно проста. В цикле осуществляется прямой проход по строке, и каждый символ, принадлежащий ей, заменяется соответствующим символом алфавита преобразования. В итоге получается зашифрованная либо дешифрованная строка. В листинге 12.7 приведен исходный код данного метода.
 //-- Листинг 12.7. Преобразование строки с помощью массива сопоставления --// 
   function TfmSubstitution.EncryptDecryptString(strMsg: String): String;
   var
   i: Integer;
   begin
   //преобразуем строку посимвольно
   for i:= 1 to Length(strMsg) do
   strMsg[i]:= RusDstAlphabet[strMsg[i]];
   Result:= strMsg;
   end;

   Теперь, используя все описанные функции, вам не составит труда зашифровать либо дешифровать сообщение. Например, чтобы зашифровать его, подготовьте массив соответствия букв вызовом функции RecalcAlphabet с параметром 0. После этого для каждой строки открытого текста вызовите функцию EncryptDecryptString и в качестве результата получите зашифрованную строку. Обработчики событий OnClick соответствующих кнопок шифруют либо дешифруют весь текст. Основная идея, заложенная в каждый из методов, заключается в том, чтобы проверить корректность заданной перестановки, после чего произвести предварительную подготовку алфавита сопоставления и затем преобразовать сообщение (листинг 12.8).
 //-- Листинг 12.8. Шифрование и дешифрование сообщения --// 
   procedureTfmSubstitution.btnEncryptMessageClick(Sender: TObject);
   var
   i: Integer;
   begin
   //проверяем корректность ввода перестановки
   if ValidateRearrangement then
   begin
   //создаем алфавит преобразования открытого текста
   RecalcAlphabet(0);
   //предотвращаем перерисовку компонента до тех пор,
   //пока не зашифруем все строки сообщения
   mmEncryptMessage.Lines.BeginUpdate;
   //очищаем текстовый редактор
   mmEncryptMessage.Clear;
   //шифруем открытый текст построчно
   for i:= 0 to mmDecryptMessage.Lines.Count – 1 do
   mmEncryptMessage.Lines.Add(EncryptDecryptString(mmDecryptMessage.Lines[i]));
   //разрешаем перерисовку компонента
   mmEncryptMessage.Lines.EndUpdate;
   end
   else
   MessageDlg('Ошибка: символы подстановки заданы неверно', mtError,
   [mbOk], 0);
   end;
   procedure TfmSubstitution.btnDecpyptMessageClick(Sender: TObject);
   var
   i: Integer;
   begin
   //проверяем корректность ввода перестановки
   if ValidateRearrangement then
   begin
   //создаем алфавит преобразования шифрованного текста
   RecalcAlphabet(1);
   mmDecryptMessage.Lines.BeginUpdate;
   mmDecryptMessage.Clear;
   //дешифруем шифрованный текст построчно
   for i:= 0 to mmEncryptMessage.Lines.Count – 1 do
   mmDecryptMessage.Lines.Add(EncryptDecryptString(mmEncryptMessage.Lines[i]));
   mmDecryptMessage.Lines.EndUpdate;
   end
   else
   MessageDlg('Ошибка: символы подстановки заданы неверно', mtError, [mbOk], 0);
   end;

   В итоге получился вполне рабочий вариант приложения, способного без особого труда шифровать и дешифровать сообщения. На рис. 12.2 представлен результат работы данного приложения.
   Рис. 12.2. Результат работы приложения «Шифр простой подстановки»


   Транспозиция

   Следующий шифр, который будет рассмотрен, называется транспозицией с фиксированным периодом d. В этом случае сообщение делится на группы символов длины d, и к каждой группе применяется одна и та же перестановка. Эта перестановка является ключом и может быть задана некоторой перестановкой первых d целых чисел.
   Таким образом, для d = 5 в качестве перестановки можно взять ключ 23154. Его использование будет означать, что m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


… переходит в m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


m -------
| Библиотека iknigi.net
|-------
|  
 -------


… Последовательное применение двух или более транспозиций будет называться составной транспозицией. Если периоды последовательно применяемых транспозиций d -------
| Библиотека iknigi.net
|-------
|  
 -------


, …, d -------
| Библиотека iknigi.net
|-------
|  
 -------


, то в результате получится составная транспозиция периода d, где d – наименьшее общее кратное d -------
| Библиотека iknigi.net
|-------
|  
 -------


, …, d -------
| Библиотека iknigi.net
|-------
|  
 -------


.
   Теперь, ознакомившись с теорией, можно переходить к рассмотрению примера одной из возможных реализаций данного шифра. Для этого, как и в предыдущем случае, создадим новое приложение, а на форму поместим те же самые компоненты, за исключением редактора значений и кнопки генерации перестановки. Вместо них используем текстовое поле класса TEdit и еще один компонент класса TLabel с соответствующими именами edRearrangement и lbRearrangement. В результате должно будет получиться приложение, подобное изображенному на рис. 12.3.
   Рис. 12.3. Интерфейс программы «Транспозиция с фиксированным периодом»

   Текстовое поле edRearrangement будет служить для задания перестановки, используемой при шифровании. Перестановка будет задаваться числами, разделенными пробелом, а их количество – указывать период транспозиции. В остальных деталях интерфейса данное приложение аналогично предыдущему.
   Стоит отметить одну неприятную особенность данного шифра. Поскольку период фиксирован, то на текст накладывается определенное ограничение. Оно заключается в том, что длина текста должна быть кратна периоду. Существует несколько вариантов решения данной проблемы. Можно дополнить открытый текст какими-либо символами – тогда зашифровать сообщение не составит труда. Однако если эти символы заранее определены, то это облегчит задачу взломщика по вскрытию шифра. Другой вариант – переписать сообщение, используя, например, синонимы, либо удалив часть сообщения, которую легко восстановить из контекста, таким образом, чтобы длина текста стала кратной периоду.
   Теперь перейдем к рассмотрению исходного кода приложения. Как и в прошлый раз, сначала объявим класс необходимых типов, а также класс формы. Соответствующий программный код показан в листинге 12.9. Здесь была введена целочисленная константа, ограничивающая длину задаваемого периода. В данном случае она равна 100. Также, поскольку необходимо помнить саму перестановку, с помощью которой будет осуществляться транспозиция сообщения, был добавлен соответствующий тип.
 //-- Листинг 12.9. Объявление типов и класса формы --// 
   const
   MaxTerm = 100;
   type
   TRearrangement = array [0..MaxTerm] of Integer;
   TfmTransposition = class(TForm)
   mmDecryptMessage: TMemo;
   mmEncryptMessage: TMemo;
   lbDecryptMessage: TLabel;
   lbEncryptMessage: TLabel;
   lbRearrangement: TLabel;
   edRearrangement: TEdit;
   btnEncryptMessage: TButton;
   btnDecpyptMessage: TButton;
   procedure btnEncryptMessageClick(Sender: TObject);
   procedure btnDecpyptMessageClick(Sender: TObject);
   private
   {Private declarations}
   Rear: TRearrangement;
   function RecalcRearrangement(nKey: Integer): Boolean;
   function GetLine(Lines: TStrings; nRow, nInd: Integer): String;
   procedure EncryptDecrypt(SrcLines, DstLines: TStrings; nKey: Integer);
   public
   {Publicccdeclarationss}
   end;

   Теперь перейдем к рассмотрению исходного кода решаемых в данном случае подзадач. Первой рассматриваемой функцией будет функция разбора введенной строки, выделяющая перестановку из нее и проверяющая, является ли она допустимой.
   Функция RecalcRearrangement подготавливает перестановку требуемым образом для шифрования либо дешифрования в зависимости от значения параметра nKey, который может быть равно 0 или 1. Значение 0 указывает на то, что будет производиться шифрование сообщения и дополнительных действий по подготовке перестановки, за исключением проверки ее корректности, не требуется. Значение 1, напротив, указывает на то, что будет производиться дешифрование сообщения и требуется дополнительно преобразовать перестановку так, чтобы она была симметрична исходной – в этом случае процесс дешифрования ничем не будет отличаться от процесса шифрования.
   Чтобы введенная перестановка считалось корректной, необходимо и достаточно, чтобы выполнялись следующие требования:
   • были введены только числа через пробел;
   • числа не повторялись;
   • числа находились в диапазоне от одного до их общего количества.
   Проверка первого условия осуществляется следующим образом. Изначально считается, что строка состоит из пробелов. Как только пробелы заканчиваются, предполагается, что началось число, и оно выделяется до следующего пробела. Как только встретится пробел, будет произведена попытка преобразовать выделенную часть из строкового представления в численное, после чего полученное число добавлено к итоговой перестановке. Когда фрагмент кода, в котором находится первый цикл с условием после него, отработает, в массиве Rear будет сохранена введенная перестановка (в массиве Rear [0] хранится количество чисел в полученной перестановке). Сразу за первой проверкой осуществляется совместно вторая и третья, то есть проверяется допустимость самих введенных чисел, а также их уникальность. После всех проверок при необходимости осуществляется преобразование исходной перестановки к симметричной.
   Для получения симметричной перестановки необходимо выполнить нехитрое действие по обмену местами индексов чисел и самих чисел, то есть если имеется перестановка 312, то она преобразуется в 231, так как 1 стоит на втором месте, 2 – на третьем, а 3 – на первом.
   Исходный код выполняющей эти действия функции приведен в листинге 12.10.
 //-- Листинг 12.10. Функция разбора строки и проверки допустимости перестановки --// 
   function TfmTransposition.RecalcRearrangement(nKey: Integer): Boolean;
   var
   i: Integer;
   s: String;
   Space: Boolean;
   Used: array [1..MaxTerm] of Boolean;
   ExRear: TRearrangement;
   begin
   Result:= False;
   Rear[0]:= 0;
   Space:= True;
   //выделяем каждое слово, разделенное пробелом,
   //и преобразовываем его к числу
   for i:= 1 to Length(edRearrangement.Text) do
   if (edRearrangement.Text[i] = '') and (not Space) then
   begin
   Inc(Rear[0]);
   Rear[Rear[0]]:= StrToInt(s);
   Space:= True;
   end
   else
   if (edRearrangement.Text[i] <> '') then
   begin
   if Space then
   begin
   Space:= False;
   s:= '';
   end;
   s:= s + edRearrangement.Text[i];
   end;
   if not Space then
   begin
   Inc(Rear[0]);
   Rear[Rear[0]]:= StrToInt(s);
   end;
   //проверяем допустимость полученных чисел
   FillChar(Used, SizeOf(Used), False);
   for i:= 1 to Rear[0] do
   if (0 < Rear[i]) and (Rear[i] <= Rear[0]) and not Used[Rear[i]] then
   Used[Rear[i]]:= True
   else
   Exit;
   //преобразуем перестановку к шифровке, обратной
   //для симметричности процесса дешифровки шифровке
   if nKey = 1 then
   begin
   ExRear[0]:= Rear[0];
   for i:= 1 to Rear[0] do
   ExRear[Rear[i]]:= i;
   Rear:= ExRear;
   end;
   Result:= Rear[0] > 1;
   end;

   Кроме того, для упрощения алгоритма шифрования необходимо уметь получать часть текста заданной длины, начиная с указанной позиции, в виде одной строки, пропуская все переводы строк. Это действие выполняет следующая описываемая функция. Алгоритм ее работы довольно прост. Изначально результирующая строка не содержит ни единого символа. Далее осуществляется двойной вложенный цикл. Цикл верхнего уровня осуществляет изменение значения переменной, начиная с указанной строки и заканчивая самой последней. Вложенный цикл, в свою очередь, изменяет значение переменной, первый раз начиная с указанной в строке позиции, а в остальных случаях всегда с единицы, и заканчивая последней позицией текущей обрабатываемой строки. Каждый очередной символ добавляется к результирующей строке до тех пор, пока не будет достигнута заданная длина строки, равная периоду транспозиции. Соответствующий код приведен в листинге 12.11.
 //-- Листинг 12.11. Функция получения части текста заданной длины, начиная с указанной позиции, в виде одной строки --// 
   function TfmTransposition.GetLine(Lines: TStrings; nRow, nInd: Integer): String;
   var
   i, j, k: Integer;
   s: String;
   begin
   Result:= '';
   s:= '';
   k:= nInd;
   for i:= nRow to Lines.Count – 1 do
   begin
   for j:= k to Length(Lines[i]) do
   begin
   s:= s + Lines[i][j];
   if Length(s) = Rear[0] then
   begin
   Result:= s;
   Exit;
   end;
   end;
   kk k:= 1;
   end;
   end;

   Подготовительный этап рассмотрен. Теперь остается рассмотреть основной код программы. Обработчики кнопок OnClick вызывают один и тот же метод и указывают необходимые параметры, чтобы зашифровать либо дешифровать текст сообщения. Процедура EncryptDecrypt в качестве параметров принимает источник текста сообщения, с которым нужно проделать необходимые действия, приемник преобразованного текста сообщения и тип преобразования. Последний параметр принимает одно из двух значений: 0 или 1. Значение 0 указывает на то, что будет производиться шифрование сообщения. Значение 1 указывает на то, что будет производиться дешифрование сообщения. Процедура EncryptDecrypt выполняет следующие действия. Сначала она пытается подготовить необходимую перестановку и, только если все прошло успешно, переходит к попытке преобразования текста сообщения, предварительно выполнив еще одну проверку. Эта проверка заключается в том, чтобы удостовериться в соответствии общей длины текста накладываемому на нее ограничению, то есть длина обязательно должна быть кратна периоду транспозиции. Если проверка выполнена успешно, далее следует код преобразования текста сообщения с использованием подготовленной транспозиции. В листинге 12.12 приведен исходный код.
 //-- Листинг 12.12. Шифрование и дешифрование текста сообщения --// 
   procedure TfmTransposition.btnEncryptMessageClick(Sender: TObject);
   begin
   EncryptDecrypt(mmDecryptMessage.Lines, mmEncryptMessage.Lines, 0);
   end;
   procedure TfmTransposition.btnDecpyptMessageClick(Sender: TObject);
   begin
   EncryptDecrypt(mmEncryptMessage.Lines, mmDecryptMessage.Lines, 1);
   end;
   procedure TfmTransposition.EncryptDecrypt(SrcLines, DstLines:
   TStrings; nKey: Integer);
   var
   i, j, Cnt: Integer;
   s, EncryptMsg: String;
   begin
   if RecalcRearrangement(nKey) then
   begin
   //вычисляем общую длину текста
   Cnt:= 0;
   for i:= 0 to SrcLines.Count – 1 do
   Inc(Cnt, Length(SrcLines[i]));
   //проверяем кратность общей длины длине перестановки
   if Cnt mod Rear[0] <> 0 then
   begin
   MessageDlg('Ошибка: текст сообщения не кратен длине
   перестановки', mtError, [mbOk], 0);
   Exit;
   end;
   //преобразовываем сообщение
   Cnt:= Rear[0];
   DstLines.BeginUpdate;
   DstLines.Clear;
   for i:= 0 to SrcLines.Count – 1 do
   begin
   EncryptMsg:= '';
   for j:= 1 to Length(SrcLines[i]) do
   begin
   if Cnt = Rear[0] then
   begin
   s:= GetLine(SrcLines, i, j);
   Cnt:= 0;
   end;
   Inc(Cnt);
   EncryptMsg:= EncryptMsg + s[Rear[Cnt]];
   end;
   DstLines.Add(EncryptMsg);
   end;
   DstLines.EndUpdate;
   end
   else
   MessageDlg('Ошибка: перестановка задана неверно', mtError,
   [mbOk], 0);
   end;

   Здесь переменная Cnt отвечает за то, какая часть очередной группы букв уже обработана. Если эта часть равна количеству чисел в перестановке, то происходит переход к очередной группе букв сообщения. Алгоритм преобразования усложняется тем, что строки текста необязательно кратны количеству чисел в перестановке. Поэтому для удобства добавлена функция GetLine, получающая часть сообщения, начиная с указанной позиции, в виде одной строки определенной длины, которая при необходимости «склеивается» из нескольких идущих подряд строк. Теперь ничего не мешает заменить очередную букву сообщения соответствующей буквой из полученной строки. Результат работы приложения показан на рис. 12.4.
   Рис. 12.4. Результат работы приложения «Транспозиция с фиксированным периодом»


   Шифр Виженера и его варианты

   Ключ в шифре Виженера задается набором из n букв. Такие наборы подписываются с повторением под текстом сообщения, и полученные две последовательности складываются по модулю m, где m – количество букв в рассматриваемом алфавите (например, для русского алфавита каждая буква нумеруется от 0 (А) до 32 (Я) и m = 33). В результате получается правило преобразования открытого текста l -------
| Библиотека iknigi.net
|-------
|  
 -------


= x -------
| Библиотека iknigi.net
|-------
|  
 -------


+ y -------
| Библиотека iknigi.net
|-------
|  
 -------


(mod m), где x -------
| Библиотека iknigi.net
|-------
|  
 -------


 – буква в открытом тексте с номером i, y -------
| Библиотека iknigi.net
|-------
|  
 -------


 – буква ключа, полученная сокращением числа i по модулю n. В табл. 12.1 приведен пример использования ключа ПБЕ.
 //-- Таблица 12.1. Шифр Виженера с ключом ПБЕ --// 
   Шифр Виженера с периодом 1 называется шифром Цезаря. По сути, он представляет собой простую подстановку, в которой каждая буква некоторого сообщения M сдвигается циклически вперед на фиксированное количество знаков по алфавиту. Именно это количество является ключом. Оно может принимать любое значение в диапазоне от 0 до m – 1. Повторное применение двух или более шифров Виженера называется составным шифром Виженера. Он имеет уравнение l -------
| Библиотека iknigi.net
|-------
|  
 -------


= x -------
| Библиотека iknigi.net
|-------
|  
 -------


+ y -------
| Библиотека iknigi.net
|-------
|  
 -------


+ … + z -------
| Библиотека iknigi.net
|-------
|  
 -------


(mod m), где x -------
| Библиотека iknigi.net
|-------
|  
 -------


+ y -------
| Библиотека iknigi.net
|-------
|  
 -------


+ … + z -------
| Библиотека iknigi.net
|-------
|  
 -------


имеют различные периоды. Период их суммы, как и в составной транспозиции, составляет наименьшее общее кратное отдельных периодов.
   Если используется шифр Виженера с неограниченным неповторяющимся ключом, то получается шифр Вернама, в котором l -------
| Библиотека iknigi.net
|-------
|  
 -------


= x -------
| Библиотека iknigi.net
|-------
|  
 -------


+ y -------
| Библиотека iknigi.net
|-------
|  
 -------


(mod m) и y -------
| Библиотека iknigi.net
|-------
|  
 -------


выбираются случайно и независимо среди чисел 0, 1, …, m – 1. Если ключом служит текст, имеющий смысл, то получается шифр «бегущего ключа».
   Теперь перейдем к примеру. Рассмотрим одну из возможных реализаций шифра Цезаря. Как обычно, будет создано новое приложение и по аналогии с предыдущим примером на его форме размещены те же компоненты. Получится приложение, приблизительный вид которого показан на рис. 12.5.
   Рис. 12.5. Интерфейс приложения «Шифр Цезаря»

   Текстовое поле имеет имя edKey и предназначено для задания ключа, с помощью которого будет происходить процесс шифрования или дешифрования. Остальная часть интерфейса программы вам уже знакома, поэтому останавливаться на ее рассмотрении повторно не имеет смысла.
   Перейдем к рассмотрению исходного кода программы. Объявление необходимых типов, описание классов и переменных приведено в листинге 12.13.
 //-- Листинг 12.13. Объявление типов и класса формы --// 
   type
   //исходный алфавит русского языка
   TRusSrcAlphabet = array [0..65] of Char;
   //сопоставление букв алфавита открытого текста и зашифрованного
   TRusDstAlphabet = array [Char] of Char;
   TfmCryptography = class(TForm)
   mmDecryptMessage: TMemo;
   mmEncryptMessage: TMemo;
   lbDecryptMessage: TLabel;
   lbEncryptMessage: TLabel;
   btnEncryptMessage: TButton;
   btnDecpyptMessage: TButton;
   edKey: TEdit;
   lbKey: TLabel;
   procedure btnEncryptMessageClick(Sender: TObject);
   procedure btnDecpyptMessageClick(Sender: TObject);
   private
   {Private declarations}
   RusDstAlphabet: TRusDstAlphabet;
   function GetKey: Integer;
   procedure RecalcAlphabet(nKey: Integer);
   function EncryptDecryptString(strMsg: String; nKey: Integer): String;
   public
   {Public declarations}
   end;
   var
   RusSrcAlph abet: TRusSrcAlph abet = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'+
   'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';
   fmCryptography: TfmCryptography;

   Далее приведено описание работы методов, которые решают определенные подзадачи, возникающие в процессе решения основной задачи. Начнем рассмотрение с функции получения введенного пользователем ключа. Ее работа заключается в следующем. Сначала текстовое представление ключа преобразуется в численное. Затем проверяется, успешно ли прошло преобразование. Если проверка выполнена успешно, то возвращается полученное значение. В противном случае функция возвращает значение -1, что свидетельствует о некорректном вводе пользователем ключа. Исходный код данной функции приведен в листинге 12.14.
 //-- Листинг 12.14. Функция получения ключа --// 
   function TfmCryptography.GetKey: Integer;
   var
   key, code: Integer;
   begin
   Result:= –1;
   //получаем текст элемента управления текстовая строка
   Val(edKey.Text, key, code);
   //ошибка во время преобразования к целому числу?
   //или ключ имеет отрицательное значение?
   if (code = 0) and (0 < key) then
   Result:= key;
   end;

   Процедура RecalcAlphabet имеет один параметр nKey, который может принимать любое целое значение. Он показывает, на сколько необходимо сдвинуть алфавит циклически вперед, то есть, если имеется алфавит АБВГД, а nKey = 3, результатом будет ВГДАБ. Первым делом алфавит соответствия заполняется один к одному, то есть каждый символ соответствует сам себе. После этого нужно пройти циклом по строке, содержащей весь необходимый алфавит, подлежащий сдвигу, и переназначить соответствия для этих букв на смещенные (листинг 12.15).
 //-- Листинг 12.15. Функция пересчета алфавита преобразования --// 
   procedure TfmCryptography.RecalcAlphabet(nKey: Integer);
   var
   Ch: Char;
   i: Integer;
   LetCnt: Integer;
   begin
   //предварительно все символы в алфавите шифрования
   //соответствуют символам из незашифрованного алфавита
   for Ch:= Low(RusDstAlphabet) to High(RusDstAlphabet) do
   RusDstAlphabet[Ch]:= Ch;
   //количество символов в алфавите
   LetCnt:= SizeOf(TRusSrcAlphabet);
   //смещаем эталонный алфавит циклически влево на значение,
   //заданное ключом nKey
   for i:= 0 to LetCnt – 1 do
   RusDstAlphabet[RusSrcAlphabet[(i – – nKey + LetCnt) mod LetCnt]]:=
   RusSrcAlphabet[i];
   end;

   Процедура RecalcAlphabet производит необходимую подготовку перед шифрованием или дешифрованием. Ее результаты используются в функции EncryptDecryptString, где каждая буква открытого текста заменяется соответствующей ей буквой из смещенного алфавита. Это преобразование осуществляется простым проходом по всей строке и выполнением операции замены символа соответствующим ему. Стоит заметить, что для дешифровки сообщения по заданному ключу вычисляется симметричный ему ключ. В результате процесс дешифровки текста сообщения ничем не отличается от процесса его шифровки (листинг 12.16).
 //-- Листинг 12.16. Шифрование и дешифрование строки --// 
   function TfmCryptography.EncryptDecryptString (strMsg: String;
   nKey: Integer): String;
   var
   i: Integer;
   begin
   //каждый символ строки заменяется соответствующим символом
   //алфавита шифрования
   for i:= 1 to Length(strMsg) do
   strMsg[i]:= RusDstAlphabet[strMsg[i]];
   Result:= strMsg;
   end;

   Теперь есть все, чтобы перейти к решению основной задачи. Процесс шифрования аналогичен процессу дешифрования текста сообщения. Для начала нужно попытаться получить ключ, который ввел пользователь. После этого проверить значение ключа. Если он равен -1, значит, ключ введен неверно и преобразование текста невозможно. В противном случае перед преобразованием текста нужно вызывать метод подготовки алфавита с полученным ключом. Стоит отметить, что во время выполнения дешифрования вычисляется обратный ключ. С его помощью можно получить алфавит, в результате использования которого аналогично процессу шифрования получается открытый текст сообщения. Далее для каждой строки текста сообщения необходимо вызвать функцию преобразования. На этом каждый метод заканчивает свою работу. Исходный код, соответствующий приведенному выше описанию, приведен в листинге 12.17.
 //-- Листинг 12.17. Шифрование и дешифрование текста сообщения --// 
   procedure TfmCryptography.btnEncryptMessageClick(Sender: TObject);
   var
   i: Integer;
   nKey: Integer;
   begin
   //получаем ключ, с помощью которого будет шифроваться сообщение
   nKey:= GetKey;
   //ключ задан верно ?
   if nKey = –1 then
   Begin
   MessageDlg('Ошибка: ключ задан неверно', mtError, [mbOk], 0);
   Exit;
   End;
   //получаем алфавит, с помощью которого будет происходить шифрование
   RecalcAlphabet(nKey);
   //предотвращаем перерисовку компонента до тех пор, пока не
   //зашифруем все строки сообщения
   mmEncryptMessage.Lines.BeginUpdate;
   //освобождаем список от любых старых значений
   mmEncryptMessage.Clear;
   //шифруем сообщение построчно
   for i:= 0 to mmDecryptMessage.Lines.Count – 1 do
   mmEncryptMessage.Lines.Add(
   EncryptDecryptString(mmDecryptMessage.Lines[i], nKey));
   //заново разрешаем перерисовку компонента
   mmEncryptMessage.Lines.EndUpdate;
   end;
   procedure TfmCryptography.btnDecpyptMessageClick(Sender: TObject);
   var
   i: Integer;
   nKey: Integer;
   begin
   nKey:= GetKey;
   if nKey = –1 then
   Begin
   MessageDlg('Ошибка: ключ задан неверно', mtError, [mbOk], 0);
   Exit;
   End;
   //получаем алфавит, с помощью которого будет происходить дешифрование
   RecalcAlphabet(SizeOf(TRusSrcAlphabet) – nKey mod SizeOf(TRusSrcAlphabet));
   mmDecryptMessage.Lines.BeginUpdate;
   mmDecryptMessage.Clear;
   for i:= 0 to mmEncryptMessage.Lines.Count – 1 do
   mmDecryptMessage.Lines.Add(
   EncryptDecryptString(mmEncryptMessage.Lines[i], nKey));
   mmDecryptMessage.Lines.EndUpdate;
   end;

   Первое, что бросается в глаза при рассмотрении всего текста приложения, – практически полная идентичность интерфейса и основной части исходного кода. На самом деле это совсем не случайно. Достаточно часто программы пишутся универсально (даже более универсально, чем здесь). Это основывается на очень простом предположении, что код должен быть многоразовым, то есть его должно быть возможно повторно использовать в других приложениях. В результате получается некий шаблон, позволяющий решать целый класс задач. Для этого нужно выполнить несколько небольших изменений, после чего можно просто забыть об этом. Результат выполнения итогового приложения показан на рис. 12.6.
   Рис. 12.6. Результат работы приложения «Шифр Цезаря»


   Шифр с автоключом

   Шифр, основывающийся на шифре Виженера, в котором или само сообщение, или результирующая криптограмма используются в качестве ключа, называется шифром с автоключом. Шифрование начинается с помощью «первичного ключа» (который является настоящим ключом в данном случае) и продолжается с помощью сообщения или криптограммы, смещенной на длину первичного ключа. Рассмотрим пример, в котором первичным ключом является набор букв ЗЕБРА. В табл. 12.2 приведено шифрование, при котором в качестве ключа используется сообщение.
 //-- Таблица 12.2. Шифр с автоключом (ключ – сообщение) --// 
   Если же в качестве ключа использовать криптограмму, получится шифрование, приведенное в табл. 12.3.

 //-- Таблица 12.3. Шифр с автоключом (ключ – криптограмма) --// 
   Теперь, когда понятно, как работает данный шифр, реализуем его второй вариант, чуть более сложный, чем первый. В интерфейсе программы ничего, кроме назначения текстового поля, изменяться не будет, поэтому он будет выглядеть точно так же, как и в предыдущем примере (см. рис. 12.5). Текстовое поле теперь будет содержать ключ уже не в виде целого числа, а в виде произвольной строки, полностью состоящей из русских букв верхнего и нижнего регистров, за исключением буквы «ё» обоих регистров.
   Ниже приведен код с объявлением необходимых типов, констант и переменных, а также объявлением класса формы (листинг 12.18).
 //-- Листинг 12.18. Объявление типов и класса формы --// 
   type
   TRusLetters = set of Char;
   TfmEncryptingAutoKey = class(TForm)
   mmDecryptMessage: TMemo;
   mmEncryptMessage: TMemo;
   lbDecryptMessage: TLabel;
   lbEncryptMessage: TLabel;
   btnEncryptMessage: TButton;
   btnDecpyptMessage: TButton;
   edKey: TEdit;
   lbKey: TLabel;
   procedure btnEncryptMessageClick(Sender: TObject);
   procedure btnDecpyptMessageClick(Sender: TObject);
   private
   {Private declarations}
   function GetKey: String;
   function EncryptString(strEncryptMsg: String; var strKey:
   String): String;
   function DecryptString(strDecryptMsg: String; var strKey:
   String): String;
   procedure EncryptDecrypt(SrcLines, DstLines: TStrings; bEncrypt:
   Boolean);
   public
   {Public declarations}
   end;
   const
   RusLett ers: TRusLett ers = ['А'..'я '];
   var
    fmEncryptingAutoKey: TfmEncryptingAutoKey;

   Рассмотрение, как и в предыдущем примере, начнем с функции получения введенного пользователем ключа. Ее работа заключается в следующем. Сначала каждый символ ключа проверяется на принадлежность алфавиту русского языка. Если найден посторонний символ, то функция возвращает пустую строку, что свидетельствует об ошибке ввода ключа пользователем. В случае успешного завершения функция возвращает исходную строку ключа. Код этой функции приведен в листинге 12.19.
 //-- Листинг 12.19. Функция получения ключа --// 
   function TfmEncryptingAutoKey.GetKey: String;
   var
   i: Integer;
   begin
   Result:= '';
   for i:= 1 to Length(edKey.Text) do
   if not (edKey.Text[i] in RusLetters) then
   Exit;
   Result:= edKey.Text;
   end;

   На входе функции EncryptString и DecryptString получают строку, которую требуется преобразовать, и первичный ключ. Внешне они очень похожи, но все же отличаются, и эти отличия существенны. Функция шифрования выполняет следующие действия. В цикле осуществляется проход по строке и проверяется, является ли очередной символ буквой русского алфавита. В случае положительного ответа этот символ преобразуется с помощью очередного символа ключа и добавляется в его конец. Преобразование осуществляется по правилу, которое указывалось при рассмотрении шифра Виженера: I = x + y -------
| Библиотека iknigi.net
|-------
|  
 -------


(mod m), то есть символ открытого текста и символ ключа складываются с последующим сокращением этой суммы по модулю m, где m – общее количество букв в алфавите (листинг 12.20).
 //-- Листинг 12.20. Функция шифрования строки с помощью ключа и криптограммы --// 
   function TfmEncryptingAutoKey.EncryptString(strEncryptMsg: String;
   var strKey: String): String;
   var
   i: Integer;
   begin
   for i:= 1 to Length(strEncryptMsg) do
   if strEncryptMsg[i] in RusLetters then
   begin
   strEncrypt Msg[i]:= Chr(((Ord(strEncrypt Msg[i]) – Ord('А')) +
   (Ord(strKey[1]) – Ord('А'))) mod 64 + Ord('А'));
   Delete(strKey, 1, 1);
   strKey:= strKey + strEncryptMsg[i];
   end;
   Result:= strEncryptMsg;
   end;

   Функция дешифрования строки с помощью ключа и криптограммы выполняет следующие операции. Как и в предыдущей функции, в цикле осуществляется проход по строке и проверяется, является ли очередной символ буквой русского алфавита. При положительном ответе данный символ сначала добавляется в конец ключа, и только потом осуществляется его преобразование. Обратное преобразование символа выполняется по следующему правилу: I = x -------
| Библиотека iknigi.net
|-------
|  
 -------


– y -------
| Библиотека iknigi.net
|-------
|  
 -------


(mod m), то есть из символа преобразованного текста вычитается символ ключа с последующим сокращением этой разности по модулю m, где m – общее количество букв в алфавите. Если результат отрицателен, происходит дополнение до положительного числа значением m. Код реализации этих действий представлен в листинге 12.21.
 //-- Листинг 12.21. Функция дешифрования строки с помощью ключа и криптограммы --// 
   function TfmEncryptingAutoKey.DecryptString(strDecryptMsg: String;
   var strKey: String): String;
   var
   i: Integer;
   begin
   for i:= 1 to Length(strDecryptMsg) do
   if strDecryptMsg[i] in RusLetters then
   begin
   strKey:= strKey + strDecryptMsg[i];
   strDecrypt Msg[i]:= Chr((((Ord(strDecrypt Msg[i]) – Ord('А')) –
   (Ord(strKey[1]) – Ord('А'))) + 64) mod 64 + Ord('А'));
   Delete(strKey, 1, 1);
   end;
   Result:= strDecryptMsg;
   end;

   Обработчики событий OnClick вызывают функцию EncryptDecrypt с необходимыми параметрами. Эта функция принимает всего три параметра. Первый указывает на источник текста сообщения, требующего преобразования, второй указывает на приемник преобразованного текста сообщения. Последний параметр определяет тип преобразования текста сообщения. Если он равен True, то текст сообщения шифруется и помещается в приемник. В противном случае текст сообщения дешифруется и также помещается в приемник. Это происходит следующим образом. Сначала получается ключ, с помощью которого будет осуществляться преобразование текста сообщения. Если ключ некорректен, то выдается соответствующее предупреждение и больше ничего не выполняется. Если ключ корректен, то в зависимости от последнего параметра вызывается соответствующая функция преобразования для каждой строки источника текста сообщения и результат добавляется в приемник (листинг 12.22).
 //-- Листинг 12.22. Функция шифрования и дешифрования текста сообщения --// 
   //bEncrypt = True – шифровать
   //bEncrypt = False – дешифровать
   procedure TfmEncryptingAutoKey.EncryptDecrypt(SrcLines,
   DstLines: TStrings; bEncrypt: Boolean);
   var
   i: Integer;
   strKey: String;
   begin
   strKey:= GetKey;
   if strKey <> '' then
   begin
   DstLines.BeginUpdate;
   DstLines.Clear;
   if bEncrypt then
   for i:= 0 to SrcLines.Count – 1 do
   DstLines.Add(EncryptString(SrcLines[i], strKey))
   else
   for i:= 0 to SrcLines.Count – 1 do
   DstLines.Add(DecryptString(SrcLines[i], strKey));
   DstLines.EndUpdate;
   end
   else
   MessageDlg(' Ошибка: ключ задан неверно', mtError, [mbOk], 0);
   end;
   procedure TfmEncryptingAutoKey.btnEncryptMessageClick(Sender: TObject);
   begin
   EncryptDecrypt(mmDecryptMessage.Lines, mmEncryptMessage.Lines, True);
   end;
   procedure TfmEncryptingAutoKey.btnDecpyptMessageClick(Sender: TObject);
   begin
   EncryptDecrypt(mmEncryptMessage.Lines, mmDecryptMessage.Lines, False);
   end;
   end.

   Пример работы полученного приложения показан на рис. 12.7.
   Рис. 12.7. Результат работы приложения «Шифр с автоключом»


   Взлом

   В заключение будет рассмотрен один из методов вскрытия шифров. Здесь будет произведена попытка реализовать приложение, способное взломать шифр Цезаря. Оно будет основываться на одном довольно распространенном методе криптоанализа, который называется частотным анализом. Суть его заключается в том, что в большинстве осмысленных текстов существует определенная закономерность относительно частоты использования тех или иных букв. Таким образом, если известно, насколько часто встречается та или иная буква в языке, на котором написано сообщение, можно сделать предположение о том, какие буквы зашифрованы в данной криптограмме. Таким образом, требуется подсчитать частоту использования каждой буквы в криптограмме и затем сопоставить их с частотами букв, известных для заданного алфавита языка.
   Абсолютная частота буквы представляет собой количество повторений в тексте. Относительная частота – отношение абсолютной частоты символов к общему количеству символов в сообщении. Данная программа будет взламывать русскоязычные тексты, поэтому в табл. 12.4 приведены относительные частоты букв русского языка.
   Теоретическая основа данной программы создана, поэтому можно переходить к ее реализации. Поместите на форму два компонента классов TMemo с соответствующими именами mmDecryptMessage и mmEncryptMessage три TLabel и по одному компоненту классов TEdit и TButton – edKey и btnHackEncrypting соответственно. Текстовый редактор mmDecryptMessage и текстовое поле edKey сделайте доступными только для чтения, поскольку вводиться будет лишь зашифрованное сообщение, а ключ и соответствующий открытый текст будут определяться программой. Результат разработки интерфейса программы показан на рис. 12.8.
   Рис. 12.8. Интерфейс программы «Шифр Цезаря – взлом»

 //-- Таблица 12.4. Относительные частоты букв русского языка --// 
   Осталась реализовать сам алгоритм по вскрытию криптограммы. Процесс вскрытия шифра часто оказывается задачей трудоемкой и требующий больше усилий, чем при написании приложений, шифрующих и дешифрующих текст сообщения, используя известный ключ. Исходный код приложения, в котором осуществляется объявление необходимых типов, констант и переменных, а также описание формы приложения приведен в листинге 12.23.
 //-- Листинг 12.23. Объявление типов и класса формы --// 
   type
   //множество всех русских букв
   TRusLetters = set of Char;
   //исходный алфавит русского языка
   TRusSrcAlphabet = array [0..65] of Char;
   //относительные частоты русских букв
   TRusFrequency= array[0..32] offReal;
   TFrequency = array [Char] of Real;
   TRusDstAlphabet = array [Char] of Char;
   TfmHackEncrypting = class(TForm)
   mmDecryptMessage: TMemo;
   mmEncryptMessage: TMemo;
   lbDecryptMessage: TLabel;
   lbEncryptMessage: TLabel;
   btnHackEncrypting: TButton;
   edKey: TEdit;
   lbKey: TLabel;
   procedure FormCreate(Sender: TObject);
   procedure btnHackEncryptingClick(Sender: TObject);
   private
   {Private declarations}
   //значение ключа, вычисляемого на основании частотного анализа
   nHackKey: Integer;
   //количество букв русского алфавита в закодированном сообщении
   nCount: LongInt;
   //абсолютная частота букв русского алфавита (то есть количество
   //каждой буквы по отдельности) в зашифрованном сообщении
   AbsFrequency: TFrequency;
   //относительная частота букв русского алфавита в шифровке
   RelFreqInMsg: TFrequency;
   //относительная частота букв русского алфавита в русском языке
   RelFreqInLang: TFrequency;
   RusDstAlphabet: TRusDstAlphabet;
   function UpCaseRus(Ch: Char): Char;
   procedure RecalcAlphabet(nKey: Integer);
   function DecryptString(strDecryptMsg: String; nKey: Integer): String;
   public
   {Public declarations}
   end;
   const
   RusLett ers: TRusLett ers = [' Ё Ё', 'ё ё', 'А'..'я '];
   RusSrcAlph abet: TRusSrcAlph abet = ' АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'+
   'абвгдеёжзийклмнопрстуфхцчшщъыьэюя ';
   //частоты в соответствии с порядком букв в русском алфавите
   RusFrequency: TRusFrequency =(
   0.063, 0.014, 0.038, 0.013, 0.025, 0.072, 0.072, 0.007,
   0.016, 0.062, 0.010, 0.028, 0.035, 0.026, 0.052, 0.090,
   0.023, 0.040, 0.045, 0.053, 0.021, 0.001, 0.009, 0.004,
   0.012, 0.005, 0.003, 0.015, 0.017, 0.015, 0.002, 0.006,
   0.018);
   var
   fmHackEncrypting: TfmHackEncrypting;

   Теперь рассмотрим инициализацию формы приложения. Таблица, объявленная в виде константы, не очень удобна, поэтому нужно преобразовать ее к другому виду. В новой таблице можно будет, зная только сам символ, получить его относительную частоту для русскоязычных текстов. Принцип выполнения данной операции показан в исходном коде листинга 12.24.
 //-- Листинг 12.24. Обработчик события формы OnCreate --// 
   procedure TfmHackEncrypting.FormCreate(Sender: TObject);
   var
   i, h: Integer;
   begin
   h:= High(RusSrcAlphabet) div 2;
   for i:= Low(RusSrcAlphabet) to High(RusSrcAlphabet) do
   RelFreqInLang[RusSrcAlphabet[i]]:= RusFrequency[i mod h];
   end;

   Вспомогательные методы UpCaseRus, RecalcAlphabet и DecryptString вам уже знакомы. Они выполняют стандартные действия из предыдущих примеров, поэтому достаточно привести их реализацию только для данного случая (листинг 12.25).
 //-- Листинг 12.25. Вспомогательные функции --// 
   function TfmHackEncrypting.UpCaseRus(Ch: Char): Char;
   begin
   if Ch = 'ё' then Ch:= 'Ё';
   if Ch in ['а'..'я'] then Dec(Ch, 32);
   Result:= Ch;
   end;
   procedure TfmHackEncrypting.RecalcAlphabet(nKey: Integer);
   var
   Ch: Char;
   i: Integer;
   LetCnt: Integer;
   begin
   for Ch:= #0 to #255 do
   RusDstAlphabet[Ch]:= Ch;
   LetCnt:= SizeOf(TRusSrcAlphabet);
   for i:= 0 to LetCnt – 1 do
   RusDstAlphabet[RusSrcAlphabet[(i – nKey + LetCnt) mod LetCnt]]:=
   RusSrcAlphabet[i];
   end;
   function TfmHackEncrypting.DecryptString(strDecryptMsg: String;
   nKey: Integer): String;
   var
   i: Integer;
   begin
   for i:= 1 to Length(strDecryptMsg) do
   strDecryptMsg[i]:= RusDstAlphabet[strDecryptMsg[i]];
   Result:= strDecryptMsg;
   end;

   Основные действия по вскрытию шифра осуществляются в обработчике события OnClick кнопки btnHackEncrypting. Первым делом подсчитываются абсолютные частоты букв и их общее количество в криптограмме. После этого на основании полученных данных производится расчет относительных частот для каждой из букв. На этом подготовительный этап заканчивается и начинается процесс вскрытия шифра. Далее проверяется каждый допустимый ключ, сокращенный по модулю количества букв алфавита, без повторения. Для каждого из них вычисляется сумма модуля разности относительных частот, вычисленных для данной криптограммы, и относительных частот для русского языка. Из всех таких сумм выбирается наименьшая, при которой относительные частоты букв практически совпадают, а следовательно, наиболее вероятно, что в данном случае ключ, соответствующий этой сумме, и есть искомый. Стоит отметить, что подобные методы вскрытия очень зависимы от сделанного в самом начале предположения, и если тот, кто передавал зашифрованное сообщение, предусмотрел возможность такого же предположения, то мог специально сделать все, чтобы метод вскрытия, построенный на нем, не сработал. Например, можно предварительно заархивировать весь текст сообщения, в результате чего получится некий текст с довольно близкими значениями частот для разных букв. В этом случае метод вскрытия по такому алгоритму может оказаться неэффективным. Исходный код приведен в листинге 12.26.
 //-- Листинг 12.26. Обработчик события кнопки OnClick --// 
   procedureTfmHackEncrypting.btnHackEncryptingClick(Sender: TObject);
   var
   Ch: Char;
   i, j, h: Integer;
   Delta, MinDelta: Real;
   begin
   //обнуляем счетчик русских букв в закодированном сообщении
   nCount:= 0;
   FillChar(AbsFrequency, SizeOf(AbsFrequency), 0);
   for i:= 0 to mmEncryptMessage.Lines.Count – 1 do
   for j:= 1 to Length(mmEncryptMessage.Lines[i]) do
   begin
   //очередной символ сообщения
   Ch:= mmEncryptMessage.Lines[i][j];
   //проверяем, принадлежит ли символ множеству русских букв?
   if Ch in RusLetters then
   begin
   //подсчитываем количество данной буквы в отдельности
   //и в совокупности со всеми русскими буквами
   UUAbsFrequency[UpCaseRus(Ch)]:= AbsFrequency[UpCaseRus(Ch)] + 1;
   Inc(nCount);
   end;
   end;
   if nCount = 0 then
   begin
   MessageDlg('Дешифровать сообщение нельзя, так как'+
   'отсутствует русский текст', mtError, [mbOk], 0);
   Exit;
   end;
   //вычисляем относительные частоты букв в закодированном сообщении
   FillChar(RelFreqInMsg, SizeOf(RelFreqInMsg), 0);
   for i:= Low(RusSrcAlphabet) to High(RusSrcAlphabet) div 2 do
   RelFreqInMsg[RusSrcAlphabet[i]]:= AbsFrequency[RusSrcAlphabet[i]] / nCount;
   //перебираем все возможные ключи и выбираем тот, при
   //использовании которого частоты появления русских букв
   //в закодированном сообщении наиболее близки к частотам
   //появления русских букв в русском языке, то есть сумма
   //абсолютных разностей частот букв наименьшая
   h:= High (RusSrcAlphabet) div 2 + 1;
   MinDelta:= h;
   for i:= 1 to h – 1 do
   begin
   Delta:= 0;
   for j:= 0 to h – 1 do
   Delta:= Delta + Abs(RelFreqInLang[RusSrcAlphabet[j]] –
   RelFreqInMsg[RusSrcAlphabet[(i + j + h) mod h]]);
   //очередная сумма разностей меньше всех предыдущих?
   if MinDelta > Delta then
   begin
   //запоминаем ее...
   MinDelta:= Delta;
   //... и запоминаем ключ, при котором получено данное значение
   nHackKey:= i;
   end;
   end;
   edKey.Text:= IntToStr(nHackKey);
   h:= High(RusSrcAlphabet) + 1;
   RecalcAlphabet(h – nHackKey mod h);
   mmDecryptMessage.Lines.BeginUpdate;
   mmDecryptMessage.Clear;
   for i:= 0 to mmEncryptMessage.Lines.Count – 1 do
   mmDecryptMessage.Lines.Add(DecryptString(mmEncryptMessage.Lines[i], nHackKey));
   mmDecryptMessage.Lines.EndUpdate;
   end;

   Результат работы написанного приложения показан на рис. 12.9. Как видите, все получилось!
   Рис. 12.9. Результат работы приложения «Шифр Цезаря – взлом»

   Хочется отметить, что частотный анализ может производиться не только по частоте использования букв, но и по частоте употребления определенных слов и даже фраз. Например, если ведется переписка между Димой и Николаем, то вероятность, что Дима начнет свое обращение со слов «Дорогой Николай» выше, чем вероятность того, что он начнет его произвольным набором символов «ЫКр2!». Поэтому, когда вы сами будете пытаться вскрыть чей-то шифр, помните о такой возможности, но не забывайте, что существуют и значительно более сложные шифры, чем рассмотренные здесь. Часто для улучшения стойкости шифров применяются различные методики сжатия информации, чтобы было сложнее воспользоваться частотным анализом, так как в этом случае частоты будут почти одинаковы.



   Заключение

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


   Приложение 1
   Коды и обозначения основных клавиш

   В табл. П1.1 приведены коды, обозначения целочисленных констант и описания основных клавиш.
 //-- Таблица П1.1. Коды, обозначения и описания клавиш --// 




   Приложение 2
   Оконные стили

   В приложении представлены таблицы, описывающие следующие оконные стили: общие (табл. П2.1), дополнительные (табл. П2.2), стили кнопок (табл. П2.3), статических надписей (табл. П2.4), текстовых полей (табл. П2.5), списков (табл. П2.6) и раскрывающихся списков (табл. П2.7).
 //-- Таблица П2.1. Общие оконные стили --// 


 //-- Таблица П2.2. Дополнительные оконные стили --// 


 //-- Таблица П2.3. Стили кнопок --// 


 //-- Таблица П2.4. Стили статических надписей --// 



 //-- Таблица П2.5. Стили текстовых полей --// 


 //-- Таблица П2.6. Стили списков (ListBox) --// 



 //-- Таблица П2.7. Стили раскрывающихся списков (ComboBox) --// 



   Приложение 3
   Сообщения

   В таблицах данного приложения приведены обозначения констант, описания сообщений, а также назначения параметров wParam и lParam сообщений. Часто параметры wParam или lParam являются указателями на структуры. Для экономии места объявления этих структур не приведены: их можно найти в модуле Windows.
   Сообщения типа WM_SETTEXT, WM_SETFONT и подобных им могут как приниматься, так и отправляться, то есть могут использоваться для управления окнами. Для большинства сообщений, обозначения которых начинаются с GET, требуемое значение возвращается функцией отправки сообщения.
   В приложении представлены таблицы с перечислением некоторых часто используемых сообщений (табл. П3.1), уведомлений от элементов управления (табл. П3.2), сообщений для управления кнопками (табл. П3.3), статическими надписями (табл. П3.4), текстовыми полями (табл. П3.5), списками (табл. П3.6) и раскрывающимися списками (табл. П3.7).
 //-- Таблица П3.1. Некоторые часто используемые сообщения --// 



 //-- Таблица П3.2. Уведомления от элементов управления --// 


 //-- Таблица П3.3. Сообщения для управления кнопками --// 

 //-- Таблица П3.4. Сообщения для управления статическими надписями --// 


 //-- Таблица П3.5. Основные сообщения для управления текстовыми полями --// 


 //-- Таблица П3.6. Основные сообщения для управления списками (ListBox) --// 


 //-- Таблица П3.7. Основные сообщения для управления раскрывающимися списками (ComboBox) --//